Chapter 13 — Memory, the Quiet Way
Here’s a question you may not have thought to ask, because Ingle has carefully arranged for you not to need to: when does all this memory get cleaned up?
The usual answers are two: call free yourself and hope you got it right, or run a garbage
collector that periodically reclaims what’s unreachable, at the cost of some runtime overhead and
the occasional pause. Ingle does neither. It has no garbage collector, and you never write
free. Memory is reclaimed deterministically — at exactly known moments — and the ownership
rules from the last chapter are what make that possible.
The discipline is simple to state:
-
Structs and arrays are unique owners, and they’re freed at the end of the scope that owns them. When a
let/varholding a struct or array goes out of scope (hits the closing brace), its memory is released right then, along with anything it owns inside it. Because the move checker guarantees there’s exactly one owner, this is a plain, immediate free with no bookkeeping and no guessing. A value that was moved out isn’t freed here — its new owner will do it. A value that’s returned escapes to the caller, who now owns it. -
Strings and enums are shared and reference-counted. These are immutable, so several names can safely point at the same one. Each holds a counted reference; aliasing bumps the count, going out of scope drops it, and the value is freed when the last reference goes. Freeing a container releases what it holds — an array of strings frees its strings on the way out.
fn main() -> string {
let p = Point { x: 1, y: 2 } // a struct...
let s = "hello" // ...and a string
let t = s // t shares s (count is now 2)
return t // t escapes to the caller; at this brace, p is
} // freed and s drops its reference (one left)
The reclamation is eager — it happens at the brace, not when the program exits — so a long-running program that loops forever (say, concurrent workers pulling jobs off a channel) doesn’t slowly accumulate garbage. And here’s the property that makes reference-counting complete rather than leaky: because the only mutable things (structs and arrays) are uniquely owned, and everything shared is immutable, no reference cycle can ever form. Cycles are the one thing naive reference-counting can’t collect; Ingle’s value model makes them impossible to build in the first place. There is nothing a tracing garbage collector would catch that this misses.
When you do want sharing: rc struct and std/slotmap
Single ownership is the default precisely because it makes the two rules above hold. But some shapes — a config read from everywhere, a node with several parents, a graph — genuinely want more than one owner, and Ingle gives you two blessed tools rather than leaving you to alias by hand.
An rc struct is a shared, immutable, reference-counted struct. Prefix a struct with rc, and
assigning it is an incref rather than a move or a deep copy, so many names can point at one heap
value — safe precisely because an rc value can’t be mutated:
rc struct Config {
host: string
port: int
}
fn main() -> int {
let a = Config { host: "localhost", port: 80 }
let b = a // a second owner — an incref, not a copy
let c = a // a third; a, b, c all name one shared value
return a.port + b.port + c.port // => 240
}
For graph-shaped data where you’d otherwise hold pointers, std/slotmap is a generational
arena: the store owns the values and you hold small copyable Handles (a slot + a generation). Removing
a value bumps its slot’s generation, so every stale handle reads back as None instead of a dangling
value — the use-after-free of a recycled slot turned into a safe Option by construction. Between
them, the awkward shapes have a home, and the no-cycles guarantee above still holds. (A generic
rc struct Box<T> isn’t supported yet — a v1 restriction.)
Fireside trivia. The “billion-dollar” sibling to Hoare’s null mistake is arguably the use-after-free, the bug where you keep using memory after it’s been handed back. Whole categories of security exploit are built on it. Ingle’s one set of rules — single ownership, immutable sharing — covers three things at once: memory safety, deterministic cleanup, and data-race freedom (next chapter).