Skip to content
📖 Auto vs RustBook contents

Understanding Ownership ​

Ownership is a set of rules that govern how an Auto program manages memory. All programs have to manage the way they use a computer's memory while running. Some languages have garbage collection that regularly looks for no-longer-used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Auto uses a third approach: Memory is managed through a system of ownership with a set of rules that the compiler checks. If any of the rules are violated, the program won't compile. None of the features of ownership will slow down your program while it's running.

Auto's ownership model is inspired by Rust and shares the same fundamental principles. The key difference is that Auto reduces the need for explicit annotations: where Rust requires lifetime markers and explicit &/&mut syntax, Auto uses property-based keywords like .view and .mut to express borrowing intent more clearly.

When you understand ownership, you'll have a solid foundation for understanding the features that make Auto unique. In this chapter, you'll learn ownership by working through some examples that focus on a very common data structure: strings.

The Stack and the Heap ​

Many programming languages don't require you to think about the stack and the heap very often. But in a systems programming language like Auto, whether a value is on the stack or the heap affects how the language behaves and why you have to make certain decisions.

Both the stack and the heap are parts of memory available to your code to use at runtime, but they are structured in different ways. The stack stores values in the order it gets them and removes the values in the opposite order. This is referred to as last in, first out (LIFO). All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.

The heap is less organized: When you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location.

Pushing to the stack is faster than allocating on the heap because the allocator never has to search for a place to store new data; that location is always at the top of the stack. Accessing data in the heap is generally slower than accessing data on the stack because you have to follow a pointer to get there.

Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so that you don't run out of space are all problems that ownership addresses.

What Is Ownership? ​

Ownership Rules ​

First, let's take a look at the ownership rules. Keep these rules in mind as we work through the examples that illustrate them:

  • Each value in Auto has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

These rules are identical to Rust's ownership rules. Auto's borrow checker enforces them at compile time, ensuring memory safety without garbage collection.

Variable Scope ​

As a first example of ownership, we'll look at the scope of some variables. A scope is the range within a program for which an item is valid.

A variable and the scope in which it is valid

In other words, there are two important points in time here:

  • When s comes into scope, it is valid.
  • It remains valid until it goes out of scope.

The String Type ​

To illustrate the rules of ownership, we need a data type that is more complex than those we covered in Chapter 3. We want to look at data that is stored on the heap and explore how Auto knows when to clean up that data.

We've already seen string literals, where a string value is hardcoded into our program. String literals are convenient, but they aren't suitable for every situation because they're immutable. Auto has the String type to manage data allocated on the heap. You can create a String from a string literal like so:

auto
let s String = String.from("hello")
rust
let s = String::from("hello");

In Auto, we use String.from() (dot syntax) instead of Rust's String::from() (double colon syntax). Both create a heap-allocated string.

This kind of string can be mutated:

auto
var s String = String.from("hello")
s.append(", world!")   // append a string
print(s)               // prints "hello, world!"
rust
let mut s = String::from("hello");
s.push_str(", world!"); // push a string
println!("{}", s);      // prints "hello, world!"

Memory and Allocation ​

In the case of a string literal, the text is hardcoded directly into the final executable. But with the String type, to support a mutable, growable piece of text, we need to allocate memory on the heap at runtime. This means:

  • The memory must be requested from the memory allocator at runtime.
  • We need a way of returning this memory to the allocator when we're done.

