Skip to content
πŸŽ‰ Welcome to the new Aptos Docs! Click here to submit feedback!

References

Move has two types of references: immutable & and mutable &mut. Immutable references are read only, and cannot modify the underlying value (or any of its fields). Mutable references allow for modifications via a write through that reference. Move’s type system enforces an ownership discipline that prevents reference errors.

For more details on the rules of references, see Structs and Resources

Reference Operators

Move provides operators for creating and extending references as well as converting a mutable reference to an immutable one. Here and elsewhere, we use the notation e: T for β€œexpression e has type T”.

SyntaxTypeDescription
&e&T where e: T and T is a non-reference typeCreate an immutable reference to e
&mut e&mut T where e: T and T is a non-reference typeCreate a mutable reference to e.
&e.f&T where e.f: TCreate an immutable reference to field f of struct e.
&mut e.f&mut T where e.f: TCreate a mutable reference to field f of structe.
freeze(e)&T where e: &mut TConvert the mutable reference e into an immutable reference.

The &e.f and &mut e.f operators can be used both to create a new reference into a struct or to extend an existing reference:

script {
  fun example() {
    let s = S { f: 10 };
    let f_ref1: &u64 = &s.f; // works
    let s_ref: &S = &s;
    let f_ref2: &u64 = &s_ref.f; // also works
  }
}

A reference expression with multiple fields works as long as both structs are in the same module:

module 0x42::example {
  struct A { b: B }
  struct B { c : u64 }
 
  fun f(a: &A): &u64 {
    &a.b.c
  }
}

Finally, note that references to references are not allowed:

script {
  fun example() {
    let x = 7;
    let y: &u64 = &x;
    let z: &&u64 = &y; // will not compile
  }
}

Reading and Writing Through References

Both mutable and immutable references can be read to produce a copy of the referenced value.

Only mutable references can be written. A write *x = v discards the value previously stored in x and updates it with v.

Both operations use the C-like * syntax. However, note that a read is an expression, whereas a write is a mutation that must occur on the left hand side of an equals.

SyntaxTypeDescription
*eT where e is &T or &mut TRead the value pointed to by e
*e1 = e2() where e1: &mut T and e2: TUpdate the value in e1 with e2.

In order for a reference to be read, the underlying type must have the copy ability as reading the reference creates a new copy of the value. This rule prevents the copying of resource values:

module 0x42::coin {
  struct Coin {} // Note does not have copy
 
  fun copy_resource_via_ref_bad(c: Coin) {
      let c_ref = &c;
      let counterfeit: Coin = *c_ref; // not allowed!
      pay(c);
      pay(counterfeit);
  }
}

Dually: in order for a reference to be written to, the underlying type must have the drop ability as writing to the reference will discard (or β€œdrop”) the old value. This rule prevents the destruction of resource values:

module 0x42::coin {
  struct Coin {} // Note does not have drop
 
  fun destroy_resource_via_ref_bad(ten_coins: Coin, c: Coin) {
      let ref = &mut ten_coins;
      *ref = c; // not allowed--would destroy 10 coins!
  }
}

freeze inference

A mutable reference can be used in a context where an immutable reference is expected:

script {
  fun example() {
    let x = 7;
    let y: &u64 = &mut x;
  }
}

This works because the under the hood, the compiler inserts freeze instructions where they are needed. Here are a few more examples of freeze inference in action:

module 0x42::example {
  fun takes_immut_returns_immut(x: &u64): &u64 { x }
 
  // freeze inference on return value
  fun takes_mut_returns_immut(x: &mut u64): &u64 { x }
 
  fun expression_examples() {
    let x = 0;
    let y = 0;
    takes_immut_returns_immut(&x); // no inference
    takes_immut_returns_immut(&mut x); // inferred freeze(&mut x)
    takes_mut_returns_immut(&mut x); // no inference
 
    assert!(&x == &mut y, 42); // inferred freeze(&mut y)
  }
 
  fun assignment_examples() {
    let x = 0;
    let y = 0;
    let imm_ref: &u64 = &x;
 
    imm_ref = &x; // no inference
    imm_ref = &mut y; // inferred freeze(&mut y)
  }
}

Subtyping

With this freeze inference, the Move type checker can view &mut T as a subtype of &T. As shown above, this means that anywhere for any expression where a &T value is used, a &mut T value can also be used. This terminology is used in error messages to concisely indicate that a &mut T was needed where a &T was supplied. For example

module 0x42::example {
  fun read_and_assign(store: &mut u64, new_value: &u64) {
    *store = *new_value
  }
 
  fun subtype_examples() {
    let x: &u64 = &0;
    let y: &mut u64 = &mut 1;
 
    x = &mut 1; // valid
    y = &2; // invalid!
 
    read_and_assign(y, x); // valid
    read_and_assign(x, y); // invalid!
  }
}

will yield the following error messages

error:
 
    β”Œβ”€β”€ example.move:12:9 ───
    β”‚
 12 β”‚         y = &2; // invalid!
    β”‚         ^ Invalid assignment to local 'y'
    Β·
 12 β”‚         y = &2; // invalid!
    β”‚             -- The type: '&{integer}'
    Β·
  9 β”‚         let y: &mut u64 = &mut 1;
    β”‚                -------- Is not a subtype of: '&mut u64'
    β”‚
 
error:
 
    β”Œβ”€β”€ example.move:15:9 ───
    β”‚
 15 β”‚         read_and_assign(x, y); // invalid!
    β”‚         ^^^^^^^^^^^^^^^^^^^^^ Invalid call of '0x42::example::read_and_assign'. Invalid argument for parameter 'store'
    Β·
  8 β”‚         let x: &u64 = &0;
    β”‚                ---- The type: '&u64'
    Β·
  3 β”‚     fun read_and_assign(store: &mut u64, new_value: &u64) {
    β”‚                                -------- Is not a subtype of: '&mut u64'
    β”‚

The only other types currently that has subtyping are tuples

Ownership

Both mutable and immutable references can always be copied and extended even if there are existing copies or extensions of the same reference:

script {
  fun reference_copies(s: &mut S) {
    let s_copy1 = s; // ok
    let s_extension = &mut s.f; // also ok
    let s_copy2 = s; // still ok
    // ...
  }
}

This might be surprising for programmers familiar with Rust’s ownership system, which would reject the code above. Move’s type system is more permissive in its treatment of copies, but equally strict in ensuring unique ownership of mutable references before writes.

References Cannot Be Stored

References and tuples are the only types that cannot be stored as a field value of structs, which also means that they cannot exist in global storage. All references created during program execution will be destroyed when a Move program terminates; they are entirely ephemeral. This invariant is also true for values of types without the store ability, but note that references and tuples go a step further by never being allowed in structs in the first place.

This is another difference between Move and Rust, which allows references to be stored inside of structs.

Currently, Move cannot support this because references cannot be serialized, but every Move value must be serializable. This requirement comes from Move’s persistent global storage, which needs to serialize values to persist them across program executions. Structs can be written to global storage, and thus they must be serializable.

One could imagine a fancier, more expressive, type system that would allow references to be stored in structs and ban those structs from existing in global storage. We could perhaps allow references inside of structs that do not have the store ability, but that would not completely solve the problem: Move has a fairly complex system for tracking static reference safety, and this aspect of the type system would also have to be extended to support storing references inside of structs. In short, Move’s type system (particularly the aspects around reference safety) would have to expand to support stored references. But it is something we are keeping an eye on as the language evolves.