Skip to content
📖 The Auto Programming LanguageBook contents

References & Pointers ​

In most programming languages, you have two ways to work with data: you can own it (a copy lives in your variable) or you can borrow it (your variable holds an address that points to someone else's data). Auto makes this distinction explicit with a layered reference system that draws from Rust's borrowing rules and C's pointer model, but presents them in a cleaner, more intuitive syntax.

In this chapter, we'll cover:

  • What Are References? -- Borrowing vs owning
  • view T -- Read-only references (like &T in Rust)
  • mut T -- Mutable references (like &mut T in Rust)
  • The Reference Rules -- One mutable OR many immutable
  • *T -- Raw pointers for system-level access
  • &self in Methods -- Self-references in method extensions
  • Lifetimes -- How long references live

What Are References? ​

Every variable in Auto owns its value. When you assign var x = 5, the value 5 belongs to x. When x goes out of scope, the value is cleaned up. This is the ownership model you've been using since Chapter 1.

But sometimes you don't want to transfer or copy a value -- you just want to look at it or modify it in place. That's what references are for. A reference is a handle that lets you access data without taking ownership of it.

Auto provides three levels of reference access, each with increasing power and corresponding restrictions:

ReferenceAutoRustC++What it does
Read-onlyview T&Tconst T&Read but not modify
Mutablemut T&mut TT&Read and modify
Raw pointer*T*T / *mut TT*Unrestricted system access

This layered design means you can pick the right tool for the job. Most code uses view T and mut T, which are safe and checked at compile time. Only low-level systems code needs *T, and it requires explicit opt-in.

view T -- Read-Only References ​

A view T reference lets a function read data without taking ownership. The original variable remains valid and usable after the function call. This is Auto's equivalent of Rust's &T or C++'s const T&.

Using `view T` to read a string without consuming it

This prints:

13
Hello, Auto!

The function print_length takes s view String, meaning it receives a read-only view of the string. It can call .len() to read the string's length, but it cannot modify the string's contents. After the call returns, name is still valid in main -- view does not consume the value.

Compare this to what would happen without view:

auto
fn consume(s String) {
    // s is now owned here
}

fn main() {
    var name = "Hello"
    consume(name)
    // print(name)  // ERROR: name was moved
}

Without view, Auto's implicit move semantics would transfer ownership of name into consume, and name would no longer be usable afterward. The view keyword explicitly opts out of this behavior.

How view T maps across languages:

  • Rust generates a &T reference parameter. The caller passes &name to borrow the value.
  • Python passes the string directly -- Python strings are immutable, so const semantics are automatic.
  • C generates a const char* parameter, enforcing read-only access at the type level.
  • TypeScript passes the string directly -- TypeScript strings are immutable by default.

mut T -- Mutable References ​

A mut T reference lets a function read and modify data in place. Changes made through the reference are visible to the original owner. This is Auto's equivalent of Rust's &mut T or C's pointer parameter.

Using `mut T` to modify a value in place

This prints:

6

The function increment takes n mut int, meaning it receives a mutable reference to an integer. Inside the function, n = n + 1 modifies the original value. Back in main, x now holds 6 because the change was made in place.

Key observations:

  • The variable x in main must be declared with var (mutable). You cannot pass an immutable let variable as a mut T reference.
  • The compiler ensures that x is not used while increment holds the mutable reference. This prevents data races and dangling references at compile time.

How mut T maps across languages:

  • Rust generates a &mut T parameter. The caller passes &mut x. The function body uses *n to dereference and assign.
  • Python has no native mutable references for primitives. The transpiler wraps the value in a list ([5]) to simulate pass-by-reference semantics.
  • C generates a pointer parameter (long long* n). The caller passes &x. This is the closest analog in C.
  • TypeScript wraps the value in an object ({ value: 5 }) to simulate pass-by-reference, since TypeScript primitives are passed by value.

The Reference Rules ​

Auto enforces the same borrowing rules as Rust at compile time. These rules prevent data races and undefined behavior without any runtime cost:

  1. You can have any number of view T (immutable) references to a value -- multiple readers are always safe.

  2. You can have at most one mut T (mutable) reference to a value -- only one writer at a time.

  3. Immutable and mutable references must not coexist -- you cannot read and write the same value simultaneously.

The reference rules: one mutable reference at a time

This prints:

5
5

The second line is commented out because it would be a compile-time error in Auto. The compiler rejects var r2 mut x = x because r1 already holds a mutable reference to x. Having two mutable references to the same data would create undefined behavior -- both could write at the same time, or one could invalidate the other.

Note that print(x) is allowed alongside r1. In Auto, reading the original value through a non-reference path while a mutable reference exists is allowed in this simple context, because the compiler can verify that x is not modified through any other path during the read.

These rules are strictly enforced by the Auto compiler. In contrast:

  • Python does not enforce borrowing rules. Multiple variables can reference and mutate the same object freely. This is flexible but can lead to subtle bugs.
  • C does not enforce borrowing rules. Multiple pointers to the same data can exist, and nothing prevents simultaneous reads and writes. This is a common source of memory safety bugs in C programs.
  • TypeScript does not enforce borrowing rules for the same reasons as Python.

Why These Rules Matter ​

The "one mutable OR many immutable" rule is the key to Auto's memory safety guarantees. It eliminates data races at compile time (no two threads can simultaneously mutate the same data through Auto's references) and prevents iterator invalidation (you can't modify a collection while iterating over it). The compiler does all the checking -- there is zero runtime cost.

*T -- Raw Pointers ​

For systems programming -- writing operating systems, device drivers, or FFI bindings to C libraries -- Auto provides raw pointers with the *T syntax. Raw pointers bypass the borrowing rules entirely. They can be null, they can dangle, and they can alias. With great power comes great responsibility.

Using `*T` raw pointers for direct memory access

This prints:

42
100

var p *int = &x creates a raw pointer p that holds the address of x. The *p syntax dereferences the pointer to read or write the value it points to. After *p = 100, the original x is also 100 because p points directly to x's memory location.

Raw pointers in Auto work identically to C pointers:

  • &x takes the address of x
  • *p dereferences the pointer
  • *p = value writes through the pointer

In Rust, raw pointer operations require unsafe blocks. The Auto compiler generates the appropriate safety wrappers. Raw pointers are intended for advanced use cases:

  • Interfacing with C libraries through FFI
  • Implementing custom data structures that need pointer arithmetic
  • Writing operating system kernels or embedded firmware

For application-level code, prefer view T and mut T -- they give you the same performance with compile-time safety guarantees.

&self in Methods ​

When you define methods on a type using ext, you often need the method to operate on the instance it's called on. Auto uses &self in the method signature to indicate that the method borrows the instance. For methods that modify the instance, you can use &mut self.

Using `&self` in method extensions

This prints:

3

The Counter type has two methods defined in its ext block:

  • increment(&self) modifies the counter. The &self parameter borrows the instance, allowing self.count to be updated. In Rust, this maps to &mut self because the method mutates data.
  • get(&self) int reads the counter's value. The &self parameter borrows the instance immutably. In Rust, this maps to &self.

The &self syntax in Auto method signatures maps to the calling convention you'd expect from object-oriented languages:

  • Rust generates &self or &mut self depending on whether the method modifies the instance.
  • Python uses the standard self parameter -- Python's class system is reference-based by default.
  • C generates a function with an explicit pointer parameter (Counter* self or const Counter* self). The -> operator accesses fields through the pointer.
  • TypeScript generates a standard class method with this -- TypeScript objects are reference types by default.

Lifetimes ​

References in Auto do not live forever. Every reference has a lifetime -- the span of code during which the reference is valid. The compiler uses lifetimes to ensure that no reference outlives the data it points to.

In most cases, Auto can infer lifetimes automatically. You don't need to write lifetime annotations for simple cases. The compiler analyzes the code and figures out how long each reference needs to be valid.

References with lifetimes -- the compiler infers validity

This prints:

world!

The function longest takes two view String references and returns a view String reference pointing to whichever string is longer. The return value is a reference to one of the inputs -- it doesn't create a new string.

The compiler needs to verify that the returned reference is valid. In this case, the returned reference points to either s1 or s2, both of which are still alive when result is used in main. So the code is valid.

In Rust, this function requires explicit lifetime annotations ('a). The annotation tells the Rust compiler that the returned reference lives as long as the shorter of the two input lifetimes. Auto's compiler performs the same analysis but doesn't require you to write the annotations in simple cases like this.

The compiler would reject code that tries to return a reference to data that is about to be destroyed:

auto
fn bad() view String {
    var s = "temporary"
    s  // ERROR: s is about to go out of scope
}

This would fail because s is a local variable. When bad returns, s is destroyed, and the returned reference would point to invalid memory. The compiler catches this at compile time.

How lifetimes map across languages:

  • Rust requires explicit lifetime annotations ('a) in function signatures when the compiler cannot infer them. This is the most explicit system.
  • Python uses reference counting. Objects are freed when their reference count drops to zero, so dangling references are not possible in normal code.
  • C has no lifetime checking. Returning a pointer to a local variable is undefined behavior -- a common source of crashes and security vulnerabilities.
  • TypeScript relies on garbage collection. Like Python, dangling references are not a concern because objects are collected when no longer reachable.

Summary ​

Auto's reference system provides three levels of access, from safe and ergonomic to powerful and low-level:

ConceptAutoRustCSafety
Read-only referenceview T&Tconst T*Compile-time checked
Mutable referencemut T&mut TT*Compile-time checked
Raw pointer*T*mut TT*Programmer's responsibility
Method self-borrow&self&self / &mut selfType* selfCompile-time checked
LifetimeInferred'a annotationsNoneCompile-time checked

Key design principles:

  1. Safe by default: view T and mut T are checked at compile time. The compiler prevents data races, dangling references, and use-after-free bugs without any runtime cost.

  2. Explicit opt-in for power: Raw pointers (*T) are available when you need them for FFI or systems programming, but you must explicitly choose to use them. The compiler won't let you accidentally bypass the safety rules.

  3. Minimal annotation burden: In most cases, Auto infers lifetimes automatically. You only need to think about lifetimes when returning references from functions -- and even then, the compiler guides you with clear error messages.

  4. Zero-cost abstraction: References have no runtime overhead. A view T is the same size as a pointer, and all checking happens at compile time. The generated code is as efficient as hand-written C.

In the next chapter, we'll explore Auto's collections -- List, Map, and Set -- which combine ownership and references to provide safe, efficient dynamic data structures.

Released under the MIT License.