Memory & Ownership
Every program must manage memory -- allocating space for data when it's needed and releasing that space when it's no longer in use. Different languages take radically different approaches to this problem. C gives you malloc and free and trusts you not to make mistakes. Java and Python use garbage collection (GC), running a background process that periodically scans for unused objects. Rust and Auto use a third approach: ownership, a set of compile-time rules that guarantee memory safety with zero runtime cost.
In this chapter, we'll cover:
- Where Values Live -- Stack vs heap allocation
- Implicit Move Semantics -- Ownership transfer without ceremony
- Cloning -- Explicit deep copy with
.clone() - Escape Analysis -- The compiler decides stack or heap
- AutoFree -- Automatic cleanup without garbage collection
- Zero-GC in Practice -- Performance implications
Where Values Live
Memory in an Auto program is divided into two regions: the stack and the heap. Understanding this distinction is essential for understanding ownership, because the rules are different for each region.
The stack is a last-in, first-out (LIFO) data structure. Local variables are pushed onto the stack when a function is called and popped off when the function returns. Stack allocation is fast -- the stack pointer just moves -- and deallocation is instantaneous. Every value on the stack has a known, fixed size at compile time.
The heap is a less structured region. When you need memory whose size is not known at compile time (a dynamically sized array, a growable string, a tree of nodes), you request it from the heap allocator. The allocator finds an empty spot, marks it as in use, and returns a pointer. Heap allocation is slower, and accessing heap data requires following a pointer.
This prints:
5
hello
[1, 2, 3]Three variables, three different storage strategies:
xholds the integer5. Integers have a fixed size (8 bytes fori64), so they live on the stack. No heap allocation is needed.sholds the string"hello". Short strings in Auto use a small string optimization (SSO) -- the string data is stored inline on the stack when it fits, avoiding a heap allocation. Longer strings are automatically moved to the heap.listholds the array[1, 2, 3]. Arrays are dynamically sized and heap-allocated. The variablelistitself lives on the stack and holds a pointer to the heap-allocated data.
How this maps across languages:
- Rust behaves identically to Auto.
i64is stack-only,Stringuses SSO, andVec<i64>is heap-allocated. - Python stores all objects on the heap and uses references. The distinction is mostly invisible to the programmer.
- C gives you full control.
long longis on the stack,const char*points to read-only data in the binary, and arrays can be on the stack or heap depending on how you allocate them. - TypeScript relies on the V8 engine's memory management. All values are conceptually on the heap, with the engine optimizing where possible.
Implicit Move Semantics
In Auto, every value has exactly one owner -- the variable that is responsible for cleaning it up when it's no longer needed. When you pass a value to a function or assign it to a new variable, Auto moves the ownership by default. The original variable becomes invalid.
This prints:
HelloWhen take_ownership(greeting) is called, Auto transfers ownership of the string from greeting into the function's parameter s. After the call, greeting is no longer valid. Attempting to use it (the commented-out line) would produce a compile-time error.
This is Auto's equivalent of Rust's move semantics. The key insight is that there is always exactly one owner. When ownership is transferred, the old owner is invalidated. This prevents double free bugs -- the situation where two variables both try to free the same heap memory.
How this maps across languages:
- Rust enforces the same move semantics. Passing a
Stringto a function moves it, and the original variable is invalidated. - Python does not have move semantics. Strings are immutable objects managed by reference counting. The original variable remains valid.
- C has no built-in move semantics. The transpiler generates explicit
free(s)insidetake_ownershipto simulate the ownership transfer. If you tried to usegreetingafter the call, you'd get a use-after-free bug -- a runtime crash. - TypeScript does not enforce move semantics. The comment is purely documentary.
Cloning
If you need to use a value after passing it to a function, you have two options: borrow it with view T (covered in Chapter 11), or clone it with .clone(). Cloning creates a deep copy of the value -- a completely independent copy with its own heap memory.
This prints:
Hello
Hellogreeting.clone() creates a full copy of the string. The copy is passed to take_ownership, which takes ownership of it and frees it when done. The original greeting remains valid because it was never moved -- only the clone was.
Cloning is explicit in Auto (and Rust) because it has a cost. When you see .clone() in code, you know that a potentially expensive memory allocation and copy is happening. This makes performance-critical code easier to audit: just search for .clone() calls and see if they're necessary.
How this maps across languages:
- Rust uses the same
.clone()method. The convention is identical -- cloning is always explicit. - Python strings are immutable, so "cloning" is effectively free (just copying a reference). The transpiler uses
[:]slicing to make the intent clear. - C uses
strdup()to create a copy. The caller must remember to free both the original and the clone. This is a common source of memory leaks. - TypeScript strings are immutable primitives, so
.slice()creates a copy (though the engine may optimize it away).
Escape Analysis
In many cases, Auto can determine at compile time whether a value needs to live on the heap or can safely stay on the stack. This optimization is called escape analysis -- the compiler checks whether a value "escapes" the scope where it was created.
If a value is created inside a function and returned to the caller, it escapes. If it's created and used entirely within the function, it doesn't escape and can stay on the stack.
This prints:
1
2make_point creates a Point struct and returns it. The Point has a fixed size (two f64 fields, 16 bytes total). The compiler can see that the value doesn't require heap allocation -- it's returned by value and stored in p on the stack.
In Rust and Auto, the compiler performs escape analysis to determine the optimal storage location:
- Values returned from functions: If the value has a fixed size, it can be returned by value (no heap allocation needed). This is the common case for structs with known sizes.
- Values that outlive the current scope: If a value is stored in a collection, passed to a thread, or captured in a closure, it may need heap allocation. The compiler handles this automatically.
- Values with dynamic size: Lists, strings, and hash maps always require heap allocation because their size is not known at compile time.
The C code shows the same pattern -- make_point returns a Point struct by value, and the compiler optimizes away any unnecessary copying using return value optimization (RVO).
Why Escape Analysis Matters
Without escape analysis, every value returned from a function would need to be heap-allocated, and every returned value would require a garbage collector or manual
free. With escape analysis, the compiler can keep values on the stack whenever possible, reducing memory pressure and improving cache performance.
AutoFree
When an owned value goes out of scope, Auto automatically frees its memory. This is called AutoFree -- the compiler inserts cleanup code at every point where a variable's lifetime ends. You never need to call free(), delete, or dispose(). The compiler handles it.
This prints:
Buffers allocated and auto-freedb1 and b2 are stack-allocated structs. When main returns, the compiler automatically inserts cleanup code. For simple structs like Buffer that contain only stack types, cleanup is a no-op -- the stack pointer simply moves back. For types with heap data (like String or List), the compiler inserts free() calls to release the heap memory.
This is fundamentally different from garbage collection:
Garbage collection runs periodically in the background, scanning for objects that are no longer reachable. This introduces pause times -- moments when the program stops to collect garbage. The pauses are unpredictable in duration and timing.
AutoFree runs deterministically at the end of each scope. Memory is freed the instant the variable goes out of scope. There are no pause times, no background threads, and no runtime overhead.
How this maps across languages:
- Rust uses the same mechanism (called
Drop). When a value goes out of scope, itsDropimplementation is called automatically. - Python uses reference counting with a cycle detector. Objects are freed when their reference count reaches zero, which is similar in spirit but adds runtime overhead.
- C requires manual
free()calls. Forgetting to free causes memory leaks; double-freeing causes crashes. The C code here is simple becauseBufferis stack-allocated and doesn't need explicit cleanup. - TypeScript relies on the V8 engine's garbage collector. The programmer has no control over when memory is freed.
Zero-GC in Practice
The combination of ownership, move semantics, and AutoFree means that Auto programs run without a garbage collector. This has real performance implications: predictable latency, consistent throughput, and lower memory overhead.
This prints:
Done processing 1M itemsThis loop creates and destroys one million arrays. In a garbage-collected language, this would create significant GC pressure -- the collector would need to run periodically to clean up the discarded arrays, causing unpredictable pauses.
In Auto (and Rust), each array is freed the instant the loop iteration ends. The allocation and deallocation are deterministic: the allocator allocates at the start of the iteration and frees at the end. There are no background threads, no stop-the-world pauses, and no memory pressure buildup.
The C code shows the same pattern -- malloc at the start, free at the end. But in C, you must remember every free(). Forgetting one causes a leak. Freeing the same pointer twice causes a crash. Auto's ownership system guarantees that every allocation is freed exactly once.
Real-World Impact
Zero-GC is not just a theoretical advantage. It matters in:
- Game development: frame times must be consistent. A GC pause in the middle of rendering a frame causes a visible stutter.
- Real-time systems: audio processing, video encoding, and embedded controllers cannot tolerate unpredictable pauses.
- Server software: request latency must be predictable. GC pauses in Java servers are a well-known source of tail latency.
- Mobile apps: GC pauses contribute to jank in UI rendering and drain battery by keeping CPU cores active for collection.
Summary
Auto's memory management is built on three pillars:
| Concept | Mechanism | Runtime cost |
|---|---|---|
| Ownership | Each value has one owner | None (compile-time) |
| Move semantics | Ownership transfers on assignment/function call | None (compile-time) |
| AutoFree | Cleanup at end of scope | None (deterministic) |
Compared to other approaches:
| Language | Memory management | Safety | Predictability |
|---|---|---|---|
| C | Manual malloc/free | Unsafe | High |
| Java/Python/TS | Garbage collection | Safe | Low (GC pauses) |
| Rust/Auto | Ownership + AutoFree | Safe | High |
The ownership model gives you the safety of a garbage-collected language with the performance and predictability of manual memory management. The compiler does the bookkeeping -- you just write the code.
In the next chapter, we'll look at Auto's collection types -- List, Map, and Set -- which build on these ownership principles to provide safe, efficient dynamic data structures.