Ownership Mental Model

How Loon manages memory without garbage collection or annotations.

Why Ownership

There are basically three ways a programming language can deal with memory. You can manage it by hand (C, C++), which is fast but terrifying. You can let a garbage collector handle it (Java, Go, Python), which is convenient but comes with latency spikes, higher memory usage, and unpredictable pauses. Or you can use ownership (Rust), which gives you deterministic, zero-overhead cleanup with compile-time safety.

Loon picks ownership. But it asks a follow-up question that Rust didn't: does the programmer actually need to see the machinery? Rust proved the ownership model works beautifully. Loon bets that the compiler can make ownership decisions on your behalf, so you get the performance benefits without 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 time reasoning about lifetimes.

Loon takes a fundamentally different position: you write code using values, and the compiler decides whether to move, borrow, or copy. It inserts borrows where safe, clones where necessary, and drops values at exactly the right time. You never write a lifetime annotation. Not because they are hidden; because they are not needed.

This is not hiding complexity. It is removing unnecessary decisions. The compiler has strictly more information than you do about value lifetimes, so it makes better choices.

The Compiler's Decision Process

When you pass a value to a function or assign it to a new binding, the compiler follows a clear decision process. Understanding these rules is not required for writing Loon, but it helps build intuition about what happens under the hood.

1. Copy for primitives

Integers, floats, booleans, and characters are always copied. They are small enough that duplicating them is essentially free, so the compiler never bothers with ownership tracking for these types.

[let x 42]
[let y x]
[println x]  ; fine — Int is copied

2. Auto-borrow when safe

If the compiler can prove that a reference will not outlive the owner, it passes a borrow instead of moving. This is the most common case for collection types, and it is why most Loon code "just works" without you thinking about ownership at all.

[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. There is no way for the reference to escape, so borrowing is safe.

3. Move by default

When the value is not a primitive and cannot be safely borrowed, ownership transfers to the new location. This is the fallback, and it is how Loon ensures memory is freed exactly once.

[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 goes through a predictable lifecycle. There are no surprises here, no finalizers that run at unpredictable times, no weak references to worry about. It is straightforward.

Creation

A value is created and bound to a name. That name becomes the owner. Ownership starts here and follows the value wherever it goes.

[let user {:name "Ada" :age 30}]

Use

The value is read, passed to functions, or used in expressions. Each time you use it, the compiler decides whether that use is a borrow (temporary access) or a move (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 is reclaimed immediately. Not "eventually, when the GC gets around to it." Right now, at a known point in the program.

[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. When that happens, clone it explicitly. This makes the cost visible in the code, which is a good thing. You can see exactly where allocations happen.

[let a #[1 2 3]]
[let b [clone a]]
; both a and b are independent owners

Return instead of mutate

Prefer returning new values over mutating existing ones. Ownership actually makes this efficient, because the compiler can often reuse the memory of the old value when it knows nobody else is looking at it.

[fn add-item [cart item]
  [append cart item]]

[let cart #[]]
[let cart [add-item cart "book"]]
[let cart [add-item cart "pen"]]

Rebinding with the same name is idiomatic in Loon. Each let creates a new binding that shadows the previous one.

Pipeline-friendly design

The pipe operator works naturally with ownership because each step consumes the previous result and produces a new one. The intermediate values live only as long as they need to.

[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 can be reused immediately. No garbage collector needed, no manual free calls, just values flowing through a pipeline.