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 copied

2. 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 consumed

The 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 error

Value 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 transfers

Drop

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 owners

Return 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.