The first part is done when we call String.from(). The second part is where ownership shines: in Auto, the memory is automatically returned once the variable that owns it goes out of scope. Auto calls a cleanup function (like Rust's drop) automatically at the closing curly bracket.

Creating a `String` and its automatic cleanup

When s goes out of scope, Auto automatically frees the heap memory. No manual deallocation needed.

Variables and Data Interacting with Move ​

Multiple variables can interact with the same data in different ways. Let's look at what happens with String values:

Move semantics — `s1` is invalidated after assignment to `s2`

In Auto, the .move property explicitly signals that ownership is being transferred. The original variable (s1) is invalidated after the move. This prevents double free errors where both variables would try to free the same heap memory.

Note: In many cases, Auto can infer when a move is intended and you can write let s2 = s1 without the explicit .move. However, using .move makes the ownership transfer clear and intentional.

Variables and Data Interacting with Clone ​

If we do want to deeply copy the heap data of a String, we can use the .clone() method:

Using `clone` for deep copy

When you see a call to .clone(), you know that some code is being executed that may be expensive — it creates a full copy of the heap data.

Stack-Only Data: Copy ​

Types with a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. There's no reason to prevent the original variable from being valid after assignment:

Stack-only data is copied, not moved

Both x and y are valid because integers implement the Copy trait. The same applies to:

  • All integer types
  • The Boolean type (bool)
  • All floating-point types
  • The character type (char)
  • Tuples, if they only contain types that also implement Copy

Ownership and Functions ​

Passing a variable to a function will move or copy it, just like assignment:

Functions with ownership and scope

Return Values and Scope ​

Returning values can also transfer ownership:

Transferring ownership of return values

Taking ownership and then returning ownership with every function is tedious. What if we want to let a function use a value but not take ownership? This is where references come in.

References and Borrowing ​

The issue with the code above is that we have to return the String to the calling function so that we can still use it after the call. Instead, we can provide a reference to the String value. A reference is like a pointer in that it's an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type.

In Auto, you create a reference using the .view property (for immutable borrows) or the .mut property (for mutable borrows):

AutoRustMeaning
s.view&sImmutable reference (borrow)
s.mut&mut sMutable reference (borrow)
s.movesOwnership transfer (move)

Immutable Borrows with .view ​

Here is how you would define and use a calculate_length function that borrows a String instead of taking ownership:

Using `.view` to borrow a value without taking ownership

The .view property creates an immutable reference. Because the reference does not own the data, s1 is still valid after the function call. The action of creating a reference is called borrowing — just like in real life, if a person owns something, you can borrow it from them. When you're done, you give it back. You don't own it.

Mutable Borrows with .mut ​

We can also borrow a value mutably using the .mut property:

Using `.mut` to borrow and modify a value

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. This prevents data races at compile time.

Borrowing Rules ​

Auto enforces the same borrowing rules as Rust:

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid.
Multiple immutable borrows are allowed

This is fine because multiple immutable references don't affect each other — no one who is just reading the data has the ability to affect anyone else's reading of the data.

But the following would not compile:

auto,ignore,does_not_compile
fn main() {
    var s String = String.from("hello")
    let r1 = s.mut
    let r2 = s.mut   // ERROR: cannot borrow `s` as mutable more than once
    print(f"r1 = $r1, r2 = $r2")
}
rust,ignore,does_not_compile
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s; // ERROR: cannot borrow `s` as mutable more than once
    println!("r1 = {}, r2 = {}", r1, r2);
}

Dangling References ​

Auto's compiler guarantees that references will never be dangling — that is, a reference will never point to memory that has been freed. If you try to create a dangling reference, the compiler will catch it at compile time.

Auto vs Rust: Explicit Lifetime Annotations ​

One key difference between Auto and Rust is that Auto does not require explicit lifetime annotations. In Rust, functions that return references sometimes need lifetime parameters (e.g., fn first_word<'a>(s: &'a str) -> &'a str). Auto's compiler performs lifetime inference automatically, similar to how it infers types. This eliminates a significant source of complexity while maintaining the same safety guarantees.

The Slice Type ​

Slices let you reference a contiguous sequence of elements in a collection. A slice is a kind of reference, so it does not have ownership.

String Slices ​

A string slice is a reference to a portion of a String:

Finding the first word using an index (problematic)

The problem with returning an index is that the index becomes stale if the string changes. Slices solve this problem by tying the reference to the original data.

Finding the first word using a string slice

Auto uses the same range syntax as Rust for slices: s[0..5], s[3..], s[..5], s[..].

String Literals as Slices ​

String literals are stored inside the binary. Their type is effectively a string slice — they're immutable references to pre-allocated data.

Array Slices ​

Slices work for arrays too, not just strings:

Array slices

This slice has the type []int in Auto (equivalent to &[i32] in Rust). It works the same way as string slices, by storing a reference to the first element and a length.

Summary ​

The concepts of ownership, borrowing, and slices ensure memory safety in Auto programs at compile time. Auto gives you the same control over memory as Rust, but with a more streamlined syntax:

ConceptAutoRust
Immutable borrows.view&s
Mutable borrows.mut&mut s
Move ownerships.moves
Deep copys.clone()s.clone()
Lifetime annotations(inferred)'a, 'b
String slices[0..5]&s[0..5]

The owner of data automatically cleans up that data when it goes out of scope, so you don't have to write and debug extra code to get memory safety. Ownership affects how lots of other parts of Auto work, so we'll talk about these concepts further throughout the rest of the book.

Let's move on to Chapter 5 and look at grouping pieces of data together using Auto's type keyword.

Released under the MIT License.