Skip to content
📖 The Auto Programming LanguageBook contents

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.

Stack vs heap: where values live

This prints:

5
hello
[1, 2, 3]

Three variables, three different storage strategies:

  • x holds the integer 5. Integers have a fixed size (8 bytes for i64), so they live on the stack. No heap allocation is needed.

  • s holds 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.

  • list holds the array [1, 2, 3]. Arrays are dynamically sized and heap-allocated. The variable list itself lives on the stack and holds a pointer to the heap-allocated data.

How this maps across languages:

  • Rust behaves identically to Auto. i64 is stack-only, String uses SSO, and Vec<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 long is 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.

Implicit move -- ownership transfers to the function

This prints:

Hello

When 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 String to 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) inside take_ownership to simulate the ownership transfer. If you tried to use greeting after 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.

Using `.clone()` to keep the original value

This prints:

Hello
Hello

greeting.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.

Escape analysis -- the compiler decides stack or heap

This prints:

1
2

make_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.

AutoFree -- automatic cleanup without garbage collection

This prints:

Buffers allocated and auto-freed

b1 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, its Drop implementation 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 because Buffer is 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.

Zero-GC in practice -- no garbage collection pauses

This prints:

Done processing 1M items

This 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:

ConceptMechanismRuntime cost
OwnershipEach value has one ownerNone (compile-time)
Move semanticsOwnership transfers on assignment/function callNone (compile-time)
AutoFreeCleanup at end of scopeNone (deterministic)

Compared to other approaches:

LanguageMemory managementSafetyPredictability
CManual malloc/freeUnsafeHigh
Java/Python/TSGarbage collectionSafeLow (GC pauses)
Rust/AutoOwnership + AutoFreeSafeHigh

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.

Released under the MIT License.