Chapter 12 — Ownership Without Tears

Ownership has a reputation for being where the fun stops and the fighting starts. The good news is that for ordinary, everyday code, you write nothing special at all. No annotations, no symbols, no lifetimes. You’ve written a dozen working programs in this book already and ownership never once got in your way.

The rules below are what’s quietly keeping you safe underneath. You mostly need them for the day the compiler stops you — and when it does, it explains itself.

Three ways to pass a value

When a function takes a parameter, there are three relationships it can have with the value:

  • Borrow (the default). Plain fn f(p: Point) borrows — it can look but the caller keeps ownership. This is what you want almost always.
  • Mutable borrow: mut. fn f(mut p: Point) borrows and may change the value; the changes are visible to the caller afterward.
  • Move: move. fn f(move p: Point) takes ownership. The caller hands the value over and can’t use it anymore.

The qualifier goes before the binding, and it reads the same for self and for named parameters: mut self, move items. There are no & or &mut symbols, and no lifetime annotations — Ingle infers all of that. The safe direction (borrowing) is the one you get for free; the consequential direction (giving a value away with move) is the one you have to type out. Hard to do the dangerous thing by accident.

struct Point { x: int  y: int }

fn bump(mut p: Point) -> int {   // mutable borrow: the caller sees the change
    p.x = p.x + 1
    return p.x
}

fn main() -> int {
    var p = Point { x: 1, y: 2 } // var = a mutable place
    p.x = 10                     // mutate a field through the var
    return bump(p)               // => 11
}

Remember from Chapter 7: you can only mutate a field through a mutable place — a var binding or a mut/move parameter. Through a let or a plain borrow, the value is read-only.

Move semantics: one owner at a time

Structs and arrays are owned values, with exactly one owner at a time. The moment you hand ownership somewhere else — assign it to another binding, pass it to a move parameter, return it, or store it in a field — the original name is “moved out” and can’t be used again. Try, and the compiler stops you:

fn main() -> int {
    let a = [1, 2, 3]
    let b = a          // ownership moves from a to b
    return b[0] + a[0] // using 'a' here is the mistake
}
error: use of 'a' after it was moved
help: a move transfers ownership; pass it without `move` to borrow it instead, or make a copy before the move
note: value moved here

Three lines: what you did wrong, how to fix it, and where the value went. That’s the compiler-as-teacher principle in action. The fix here is to borrow instead of moving, or to make a copy before the move — exactly as it says.

This is why borrowing is the default for parameters. A function that just reads an array should take it plain, so the caller keeps it:

fn total(xs: [int]) -> int {       // borrows xs
    var s = 0
    for x in xs { s = s + x }
    return s
}

fn main() -> int {
    let a = [1, 2, 3]
    let t = total(a)               // a is only borrowed...
    return t + a.len()             // ...so a is still ours here. => 9
}
=> 9

Returning an owned value: use move

A plain parameter is borrowed, and you can’t return a borrowed value — it would outlive the call and dangle. Ingle catches that too:

struct User { name: string  age: int }

fn pick(u: User) -> User { return u }   // error: cannot return a borrowed value
error: cannot return a borrowed value — it would escape the function; take the parameter as 'move'

Do as it says — take it move — and all is well, because now the function genuinely owns the value it’s handing back:

fn pick(move u: User) -> User { return u }   // owns u, may return it. Fine.

The shortcut that means you’ll rarely fight this

Here’s a detail worth knowing: small all-scalar structs held in a let are quietly copied, not moved. A Point { x: int, y: int } bound with let can be read through two names at once — Ingle duplicates it for you, because it’s tiny and made entirely of numbers. The move rules above kick in fully for arrays, for structs that contain a string or an array, and for structs held in a var.

The safe way to hold all this in your head is one sentence: write as if every struct and array moves when you hand it off. If you do, you’ll never be surprised by an error — at worst you’ll occasionally be allowed something the strict model would forbid, which never hurt anyone. Lead with borrowing, reach for move when you mean to give a value away, and the compiler will tell you on the rare occasion you’ve got it wrong.

The rest of the safety net, briefly

The same analysis quietly enforces a few more things, all in the name of “no value is ever read after it’s gone, and nothing mutable is aliased”:

  • No aliased mutation. Because a move consumes the source, you can’t end up with two names for one mutable value and change it through both.
  • Mutable XOR shared. In a single call you can’t pass the same value as a mut/move argument and also borrow it elsewhere in the same call.
  • Branches merge sensibly. Moving a value in one arm of an if/match is fine; the compiler tracks each path. Moving a value inside a loop body is rejected, because the next turn would move it again.
  • Scalars, strings, and enums copy freely. None of this applies to them — only to the unique-owner aggregates (structs and arrays).

Fireside trivia. Earlier in Ingle’s development, a generic function that aliased a struct argument managed to free the same memory twice at runtime — precisely the class of bug an ownership system exists to abolish, committed by the ownership system’s own compiler. It was caught, written up, and fixed by making type parameters move-by-default (the move/Copy rules you met last chapter). The lesson stuck: the rules are load-bearing, and the language holds itself to them too.