Ownership

Move semantics and borrow inference without annotations.

Three Rules

Loon keeps memory safe without a garbage collector, and the whole system rests on three rules. Internalize these and the rest is consequence.

  • Every value has exactly one owner.
  • When a value is assigned or passed, ownership transfers (moves).
  • When the owner goes out of scope, the value is dropped.

If you have written Rust, this will feel like home. The difference is what is missing: Loon never asks you for a lifetime annotation. The compiler works out borrows by itself, so the guarantees arrive without the syntax that usually pays for them.

Note

Unlike Rust, you never write lifetime annotations. The compiler infers borrows automatically.

Move Semantics

Assign a value to a new binding and ownership goes with it. The original binding falls invalid, and touching it again is a compile error. The rule surprises newcomers, then earns its keep: two parts of a program can never both believe they own the same data.

LOON
[let a #[1 2 3]]
[let b a]
; a is no longer valid here
[println b]

Passing a value to a function works the same way. The function takes ownership, and the caller is done with it.

LOON
[fn consume [xs]
  [println [len xs]]]

[let items #[10 20 30]]
[consume items]
; items has been moved

Strict, certainly. In return, you always know exactly who answers for a piece of data. No shared mutable state, no use-after-free, no data races.

Warning

Using a value after it has been moved is a compile-time error.

Auto Borrow Inference

If every call moved its arguments, you would spend half your day cloning. Auto borrow inference is the way out. When the compiler can prove that a function merely reads a value, and that the reference cannot outlive the owner, it passes a borrow rather than a move, and never mentions it.

LOON
[fn total [xs]
  [fold xs 0 +]]

[let nums #[1 2 3]]
[println [total nums]]
[println [total nums]]  ; works, compiler inferred a borrow

Nothing was annotated. The compiler saw that total only reads xs and borrowed instead of moving, which is why you can call total twice on the same value. Here is Loon's central ergonomic win over Rust: the safety model is identical, but more of the bookkeeping falls to the compiler.

Copy Types

Not everything moves. Primitive types are small and cheap, so Loon copies them outright. Assign an integer to a new binding and both stay valid, each holding its own independent copy.

  • Int and Float, numeric types
  • Bool, true and false
  • Char, single characters
  • String, strings are immutable and reference-counted
LOON
[let x 42]
[let y x]
[println x]  ; still valid, Int is Copy

Strings are the interesting case. Being reference-counted, "copying" one amounts to bumping a counter, which is both cheap and safe, and which lets you pass strings around freely without a thought for ownership.

Mutation

Loon values are immutable by default. When you need to change one, you opt in by hand with mut, which makes every mutation visible and deliberate.

LOON
[let mut items #[1 2 3]]
[set! items [push! items 4]]
[set! items [map items [fn [n] [* n 2]]]]
[println items]  ; #[2 4 6 8]

The ! on push! and set! is a standing signal that these functions modify their argument. Only the owner of a mutable binding may mutate it, and borrows are always immutable, so data can never be changed out from under you through a borrowed reference.

Tip

Only the owner of a mutable binding can mutate it. Borrows are always immutable.

Common Errors

The ownership system turns what would be subtle runtime bugs into compile errors you read on the spot. Two of them greet nearly everyone starting out.

Use After Move

The classic ownership error. You move a value, then reach for the original binding as if it were still there. The fix is to clone the value when you truly need it in two places, or to reshape the code so that only one place ever does.

LOON
[let a #[1 2 3]]
[let b a]
[println a]  ; ERROR: value moved to b

Returning References to Locals

You cannot return a reference to a local variable, because the variable is dropped the moment the function returns. The reference would point at nothing.

LOON
[fn bad []
  [let x 42]
  &x]  ; ERROR: x dropped at end of fn

The remedy: return the value itself, not a reference to it. The compiler is good at optimizing away copies that earn nothing, so returning by value is at once safe and fast.

Tip

Return the value itself. The compiler will optimize away unnecessary copies.