The Move Reference
Welcome to Move, a next generation language for secure asset programming. Its primary use case is in blockchain environments, where Move programs are used to construct state changes. Move allows developers to write programs that flexibly manage and transfer assets, while providing the security and protections against attacks on those assets. However, Move has been developed with use cases in mind outside a blockchain context as well.
Move takes its cue from Rust by using resource types with move (hence the name) semantics as an explicit representation of digital assets, such as currency.
Modules
Modules are the core program unit that define types along with functions that operate on these
types. Struct types define the schema of Move's storage, and module functions
define the rules interacting with values of those types. While modules themselves are also stored in
storage, they are not accessible from within a Move program. In a blockchain environment, the
modules are stored on chain in a process typically referred to as "publishing". After being
published, entry
and public
functions can be invoked according to the rules of that particular Move instance.
Syntax
A module has the following syntax:
module <address>::<identifier> {
(<use> | <type> | <function> | <constant>)*
}
where <address>
is a valid address specifying the module's
package.
For example:
module 0x42::test {
public struct Example has copy, drop { i: u64 }
use std::debug;
const ONE: u64 = 1;
public fun print(x: u64) {
let sum = x + ONE;
let example = Example { i: sum };
debug::print(&sum)
}
}
Names
The module test_addr::test
part specifies that the module test
will be published under the
numerical address value assigned for the name test_addr
in the
package settings.
Modules should normally be declared using named addresses (as opposed to using the numerical value directly). For example:
module test_addr::test {
public struct Example has copy, drop { a: address }
friend test_addr::another_test;
public fun print() {
let example = Example { a: @test_addr };
debug::print(&example)
}
}
These named addresses commonly match the name of the package.
Because named addresses only exist at the source language level and during compilation, named addresses will be fully substituted for their value at the bytecode level. For example if we had the following code:
fun example() {
my_addr::m::foo(@my_addr);
}
and we compiled it with my_addr
set to 0xC0FFEE
, then it would be operationally equivalent to
the following:
fun example() {
0xC0FFEE::m::foo(@0xC0FFEE);
}
While at the source level these two different accesses are equivalent, it is a best practice to always use the named address and not the numerical value assigned to that address.
Module names can start with a lowercase letter from a
to z
or an uppercase letter from A
to
Z
. After the first character, module names can contain underscores _
, letters a
to z
,
letters A
to Z
, or digits 0
to 9
.
module a::my_module {}
module a::foo_bar_42 {}
Typically, module names start with a lowercase letter. A module named my_module
should be stored
in a source file named my_module.move
.
Members
All members inside a module
block can appear in any order. Fundamentally, a module is a collection
of types
and functions
. The use
keyword refers
to members from other modules. The const
keyword defines constants that can be
used in the functions of a module.
The friend
syntax is a deprecated concept for specifying a list of trusted
modules. The concept has been superseded by public(package)
Primitive Types
The primitive types are the basic building blocks of the language.
These primitive types can be used on their own or can be used be used to build more complex,
user-defined types, e.g. in a struct
.
These primitive types are used in conjunction with other types
Integers
Move supports six unsigned integer types: u8
, u16
, u32
, u64
, u128
, and u256
. Values of
these types range from 0 to a maximum that depends on the size of the type.
Type | Value Range |
---|---|
Unsigned 8-bit integer, u8 | 0 to 28 - 1 |
Unsigned 16-bit integer, u16 | 0 to 216 - 1 |
Unsigned 32-bit integer, u32 | 0 to 232 - 1 |
Unsigned 64-bit integer, u64 | 0 to 264 - 1 |
Unsigned 128-bit integer, u128 | 0 to 2128 - 1 |
Unsigned 256-bit integer, u256 | 0 to 2256 - 1 |
Literals
Literal values for these types are specified either as a sequence of digits (e.g.,112
) or as hex
literals, e.g., 0xFF
. The type of the literal can optionally be added as a suffix, e.g., 112u8
.
If the type is not specified, the compiler will try to infer the type from the context where the
literal is used. If the type cannot be inferred, it is assumed to be u64
.
Number literals can be separated by underscores for grouping and readability. (e.g.,1_234_5678
,
1_000u128
, 0xAB_CD_12_35
).
If a literal is too large for its specified (or inferred) size range, an error is reported.
Examples
// literals with explicit annotations;
let explicit_u8 = 1u8;
let explicit_u16 = 1u16;
let explicit_u32 = 1u32;
let explicit_u64 = 2u64;
let explicit_u128 = 3u128;
let explicit_u256 = 1u256;
let explicit_u64_underscored = 154_322_973u64;
// literals with simple inference
let simple_u8: u8 = 1;
let simple_u16: u16 = 1;
let simple_u32: u32 = 1;
let simple_u64: u64 = 2;
let simple_u128: u128 = 3;
let simple_u256: u256 = 1;
// literals with more complex inference
let complex_u8 = 1; // inferred: u8
// right hand argument to shift must be u8
let _unused = 10 << complex_u8;
let x: u8 = 38;
let complex_u8 = 2; // inferred: u8
// arguments to `+` must have the same type
let _unused = x + complex_u8;
let complex_u128 = 133_876; // inferred: u128
// inferred from function argument type
function_that_takes_u128(complex_u128);
// literals can be written in hex
let hex_u8: u8 = 0x1;
let hex_u16: u16 = 0x1BAE;
let hex_u32: u32 = 0xDEAD80;
let hex_u64: u64 = 0xCAFE;
let hex_u128: u128 = 0xDEADBEEF;
let hex_u256: u256 = 0x1123_456A_BCDE_F;
Operations
Arithmetic
Each of these types supports the same set of checked arithmetic operations. For all of these operations, both arguments (the left and right side operands) must be of the same type. If you need to operate over values of different types, you will need to first perform a cast. Similarly, if you expect the result of the operation to be too large for the integer type, perform a cast to a larger size before performing the operation.
All arithmetic operations abort instead of behaving in a way that mathematical integers would not (e.g., overflow, underflow, divide-by-zero).
Syntax | Operation | Aborts If |
---|---|---|
+ | addition | Result is too large for the integer type |
- | subtraction | Result is less than zero |
* | multiplication | Result is too large for the integer type |
% | modular division | The divisor is 0 |
/ | truncating division | The divisor is 0 |
Bitwise
The integer types support the following bitwise operations that treat each number as a series of individual bits, either 0 or 1, instead of as numerical integer values.
Bitwise operations do not abort.
Syntax | Operation | Description |
---|---|---|
& | bitwise and | Performs a boolean and for each bit pairwise |
| | bitwise or | Performs a boolean or for each bit pairwise |
^ | bitwise xor | Performs a boolean exclusive or for each bit pairwise |
Bit Shifts
Similar to the bitwise operations, each integer type supports bit shifts. But unlike the other
operations, the righthand side operand (how many bits to shift by) must always be a u8
and need
not match the left side operand (the number you are shifting).
Bit shifts can abort if the number of bits to shift by is greater than or equal to 8
, 16
, 32
,
64
, 128
or 256
for u8
, u16
, u32
, u64
, u128
and u256
respectively.
Syntax | Operation | Aborts if |
---|---|---|
<< | shift left | Number of bits to shift by is greater than the size of the integer type |
>> | shift right | Number of bits to shift by is greater than the size of the integer type |
Comparisons
Integer types are the only types in Move that can use the comparison operators. Both arguments need to be of the same type. If you need to compare integers of different types, you must cast one of them first.
Comparison operations do not abort.
Syntax | Operation |
---|---|
< | less than |
> | greater than |
<= | less than or equal to |
>= | greater than or equal to |
Equality
Like all types with drop
, all integer types support the
"equal" and "not equal" operations. Both arguments need to be of
the same type. If you need to compare integers of different types, you must cast one of
them first.
Equality operations do not abort.
Syntax | Operation |
---|---|
== | equal |
!= | not equal |
For more details see the section on equality
Casting
Integer types of one size can be cast to integer types of another size. Integers are the only types in Move that support casting.
Casts do not truncate. Casting aborts if the result is too large for the specified type.
Syntax | Operation | Aborts if |
---|---|---|
(e as T) | Cast integer expression e into an integer type T | e is too large to represent as a T |
Here, the type of e
must be 8
, 16
, 32
, 64
, 128
or 256
and T
must be u8
, u16
,
u32
, u64
, u128
, or u256
.
For example:
(x as u8)
(y as u16)
(873u16 as u32)
(2u8 as u64)
(1 + 3 as u128)
(4/2 + 12345 as u256)
Ownership
As with the other scalar values built-in to the language, integer values are implicitly copyable,
meaning they can be copied without an explicit instruction such as
copy
.
Bool
bool
is Move's primitive type for boolean true
and false
values.
Literals
Literals for bool
are either true
or false
.
Operations
Logical
bool
supports three logical operations:
Syntax | Description | Equivalent Expression |
---|---|---|
&& | short-circuiting logical and | p && q is equivalent to if (p) q else false |
|| | short-circuiting logical or | p || q is equivalent to if (p) true else q |
! | logical negation | !p is equivalent to if (p) false else true |
Control Flow
bool
values are used in several of Move's control-flow constructs:
Ownership
As with the other scalar values built-in to the language, boolean values are implicitly copyable,
meaning they can be copied without an explicit instruction such as
copy
.
Address
address
is a built-in type in Move that is used to represent locations (sometimes called accounts)
in storage. An address
value is a 256-bit (32 byte) identifier. Move uses addresses to
differentiate packages of modules, where each package has its own address and
modules. Specific deployments of Move might also use the address
value for
storage operations.
For Sui,
address
is used to represent "accounts", and also objects via strong type wrappers (withsui::object::UID
andsui::object::ID
).
Although an address
is a 256 bit integer under the hood, Move addresses are intentionally
opaque---they cannot be created from integers, they do not support arithmetic operations, and they
cannot be modified. Specific deployments of Move might have native
functions to enable some of
these operations (e.g., creating an address
from bytes vector<u8>
), but these are not part of
the Move language itself.
While there are runtime address values (values of type address
), they cannot be used to access
modules at runtime.
Addresses and Their Syntax
Addresses come in two flavors, named or numerical. The syntax for a named address follows the same
rules for any named identifier in Move. The syntax of a numerical address is not restricted to
hex-encoded values, and any valid u256
numerical value can be used as an address
value, e.g., 42
, 0xCAFE
, and 10_000
are all valid numerical address literals.
To distinguish when an address is being used in an expression context or not, the syntax when using an address differs depending on the context where it's used:
- When an address is used as an expression, the address must be prefixed by the
@
character, i.e.,@<numerical_value>
or@<named_address_identifier>
. - Outside of expression contexts, the address may be written without the leading
@
character, i.e.,<numerical_value>
or<named_address_identifier>
.
In general, you can think of @
as an operator that takes an address from being a namespace item to
being an expression item.
Named Addresses
Named addresses are a feature that allow identifiers to be used in place of numerical values in any spot where addresses are used, and not just at the value level. Named addresses are declared and bound as top level elements (outside of modules and scripts) in Move packages, or passed as arguments to the Move compiler.
Named addresses only exist at the source language level and will be fully substituted for their
value at the bytecode level. Because of this, modules and module members should be accessed through
the module's named address and not through the numerical value assigned to the named address during
compilation. So while use my_addr::foo
is equivalent to use 0x2::foo
(if my_addr
is assigned
0x2
), it is a best practice to always use the my_addr
name.
Examples
// shorthand for
// 0x0000000000000000000000000000000000000000000000000000000000000001
let a1: address = @0x1;
// shorthand for
// 0x0000000000000000000000000000000000000000000000000000000000000042
let a2: address = @0x42;
// shorthand for
// 0x00000000000000000000000000000000000000000000000000000000DEADBEEF
let a3: address = @0xDEADBEEF;
// shorthand for
// 0x000000000000000000000000000000000000000000000000000000000000000A
let a4: address = @0x0000000000000000000000000000000A;
// Assigns `a5` the value of the named address `std`
let a5: address = @std;
// Any valid numerical value can be used as an address
let a6: address = @66;
let a7: address = @42_000;
module 66::some_module { // Not in expression context, so no @ needed
use 0x1::other_module; // Not in expression context so no @ needed
use std::vector; // Can use a named address as a namespace item
...
}
module std::other_module { // Can use a named address when declaring a module
...
}
Vector
vector<T>
is the only primitive collection type provided by Move. A vector<T>
is a homogeneous
collection of T
's that can grow or shrink by pushing/popping values off the "end".
A vector<T>
can be instantiated with any type T
. For example, vector<u64>
, vector<address>
,
vector<0x42::my_module::MyData>
, and vector<vector<u8>>
are all valid vector types.
Literals
General vector
Literals
Vectors of any type can be created with vector
literals.
Syntax | Type | Description |
---|---|---|
vector[] | vector[]: vector<T> where T is any single, non-reference type | An empty vector |
vector[e1, ..., en] | vector[e1, ..., en]: vector<T> where e_i: T s.t. 0 < i <= n and n > 0 | A vector with n elements (of length n ) |
In these cases, the type of the vector
is inferred, either from the element type or from the
vector's usage. If the type cannot be inferred, or simply for added clarity, the type can be
specified explicitly:
vector<T>[]: vector<T>
vector<T>[e1, ..., en]: vector<T>
Example Vector Literals
(vector[]: vector<bool>);
(vector[0u8, 1u8, 2u8]: vector<u8>);
(vector<u128>[]: vector<u128>);
(vector<address>[@0x42, @0x100]: vector<address>);
vector<u8>
literals
A common use-case for vectors in Move is to represent "byte arrays", which are represented with
vector<u8>
. These values are often used for cryptographic purposes, such as a public key or a hash
result. These values are so common that specific syntax is provided to make the values more
readable, as opposed to having to use vector[]
where each individual u8
value is specified in
numeric form.
There are currently two supported types of vector<u8>
literals, byte strings and hex strings.
Byte Strings
Byte strings are quoted string literals prefixed by a b
, e.g. b"Hello!\n"
.
These are ASCII encoded strings that allow for escape sequences. Currently, the supported escape sequences are:
Escape Sequence | Description |
---|---|
\n | New line (or Line feed) |
\r | Carriage return |
\t | Tab |
\\ | Backslash |
\0 | Null |
\" | Quote |
\xHH | Hex escape, inserts the hex byte sequence HH |
Hex Strings
Hex strings are quoted string literals prefixed by a x
, e.g. x"48656C6C6F210A"
.
Each byte pair, ranging from 00
to FF
, is interpreted as hex encoded u8
value. So each byte
pair corresponds to a single entry in the resulting vector<u8>
.
Example String Literals
fun byte_and_hex_strings() {
assert!(b"" == x"", 0);
assert!(b"Hello!\n" == x"48656C6C6F210A", 1);
assert!(b"\x48\x65\x6C\x6C\x6F\x21\x0A" == x"48656C6C6F210A", 2);
assert!(
b"\"Hello\tworld!\"\n \r \\Null=\0" ==
x"2248656C6C6F09776F726C6421220A200D205C4E756C6C3D00",
3
);
}
Operations
vector
supports the following operations via the std::vector
module in the Move standard
library:
Function | Description | Aborts? |
---|---|---|
vector::empty<T>(): vector<T> | Create an empty vector that can store values of type T | Never |
vector::singleton<T>(t: T): vector<T> | Create a vector of size 1 containing t | Never |
vector::push_back<T>(v: &mut vector<T>, t: T) | Add t to the end of v | Never |
vector::pop_back<T>(v: &mut vector<T>): T | Remove and return the last element in v | If v is empty |
vector::borrow<T>(v: &vector<T>, i: u64): &T | Return an immutable reference to the T at index i | If i is not in bounds |
vector::borrow_mut<T>(v: &mut vector<T>, i: u64): &mut T | Return a mutable reference to the T at index i | If i is not in bounds |
vector::destroy_empty<T>(v: vector<T>) | Delete v | If v is not empty |
vector::append<T>(v1: &mut vector<T>, v2: vector<T>) | Add the elements in v2 to the end of v1 | Never |
vector::contains<T>(v: &vector<T>, e: &T): bool | Return true if e is in the vector v . Otherwise, returns false | Never |
vector::swap<T>(v: &mut vector<T>, i: u64, j: u64) | Swaps the elements at the i th and j th indices in the vector v | If i or j is out of bounds |
vector::reverse<T>(v: &mut vector<T>) | Reverses the order of the elements in the vector v in place | Never |
vector::index_of<T>(v: &vector<T>, e: &T): (bool, u64) | Return (true, i) if e is in the vector v at index i . Otherwise, returns (false, 0) | Never |
vector::remove<T>(v: &mut vector<T>, i: u64): T | Remove the i th element of the vector v , shifting all subsequent elements. This is O(n) and preserves ordering of elements in the vector | If i is out of bounds |
vector::swap_remove<T>(v: &mut vector<T>, i: u64): T | Swap the i th element of the vector v with the last element and then pop the element, This is O(1), but does not preserve ordering of elements in the vector | If i is out of bounds |
More operations may be added over time.
Example
use std::vector;
let mut v = vector::empty<u64>();
vector::push_back(&mut v, 5);
vector::push_back(&mut v, 6);
assert!(*vector::borrow(&v, 0) == 5, 42);
assert!(*vector::borrow(&v, 1) == 6, 42);
assert!(vector::pop_back(&mut v) == 6, 42);
assert!(vector::pop_back(&mut v) == 5, 42);
Destroying and copying vector
s
Some behaviors of vector<T>
depend on the abilities of the element type, T
. For example, vectors
containing elements that do not have drop
cannot be implicitly discarded like v
in the example
above--they must be explicitly destroyed with vector::destroy_empty
.
Note that vector::destroy_empty
will abort at runtime unless vec
contains zero elements:
fun destroy_any_vector<T>(vec: vector<T>) {
vector::destroy_empty(vec) // deleting this line will cause a compiler error
}
But no error would occur for dropping a vector that contains elements with drop
:
fun destroy_droppable_vector<T: drop>(vec: vector<T>) {
// valid!
// nothing needs to be done explicitly to destroy the vector
}
Similarly, vectors cannot be copied unless the element type has copy
. In other words, a
vector<T>
has copy
if and only if T
has copy
. Note that it will be implicitly copied if
needed:
let x = vector[10];
let y = x; // implicit copy
let z = x;
(y, z)
Keep in mind, copies of large vectors can be expensive. If this is a concern, annotating the
intended
usage can prevent accidental copies. For example,
let x = vector[10];
let y = move x;
let z = x; // ERROR! x has been moved
(y, z)
For more details see the sections on type abilities and generics.
Ownership
As mentioned above, vector
values can be copied only if the
elements can be copied. In that case, the copy can be done via a
copy
or a
dereference *
.
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.
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
".
Syntax | Type | Description |
---|---|---|
&e | &T where e: T and T is a non-reference type | Create an immutable reference to e |
&mut e | &mut T where e: T and T is a non-reference type | Create a mutable reference to e . |
&e.f | &T where e.f: T | Create an immutable reference to field f of struct e . |
&mut e.f | &mut T where e.f: T | Create a mutable reference to field f of structe . |
freeze(e) | &T where e: &mut T | Convert 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:
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:
public struct A { b: B }
public struct B { c : u64 }
fun f(a: &A): &u64 {
&a.b.c
}
Finally, note that references to references are not allowed:
let x = 7;
let y: &u64 = &x;
let z: &&u64 = &y; // ERROR! 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.
Syntax | Type | Description |
---|---|---|
*e | T where e is &T or &mut T | Read the value pointed to by e |
*e1 = e2 | () where e1: &mut T and e2: T | Update 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 assets:
fun copy_coin_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:
fun destroy_coin_via_ref_bad(mut ten_coins: Coin, c: Coin) {
let ref = &mut ten_coins;
*ref = c; // ERROR! not allowed--would destroy 10 coins!
}
freeze
inference
A mutable reference can be used in a context where an immutable reference is expected:
let mut 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:
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 mut x = 0;
let mut 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 a::example {
fun read_and_assign(store: &mut u64, new_value: &u64) {
*store = *new_value
}
fun subtype_examples() {
let mut x: &u64 = &0;
let mut y: &mut u64 = &mut 1;
x = &mut 1; // valid
y = &2; // ERROR! invalid!
read_and_assign(y, x); // valid
read_and_assign(x, y); // ERROR! invalid!
}
}
will yield the following error messages
error:
┌── example.move:11:9 ───
│
12 │ y = &2; // invalid!
│ ^ Invalid assignment to local 'y'
·
12 │ y = &2; // invalid!
│ -- The type: '&{integer}'
·
9 │ let mut y: &mut u64 = &mut 1;
│ -------- Is not a subtype of: '&mut u64'
│
error:
┌── example.move:14:9 ───
│
15 │ read_and_assign(x, y); // invalid!
│ ^^^^^^^^^^^^^^^^^^^^^ Invalid call of 'a::example::read_and_assign'. Invalid argument for parameter 'store'
·
8 │ let mut 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 that currently have 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:
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 storage or objects. All references
created during program execution will be destroyed when a Move program terminates; they are entirely
ephemeral. This also applies to all types without the store
ability: any value of a non-store
type must be destroyed before the program terminates. 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.
One could imagine a fancier, more expressive, type system that would allow references to be stored
in structs. We could allow references inside of structs that do not have the store
ability, but the core difficulty is that Move has a fairly complex system for
tracking static reference safety. This aspect of the type system would also have to be extended to
support storing references inside of structs. In short, Move's reference safety system would have to
expand to support stored references, and it is something we are keeping an eye on as the language
evolves.
Tuples and Unit
Move does not fully support tuples as one might expect coming from another language with them as a first-class value. However, in order to support multiple return values, Move has tuple-like expressions. These expressions do not result in a concrete value at runtime (there are no tuples in the bytecode), and as a result they are very limited:
- They can only appear in expressions (usually in the return position for a function).
- They cannot be bound to local variables.
- They cannot be bound to local variables.
- They cannot be stored in structs.
- Tuple types cannot be used to instantiate generics.
Similarly, unit ()
is a type created by the Move source
language in order to be expression based. The unit value ()
does not result in any runtime value.
We can consider unit()
to be an empty tuple, and any restrictions that apply to tuples also apply
to unit.
It might feel weird to have tuples in the language at all given these restrictions. But one of the most common use cases for tuples in other languages is for functions to allow functions to return multiple values. Some languages work around this by forcing the users to write structs that contain the multiple return values. However in Move, you cannot put references inside of structs. This required Move to support multiple return values. These multiple return values are all pushed on the stack at the bytecode level. At the source level, these multiple return values are represented using tuples.
Literals
Tuples are created by a comma separated list of expressions inside of parentheses.
Syntax | Type | Description |
---|---|---|
() | (): () | Unit, the empty tuple, or the tuple of arity 0 |
(e1, ..., en) | (e1, ..., en): (T1, ..., Tn) where e_i: Ti s.t. 0 < i <= n and n > 0 | A n -tuple, a tuple of arity n , a tuple with n elements |
Note that (e)
does not have type (e): (t)
, in other words there is no tuple with one element. If
there is only a single element inside of the parentheses, the parentheses are only used for
disambiguation and do not carry any other special meaning.
Sometimes, tuples with two elements are called "pairs" and tuples with three elements are called "triples."
Examples
module 0x42::example {
// all 3 of these functions are equivalent
// when no return type is provided, it is assumed to be `()`
fun returns_unit_1() { }
// there is an implicit () value in empty expression blocks
fun returns_unit_2(): () { }
// explicit version of `returns_unit_1` and `returns_unit_2`
fun returns_unit_3(): () { () }
fun returns_3_values(): (u64, bool, address) {
(0, false, @0x42)
}
fun returns_4_values(x: &u64): (&u64, u8, u128, vector<u8>) {
(x, 0, 1, b"foobar")
}
}
Operations
The only operation that can be done on tuples currently is destructuring.
Destructuring
For tuples of any size, they can be destructured in either a let
binding or in an assignment.
For example:
module 0x42::example {
// all 3 of these functions are equivalent
fun returns_unit() {}
fun returns_2_values(): (bool, bool) { (true, false) }
fun returns_4_values(x: &u64): (&u64, u8, u128, vector<u8>) { (x, 0, 1, b"foobar") }
fun examples(cond: bool) {
let () = ();
let (mut x, mut y): (u8, u64) = (0, 1);
let (mut a, mut b, mut c, mut d) = (@0x0, 0, false, b"");
() = ();
(x, y) = if (cond) (1, 2) else (3, 4);
(a, b, c, d) = (@0x1, 1, true, b"1");
}
fun examples_with_function_calls() {
let () = returns_unit();
let (mut x, mut y): (bool, bool) = returns_2_values();
let (mut a, mut b, mut c, mut d) = returns_4_values(&0);
() = returns_unit();
(x, y) = returns_2_values();
(a, b, c, d) = returns_4_values(&1);
}
}
For more details, see Move Variables.
Subtyping
Along with references, tuples are the only types that have subtyping in Move. Tuples have subtyping only in the sense that subtype with references (in a covariant way).
For example:
let x: &u64 = &0;
let y: &mut u64 = &mut 1;
// (&u64, &mut u64) is a subtype of (&u64, &u64)
// since &mut u64 is a subtype of &u64
let (a, b): (&u64, &u64) = (x, y);
// (&mut u64, &mut u64) is a subtype of (&u64, &u64)
// since &mut u64 is a subtype of &u64
let (c, d): (&u64, &u64) = (y, y);
// ERROR! (&u64, &mut u64) is NOT a subtype of (&mut u64, &mut u64)
// since &u64 is NOT a subtype of &mut u64
let (e, f): (&mut u64, &mut u64) = (x, y);
Ownership
As mentioned above, tuple values don't really exist at runtime. And currently they cannot be stored into local variables because of this (but it is likely that this feature will come at some point in the future). As such, tuples can only be moved currently, as copying them would require putting them into a local variable first.
Local Variables and Scope
Local variables in Move are lexically (statically) scoped. New variables are introduced with the
keyword let
, which will shadow any previous local with the same name. Locals marked as mut
are
mutable and can be updated both directly and via a mutable reference.
Declaring Local Variables
let
bindings
Move programs use let
to bind variable names to values:
let x = 1;
let y = x + x:
let
can also be used without binding a value to the local.
let x;
The local can then be assigned a value later.
let x;
if (cond) {
x = 1
} else {
x = 0
}
This can be very helpful when trying to extract a value from a loop when a default value cannot be provided.
let x;
let cond = true;
let i = 0;
loop {
let (res, cond) = foo(i);
if (!cond) {
x = res;
break;
};
i = i + 1;
}
To modify a local variable after it is assigned, or to borrow it mutably &mut
, it must be
declared as mut
.
let mut x = 0;
if (cond) x = x + 1;
foo(&mut x);
For more details see the section on assignments below.
Variables must be assigned before use
Move's type system prevents a local variable from being used before it has been assigned.
let x;
x + x // ERROR! x is used before being assigned
let x;
if (cond) x = 0;
x + x // ERROR! x does not have a value in all cases
let x;
while (cond) x = 0;
x + x // ERROR! x does not have a value in all cases
Valid variable names
Variable names can contain underscores _
, letters a
to z
, letters A
to Z
, and digits 0
to 9
. Variable names must start with either an underscore _
or a letter a
through z
. They
cannot start with uppercase letters.
// all valid
let x = e;
let _x = e;
let _A = e;
let x0 = e;
let xA = e;
let foobar_123 = e;
// all invalid
let X = e; // ERROR!
let Foo = e; // ERROR!
Type annotations
The type of a local variable can almost always be inferred by Move's type system. However, Move allows explicit type annotations that can be useful for readability, clarity, or debuggability. The syntax for adding a type annotation is:
let x: T = e; // "Variable x of type T is initialized to expression e"
Some examples of explicit type annotations:
module 0x42::example {
public struct S { f: u64, g: u64 }
fun annotated() {
let u: u8 = 0;
let b: vector<u8> = b"hello";
let a: address = @0x0;
let (x, y): (&u64, &mut u64) = (&0, &mut 1);
let S { f, g: f2 }: S = S { f: 0, g: 1 };
}
}
Note that the type annotations must always be to the right of the pattern:
// ERROR! should be let (x, y): (&u64, &mut u64) = ...
let (x: &u64, y: &mut u64) = (&0, &mut 1);
When annotations are necessary
In some cases, a local type annotation is required if the type system cannot infer the type. This commonly occurs when the type argument for a generic type cannot be inferred. For example:
let _v1 = vector[]; // ERROR!
// ^^^^^^^^ Could not infer this type. Try adding an annotation
let v2: vector<u64> = vector[]; // no error
In a rarer case, the type system might not be able to infer a type for divergent code (where all the
following code is unreachable). Both return
and
abort
are expressions and can have any type. A
loop
has type ()
if it has a break
(or T
if has a break e
where
e: T
), but if there is no break out of the loop
, it could have any type. If these types cannot
be inferred, a type annotation is required. For example, this code:
let a: u8 = return ();
let b: bool = abort 0;
let c: signer = loop ();
let x = return (); // ERROR!
// ^ Could not infer this type. Try adding an annotation
let y = abort 0; // ERROR!
// ^ Could not infer this type. Try adding an annotation
let z = loop (); // ERROR!
// ^ Could not infer this type. Try adding an annotation
Adding type annotations to this code will expose other errors about dead code or unused local variables, but the example is still helpful for understanding this problem.
Multiple declarations with tuples
let
can introduce more than one local at a time using tuples. The locals declared inside the
parenthesis are initialized to the corresponding values from the tuple.
let () = ();
let (x0, x1) = (0, 1);
let (y0, y1, y2) = (0, 1, 2);
let (z0, z1, z2, z3) = (0, 1, 2, 3);
The type of the expression must match the arity of the tuple pattern exactly.
let (x, y) = (0, 1, 2); // ERROR!
let (x, y, z, q) = (0, 1, 2); // ERROR!
You cannot declare more than one local with the same name in a single let
.
let (x, x) = 0; // ERROR!
The mutability of the local variables declared can be mixed.
let (mut x, y) = (0, 1);
x = 1;
Multiple declarations with structs
let
can also introduce more than one local variables at a time when destructuring (or matching against) a
struct. In this form, the let
creates a set of local variables that are initialized to the values
of the fields from a struct. The syntax looks like this:
public struct T { f1: u64, f2: u64 }
let T { f1: local1, f2: local2 } = T { f1: 1, f2: 2 };
// local1: u64
// local2: u64
Similarly for positional structs
public struct P(u64, u64)
and
let P (local1, local2) = P ( 1, 2 );
// local1: u64
// local2: u64
Here is a more complicated example:
module 0x42::example {
public struct X(u64)
public struct Y { x1: X, x2: X }
fun new_x(): X {
X(1)
}
fun example() {
let Y { x1: X(f), x2 } = Y { x1: new_x(), x2: new_x() };
assert!(f + x2.f == 2, 42);
let Y { x1: X(f1), x2: X(f2) } = Y { x1: new_x(), x2: new_x() };
assert!(f1 + f2 == 2, 42);
}
}
Fields of structs can serve double duty, identifying the field to bind and the name of the variable. This is sometimes referred to as punning.
let Y { x1, x2 } = e;
is equivalent to:
let Y { x1: x1, x2: x2 } = e;
As shown with tuples, you cannot declare more than one local with the same name in a single let
.
let Y { x1: x, x2: x } = e; // ERROR!
And as with tuples, the mutability of the local variables declared can be mixed.
let Y { x1: mut x1, x2 } = e;
Furthermore, the mutability of annotation can be applied to the punned fields. Giving the equivalent example
let Y { mut x1, x2 } = e;
Destructuring against references
In the examples above for structs, the bound value in the let was moved, destroying the struct value and binding its fields.
public struct T { f1: u64, f2: u64 }
let T { f1: local1, f2: local2 } = T { f1: 1, f2: 2 };
// local1: u64
// local2: u64
In this scenario the struct value T { f1: 1, f2: 2 }
no longer exists after the let
.
If you wish instead to not move and destroy the struct value, you can borrow each of its fields. For example:
let t = T { f1: 1, f2: 2 };
let T { f1: local1, f2: local2 } = &t;
// local1: &u64
// local2: &u64
And similarly with mutable references:
let mut t = T { f1: 1, f2: 2 };
let T { f1: local1, f2: local2 } = &mut t;
// local1: &mut u64
// local2: &mut u64
This behavior can also work with nested structs.
module 0x42::example {
public struct X(u64)
public struct Y { x1: X, x2: X }
fun new_x(): X {
X(1)
}
fun example() {
let mut y = Y { x1: new_x(), x2: new_x() };
let Y { x1: X(f), x2 } = &y;
assert!(*f + x2.f == 2, 42);
let Y { x1: X(f1), x2: X(f2) } = &mut y;
*f1 = *f1 + 1;
*f2 = *f2 + 1;
assert!(*f1 + *f2 == 4, 42);
}
}
Ignoring Values
In let
bindings, it is often helpful to ignore some values. Local variables that start with _
will be ignored and not introduce a new variable
fun three(): (u64, u64, u64) {
(0, 1, 2)
}
let (x1, _, z1) = three();
let (x2, _y, z2) = three();
assert!(x1 + z1 == x2 + z2, 42);
This can be necessary at times as the compiler will warn on unused local variables
let (x1, y, z1) = three(); // WARNING!
// ^ unused local 'y'
General let
grammar
All of the different structures in let
can be combined! With that we arrive at this general
grammar for let
statements:
let-binding → let pattern-or-list type-annotationopt > initializeropt > pattern-or-list → pattern | ( pattern-list ) > pattern-list → pattern ,opt | pattern , pattern-list > type-annotation → : type initializer → = expression
The general term for the item that introduces the bindings is a pattern. The pattern serves to both destructure data (possibly recursively) and introduce the bindings. The pattern grammar is as follows:
pattern → local-variable | struct-type { field-binding-list } > field-binding-list → field-binding ,opt | field-binding , > field-binding-list > field-binding → field | field : pattern
A few concrete examples with this grammar applied:
let (x, y): (u64, u64) = (0, 1);
// ^ local-variable
// ^ pattern
// ^ local-variable
// ^ pattern
// ^ pattern-list
// ^^^^ pattern-list
// ^^^^^^ pattern-or-list
// ^^^^^^^^^^^^ type-annotation
// ^^^^^^^^ initializer
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ let-binding
let Foo { f, g: x } = Foo { f: 0, g: 1 };
// ^^^ struct-type
// ^ field
// ^ field-binding
// ^ field
// ^ local-variable
// ^ pattern
// ^^^^ field-binding
// ^^^^^^^ field-binding-list
// ^^^^^^^^^^^^^^^ pattern
// ^^^^^^^^^^^^^^^ pattern-or-list
// ^^^^^^^^^^^^^^^^^^^^ initializer
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ let-binding
Mutations
Assignments
After the local is introduced (either by let
or as a function parameter), a mut
local can be
modified via an assignment:
x = e
Unlike let
bindings, assignments are expressions. In some languages, assignments return the value
that was assigned, but in Move, the type of any assignment is always ()
.
(x = e: ())
Practically, assignments being expressions means that they can be used without adding a new
expression block with braces ({
...}
).
let x;
if (cond) x = 1 else x = 2;
The assignment uses the similar pattern syntax scheme as let
bindings, but with absence of mut
:
module 0x42::example {
public struct X { f: u64 }
fun new_x(): X {
X { f: 1 }
}
// Note: this example will complain about unused variables and assignments.
fun example() {
let (mut x, mut y, mut f, mut g) = (0, 0, 0, 0);
(X { f }, X { f: x }) = (new_x(), new_x());
assert!(f + x == 2, 42);
(x, y, f, _, g) = (0, 0, 0, 0, 0);
}
}
Note that a local variable can only have one type, so the type of the local cannot change between assignments.
let mut x;
x = 0;
x = false; // ERROR!
Mutating through a reference
In addition to directly modifying a local with assignment, a mut
local can be modified via a
mutable reference &mut
.
let mut x = 0;
let r = &mut x;
*r = 1;
assert!(x == 1, 42);
This is particularly useful if either:
(1) You want to modify different variables depending on some condition.
let mut x = 0;
let mut y = 1;
let r = if (cond) &mut x else &mut y;
*r = *r + 1;
(2) You want another function to modify your local value.
let mut x = 0;
modify_ref(&mut x);
This sort of modification is how you modify structs and vectors!
let mut v = vector[];
vector::push_back(&mut v, 100);
assert!(*vector::borrow(&v, 0) == 100, 42);
For more details, see Move references.
Scopes
Any local declared with let
is available for any subsequent expression, within that scope.
Scopes are declared with expression blocks, {
...}
.
Locals cannot be used outside of the declared scope.
let x = 0;
{
let y = 1;
};
x + y // ERROR!
// ^ unbound local 'y'
But, locals from an outer scope can be used in a nested scope.
{
let x = 0;
{
let y = x + 1; // valid
}
}
Locals can be mutated in any scope where they are accessible. That mutation survives with the local, regardless of the scope that performed the mutation.
let mut x = 0;
x = x + 1;
assert!(x == 1, 42);
{
x = x + 1;
assert!(x == 2, 42);
};
assert!(x == 2, 42);
Expression Blocks
An expression block is a series of statements separated by semicolons (;
). The resulting value of
an expression block is the value of the last expression in the block.
{ let x = 1; let y = 1; x + y }
In this example, the result of the block is x + y
.
A statement can be either a let
declaration or an expression. Remember that assignments (x = e
)
are expressions of type ()
.
{ let x; let y = 1; x = 1; x + y }
Function calls are another common expression of type ()
. Function calls that modify data are
commonly used as statements.
{ let v = vector[]; vector::push_back(&mut v, 1); v }
This is not just limited to ()
types---any expression can be used as a statement in a sequence!
{
let x = 0;
x + 1; // value is discarded
x + 2; // value is discarded
b"hello"; // value is discarded
}
But! If the expression contains a resource (a value without the drop
ability),
you will get an error. This is because Move's type system guarantees that any value that is dropped
has the drop
ability. (Ownership must be transferred or the value must be
explicitly destroyed within its declaring module.)
{
let x = 0;
Coin { value: x }; // ERROR!
// ^^^^^^^^^^^^^^^^^ unused value without the `drop` ability
x
}
If a final expression is not present in a block---that is, if there is a trailing semicolon ;
,
there is an implicit unit ()
value. Similarly, if the
expression block is empty, there is an implicit unit ()
value.
Both are equivalent
{ x = x + 1; 1 / x; }
{ x = x + 1; 1 / x; () }
Similarly both are equivalent
{ }
{ () }
An expression block is itself an expression and can be used anyplace an expression is used. (Note: The body of a function is also an expression block, but the function body cannot be replaced by another expression.)
let my_vector: vector<vector<u8>> = {
let mut v = vector[];
vector::push_back(&mut v, b"hello");
vector::push_back(&mut v, b"goodbye");
v
};
(The type annotation is not needed in this example and only added for clarity.)
Shadowing
If a let
introduces a local variable with a name already in scope, that previous variable can no
longer be accessed for the rest of this scope. This is called shadowing.
let x = 0;
assert!(x == 0, 42);
let x = 1; // x is shadowed
assert!(x == 1, 42);
When a local is shadowed, it does not need to retain the same type as before.
let x = 0;
assert!(x == 0, 42);
let x = b"hello"; // x is shadowed
assert!(x == b"hello", 42);
After a local is shadowed, the value stored in the local still exists, but will no longer be
accessible. This is important to keep in mind with values of types without the
drop
ability, as ownership of the value must be transferred by the end of the
function.
module 0x42::example {
public struct Coin has store { value: u64 }
fun unused_coin(): Coin {
let x = Coin { value: 0 }; // ERROR!
// ^ This local still contains a value without the `drop` ability
x.value = 1;
let x = Coin { value: 10 };
x
// ^ Invalid return
}
}
When a local is shadowed inside a scope, the shadowing only remains for that scope. The shadowing is gone once that scope ends.
let x = 0;
{
let x = 1;
assert!(x == 1, 42);
};
assert!(x == 0, 42);
Remember, locals can change type when they are shadowed.
let x = 0;
{
let x = b"hello";
assert!(x = b"hello", 42);
};
assert!(x == 0, 42);
Move and Copy
All local variables in Move can be used in two ways, either by move
or copy
. If one or the other
is not specified, the Move compiler is able to infer whether a copy
or a move
should be used.
This means that in all of the examples above, a move
or a copy
would be inserted by the
compiler. A local variable cannot be used without the use of move
or copy
.
copy
will likely feel the most familiar coming from other programming languages, as it creates a
new copy of the value inside of the variable to use in that expression. With copy
, the local
variable can be used more than once.
let x = 0;
let y = copy x + 1;
let z = copy x + 2;
Any value with the copy
ability can be copied in this way, and will be copied
implicitly unless a move
is specified.
move
takes the value out of the local variable without copying the data. After a move
occurs,
the local variable is unavailable, even if the value's type has the copy
ability.
let x = 1;
let y = move x + 1;
// ------ Local was moved here
let z = move x + 2; // Error!
// ^^^^^^ Invalid usage of local 'x'
y + z
Safety
Move's type system will prevent a value from being used after it is moved. This is the same safety
check described in let
declaration that prevents local variables from being used
before it is assigned a value.
Inference
As mentioned above, the Move compiler will infer a copy
or move
if one is not indicated. The
algorithm for doing so is quite simple:
- Any value with the
copy
ability is given acopy
. - Any reference (both mutable
&mut
and immutable&
) is given acopy
.- Except under special circumstances where it is made a
move
for predictable borrow checker errors. This will happen once the reference is no longer used.
- Except under special circumstances where it is made a
- Any other value is given a
move
.
Given the structs
public struct Foo has copy, drop, store { f: u64 }
public struct Coin has store { value: u64 }
we have the following example
let s = b"hello";
let foo = Foo { f: 0 };
let coin = Coin { value: 0 };
let coins = vector[Coin { value: 0 }, Coin { value: 0 }];
let s2 = s; // copy
let foo2 = foo; // copy
let coin2 = coin; // move
let coins2 = coin; // move
let x = 0;
let b = false;
let addr = @0x42;
let x_ref = &x;
let coin_ref = &mut coin2;
let x2 = x; // copy
let b2 = b; // copy
let addr2 = @0x42; // copy
let x_ref2 = x_ref; // copy
let coin_ref2 = coin_ref; // copy
Equality
Move supports two equality operations ==
and !=
Operations
Syntax | Operation | Description |
---|---|---|
== | equal | Returns true if the two operands have the same value, false otherwise |
!= | not equal | Returns true if the two operands have different values, false otherwise |
Typing
Both the equal (==
) and not-equal (!=
) operations only work if both operands are the same type
0 == 0; // `true`
1u128 == 2u128; // `false`
b"hello" != x"00"; // `true`
Equality and non-equality also work over all user defined types!
module 0x42::example {
public struct S has copy, drop { f: u64, s: vector<u8> }
fun always_true(): bool {
let s = S { f: 0, s: b"" };
s == s
}
fun always_false(): bool {
let s = S { f: 0, s: b"" };
s != s
}
}
If the operands have different types, there is a type checking error
1u8 == 1u128; // ERROR!
// ^^^^^ expected an argument of type 'u8'
b"" != 0; // ERROR!
// ^ expected an argument of type 'vector<u8>'
Typing with references
When comparing references, the type of the reference (immutable
or mutable) does not matter. This means that you can compare an immutable &
reference with a
mutable one &mut
of the same underlying type.
let i = &0;
let m = &mut 1;
i == m; // `false`
m == i; // `false`
m == m; // `true`
i == i; // `true`
The above is equivalent to applying an explicit freeze to each mutable reference where needed
let i = &0;
let m = &mut 1;
i == freeze(m); // `false`
freeze(m) == i; // `false`
m == m; // `true`
i == i; // `true`
But again, the underlying type must be the same type
let i = &0;
let s = &b"";
i == s; // ERROR!
// ^ expected an argument of type '&u64'
Automatic Borrowing
Starting in Move 2024 edition, the ==
and !=
operators automatically borrow their operands if
one of the operands is a reference and the other is not. This means that the following code works
without any errors:
let r = &0;
// In all cases, `0` is automatically borrowed as `&0`
r == 0; // `true`
0 == r; // `true`
r != 0; // `false`
0 != r; // `false`
This automatic borrow is always an immutable borrow.
Restrictions
Both ==
and !=
consume the value when comparing them. As a result, the type system enforces that
the type must have drop
. Recall that without the
drop
ability, ownership must be transferred by the end of the function, and such
values can only be explicitly destroyed within their declaring module. If these were used directly
with either equality ==
or non-equality !=
, the value would be destroyed which would break
drop
ability safety guarantees!
module 0x42::example {
public struct Coin has store { value: u64 }
fun invalid(c1: Coin, c2: Coin) {
c1 == c2 // ERROR!
// ^^ ^^ These assets would be destroyed!
}
}
But, a programmer can always borrow the value first instead of directly comparing the value, and
reference types have the drop
ability. For example
module 0x42::example {
public struct Coin has store { value: u64 }
fun swap_if_equal(c1: Coin, c2: Coin): (Coin, Coin) {
let are_equal = &c1 == c2; // valid, note `c2` is automatically borrowed
if (are_equal) (c2, c1) else (c1, c2)
}
}
Avoid Extra Copies
While a programmer can compare any value whose type has drop
, a programmer
should often compare by reference to avoid expensive copies.
let v1: vector<u8> = function_that_returns_vector();
let v2: vector<u8> = function_that_returns_vector();
assert!(copy v1 == copy v2, 42);
// ^^^^ ^^^^
use_two_vectors(v1, v2);
let s1: Foo = function_that_returns_large_struct();
let s2: Foo = function_that_returns_large_struct();
assert!(copy s1 == copy s2, 42);
// ^^^^ ^^^^
use_two_foos(s1, s2);
This code is perfectly acceptable (assuming Foo
has drop
), just not efficient.
The highlighted copies can be removed and replaced with borrows
let v1: vector<u8> = function_that_returns_vector();
let v2: vector<u8> = function_that_returns_vector();
assert!(&v1 == &v2, 42);
// ^ ^
use_two_vectors(v1, v2);
let s1: Foo = function_that_returns_large_struct();
let s2: Foo = function_that_returns_large_struct();
assert!(&s1 == &s2, 42);
// ^ ^
use_two_foos(s1, s2);
The efficiency of the ==
itself remains the same, but the copy
s are removed and thus the program
is more efficient.
Abort and Assert
return
and abort
are two control flow constructs that end execution, one for
the current function and one for the entire transaction.
More information on return
can be found in the linked section
abort
abort
is an expression that takes one argument: an abort code of type u64
. For example:
abort 42
The abort
expression halts execution the current function and reverts all changes made to state by
the current transaction (note though that this guarantee must be upheld by the adapter of the
specific deployment of Move). There is no mechanism for "catching" or otherwise handling an abort
.
Luckily, in Move transactions are all or nothing, meaning any changes to storage are made all at once only if the transaction succeeds. For Sui, this means no objects are modified.
Because of this transactional commitment of changes, after an abort there is no need to worry about backing out changes. While this approach is lacking in flexibility, it is incredibly simple and predictable.
Similar to return
, abort
is useful for exiting control flow when some
condition cannot be met.
In this example, the function will pop two items off of the vector, but will abort early if the vector does not have two items
use std::vector;
fun pop_twice<T>(v: &mut vector<T>): (T, T) {
if (vector::length(v) < 2) abort 42;
(vector::pop_back(v), vector::pop_back(v))
}
This is even more useful deep inside a control-flow construct. For example, this function checks
that all numbers in the vector are less than the specified bound
. And aborts otherwise
use std::vector;
fun check_vec(v: &vector<u64>, bound: u64) {
let i = 0;
let n = vector::length(v);
while (i < n) {
let cur = *vector::borrow(v, i);
if (cur > bound) abort 42;
i = i + 1;
}
}
assert
assert
is a builtin, macro operation provided by the Move compiler. It takes two arguments, a
condition of type bool
and a code of type u64
assert!(condition: bool, code: u64)
Since the operation is a macro, it must be invoked with the !
. This is to convey that the
arguments to assert
are call-by-expression. In other words, assert
is not a normal function and
does not exist at the bytecode level. It is replaced inside the compiler with
if (condition) () else abort code
assert
is more commonly used than just abort
by itself. The abort
examples above can be
rewritten using assert
use std::vector;
fun pop_twice<T>(v: &mut vector<T>): (T, T) {
assert!(vector::length(v) >= 2, 42); // Now uses 'assert'
(vector::pop_back(v), vector::pop_back(v))
}
and
use std::vector;
fun check_vec(v: &vector<u64>, bound: u64) {
let i = 0;
let n = vector::length(v);
while (i < n) {
let cur = *vector::borrow(v, i);
assert!(cur <= bound, 42); // Now uses 'assert'
i = i + 1;
}
}
Note that because the operation is replaced with this if-else
, the argument for the code
is not
always evaluated. For example:
assert!(true, 1 / 0)
Will not result in an arithmetic error, it is equivalent to
if (true) () else (1 / 0)
So the arithmetic expression is never evaluated!
Abort codes in the Move VM
When using abort
, it is important to understand how the u64
code will be used by the VM.
Normally, after successful execution, the Move VM, and the adapter for the specific deployment, determine the changes made to storage.
If an abort
is reached, the VM will instead indicate an error. Included in that error will be two
pieces of information:
- The module that produced the abort (package/address value and module name)
- The abort code.
For example
module 0x2::example {
public fun aborts() {
abort 42
}
}
module 0x3::invoker {
public fun always_aborts() {
0x2::example::aborts()
}
}
If a transaction, such as the function always_aborts
above, calls 0x2::example::aborts
, the VM
would produce an error that indicated the module 0x2::example
and the code 42
.
This can be useful for having multiple aborts being grouped together inside a module.
In this example, the module has two separate error codes used in multiple functions
module 0x42::example {
use std::vector;
const EEmptyVector: u64 = 0;
const EIndexOutOfBounds: u64 = 1;
// move i to j, move j to k, move k to i
public fun rotate_three<T>(v: &mut vector<T>, i: u64, j: u64, k: u64) {
let n = vector::length(v);
assert!(n > 0, EEmptyVector);
assert!(i < n, EIndexOutOfBounds);
assert!(j < n, EIndexOutOfBounds);
assert!(k < n, EIndexOutOfBounds);
vector::swap(v, i, k);
vector::swap(v, j, k);
}
public fun remove_twice<T>(v: &mut vector<T>, i: u64, j: u64): (T, T) {
let n = vector::length(v);
assert!(n > 0, EEmptyVector);
assert!(i < n, EIndexOutOfBounds);
assert!(j < n, EIndexOutOfBounds);
assert!(i > j, EIndexOutOfBounds);
(vector::remove<T>(v, i), vector::remove<T>(v, j))
}
}
The type of abort
The abort i
expression can have any type! This is because both constructs break from the normal
control flow, so they never need to evaluate to the value of that type.
The following are not useful, but they will type check
let y: address = abort 0;
This behavior can be helpful in situations where you have a branching instruction that produces a value on some branches, but not all. For example:
let b =
if (x == 0) false
else if (x == 1) true
else abort 42;
// ^^^^^^^^ `abort 42` has type `bool`
Control Flow
Move offers multiple constructs for control flow based on
boolean expressions, including common programming constructs such as
if
expressions and while
and for
loops, along with advanced control flow structures including
labels for loops and escapable named blocks. It also supports more more complex constructs based on
structural pattern matching.
Conditional if
Expressions
An if
expression specifies that some code should only be evaluated if a certain condition is true.
For example:
if (x > 5) x = x - 5
The condition must be an expression of type bool
.
An if
expression can optionally include an else
clause to specify another expression to evaluate
when the condition is false.
if (y <= 10) y = y + 1 else y = 10
Either the "true" branch or the "false" branch will be evaluated, but not both. Either branch can be a single expression or an expression block.
The conditional expressions may produce values so that the if
expression has a result.
let z = if (x < 100) x else 100;
The expressions in the true and false branches must have compatible types. For example:
// x and y must be u64 integers
let maximum: u64 = if (x > y) x else y;
// ERROR! branches different types
let z = if (maximum < 10) 10u8 else 100u64;
// ERROR! branches different types, as default false-branch is () not u64
if (maximum >= 10) maximum;
If the else
clause is not specified, the false branch defaults to the unit value. The following
are equivalent:
if (condition) true_branch // implied default: else ()
if (condition) true_branch else ()
Commonly, if
expressions are used in conjunction with
expression blocks.
let maximum = if (x > y) x else y;
if (maximum < 10) {
x = x + 10;
y = y + 10;
} else if (x >= 10 && y >= 10) {
x = x - 10;
y = y - 10;
}
Grammar for Conditionals
if-expression → if ( expression ) expression else-clauseopt > else-clause → else expression
Loop Constructs in Move
Many programs require iteration over values, and Move provides while
and loop
forms to allow you
to write code in these situations. In addition, you can also modify control flow of these loops
during execution by using break
(to exit the loop) and continue
(to skip the remainder of this
iteration and return to the top of the control flow structure).
while
Loops
The while
construct repeats the body (an expression of type unit) until the condition (an
expression of type bool
) evaluates to false
.
Here is an example of simple while
loop that computes the sum of the numbers from 1
to n
:
fun sum(n: u64): u64 {
let mut sum = 0;
let mut i = 1;
while (i <= n) {
sum = sum + i;
i = i + 1
};
sum
}
Infinite while
loops are also allowed:
fun foo() {
while (true) { }
}
Using break
Inside of while
Loops
In Move, while
loops can use break
to exit early. For example, suppose we were looking for the
position of a value in a vector, and would like to break
if we find it:
fun find_position(values: &vector<u64>, target_value: u64): Option<u64> {
let size = vector::length(values);
let mut i = 0;
let mut found = false;
while (i < size) {
if (vector::borrow(values, i) == &target_value) {
found = true;
break
};
i = i + 1
};
if (found) {
Option::Some(i)
} else {
Option::None
}
}
Here, if the borrowed vector value is equal to our target value, we set the found
flag to true
and then call break
, which will cause the program to exit the loop.
Finally, note that break
for while
loops cannot take a value: while
loops always return the
unit type ()
and thus break
does, too.
Using continue
Inside of while
Loops
Similar to break
, Move's while
loops can invoke continue
to skip over part of the loop body.
This allows us to skip part of a computation if a condition is not met, such as in the following
example:
fun sum_even(values: &vector<u64>): u64 {
let size = vector::length(values);
let mut i = 0;
let mut even_sum = 0;
while (i < size) {
let number = *vector::borrow(values, i);
i = i + 1;
if (number % 2 == 1) continue;
even_sum = even_sum + number;
};
even_sum
}
This code will iterate over the provided vector. For each entry, if that entry is an even number, it
will add it to the even_sum
. If it is not, however, it will call continue
, skipping the sum
operation and returning to the while
loop conditional check.
loop
Expressions
The loop
expression repeats the loop body (an expression with type ()
) until it hits a break
:
fun sum(n: u64): u64 {
let mut sum = 0;
let mut i = 1;
loop {
i = i + 1;
if (i >= n) break;
sum = sum + i;
};
sum
}
Without a break
, the loop will continue forever. In the example below, the program will run
forever because the loop
does not have a break
:
fun foo() {
let mut i = 0;
loop { i = i + 1 }
}
Here is an example that uses loop
to write the sum
function:
fun sum(n: u64): u64 {
let sum = 0;
let i = 0;
loop {
i = i + 1;
if (i > n) break;
sum = sum + i
};
sum
}
Using break
with Values in loop
Unlike while
loops, which always return ()
, a loop
may return a value using break
. In doing
so, the overall loop
expression evaluates to a value of that type. For example, we can rewrite
find_position
from above using loop
and break
, immediately returning the index if we find it:
fun find_position(values: &vector<u64>, target_value: u64): Option<u64> {
let size = vector::length(values);
let mut i = 0;
loop {
if (vector::borrow(values, i) == &target_value) {
break Option::Some(i)
} else if (i >= size) {
break Option::None
};
i = i + 1;
}
}
This loop will break with an option result, and, as the last expression in the function body, will produce that value as the final function result.
Using continue
Inside of loop
Expressions
As you might expect, continue
can also be used inside a loop
. Here is the previous sum_even
function rewritten using loop
with break
and continue
instead of while
.
fun sum_even(values: &vector<u64>): u64 {
let size = vector::length(values);
let mut i = 0;
let mut even_sum = 0;
loop {
if (i >= size) break;
let number = *vector::borrow(values, i);
i = i + 1;
if (number % 2 == 1) continue;
even_sum = even_sum + number;
};
even_sum
}
The Type of while
and loop
In Move, loops are typed expressions. A while
expression always has type ()
.
let () = while (i < 10) { i = i + 1 };
If a loop
contains a break
, the expression has the type of the break. A break with no value has
the unit type ()
.
(loop { if (i < 10) i = i + 1 else break }: ());
let () = loop { if (i < 10) i = i + 1 else break };
let x: u64 = loop { if (i < 10) i = i + 1 else break 5 };
let x: u64 = loop { if (i < 10) { i = i + 1; continue} else break 5 };
In addition, if a loop contains multiple breaks, they must all return the same type:
// invalid -- first break returns (), second returns 5
let x: u64 = loop { if (i < 10) break else break 5 };
If loop
does not have a break
, loop
can have any type much like return
, abort
, break
,
and continue
.
(loop (): u64);
(loop (): address);
(loop (): &vector<vector<u8>>);
If you need even more-precise control flow, such as breaking out of nested loops, the next chapter presents the use of labeled control flow in Move.
Labeled Control Flow
Move supports labeled control flow when writing both loops and blocks of code, allowing you
to break
and continue
loops and return
from blocks (which can be particularly helpful in the
presence of macros).
Loops
Loops allow you to define and transfer control to specific labels in a function. For example, we can
nest two loops and use break
and continue
with those labels to precisely specify control flow.
You can prefix any loop
or while
form with a 'label:
form to allow breaking or continuing
directly there.
To demonstrate this behavior, consider a function that takes nested vectors of numbers (i.e.,
vector<vector<u64>>
) to sum against some threshold, which behaves as follows:
- If the sum of all the numbers are under the threshold, return that sum.
- If adding a number to the current sum would surpass the threshold, return the current sum.
We can write this by iterating over the vector of vectors as nested loops and labelling the outer
one. If any addition in the inner loop would push us over the threshold, we can use break
with the
outer label to escape both loops at once:
fun sum_until_threshold(input: &vector<vector<u64>>, threshold: u64): u64 {
let mut sum = 0;
let mut i = 0;
let input_size = input.length();
'outer: loop {
// breaks to outer since it is the closest enclosing loop
if (i >= input_size) break sum;
let vec = &input[i];
let size = vec.length();
let mut j = 0;
while (j < size) {
let v_entry = vec[j];
if (sum + v_entry < threshold) {
sum = sum + v_entry;
} else {
// the next element we saw would break the threshold,
// so we return the current sum
break 'outer sum
};
j = j + 1;
};
i = i + 1;
}
}
These sorts of labels can also be used with a nested loop form, providing precise control in larger bodies of code. For example, if we were processing a large table where each entry required iteration that might see us continuing the inner or outer loop, we could express that code using labels:
let x = 'outer: loop {
...
'inner: while (cond) {
...
if (cond0) { break 'outer value };
...
if (cond1) { continue 'inner }
else if (cond2) { continue 'outer }
...
}
...
};
Labeled Blocks
Labeled blocks allow you to write Move programs that contain intra-function non-local control flow, including inside of macro lambdas and returning values:
fun named_return(n: u64): vector<u8> {
let x = 'a: {
if (n % 2 == 0) {
return 'a b"even"
};
b"odd"
};
x
}
In this simple example, the program checks if the input n
is even. If it is, the program leaves
the block labeled 'a:
with the value b"even"
. If not, the code continues, ending the block
labeled 'a:
with the value b"odd"
. At the end, we set x
to the value and then return it.
This control flow feature works across macro bodies as well. For example, suppose we wanted to write
a function to find the first even number in a vector, and that we have some macro for_ref
that
iterates the vector elements in a loop:
macro fun for_ref<$T>($vs: &vector<$T>, $f: |&$T|) {
let vs = $vs;
let mut i = 0;
let end = vs.length();
while (i < end) {
$f(vs.borrow(i));
i = i + 1;
}
}
Using for_ref
and a label, we can write a lambda expression to pass for_ref
that will escape the
loop, returning the first even number it finds:
fun find_first_even(vs: vector<u64>): Option<u64> {
'result: {
for_ref!(&vs, |n| if (*n % 2 == 0) { return 'result option::some(*n)});
option::none()
}
}
This function will iterate vs
until it finds an even number, and return that (or return
option::none()
if no even number exists). This makes named labels a powerful tool for interacting
with control flow macros such as for!
, allowing you to customize iteration behavior in those
contexts.
Restrictions
To clarify program behavior, you may only use break
and continue
with loop labels, while
return
will only work with block labels. To this end, the following programs produce errors:
fun bad_loop() {
'name: loop {
return 'name 5
// ^^^^^ Invalid usage of 'return' with a loop block label
}
}
fun bad_block() {
'name: {
continue 'name;
// ^^^^^ Invalid usage of 'break' with a loop block label
break 'name;
// ^^^^^ Invalid usage of 'break' with a loop block label
}
}
Pattern Matching
A match
expression is a powerful control structure that allows you to compare a value against a
series of patterns and then execute code based on which pattern matches first. Patterns can be
anything from simple literals to complex, nested struct and enum definitions. As opposed to if
expressions, which change control flow based on a bool
-typed test expression, a match
expression
operates over a value of any type and selects one of many arms.
A match
expression can match Move values as well as mutable or immutable references, binding
sub-patterns accordingly.
For example:
fun run(x: u64): u64 {
match (x) {
1 => 2,
2 => 3,
x => x,
}
}
run(1); // returns 2
run(2); // returns 3
run(3); // returns 3
run(0); // returns 0
match
Syntax
A match
takes an expression and a non-empty series of match arms delimited by commas.
Each match arm consists of a pattern (p
), an optional guard (if (g)
where g
is an expression
of type bool
), an arrow (=>
), and an arm expression (e
) to execute when the pattern matches.
For example,
match (expression) {
pattern1 if (guard_expression) => expression1,
pattern2 => expression2,
pattern3 => { expression3, expression4, ... },
}
Match arms are checked in order from top to bottom, and the first pattern that matches (with a guard
expression, if present, that evaluates to true
) will be executed.
Note that the series of match arms within a match
must be exhaustive, meaning that every possible
value of the type being matched must be covered by one of the patterns in the match
. If the series
of match arms is not exhaustive, the compiler will raise an error.
Pattern Syntax
A pattern is matched by a value if the value is equal to the pattern, and where variables and
wildcards (e.g., x
, y
, _
, or ..
) are "equal" to anything.
Patterns are used to match values. Patterns can be
Pattern | Description |
---|---|
Literal | A literal value, such as 1 , true , @0x1 |
Constant | A constant value, e.g., MyConstant |
Variable | A variable, e.g., x , y , z |
Wildcard | A wildcard, e.g., _ |
Constructor | A constructor pattern, e.g., MyStruct { x, y } , MyEnum::Variant(x) |
At-pattern | An at-pattern, e.g., x @ MyEnum::Variant(..) |
Or-pattern | An or-pattern, e.g., MyEnum::Variant(..) \| MyEnum::OtherVariant(..) |
Multi-arity wildcard | A multi-arity wildcard, e.g., MyEnum::Variant(..) |
Mutable-binding | A mutable-binding pattern, e.g., mut x |
Patterns in Move have the following grammar:
pattern = <literal>
| <constant>
| <variable>
| _
| C { <variable> : inner-pattern ["," <variable> : inner-pattern]* } // where C is a struct or enum variant
| C ( inner-pattern ["," inner-pattern]* ... ) // where C is a struct or enum variant
| C // where C is an enum variant
| <variable> @ top-level-pattern
| pattern | pattern
| mut <variable>
inner-pattern = pattern
| .. // multi-arity wildcard
Some examples of patterns are:
// literal pattern
1
// constant pattern
MyConstant
// variable pattern
x
// wildcard pattern
_
// constructor pattern that matches `MyEnum::Variant` with the fields `1` and `true`
MyEnum::Variant(1, true)
// constructor pattern that matches `MyEnum::Variant` with the fields `1` and binds the second field's value to `x`
MyEnum::Variant(1, x)
// multi-arity wildcard pattern that matches multiple fields within the `MyEnum::Variant` variant
MyEnum::Variant(..)
// constructor pattern that matches the `x` field of `MyStruct` and binds the `y` field to `other_variable`
MyStruct { x, y: other_variable }
// at-pattern that matches `MyEnum::Variant` and binds the entire value to `x`
x @ MyEnum::Variant(..)
// or-pattern that matches either `MyEnum::Variant` or `MyEnum::OtherVariant`
MyEnum::Variant(..) | MyEnum::OtherVariant(..)
// same as the above or-pattern, but with explicit wildcards
MyEnum::Variant(_, _) | MyEnum::OtherVariant(_, _)
// or-pattern that matches either `MyEnum::Variant` or `MyEnum::OtherVariant` and binds the u64 field to `x`
MyEnum::Variant(x, _) | MyEnum::OtherVariant(_, x)
// constructor pattern that matches `OtherEnum::V` and if the inner `MyEnum` is `MyEnum::Variant`
OtherEnum::V(MyEnum::Variant(..))
Patterns and Variables
Patterns that contain variables bind them to the match subject or subject subcomponent being matched. These variables can then be used either in any match guard expressions, or on the right-hand side of the match arm. For example:
public struct Wrapper(u64)
fun add_under_wrapper_unless_equal(wrapper: Wrapper, x: u64): u64 {
match (wrapper) {
Wrapper(y) if (y == x) => Wrapper(y),
Wrapper(y) => y + x,
}
}
add_under_wrapper_unless_equal(Wrapper(1), 2); // returns Wrapper(3)
add_under_wrapper_unless_equal(Wrapper(2), 3); // returns Wrapper(5)
add_under_wrapper_unless_equal(Wrapper(3), 3); // returns Wrapper(3)
Combining Patterns
Patterns can be nested, but patterns can also be combined using the or operator (|
). For example,
p1 | p2
succeeds if either pattern p1
or p2
matches the subject. This pattern can occur
anywhere -- either as a top-level pattern or a sub-pattern within another pattern.
public enum MyEnum has drop {
Variant(u64, bool),
OtherVariant(bool, u64),
}
fun test_or_pattern(x: u64): u64 {
match (x) {
MyEnum::Variant(1 | 2 | 3, true) | MyEnum::OtherVariant(true, 1 | 2 | 3) => 1,
MyEnum::Variant(8, true) | MyEnum::OtherVariant(_, 6 | 7) => 2,
_ => 3,
}
}
test_or_pattern(MyEnum::Variant(3, true)); // returns 1
test_or_pattern(MyEnum::OtherVariant(true, 2)); // returns 1
test_or_pattern(MyEnum::Variant(8, true)); // returns 2
test_or_pattern(MyEnum::OtherVariant(false, 7)); // returns 2
test_or_pattern(MyEnum::OtherVariant(false, 80)); // returns 3
Restrictions on Some Patterns
The mut
and ..
patterns also have specific conditions placed on when, where, and how they can be
used, as detailed in Limitations on Specific Patterns. At a
high level, the mut
modifier can only be used on variable patterns, and the ..
pattern can only
be used once within a constructor pattern -- and not as a top-level pattern.
The following is an invalid usage of the ..
pattern because it is used as a top-level pattern:
match (x) {
.. => 1,
// ERROR: `..` pattern can only be used within a constructor pattern
}
match (x) {
MyStruct(.., ..) => 1,
// ERROR: ^^ `..` pattern can only be used once within a constructor pattern
}
Pattern Typing
Patterns are not expressions, but they are nevertheless typed. This means that the type of a pattern
must match the type of the value it matches. For example, the pattern 1
has an integer type, the
pattern MyEnum::Variant(1, true)
has type MyEnum
, the pattern MyStruct { x, y }
has type
MyStruct
, and OtherStruct<bool> { x: true, y: 1}
has type OtherStruct<bool>
. If you try to
match on an expression that differs from the type of the pattern in the match, this will result in a
type error. For example:
match (1) {
// The `true` literal pattern is of type `bool` so this is a type error.
true => 1,
// TYPE ERROR: expected type u64, found bool
_ => 2,
}
Similarly, the following would also result in a type error because MyEnum
and MyStruct
are
different types:
match (MyStruct { x: 0, y: 0 }) {
MyEnum::Variant(..) => 1,
// TYPE ERROR: expected type MyEnum, found MyStruct
}
Matching
Prior to delving into the specifics of pattern matching and what it means for a value to "match" a pattern, let's examine a few examples to provide an intuition for the concept.
fun test_lit(x: u64): u8 {
match (x) {
1 => 2,
2 => 3,
_ => 4,
}
}
test_lit(1); // returns 2
test_lit(2); // returns 3
test_lit(3); // returns 4
test_lit(10); // returns 4
fun test_var(x: u64): u64 {
match (x) {
y => y,
}
}
test_var(1); // returns 1
test_var(2); // returns 2
test_var(3); // returns 3
...
const MyConstant: u64 = 10;
fun test_constant(x: u64): u64 {
match (x) {
MyConstant => 1,
_ => 2,
}
}
test_constant(MyConstant); // returns 1
test_constant(10); // returns 1
test_constant(20); // returns 2
fun test_or_pattern(x: u64): u64 {
match (x) {
1 | 2 | 3 => 1,
4 | 5 | 6 => 2,
_ => 3,
}
}
test_or_pattern(3); // returns 1
test_or_pattern(5); // returns 2
test_or_pattern(70); // returns 3
fun test_or_at_pattern(x: u64): u64 {
match (x) {
x @ (1 | 2 | 3) => x + 1,
y @ (4 | 5 | 6) => y + 2,
z => z + 3,
}
}
test_or_pattern(2); // returns 3
test_or_pattern(5); // returns 7
test_or_pattern(70); // returns 73
The most important thing to note from these examples is that a pattern matches a value if the value
is equal to the pattern, and wildcard/variable patterns match anything. This is true for literals,
variables, and constants. For example, in the test_lit
function, the value 1
matches the pattern
1
, the value 2
matches the pattern 2
, and the value 3
matches the wildcard _
. Similarly,
in the test_var
function, both the value 1
and the value 2
matches the pattern y
.
A variable x
matches (or "equals") any value, and a wildcard _
matches any value (but only one
value). Or-patterns are like a logical OR, where a value matches the pattern if it matches any of
patterns in the or-pattern so p1 | p2 | p3
should be read "matches p1, or p2, or p3".
Matching Constructors
Pattern matching includes the concept of constructor patterns. These patterns allow you to inspect and access deep within both structs and enums, and are one of the most powerful parts of pattern matching. Constructor patterns, coupled with variable bindings, allow you to match on values by their structure, and pull out the parts of the value you care about for usage on the right-hand side of the match arm.
Take the following:
fun f(x: MyEnum) {
match (x) {
MyEnum::Variant(1, true) => 1,
MyEnum::OtherVariant(_, 3) => 2,
MyEnum::Variant(..) => 3,
MyEnum::OtherVariant(..) => 4,
}
f(MyEnum::Variant(1, true)); // returns 1
f(MyEnum::Variant(2, true)); // returns 3
f(MyEnum::OtherVariant(false, 3)); // returns 2
f(MyEnum::OtherVariant(true, 3)); // returns 2
f(MyEnum::OtherVariant(true, 2)); // returns 4
This is saying that "if x
is MyEnum::Variant
with the fields 1
and true
, then return 1
. If
it is MyEnum::OtherVariant
with any value for the first field, and 3
for the second, then return
2
. If it is MyEnum::Variant
with any fields, then return 3
. Finally, if it is
MyEnum::OtherVariant
with any fields, then return 4
".
You can also nest patterns. So, if you wanted to match either 1, 2, or 10, instead of just matching
1 in the previous MyEnum::Variant
, you could do so with an or-pattern:
fun f(x: MyEnum) {
match (x) {
MyEnum::Variant(1 | 2 | 10, true) => 1,
MyEnum::OtherVariant(_, 3) => 2,
MyEnum::Variant(..) => 3,
MyEnum::OtherVariant(..) => 4,
}
f(MyEnum::Variant(1, true)); // returns 1
f(MyEnum::Variant(2, true)); // returns 1
f(MyEnum::Variant(10, true)); // returns 1
f(MyEnum::Variant(10, false)); // returns 3
Ability Constraints
Additionally, match bindings are subject to the same ability restrictions as other aspects of Move.
In particular, the compiler will signal an error if you try to match a value (not-reference) without
drop
using a wildcard, as the wildcard expects to drop the value. Similarly, if you bind a
non-drop
value using a binder, it must be used in the right-hand side of the match arm. In
addition, if you fully destruct that value, you have unpacked it, matching the semantics of
non-drop
struct unpacking. See the
abilities section on drop
for more details about the drop
capability.
public struct NonDrop(u64)
fun drop_nondrop(x: NonDrop) {
match (x) {
NonDrop(1) => 1,
_ => 2
// ERROR: cannot wildcard match on a non-droppable value
}
}
fun destructure_nondrop(x: NonDrop) {
match (x) {
NonDrop(1) => 1,
NonDrop(_) => 2
// OK!
}
}
fun use_nondrop(x: NonDrop): NonDrop {
match (x) {
NonDrop(1) => NonDrop(8),
x => x
}
}
Exhaustiveness
The match
expression in Move must be exhaustive: every possible value of the type being matched
must be covered by one of the patterns in one of the match's arms. If the series of match arms is
not exhaustive, the compiler will raise an error. Note that any arm with a guard expression does not
contribute to match exhaustion, as it might fail to match at runtime.
As an example, a match on a u8
is exhaustive only if it matches on every number from 0 to 255
inclusive, unless there is a wildcard or variable pattern present. Similarly, a match on a bool
would need to match on both true
and false
, unless there is a wildcard or variable pattern
present.
For structs, because there is only one type of constructor for the type, only one constructor needs to be matched, but the fields within the struct need to be matched exhaustively as well. Conversely, enums may define multiple variants, and each variant must be matched (including any sub-fields) for the match to be considered exhaustive.
Because underscores and variables are wildcards that match anything, they count as matching all
values of the type they are matching on in that position. Additionally, the multi-arity wildcard
pattern ..
can be used to match on multiple values within a struct or enum variant.
To see some examples of non-exhaustive matches, consider the following:
public enum MyEnum {
Variant(u64, bool),
OtherVariant(bool, u64),
}
public struct Pair<T>(T, T)
fun f(x: MyEnum): u8 {
match (x) {
MyEnum::Variant(1, true) => 1,
MyEnum::Variant(_, _) => 1,
MyEnum::OtherVariant(_, 3) => 2,
// ERROR: not exhaustive as the value `MyEnum::OtherVariant(_, 4)` is not matched.
}
}
fun match_pair_bool(x: Pair<bool>): u8 {
match (x) {
Pair(true, true) => 1,
Pair(true, false) => 1,
Pair(false, false) => 1,
// ERROR: not exhaustive as the value `Pair(false, true)` is not matched.
}
}
These examples can then be made exhaustive by adding a wildcard pattern to the end of the match arm, or by fully matching on the remaining values:
fun f(x: MyEnum): u8 {
match (x) {
MyEnum::Variant(1, true) => 1,
MyEnum::Variant(_, _) => 1,
MyEnum::OtherVariant(_, 3) => 2,
// Now exhaustive since this will match all values of MyEnum::OtherVariant
MyEnum::OtherVariant(..) => 2,
}
}
fun match_pair_bool(x: Pair<bool>): u8 {
match (x) {
Pair(true, true) => 1,
Pair(true, false) => 1,
Pair(false, false) => 1,
// Now exhaustive since this will match all values of Pair<bool>
Pair(false, true) => 1,
}
}
Guards
As previously mentioned, you can add a guard to a match arm by adding an if
clause after the
pattern. This guard will run after the pattern has been matched but before the expression on the
right hand side of the arrow is evaluated. If the guard expression evaluates to true
then the
expression on the right hand side of the arrow will be evaluated, if it evaluates to false
then it
will be considered a failed match and the next match arm in the match
expression will be checked.
fun match_with_guard(x: u64): u64 {
match (x) {
1 if (false) => 1,
1 => 2,
_ => 3,
}
}
match_with_guard(1); // returns 2
match_with_guard(0); // returns 3
Guard expressions can reference variables bound in the pattern during evaluation. However, note that variables are only available as immutable reference in guards regardless of the pattern being matched -- even if there are mutability specifiers on the variable or if the pattern is being matched by value.
fun incr(x: &mut u64) {
*x = *x + 1;
}
fun match_with_guard_incr(x: u64): u64 {
match (x) {
x if ({ incr(&mut x); x == 1 }) => 1,
// ERROR: ^^^ invalid borrow of immutable value
_ => 2,
}
}
fun match_with_guard_incr2(x: &mut u64): u64 {
match (x) {
x if ({ incr(&mut x); x == 1 }) => 1,
// ERROR: ^^^ invalid borrow of immutable value
_ => 2,
}
}
Additionally, it is important to note any match arms that have guard expressions will not be considered either for exhaustivity purposes because the compiler has no way of evaluating the guard expression statically.
Limitations on Specific Patterns
There are some restrictions on when the ..
and mut
pattern modifiers can be used in a pattern.
Mutability Usage
A mut
modifier can be placed on a variable pattern to specify that the variable is to be mutated
in the right-hand expression of the match arm. Note that since the mut
modifier only signifies
that the variable is to be mutated, not the underlying data, this can be used on all types of match
(by value, immutable reference, and mutable reference).
Note that the mut
modifier can only be applied to variables, and not other types of patterns.
public struct MyStruct(u64)
fun top_level_mut(x: MyStruct) {
match (x) {
mut MyStruct(y) => 1,
// ERROR: cannot use mut on a non-variable pattern
}
}
fun mut_on_immut(x: &MyStruct): u64 {
match (x) {
MyStruct(mut y) => {
y = &(*y + 1);
*y
}
}
}
fun mut_on_value(x: MyStruct): u64 {
match (x) {
MyStruct(mut y) => {
*y = *y + 1;
*y
},
}
}
fun mut_on_mut(x: &mut MyStruct): u64 {
match (x) {
MyStruct(mut y) => {
*y = *y + 1;
*y
},
}
}
let mut x = MyStruct(1);
mut_on_mut(&mut x); // returns 2
x.0; // returns 2
mut_on_immut(&x); // returns 3
x.0; // returns 2
mut_on_value(x); // returns 3
..
Usage
The ..
pattern can only be used within a constructor pattern as a wildcard that matches any number
of fields -- the
the compiler expands the ..
to inserting _
in any missing fields in the constructor pattern (if
any). So MyStruct(_, _, _)
is the same as MyStruct(..)
, MyStruct(1, _, _)
is the same as
MyStruct(1, ..)
. Because of this, there are some restrictions on how, and where the ..
pattern
can be used:
- It can only be used once within the constructor pattern;
- In positional arguments it can be used at the beginning, middle, or end of the patterns within the constructor;
- In named arguments it can only be used at the end of the patterns within the constructor;
public struct MyStruct(u64, u64, u64, u64) has drop;
public struct MyStruct2 {
x: u64,
y: u64,
z: u64,
w: u64,
}
fun wild_match(x: MyStruct) {
match (x) {
MyStruct(.., 1) => 1,
// OK! The `..` pattern can be used at the beginning of the constructor pattern
MyStruct(1, ..) => 2,
// OK! The `..` pattern can be used at the end of the constructor pattern
MyStruct(1, .., 1) => 3,
// OK! The `..` pattern can be used at the middle of the constructor pattern
MyStruct(1, .., 1, 1) => 4,
MyStruct(..) => 5,
}
}
fun wild_match2(x: MyStruct2) {
match (x) {
MyStruct2 { x: 1, .. } => 1,
MyStruct2 { x: 1, w: 2 .. } => 2,
MyStruct2 { .. } => 3,
}
}
Functions
Functions are declared inside of modules and define the logic and behavior of the module. Functions can be reused, either being called from other functions or as entry points for execution.
Declaration
Functions are declared with the fun
keyword followed by the function name, type parameters,
parameters, a return type, and finally the function body.
<visibility>? <entry>? <macro>? fun <identifier><[type_parameters: constraint],*>([identifier: type],*): <return_type> <function_body>
For example
fun foo<T1, T2>(x: u64, y: T1, z: T2): (T2, T1, u64) { (z, y, x) }
Visibility
Module functions, by default, can only be called within the same module. These internal (sometimes called private) functions cannot be called from other modules or as entry points.
module a::m {
fun foo(): u64 { 0 }
fun calls_foo(): u64 { foo() } // valid
}
module b::other {
fun calls_m_foo(): u64 {
a::m::foo() // ERROR!
// ^^^^^^^^^^^ 'foo' is internal to 'a::m'
}
}
To allow access from other modules, the function must be declared public
or public(package)
.
Tangential to visibility, an entry
function can be called as an entry point for
execution.
public
visibility
A public
function can be called by any function defined in any module. As shown in the
following example, a public
function can be called by:
- other functions defined in the same module,
- functions defined in another module, or
- as an entry point for execution.
module a::m {
public fun foo(): u64 { 0 }
fun calls_foo(): u64 { foo() } // valid
}
module b::other {
fun calls_m_foo(): u64 {
a::m::foo() // valid
}
}
Fore more details on the entry point to execution see the section below.
public(package)
visibility
The public(package)
visibility modifier is a more restricted form of the public
modifier to give
more control about where a function can be used. A public(package)
function can be called by:
- other functions defined in the same module, or
- other functions defined in the same package (the same address)
module a::m {
public(package) fun foo(): u64 { 0 }
fun calls_foo(): u64 { foo() } // valid
}
module a::n {
fun calls_m_foo(): u64 {
a::m::foo() // valid, also in `a`
}
}
module b::other {
fun calls_m_foo(): u64 {
b::m::foo() // ERROR!
// ^^^^^^^^^^^ 'foo' can only be called from a module in `a`
}
}
DEPRECATED public(friend)
visibility
Before the addition of public(package)
, public(friend)
was used to allow limited public access
to functions in the same package, but where the list of allowed modules had to be explicitly
enumerated by the callee's module. see Friends for more details.
entry
modifier
In addition to public
functions, you might have some functions in your modules that you want to
use as the entry point to execution. The entry
modifier is designed to allow module functions to
initiate execution, without having to expose the functionality to other modules.
Essentially, the combination of public
and entry
functions define the "main" functions of a
module, and they specify where Move programs can start executing.
Keep in mind though, an entry
function can still be called by other Move functions. So while
they can serve as the start of a Move program, they aren't restricted to that case.
For example:
module a::m {
entry fun foo(): u64 { 0 }
fun calls_foo(): u64 { foo() } // valid!
}
module a::n {
fun calls_m_foo(): u64 {
a::m::foo() // ERROR!
// ^^^^^^^^^^^ 'foo' is internal to 'a::m'
}
}
entry
functions may have restrictions on their parameters and return types. Although, these
restrictions are specific to each individual deployment of Move.
The documentation for entry
functions on Sui can be found here..
To enable easier testing, entry
functions can be called from
#[test]
and #[test_only]
contexts.
module a::m {
entry fun foo(): u64 { 0 }
}
module a::m_test {
#[test]
fun my_test(): u64 { a::m::foo() } // valid!
#[test_only]
fun my_test_helper(): u64 { a::m::foo() } // valid!
}
macro
modifier
Unlike normal functions, macro
functions do not exist at runtime. Instead, these functions are
substituted inline at each call site during compilation. These macro
functions leverage this
compilation process to provide functionality beyond standard functions, such as accepting
higher-order lambda-style functions as arguments. These lambda arguments, also expanded during
compilation, allow you to pass parts of the function body to the macro as arguments. For instance,
consider the following simple loop macro, where the loop body is supplied as a lambda:
macro fun ntimes($n: u64, $body: |u64| -> ()) {
let n = $n;
let mut i = 0;
while (i < n) {
$body(i);
i = i + 1;
}
}
fun example() {
let mut sum = 0;
ntimes!(10, |x| sum = sum + x );
}
See the chapter on macros for more information.
Name
Function names can start with letters a
to z
. After the first character, function names can
contain underscores _
, letters a
to z
, letters A
to Z
, or digits 0
to 9
.
fun fOO() {}
fun bar_42() {}
fun bAZ_19() {}
Type Parameters
After the name, functions can have type parameters
fun id<T>(x: T): T { x }
fun example<T1: copy, T2>(x: T1, y: T2): (T1, T1, T2) { (copy x, x, y) }
For more details, see Move generics.
Parameters
Functions parameters are declared with a local variable name followed by a type annotation
fun add(x: u64, y: u64): u64 { x + y }
We read this as x
has type u64
A function does not have to have any parameters at all.
fun useless() { }
This is very common for functions that create new or empty data structures
module a::example {
public struct Counter { count: u64 }
fun new_counter(): Counter {
Counter { count: 0 }
}
}
Return type
After the parameters, a function specifies its return type.
fun zero(): u64 { 0 }
Here : u64
indicates that the function's return type is u64
.
Using tuples, a function can return multiple values:
fun one_two_three(): (u64, u64, u64) { (0, 1, 2) }
If no return type is specified, the function has an implicit return type of unit ()
. These
functions are equivalent:
fun just_unit(): () { () }
fun just_unit() { () }
fun just_unit() { }
As mentioned in the tuples section, these tuple "values" do not exist
as runtime values. This means that a function that returns unit ()
does not return any value
during execution.
Function body
A function's body is an expression block. The return value of the function is the last value in the sequence
fun example(): u64 {
let x = 0;
x = x + 1;
x // returns 'x'
}
See the section below for more information on returns
For more information on expression blocks, see Move variables.
Native Functions
Some functions do not have a body specified, and instead have the body provided by the VM. These
functions are marked native
.
Without modifying the VM source code, a programmer cannot add new native functions. Furthermore, it
is the intent that native
functions are used for either standard library code or for functionality
needed for the given Move environment.
Most native
functions you will likely see are in standard library code, such as vector
module std::vector {
native public fun length<Element>(v: &vector<Element>): u64;
...
}
Calling
When calling a function, the name can be specified either through an alias or fully qualified
module a::example {
public fun zero(): u64 { 0 }
}
module b::other {
use a::example::{Self, zero};
fun call_zero() {
// With the `use` above all of these calls are equivalent
a::example::zero();
example::zero();
zero();
}
}
When calling a function, an argument must be given for every parameter.
module a::example {
public fun takes_none(): u64 { 0 }
public fun takes_one(x: u64): u64 { x }
public fun takes_two(x: u64, y: u64): u64 { x + y }
public fun takes_three(x: u64, y: u64, z: u64): u64 { x + y + z }
}
module b::other {
fun call_all() {
a::example::takes_none();
a::example::takes_one(0);
a::example::takes_two(0, 1);
a::example::takes_three(0, 1, 2);
}
}
Type arguments can be either specified or inferred. Both calls are equivalent.
module a::example {
public fun id<T>(x: T): T { x }
}
module b::other {
fun call_all() {
a::example::id(0);
a::example::id<u64>(0);
}
}
For more details, see Move generics.
Returning values
The result of a function, its "return value", is the final value of its function body. For example
fun add(x: u64, y: u64): u64 {
x + y
}
The return value here is the result of x + y
.
As mentioned above, the function's body is an expression block. The expression block can sequence various statements, and the final expression in the block will be be the value of that block
fun double_and_add(x: u64, y: u64): u64 {
let double_x = x * 2;
let double_y = y * 2;
double_x + double_y
}
The return value here is the result of double_x + double_y
return
expression
A function implicitly returns the value that its body evaluates to. However, functions can also use
the explicit return
expression:
fun f1(): u64 { return 0 }
fun f2(): u64 { 0 }
These two functions are equivalent. In this slightly more involved example, the function subtracts
two u64
values, but returns early with 0
if the second value is too large:
fun safe_sub(x: u64, y: u64): u64 {
if (y > x) return 0;
x - y
}
Note that the body of this function could also have been written as if (y > x) 0 else x - y
.
However return
really shines is in exiting deep within other control flow constructs. In this
example, the function iterates through a vector to find the index of a given value:
use std::vector;
use std::option::{Self, Option};
fun index_of<T>(v: &vector<T>, target: &T): Option<u64> {
let i = 0;
let n = vector::length(v);
while (i < n) {
if (vector::borrow(v, i) == target) return option::some(i);
i = i + 1
};
option::none()
}
Using return
without an argument is shorthand for return ()
. That is, the following two
functions are equivalent:
fun foo() { return }
fun foo() { return () }
Macro Functions
Macro functions are a way of defining functions that are expanded during compilation at each call site. The arguments of the macro are not evaluated eagerly like a normal function, and instead are substituted by expression. In addition, the caller can supply code to the macro via lambdas.
These expression substitution mechanics make macro
functions similar
to macros found in other programming languages;
however, they are more constrained in Move than you might expect from other languages. The
parameters and return values of macro
functions are still typed--though this can be partially
relaxed with the _
type. The upside of this restriction however, is that
macro
functions can be used anywhere a normal function can be used, which is notably helpful with
method syntax.
A more extensive syntactic macro system may come in the future.
Syntax
macro
functions have a similar syntax to normal functions. However, all type parameter names and
all parameter names must start with a $
. Note that _
can still be used by itself, but not as a
prefix, and $_
must be used instead.
<visibility>? macro fun <identifier><[$type_parameters: constraint],*>([$identifier: type],*): <return_type> <function_body>
For example, the following macro
function takes a vector and a lambda, and applies the lambda to
each element of the vector to construct a new vector.
macro fun map<$T, $U>($v: vector<$T>, $f: |$T| -> $U): vector<$U> {
let mut v = $v;
v.reverse();
let mut i = 0;
let mut result = vector[];
while (!v.is_empty()) {
result.push_back($f(v.pop_back()));
i = i + 1;
};
result
}
The $
is there to indicate that the parameters (both type and value parameters) do not behave like
their normal, non-macro counterparts. For type parameters, they can be instantiated with any type
(even a reference type &
or &mut
), and they will satisfy any constraint. Similarly for
parameters, they will not be evaluated eagerly, and instead the argument expression will be
substituted at each usage.
Lambdas
Lambdas are a new type of expression that can only be used with macro
s. These are used to pass
code from the caller into the body of the macro
. While the substitution is done at compile time,
they are used similarly to anonymous functions,
lambdas, or
closures in other languages.
As seen in the example above ($f: |$T| -> $U
), lambda types are defined with the syntax
|<type>,*| (-> <type>)?
A few examples
|u64, u64| -> u128 // a lambda that takes two u64s and returns a u128
|&mut vector<u8>| -> &mut u8 // a lambda that takes a &mut vector<u8> and returns a &mut u8
If the return type is not annotated, it is unit ()
by default.
// the following are equivalent
|&mut vector<u8>, u64|
|&mut vector<u8>, u64| -> ()
Lambda expressions are then defined at the call site of the macro
with the syntax
|(<identifier> (: <type>)?),*| <expression>
|(<identifier> (: <type>)?),*| -> <type> { <expression> }
Note that if the return type is annotated, the body of the lambda must be enclosed in {}
.
Using the map
macro defined above
let v = vector[1, 2, 3];
let doubled: vector<u64> = map!(v, |x| 2 * x);
let bytes: vector<vector<u8>> = map!(v, |x| std::bcs::to_bytes(&x));
And with type annotations
let doubled: vector<u64> = map!(v, |x: u64| 2 * x); // return type annotation optional
let bytes: vector<vector<u8>> = map!(v, |x: u64| -> vector<u8> { std::bcs::to_bytes(&x) });
Capturing
Lambda expressions can also refer to variables in the scope where the lambda is defined. This is sometimes called "capturing".
let res = foo();
let incremented = map!(vector[1, 2, 3], |x| x + res);
Any variable can be captured, including mutable and immutable references.
See the Examples section for more complicated usages.
Limitations
Currently, lambdas can only be used directly in the call of a macro
function. They cannot be bound
to a variable. For example, the following is code will produce an error:
let f = |x| 2 * x;
// ^^^^^^^^^ Error! Lambdas must be used directly in 'macro' calls
let doubled: vector<u64> = map!(vector[1, 2, 3], f);
Typing
Like normal functions, macro
functions are typed--the types of the parameters and return value
must be annotated. However, the body of the function is not type checked until the macro is
expanded. This means that not all usages of a given macro may be valid. For example
macro fun add_one<$T>($x: $T): $T {
$x + 1
}
The above macro will not type check if $T
is not a primitive integer type.
This can be particularly useful in conjunction with method syntax, where the function is not resolved until after the macro is expanded.
macro fun call_foo<$T, $U>($x: $T): &$U {
$x.foo()
}
This macro will only expand successfully if $T
has a method foo
that returns a reference &$U
.
As described in the hygiene section, foo
will be resolved based on the scope where
call_foo
was defined--not where it was expanded.
Type Parameters
Type parameters can be instantiated with any type, including reference types &
and &mut
. They
can also be instantiated with tuple types, though the utility of this
is limited currently since tuples cannot be bound to a variable.
This relaxation forces the constraints of a type parameter to be satisfied at the call site in a way that does not normally occur. It is generally recommended however to add all necessary constraints to a type parameter. For example
public struct NoAbilities()
public struct CopyBox<T: copy> has copy, drop { value: T }
macro fun make_box<$T>($x: $T): CopyBox<$T> {
CopyBox { value: $x }
}
This macro will expand only if $T
is instantiated with a type with the copy
ability.
make_box!(1); // Valid!
make_box!(NoAbilities()); // Error! 'NoAbilities' does not have the copy ability
The suggested declaration of make_box
would be to add the copy
constraint to the type parameter.
This then communicates to the caller that the type must have the copy
ability.
macro fun make_box<$T: copy>($x: $T): CopyBox<$T> {
CopyBox { value: $x }
}
One might reasonably ask then, why have this relaxation if the recommendation is not to use it? The
constraints on type parameters simply cannot be enforced in all cases because the bodies are not
checked until expansion. In the following example, the copy
constraint on $T
is not necessary in
the signature, but is necessary in the body.
macro fun read_ref<$T>($r: &$T): $T {
*$r
}
If however, you want to have an extremely relaxed type signature, it is instead recommended to use
the _
type.
_
Type
Normally, the _
placeholder type is used in expressions to allow for
partial annotations of type arguments. However, with macro
functions, the _
type can be used in
place of type parameters to relax the signature for any type. This should increase the ergonomics of
declaring "generic" macro
functions.
For example, we could take any combination of integers and add them together.
macro fun add($x: _, $y: _, $z: _): u256 {
($x as u256) + ($y as u256) + ($z as u256)
}
Additionally, the _
type can be instantiated multiple times with different types. For example
public struct Box<T> has copy, drop, store { value: T }
macro fun create_two($f: |_| -> Box<_>): (Box<u8>, Box<u16>) {
($f(0u8), $f(0u16))
}
If we declared the function with type parameters instead, the types would have to unify to a common type, which is not possible in this case.
macro fun create_two<$T>($f: |$T| -> Box<$T>): (Box<u8>, Box<u16>) {
($f(0u8), $f(0u16))
// ^^^^ Error! expected `u8` but found `u16`
}
...
let (a, b) = create_two!(|value| Box { value });
In this case, $T
must be instantiated with a single type, but inference finds that $T
must be
bound to both u8
and u16
.
There is a tradeoff however, as the _
type conveys less meaning and intention for the caller.
Consider map
macro from above re-declared with _
instead of $T
and $U
.
macro fun map($v: vector<_>, $f: |_| -> _): vector<_> {
There is no longer any indication of behavior of $f
at the type level. The caller must gain
understanding from comments or the body of the macro.
Expansion and Substitution
The body of the macro
is substituted into the call site at compile time. Each parameter is
replaced by the expression, not the value, of its argument. For lambdas, additional local
variables can have values bound within the context of the macro
body.
Taking a very simple example
macro fun apply($f: |u64| -> u64, $x: u64): u64 {
$f($x)
}
With the call site
let incremented = apply!(|x| x + 1, 5);
This will roughly be expanded to
let incremented = {
let x = { 5 };
{ x + 1 }
};
Again, the value of x
is not substituted, but the expression 5
is. This might mean that an
argument is evaluated multiple times, or not at all, depending on the body of the macro
.
macro fun dup($f: |u64, u64| -> u64, $x: u64): u64 {
$f($x, $x)
}
let sum = dup!(|x, y| x + y, foo());
is expanded to
let sum = {
let x = { foo() };
let y = { foo() };
{ x + y }
};
Note that foo()
will be called twice. Which would not happen if dup
were a normal function.
It is often recommended to create predictable evaluation behavior by binding arguments to local variables.
macro fun dup($f: |u64, u64| -> u64, $x: u64): u64 {
let a = $x;
$f(a, a)
}
Now that same call site will expand to
let sum = {
let a = { foo() };
{
let x = { a };
let y = { a };
{ x + y }
}
};
Hygiene
In the example above, the dup
macro had a local variable a
that was used to bind the argument
$x
. You might ask, what would happen if the variable was instead named x
? Would that conflict
with the x
in the lambda?
The short answer is, no. macro
functions are
hygienic, meaning that the expansion of macro
s and
lambdas will not accidentally capture variables from another scope.
The compiler does this by associating a unique number with each scope. When the macro
is expanded,
the macro body gets its own scope. Additionally, the arguments are re-scoped on each usage.
Modifying the dup
macro to use x
instead of a
macro fun dup($f: |u64, u64| -> u64, $x: u64): u64 {
let a = $x;
$f(a, a)
}
The expansion of the call site
// let sum = dup!(|x, y| x + y, foo());
let sum = {
let x#1 = { foo() };
{
let x#2 = { x#1 };
let y#2 = { x#1 };
{ x#2 + y#2 }
}
};
This is an approximation of the compiler's internal representation, some details are omitted for the simplicity of this example.
And each usage of an argument is re-scoped so that the different usages do not conflict.
macro fun apply_twice($f: |u64| -> u64, $x: u64): u64 {
$f($x) + $f($x)
}
let result = apply_twice!(|x| x + 1, { let x = 5; x });
Expands to
let result = {
{
let x#1 = { let x#2 = { 5 }; x#2 };
{ x#1 + x#1 }
}
+
{
let x#3 = { let x#4 = { 5 }; x#4 };
{ x#3 + x#3 }
}
};
Similar to variable hygiene, method resolution is also scoped to the macro definition. For example
public struct S { f: u64, g: u64 }
fun f(s: &S): u64 {
s.f
}
fun g(s: &S): u64 {
s.g
}
use fun f as foo;
macro fun call_foo($s: &S): u64 {
let s = $s;
s.foo()
}
The method call foo
will in this case always resolve to the function f
, even if call_foo
is
used in a scope where foo
is bound to a different function, such as g
.
fun example(s: &S): u64 {
use fun g as foo;
call_foo!(s) // expands to 'f(s)', not 'g(s)'
}
Due to this though, unused use fun
declarations might not get warnings in modules with macro
functions.
Control Flow
Similar to variable hygiene, control flow constructs are also always scoped to where they are defined, not to where they are expanded.
macro fun maybe_div($x: u64, $y: u64): u64 {
let x = $x;
let y = $y;
if (y == 0) return 0;
x / y
}
At the call site, return
will always return from the macro
body, not from the caller.
let result: vector<u64> = vector[maybe_div!(10, 0)];
Will expand to
let result: vector<u64> = vector['a: {
let x = { 10 };
let y = { 0 };
if (y == 0) return 'a 0;
x / y
}];
Where return 'a 0
will return to the block 'a: { ... }
and not to the caller's body. See the
section on labeled control flow for more details.
Similarly, return
in a lambda will return from the lambda, not from the macro
body and not from
the outer function.
macro fun apply($f: |u64| -> u64, $x: u64): u64 {
$f($x)
}
and
let result = apply!(|x| { if (x == 0) return 0; x + 1 }, 100);
will expand to
let result = {
let x = { 100 };
'a: {
if (x == 0) return 'a 0;
x + 1
}
};
In addition to returning from the lambda, a label can be used to return to the outer function. In
the vector::any
macro, a return
with a label is used to return from the entire macro
early
public macro fun any<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool {
let v = $v;
'any: {
v.do_ref!(|e| if ($f(e)) return 'any true);
false
}
}
The return 'any true
exits from the "loop" early when the condition is met. Otherwise, the macro
"returns" false
.
Method Syntax
When applicable, macro
functions can be called using method syntax. When
using method syntax, the evaluation of the arguments will change in that the first argument (the
"receiver" of the method) will be evaluated outside of the macro expansion. This example is
contrived, but will concisely demonstrate the behavior.
public struct S() has copy, drop;
public fun foo(): S { abort 0 }
public macro fun maybe_s($s: S, $cond: bool): S {
if ($cond) $s
else S()
}
Even though foo()
will abort, its return type can be used to start a method call.
$s
will not be evaluated if $cond
is false
, and under a normal non-method call, an argument of
foo()
would not be evaluated and would not abort. The following example demonstrates $s
not
being evaluated with an argument of foo()
.
maybe_s!(foo(), false) // does not abort
It becomes more clear as to why it does not abort when looking at the expanded form
if (false) foo()
else S()
However, when using method syntax, the first argument is evaluated before the macro is expanded. So
the same argument of foo()
for $s
will now be evaluated and will abort.
foo().maybe_s!(false) // aborts
We can see this more clearly when looking the expanded form
let tmp = foo(); // aborts
if (false) tmp
else S()
Conceptually, the receiver for a method call is bound to a temporary variable before the macro is expanded, which forces the evaluation and thus the abort.
Parameter Limitations
The parameters of a macro
function must always be used as expressions. They cannot be used in
situations where the argument might be re-interpreted. For example, the following is not allowed
macro fun no($x: _): _ {
$x.f
}
The reason is that if the argument $x
was not a reference, it would be borrowed first, which would
could re-interpret the argument. To get around this limitation, you should bind the argument to a
local variable.
macro fun yes($x: _): _ {
let x = $x;
x.f
}
Examples
Lazy arguments: assert_eq
macro fun assert_eq<$T>($left: $T, $right: $T, $code: u64) {
let left = $left;
let right = $right;
if (left != right) {
std::debug::print(&b"assertion failed.\n left: ");
std::debug::print(&left);
std::debug::print(&b"\n does not equal right: ");
std::debug::print(&right);
abort $code;
}
}
In this case the argument to $code
is not evaluated unless the assertion fails.
assert_eq!(vector[true, false], vector[true, false], 1 / 0); // division by zero is not evaluated
Any integer square root
This macro calculates the integer square root for any integer type, besides u256
.
$T
is the type of the input and $bitsize
is the number of bits in that type, for example u8
has 8 bits. $U
should be set to the next larger integer type, for example u16
for u8
.
In this macro
, the type of the integer literals are 1
and 0
are annotated, e.g. (1: $U)
allowing for the type of the literal to differ with each call. Similarly, as
can be used with the
type parameters $T
and $U
. This macro will then only successfully expand if $T
and $U
are
instantiated with the integer types.
macro fun num_sqrt<$T, $U>($x: $T, $bitsize: u8): $T {
let x = $x;
let mut bit = (1: $U) << $bitsize;
let mut res = (0: $U);
let mut x = x as $U;
while (bit != 0) {
if (x >= res + bit) {
x = x - (res + bit);
res = (res >> 1) + bit;
} else {
res = res >> 1;
};
bit = bit >> 2;
};
res as $T
}
Iterating over a vector
The two macro
s iterate over a vector, immutably and mutably respectively.
macro fun for_imm<$T>($v: &vector<$T>, $f: |&$T|) {
let v = $v;
let n = v.length();
let mut i = 0;
while (i < n) {
$f(&v[i]);
i = i + 1;
}
}
macro fun for_mut<$T>($v: &mut vector<$T>, $f: |&mut $T|) {
let v = $v;
let n = v.length();
let mut i = 0;
while (i < n) {
$f(&mut v[i]);
i = i + 1;
}
}
A few examples of usage
fun imm_examples(v: &vector<u64>) {
// print all elements
for_imm!(v, |x| std::debug::print(x));
// sum all elements
let mut sum = 0;
for_imm!(v, |x| sum = sum + x);
// find the max element
let mut max = 0;
for_imm!(v, |x| if (x > max) max = x);
}
fun mut_examples(v: &mut vector<u64>) {
// increment each element
for_mut!(v, |x| *x = *x + 1);
// set each element to the previous value, and the first to last value
let mut prev = v[v.length() - 1];
for_mut!(v, |x| {
let tmp = *x;
*x = prev;
prev = tmp;
});
// set the max element to 0
let mut max = &mut 0;
for_mut!(v, |x| if (*x > *max) max = x);
*max = 0;
}
Non-loop lambda usage
Lambdas do not need to be used in loops, and are often useful for conditionally applying code.
macro fun inspect<$T>($opt: &Option<$T>, $f: |&$T|) {
let opt = $opt;
if (opt.is_some()) $f(opt.borrow())
}
macro fun is_some_and<$T>($opt: &Option<$T>, $f: |&$T| -> bool): bool {
let opt = $opt;
if (opt.is_some()) $f(opt.borrow())
else false
}
macro fun map<$T, $U>($opt: Option<$T>, $f: |$T| -> $U): Option<$U> {
let opt = $opt;
if (opt.is_some()) {
option::some($f(opt.destroy_some()))
} else {
opt.destroy_none();
option::none()
}
}
And some examples of usage
fun examples(opt: Option<u64>) {
// print the value if it exists
inspect!(&opt, |x| std::debug::print(x));
// check if the value is 0
let is_zero = is_some_and!(&opt, |x| *x == 0);
// upcast the u64 to a u256
let str_opt = map!(opt, |x| x as u256);
}
Structs and Resources
A struct is a user-defined data structure containing typed fields. Structs can store any non-reference, non-tuple type, including other structs.
Structs can be used to define all "asset" values or unrestricted values, where the operations performed on those values can be controlled by the struct's abilities. By default, structs are linear and ephemeral. By this we mean that they: cannot be copied, cannot be dropped, and cannot be stored in storage. This means that all values have to have ownership transferred (linear) and the values must be dealt with by the end of the program's execution (ephemeral). We can relax this behavior by giving the struct abilities which allow values to be copied or dropped and also to be stored in storage or to define storage schemas.
Defining Structs
Structs must be defined inside a module, and the struct's fields can either be named or positional:
module a::m {
public struct Foo { x: u64, y: bool }
public struct Bar {}
public struct Baz { foo: Foo, }
// ^ note: it is fine to have a trailing comma
public struct PosFoo(u64, bool)
public struct PosBar()
public struct PosBaz(Foo)
}
Structs cannot be recursive, so the following definitions are invalid:
public struct Foo { x: Foo }
// ^ ERROR! recursive definition
public struct A { b: B }
public struct B { a: A }
// ^ ERROR! recursive definition
public struct D(D)
// ^ ERROR! recursive definition
Visibility
As you may have noticed, all structs are declared as public
. This means that the type of the
struct can be referred to from any other module. However, the fields of the struct, and the ability
to create or destroy the struct, are still internal to the module that defines the struct.
In the future, we plan on adding to declare structs as public(package)
or as internal, much like
functions.
Abilities
As mentioned above: by default, a struct declaration is linear and ephemeral. So to allow the value
to be used in these ways (e.g., copied, dropped, stored in an object, or
used to define a storable object), structs can be granted
abilities by annotating them with has <ability>
:
module a::m {
public struct Foo has copy, drop { x: u64, y: bool }
}
The ability declaration can occur either before or after the struct's fields. However, only one or the other can be used, and not both. If declared after the struct's fields, the ability declaration must be terminated with a semicolon:
module a::m {
public struct PreNamedAbilities has copy, drop { x: u64, y: bool }
public struct PostNamedAbilities { x: u64, y: bool } has copy, drop;
public struct PostNamedAbilitiesInvalid { x: u64, y: bool } has copy, drop
// ^ ERROR! missing semicolon
public struct NamedInvalidAbilities has copy { x: u64, y: bool } has drop;
// ^ ERROR! duplicate ability declaration
public struct PrePositionalAbilities has copy, drop (u64, bool)
public struct PostPositionalAbilities (u64, bool) has copy, drop;
public struct PostPositionalAbilitiesInvalid (u64, bool) has copy, drop
// ^ ERROR! missing semicolon
public struct InvalidAbilities has copy (u64, bool) has drop;
// ^ ERROR! duplicate ability declaration
}
For more details, see the section on annotating a struct's abilities.
Naming
Structs must start with a capital letter A
to Z
. After the first letter, struct names can
contain underscores _
, letters a
to z
, letters A
to Z
, or digits 0
to 9
.
public struct Foo {}
public struct BAR {}
public struct B_a_z_4_2 {}
public struct P_o_s_Foo()
This naming restriction of starting with A
to Z
is in place to give room for future language
features. It may or may not be removed later.
Using Structs
Creating Structs
Values of a struct type can be created (or "packed") by indicating the struct name, followed by value for each field.
For a struct with named fields, the order of the fields does not matter, but the field name needs to
be provided. For a struct with positional fields, the order of the fields must match the order of
the fields in the struct definition, and it must be created using ()
instead of {}
to enclose
the parameters.
module a::m {
public struct Foo has drop { x: u64, y: bool }
public struct Baz has drop { foo: Foo }
public struct Positional(u64, bool) has drop;
fun example() {
let foo = Foo { x: 0, y: false };
let baz = Baz { foo: foo };
// Note: positional struct values are created using parentheses and
// based on position instead of name.
let pos = Positional(0, false);
let pos_invalid = Positional(false, 0);
// ^ ERROR! Fields are out of order and the types don't match.
}
}
For structs with named fields, you can use the following shorthand if you have a local variable with the same name as the field:
let baz = Baz { foo: foo };
// is equivalent to
let baz = Baz { foo };
This is sometimes called "field name punning".
Destroying Structs via Pattern Matching
Struct values can be destroyed by binding or assigning them in patterns using similar syntax to constructing them.
module a::m {
public struct Foo { x: u64, y: bool }
public struct Bar(Foo)
public struct Baz {}
public struct Qux()
fun example_destroy_foo() {
let foo = Foo { x: 3, y: false };
let Foo { x, y: foo_y } = foo;
// ^ shorthand for `x: x`
// two new bindings
// x: u64 = 3
// foo_y: bool = false
}
fun example_destroy_foo_wildcard() {
let foo = Foo { x: 3, y: false };
let Foo { x, y: _ } = foo;
// only one new binding since y was bound to a wildcard
// x: u64 = 3
}
fun example_destroy_foo_assignment() {
let x: u64;
let y: bool;
Foo { x, y } = Foo { x: 3, y: false };
// mutating existing variables x and y
// x = 3, y = false
}
fun example_foo_ref() {
let foo = Foo { x: 3, y: false };
let Foo { x, y } = &foo;
// two new bindings
// x: &u64
// y: &bool
}
fun example_foo_ref_mut() {
let foo = Foo { x: 3, y: false };
let Foo { x, y } = &mut foo;
// two new bindings
// x: &mut u64
// y: &mut bool
}
fun example_destroy_bar() {
let bar = Bar(Foo { x: 3, y: false });
let Bar(Foo { x, y }) = bar;
// ^ nested pattern
// two new bindings
// x: u64 = 3
// y: bool = false
}
fun example_destroy_baz() {
let baz = Baz {};
let Baz {} = baz;
}
fun example_destroy_qux() {
let qux = Qux();
let Qux() = qux;
}
}
Accessing Struct Fields
Fields of a struct can be accessed using the dot operator .
.
For structs with named fields, the fields can be accessed by their name:
public struct Foo { x: u64, y: bool }
let foo = Foo { x: 3, y: true };
let x = foo.x; // x == 3
let y = foo.y; // y == true
For positional structs, fields can be accessed by their position in the struct definition:
public struct PosFoo(u64, bool)
let pos_foo = PosFoo(3, true);
let x = pos_foo.0; // x == 3
let y = pos_foo.1; // y == true
Accessing struct fields without borrowing or copying them is subject to the field's ability constraints. For more details see the sections on borrowing structs and fields and reading and writing fields for more information.
Borrowing Structs and Fields
The &
and &mut
operator can be used to create references to structs or fields. These examples
include some optional type annotations (e.g., : &Foo
) to demonstrate the type of operations.
let foo = Foo { x: 3, y: true };
let foo_ref: &Foo = &foo;
let y: bool = foo_ref.y; // reading a field via a reference to the struct
let x_ref: &u64 = &foo.x; // borrowing a field by extending a reference to the struct
let x_ref_mut: &mut u64 = &mut foo.x;
*x_ref_mut = 42; // modifying a field via a mutable reference
It is possible to borrow inner fields of nested structs:
let foo = Foo { x: 3, y: true };
let bar = Bar(foo);
let x_ref = &bar.0.x;
You can also borrow a field via a reference to a struct:
let foo = Foo { x: 3, y: true };
let foo_ref = &foo;
let x_ref = &foo_ref.x;
// this has the same effect as let x_ref = &foo.x
Reading and Writing Fields
If you need to read and copy a field's value, you can then dereference the borrowed field:
let foo = Foo { x: 3, y: true };
let bar = Bar(copy foo);
let x: u64 = *&foo.x;
let y: bool = *&foo.y;
let foo2: Foo = *&bar.0;
More canonically, the dot operator can be used to read fields of a struct without any borrowing. As
is true with
dereferencing, the field
type must have the copy
ability.
let foo = Foo { x: 3, y: true };
let x = foo.x; // x == 3
let y = foo.y; // y == true
Dot operators can be chained to access nested fields:
let bar = Bar(Foo { x: 3, y: true });
let x = baz.0.x; // x = 3;
However, this is not permitted for fields that contain non-primitive types, such a vector or another struct:
let foo = Foo { x: 3, y: true };
let bar = Bar(foo);
let foo2: Foo = *&bar.0;
let foo3: Foo = bar.0; // error! must add an explicit copy with *&
We can mutably borrow a field to a struct to assign it a new value:
let mut foo = Foo { x: 3, y: true };
*&mut foo.x = 42; // foo = Foo { x: 42, y: true }
*&mut foo.y = !foo.y; // foo = Foo { x: 42, y: false }
let mut bar = Bar(foo); // bar = Bar(Foo { x: 42, y: false })
*&mut bar.0.x = 52; // bar = Bar(Foo { x: 52, y: false })
*&mut bar.0 = Foo { x: 62, y: true }; // bar = Bar(Foo { x: 62, y: true })
Similar to dereferencing, we can instead directly use the dot operator to modify a field. And in
both cases, the field type must have the drop
ability.
let mut foo = Foo { x: 3, y: true };
foo.x = 42; // foo = Foo { x: 42, y: true }
foo.y = !foo.y; // foo = Foo { x: 42, y: false }
let mut bar = Bar(foo); // bar = Bar(Foo { x: 42, y: false })
bar.0.x = 52; // bar = Bar(Foo { x: 52, y: false })
bar.0 = Foo { x: 62, y: true }; // bar = Bar(Foo { x: 62, y: true })
The dot syntax for assignment also works via a reference to a struct:
let foo = Foo { x: 3, y: true };
let foo_ref = &mut foo;
foo_ref.x = foo_ref.x + 1;
Privileged Struct Operations
Most struct operations on a struct type T
can only be performed inside the module that declares
T
:
- Struct types can only be created ("packed"), destroyed ("unpacked") inside the module that defines the struct.
- The fields of a struct are only accessible inside the module that defines the struct.
Following these rules, if you want to modify your struct outside the module, you will need to provide public APIs for them. The end of the chapter contains some examples of this.
However as stated in the visibility section above, struct types are always visible to another module
module a::m {
public struct Foo has drop { x: u64 }
public fun new_foo(): Foo {
Foo { x: 42 }
}
}
module a::n {
use a::m::Foo;
public struct Wrapper has drop {
foo: Foo
// ^ valid the type is public
}
fun f1(foo: Foo) {
let x = foo.x;
// ^ ERROR! cannot access fields of `Foo` outside of `a::m`
}
fun f2() {
let foo_wrapper = Wrapper { foo: m::new_foo() };
// ^ valid the function is public
}
}
Ownership
As mentioned above in Defining Structs, structs are by default linear and ephemeral. This means they cannot be copied or dropped. This property can be very useful when modeling real world assets like money, as you do not want money to be duplicated or get lost in circulation.
module a::m {
public struct Foo { x: u64 }
public fun copying() {
let foo = Foo { x: 100 };
let foo_copy = copy foo; // ERROR! 'copy'-ing requires the 'copy' ability
let foo_ref = &foo;
let another_copy = *foo_ref // ERROR! dereference requires the 'copy' ability
}
public fun destroying_1() {
let foo = Foo { x: 100 };
// error! when the function returns, foo still contains a value.
// This destruction requires the 'drop' ability
}
public fun destroying_2(f: &mut Foo) {
*f = Foo { x: 100 } // error!
// destroying the old value via a write requires the 'drop' ability
}
}
To fix the example fun destroying_1
, you would need to manually "unpack" the value:
module a::m {
public struct Foo { x: u64 }
public fun destroying_1_fixed() {
let foo = Foo { x: 100 };
let Foo { x: _ } = foo;
}
}
Recall that you are only able to deconstruct a struct within the module in which it is defined. This can be leveraged to enforce certain invariants in a system, for example, conservation of money.
If on the other hand, your struct does not represent something valuable, you can add the abilities
copy
and drop
to get a struct value that might feel more familiar from other programming
languages:
module a::m {
public struct Foo has copy, drop { x: u64 }
public fun run() {
let foo = Foo { x: 100 };
let foo_copy = foo;
// ^ this code copies foo,
// whereas `let x = move foo` would move foo
let x = foo.x; // x = 100
let x_copy = foo_copy.x; // x = 100
// both foo and foo_copy are implicitly discarded when the function returns
}
}
Storage
Structs can be used to define storage schemas, but the details are different per deployment of Move.
See the documentation for the key
ability and
Sui objects for more details.
Enumerations
An enum is a user-defined data structure containing one or more variants. Each variant can optionally contain typed fields. The number, and types of these fields can differ for each variant in the enumeration. Fields in enums can store any non-reference, non-tuple type, including other structs or enums.
As a simple example, consider the following enum definition in Move:
public enum Action {
Stop,
Pause { duration: u32 },
MoveTo { x: u64, y: u64 },
Jump(u64),
}
This declares an enum Action
that represents different actions that can be taken by a game -- you
can Stop
, Pause
for a given duration, MoveTo
a specific location, or Jump
to a specific
height.
Similar to structs, enums can have abilities that control what operations can be
performed on them. It is important to note however that enums cannot have the key
ability since
they cannot be top-level objects.
Defining Enums
Enums must be defined in a module, an enum must contain at least one variant, and each variant of an enum can either have no fields, positional fields, or named fields. Here are some examples of each:
module a::m {
public enum Foo has drop {
VariantWithNoFields,
// ^ note: it is fine to have a trailing comma after variant declarations
}
public enum Bar has copy, drop {
VariantWithPositionalFields(u64, bool),
}
public enum Baz has drop {
VariantWithNamedFields { x: u64, y: bool, z: Bar },
}
}
Enums cannot be recursive in any of their variants, so the following definitions of an enum are not allowed because they would be recursive in at least one variant.
Incorrect:
module a::m {
public enum Foo {
Recursive(Foo),
// ^ error: recursive enum variant
}
public enum List {
Nil,
Cons { head: u64, tail: List },
// ^ error: recursive enum variant
}
public enum BTree<T> {
Leaf(T),
Node { left: BTree<T>, right: BTree<T> },
// ^ error: recursive enum variant
}
// Mutually recursive enums are also not allowed
public enum MutuallyRecursiveA {
Base,
Other(MutuallyRecursiveB),
// ^^^^^^^^^^^^^^^^^^ error: recursive enum variant
}
public enum MutuallyRecursiveB {
Base,
Other(MutuallyRecursiveA),
// ^^^^^^^^^^^^^^^^^^ error: recursive enum variant
}
}
Visibility
All enums are declared as public
. This means that the type of the enum can be referred to from any
other module. However, the variants of the enum, the fields within each variant, and the ability to
create or destroy variants of the enum are internal to the module that defines the enum.
Abilities
Just like with structs, by default an enum declaration is linear and ephemeral. To use an enum value
in a non-linear or non-ephemeral way -- i.e., copied, dropped, or stored in an
object -- you need to grant it additional abilities by
annotating them with has <ability>
:
module a::m {
public enum Foo has copy, drop {
VariantWithNoFields,
}
}
The ability declaration can occur either before or after the enum's variants, however only one or the other can be used, and not both. If declared after the variants, the ability declaration must be terminated with a semicolon:
module a::m {
public enum PreNamedAbilities has copy, drop { Variant }
public enum PostNamedAbilities { Variant } has copy, drop;
public enum PostNamedAbilitiesInvalid { Variant } has copy, drop
// ^ ERROR! missing semicolon
public enum NamedInvalidAbilities has copy { Variant } has drop;
// ^ ERROR! duplicate ability declaration
}
For more details, see the section on annotating abilities.
Naming
Enums and variants within enums must start with a capital letter A
to Z
. After the first letter,
enum names can contain underscores _
, lowercase letters a
to z
, uppercase letters A
to Z
,
or digits 0
to 9
.
public enum Foo { Variant }
public enum BAR { Variant }
public enum B_a_z_4_2 { V_a_riant_0 }
This naming restriction of starting with A
to Z
is in place to give room for future language
features.
Using Enums
Creating Enum Variants
Values of an enum type can be created (or "packed") by indicating a variant of the enum, followed by a value for each field in the variant. The variant name must always be qualified by the enum's name.
Similarly to structs, for a variant with named fields, the order of the fields does not matter but
the field names need to be provided. For a variant with positional fields, the order of the fields
matters and the order of the fields must match the order in the variant declaration. It must also be
created using ()
instead of {}
. If the variant has no fields, the variant name is sufficient and
no ()
or {}
needs to be used.
module a::m {
public enum Action has drop {
Stop,
Pause { duration: u32 },
MoveTo { x: u64, y: u64 },
Jump(u64),
}
public enum Other has drop {
Stop(u64),
}
fun example() {
// Note: The `Stop` variant of `Action` doesn't have fields so no parentheses or curlies are needed.
let stop = Action::Stop;
let pause = Action::Pause { duration: 10 };
let move_to = Action::MoveTo { x: 10, y: 20 };
let jump = Action::Jump(10);
// Note: The `Stop` variant of `Other` does have positional fields so we need to supply them.
let other_stop = Other::Stop(10);
}
}
For variants with named fields you can also use the shorthand syntax that you might be familiar with from structs to create the variant:
let duration = 10;
let pause = Action::Pause { duration: duration };
// is equivalent to
let pause = Action::Pause { duration };
Pattern Matching Enum Variants and Destructuring
Since enum values can take on different shapes, dot access to fields of variants is not allowed like it is for struct fields. Instead, to access fields within a variant -- either by value, or immutable or mutable reference -- you must use pattern matching.
You can pattern match on Move values by value, immutable reference, and mutable reference. When
pattern matching by value, the value is moved into the match arm. When pattern matching by
reference, the value is borrowed into the match arm (either immutably or mutably). We'll go through
a brief description of pattern matching using match
here, but for more information on pattern
matching using match
in Move see the Pattern Matching
section.
A match
statement is used to pattern match on a Move value and consists of a number of match
arms. Each match arm consists of a pattern, an arrow =>
, and an expression, followed by a comma
,
. The pattern can be a struct, enum variant, binding (x
, y
), wildcard (_
or ..
), constant
(ConstValue
), or literal value (true
, 42
, and so on). The value is matched against each
pattern from the top-down, and will match the first pattern that structurally matches the value.
Once the value is matched, the expression on the right hand side of the =>
is executed.
Additionally, match arms can have optional guards that are checked after the pattern matches but
before the expression is executed. Guards are specified by the if
keyword followed by an
expression that must evaluate to a boolean value before the =>
.
module a::m {
public enum Action has drop {
Stop,
Pause { duration: u32 },
MoveTo { x: u64, y: u64 },
Jump(u64),
}
public struct GameState {
// Fields containing a game state
character_x: u64,
character_y: u64,
character_height: u64,
// ...
}
fun perform_action(stat: &mut GameState, action: Action) {
match (action) {
// Handle the `Stop` variant
Action::Stop => state.stop(),
// Handle the `Pause` variant
// If the duration is 0, do nothing
Action::Pause { duration: 0 } => (),
Action::Pause { duration } => state.pause(duration),
// Handle the `MoveTo` variant
Action::MoveTo { x, y } => state.move_to(x, y),
// Handle the `Jump` variant
// if the game disallows jumps then do nothing
Action::Jump(_) if (state.jumps_not_allowed()) => (),
// otherwise, jump to the specified height
Action::Jump(height) => state.jump(height),
}
}
}
To see how to pattern match on an enum to update values within it mutably, let's take the following example of a simple enum that has two variants, each with a single field. We can then write two functions, one that only increments the value of the first variant, and another that only increments the value of the second variant:
module a::m {
public enum SimpleEnum {
Variant1(u64),
Variant2(u64),
}
public fun incr_enum_variant1(simple_enum: &mut SimpleEnum) {
match (simple_enum) {
SimpleEnum::Variant1(mut value) => *value += 1,
_ => (),
}
}
public fun incr_enum_variant2(simple_enum: &mut SimpleEnum) {
match (simple_enum) {
SimpleEnum::Variant2(mut value) => *value += 1,
_ => (),
}
}
}
Now, if we have a value of SimpleEnum
we can use the functions to increment the value of this
variant:
let mut x = SimpleEnum::Variant1(10);
incr_enum_variant1(&mut x);
assert!(x == SimpleEnum::Variant1(11));
// Doesn't increment since it increments a different variant
incr_enum_variant2(&mut x);
assert!(x == SimpleEnum::Variant1(11));
When pattern matching on a Move value that does not have the drop
ability, the value must be
consumed or destructured in each match arm. If the value is not consumed or destructured in a match
arm, the compiler will raise an error. This is to ensure that all possible values are handled in the
match statement.
As an example, consider the following code:
module a::m {
public enum X { Variant { x: u64 } }
public fun bad(x: X) {
match (x) {
_ => (),
// ^ ERROR! value of type `X` is not consumed or destructured in this match arm
}
}
}
To properly handle this, you will need to destructure X
and all its variants in the match's
arm(s):
module a::m {
public enum X { Variant { x: u64 } }
public fun good(x: X) {
match (x) {
// OK! Compiles since the value is destructured
X::Variant { x: _ } => (),
}
}
}
Overwriting to Enum Values
As long as the enum has the drop
ability, you can overwrite the value of an enum with a new value
of the same type just as you might with other values in Move.
module a::m {
public enum X has drop {
A(u64),
B(u64),
}
public fun overwrite_enum(x: &mut X) {
*x = X::A(10);
}
}
let mut x = X::B(20);
overwrite_enum(&mut x);
assert!(x == X::A(10));
Constants
Constants are a way of giving a name to shared, static values inside of a module
.
The constant's value must be known at compilation. The constant's value is stored in the compiled module. And each time the constant is used, a new copy of that value is made.
Declaration
Constant declarations begin with the const
keyword, followed by a name, a type, and a value.
const <name>: <type> = <expression>;
For example
module a::example {
const MY_ADDRESS: address = @a;
public fun permissioned(addr: address) {
assert!(addr == MY_ADDRESS, 0);
}
}
Naming
Constants must start with a capital letter A
to Z
. After the first letter, constant names can
contain underscores _
, letters a
to z
, letters A
to Z
, or digits 0
to 9
.
const FLAG: bool = false;
const EMyErrorCode: u64 = 0;
const ADDRESS_42: address = @0x42;
Even though you can use letters a
to z
in a constant. The
general style guidelines are to use just uppercase letters A
to Z
,
with underscores _
between each word. For error codes, we use E
as a prefix and then upper camel
case (also known as Pascal case) for the rest of the name, as seen in EMyErrorCode
.
The current naming restriction of starting with A
to Z
is in place to give room for future
language features.
Visibility
public
or public(package)
constants are not currently supported. const
values can be used only
in the declaring module. However, as a convenience, they can be used across modules in
unit tests attributes.
Valid Expressions
Currently, constants are limited to the primitive types bool
, u8
, u16
, u32
, u64
, u128
,
u256
, address
, and vector<T>
, where T
is the valid type for a constant.
Values
Commonly, const
s are assigned a simple value, or literal, of their type. For example
const MY_BOOL: bool = false;
const MY_ADDRESS: address = @0x70DD;
const BYTES: vector<u8> = b"hello world";
const HEX_BYTES: vector<u8> = x"DEADBEEF";
Complex Expressions
In addition to literals, constants can include more complex expressions, as long as the compiler is able to reduce the expression to a value at compile time.
Currently, equality operations, all boolean operations, all bitwise operations, and all arithmetic operations can be used.
const RULE: bool = true && false;
const CAP: u64 = 10 * 100 + 1;
const SHIFTY: u8 = {
(1 << 1) * (1 << 2) * (1 << 3) * (1 << 4)
};
const HALF_MAX: u128 = 340282366920938463463374607431768211455 / 2;
const REM: u256 =
57896044618658097711785492504343953926634992332820282019728792003956564819968 % 654321;
const EQUAL: bool = 1 == 1;
If the operation would result in a runtime exception, the compiler will give an error that it is unable to generate the constant's value
const DIV_BY_ZERO: u64 = 1 / 0; // ERROR!
const SHIFT_BY_A_LOT: u64 = 1 << 100; // ERROR!
const NEGATIVE_U64: u64 = 0 - 1; // ERROR!
Additionally, constants can refer to other constants within the same module.
const BASE: u8 = 4;
const SQUARE: u8 = BASE * BASE;
Note though, that any cycle in the constant definitions results in an error.
const A: u16 = B + 1;
const B: u16 = A + 1; // ERROR!
Generics
Generics can be used to define functions and structs over different input data types. This language feature is sometimes referred to as parametric polymorphism. In Move, we will often use the term generics interchangeably with type parameters and type arguments.
Generics are commonly used in library code, such as in vector, to declare code that works over any possible type (that satisfies the specified constraints). This sort of parameterization allows you to reuse the same implementation across multiple types and situations.
Declaring Type Parameters
Both functions and structs can take a list of type parameters in their signatures, enclosed by a
pair of angle brackets <...>
.
Generic Functions
Type parameters for functions are placed after the function name and before the (value) parameter list. The following code defines a generic identity function that takes a value of any type and returns that value unchanged.
fun id<T>(x: T): T {
// this type annotation is unnecessary but valid
(x: T)
}
Once defined, the type parameter T
can be used in parameter types, return types, and inside the
function body.
Generic Structs
Type parameters for structs are placed after the struct name, and can be used to name the types of the fields.
public struct Foo<T> has copy, drop { x: T }
public struct Bar<T1, T2> has copy, drop {
x: T1,
y: vector<T2>,
}
Note that type parameters do not have to be used
Type Arguments
Calling Generic Functions
When calling a generic function, one can specify the type arguments for the function's type parameters in a list enclosed by a pair of angle brackets.
fun foo() {
let x = id<bool>(true);
}
If you do not specify the type arguments, Move's type inference will supply them for you.
Using Generic Structs
Similarly, one can attach a list of type arguments for the struct's type parameters when constructing or destructing values of generic types.
fun foo() {
// type arguments on construction
let foo = Foo<bool> { x: true };
let bar = Bar<u64, u8> { x: 0, y: vector<u8>[] };
// type arguments on destruction
let Foo<bool> { x } = foo;
let Bar<u64, u8> { x, y } = bar;
}
In any case if you do not specify the type arguments, Move's type inference will supply them for you.
Type Argument Mismatch
If you specify the type arguments and they conflict with the actual values supplied, an error will be given:
fun foo() {
let x = id<u64>(true); // ERROR! true is not a u64
}
and similarly:
fun foo() {
let foo = Foo<bool> { x: 0 }; // ERROR! 0 is not a bool
let Foo<address> { x } = foo; // ERROR! bool is incompatible with address
}
Type Inference
In most cases, the Move compiler will be able to infer the type arguments so you don't have to write them down explicitly. Here's what the examples above would look like if we omit the type arguments:
fun foo() {
let x = id(true);
// ^ <bool> is inferred
let foo = Foo { x: true };
// ^ <bool> is inferred
let Foo { x } = foo;
// ^ <bool> is inferred
}
Note: when the compiler is unable to infer the types, you'll need annotate them manually. A common scenario is to call a function with type parameters appearing only at return positions.
module a::m {
fun foo() {
let v = vector[]; // ERROR!
// ^ The compiler cannot figure out the element type, since it is never used
let v = vector<u64>[];
// ^~~~~ Must annotate manually in this case.
}
}
Note that these cases are a bit contrived since the vector[]
is never used, ad as such, Move's
type inference cannot infer the type.
However, the compiler will be able to infer the type if that value is used later in that function:
module a::m {
fun foo() {
let v = vector[];
// ^ <u64> is inferred
vector::push_back(&mut v, 42);
// ^ <u64> is inferred
}
}
_
Type
In some cases, you might want to explicitly annotate some of the type arguments, but let the
compiler infer the others. The _
type serves as such a placeholder for the compiler to infer the
type.
let bar = Bar<u64, _> { x: 0, y: vector[b"hello"] };
// ^ vector<u8> is inferred
The placeholder _
may only appear in expressions and macro function definitions, not signatures.
This means you cannot use _
as part of the definition of a function parameter, function return
type, constant definition type, and datatype field.
Integers
In Move, the integer types u8
, u16
, u32
, u64
, u128
, and u256
are all distinct types.
However, each one of these types can be created with the same numerical value syntax. In other
words, if a type suffix is not provided, the compiler will infer the integer type based on the usage
of the value.
let x8: u8 = 0;
let x16: u16 = 0;
let x32: u32 = 0;
let x64: u64 = 0;
let x128: u128 = 0;
let x256: u256 = 0;
If the value is not used in a context that requires a specific integer type, u64
is taken as a
default.
let x = 0;
// ^ u64 is used by default
If the value however is too large for the inferred type, an error will be given
let i: u8 = 256; // ERROR!
// ^^^ too large for u8
let x = 340282366920938463463374607431768211454;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ too large for u64
In cases where the number is too large, you might need to annotate it explicitly
let x = 340282366920938463463374607431768211454u128;
// ^^^^ valid!
Unused Type Parameters
For a struct definition, an unused type parameter is one that does not appear in any field defined in the struct, but is checked statically at compile time. Move allows unused type parameters so the following struct definition is valid:
public struct Foo<T> {
foo: u64
}
This can be convenient when modeling certain concepts. Here is an example:
module a::m {
// Currency Specifiers
public struct A {}
public struct B {}
// A generic coin type that can be instantiated using a currency
// specifier type.
// e.g. Coin<A>, Coin<B> etc.
public struct Coin<Currency> has store {
value: u64
}
// Write code generically about all currencies
public fun mint_generic<Currency>(value: u64): Coin<Currency> {
Coin { value }
}
// Write code concretely about one currency
public fun mint_a(value: u64): Coin<A> {
mint_generic(value)
}
public fun mint_b(value: u64): Coin<B> {
mint_generic(value)
}
}
In this example, Coin<Currency>
is generic on the Currency
type parameter, which specifies the
currency of the coin and allows code to be written either generically on any currency or concretely
on a specific currency. This generality applies even when the Currency
type parameter does not
appear in any of the fields defined in Coin
.
Phantom Type Parameters
In the example above, although struct Coin
asks for the store
ability, neither Coin<A>
nor
Coin<B>
will have the store
ability. This is because of the rules for
Conditional Abilities and Generic Types
and the fact that A
and B
don't have the store
ability, despite the fact that they are not
even used in the body of struct Coin
. This might cause some unpleasant consequences. For example,
we are unable to put Coin<A>
into a wallet in storage.
One possible solution would be to add spurious ability annotations to A
and B
(i.e.,
public struct Currency1 has store {}
). But, this might lead to bugs or security vulnerabilities
because it weakens the types with unnecessary ability declarations. For example, we would never
expect a value in the storage to have a field in type A
, but this would be possible with the
spurious store
ability. Moreover, the spurious annotations would be infectious, requiring many
functions generic on the unused type parameter to also include the necessary constraints.
Phantom type parameters solve this problem. Unused type parameters can be marked as phantom type
parameters, which do not participate in the ability derivation for structs. In this way, arguments
to phantom type parameters are not considered when deriving the abilities for generic types, thus
avoiding the need for spurious ability annotations. For this relaxed rule to be sound, Move's type
system guarantees that a parameter declared as phantom
is either not used at all in the struct
definition, or it is only used as an argument to type parameters also declared as phantom
.
Declaration
In a struct definition a type parameter can be declared as phantom by adding the phantom
keyword
before its declaration.
public struct Coin<phantom Currency> has store {
value: u64
}
If a type parameter is declared as phantom we say it is a phantom type parameter. When defining a struct, Move's type checker ensures that every phantom type parameter is either not used inside the struct definition or it is only used as an argument to a phantom type parameter.
public struct S1<phantom T1, T2> { f: u64 }
// ^^^^^^^ valid, T1 does not appear inside the struct definition
public struct S2<phantom T1, T2> { f: S1<T1, T2> }
// ^^^^^^^ valid, T1 appears in phantom position
The following code shows examples of violations of the rule:
public struct S1<phantom T> { f: T }
// ^^^^^^^ ERROR! ^ Not a phantom position
public struct S2<T> { f: T }
public struct S3<phantom T> { f: S2<T> }
// ^^^^^^^ ERROR! ^ Not a phantom position
More formally, if a type is used as an argument to a phantom type parameter we say the type appears in phantom position. With this definition in place, the rule for the correct use of phantom parameters can be specified as follows: A phantom type parameter can only appear in phantom position.
Note that specifying phantom
is not required, but the compiler will warn if a type parameter could
be phantom
but was not marked as such.
Instantiation
When instantiating a struct, the arguments to phantom parameters are excluded when deriving the struct abilities. For example, consider the following code:
public struct S<T1, phantom T2> has copy { f: T1 }
public struct NoCopy {}
public struct HasCopy has copy {}
Consider now the type S<HasCopy, NoCopy>
. Since S
is defined with copy
and all non-phantom
arguments have copy
then S<HasCopy, NoCopy>
also has copy
.
Phantom Type Parameters with Ability Constraints
Ability constraints and phantom type parameters are orthogonal features in the sense that phantom parameters can be declared with ability constraints.
public struct S<phantom T: copy> {}
When instantiating a phantom type parameter with an ability constraint, the type argument has to
satisfy that constraint, even though the parameter is phantom. The usual restrictions apply and T
can only be instantiated with arguments having copy
.
Constraints
In the examples above, we have demonstrated how one can use type parameters to define "unknown" types that can be plugged in by callers at a later time. This however means the type system has little information about the type and has to perform checks in a very conservative way. In some sense, the type system must assume the worst case scenario for an unconstrained generic--a type with no abilities.
Constraints offer a way to specify what properties these unknown types have so the type system can allow operations that would otherwise be unsafe.
Declaring Constraints
Constraints can be imposed on type parameters using the following syntax.
// T is the name of the type parameter
T: <ability> (+ <ability>)*
The <ability>
can be any of the four abilities, and a type parameter can be
constrained with multiple abilities at once. So all of the following would be valid type parameter
declarations:
T: copy
T: copy + drop
T: copy + drop + store + key
Verifying Constraints
Constraints are checked at instantiation sites
public struct Foo<T: copy> { x: T }
public struct Bar { x: Foo<u8> }
// ^^ valid, u8 has `copy`
public struct Baz<T> { x: Foo<T> }
// ^ ERROR! T does not have 'copy'
And similarly for functions
fun unsafe_consume<T>(x: T) {
// ERROR! x does not have 'drop'
}
fun consume<T: drop>(x: T) {
// valid, x will be dropped automatically
}
public struct NoAbilities {}
fun foo() {
let r = NoAbilities {};
consume<NoAbilities>(NoAbilities);
// ^^^^^^^^^^^ ERROR! NoAbilities does not have 'drop'
}
And some similar examples, but with copy
fun unsafe_double<T>(x: T) {
(copy x, x)
// ERROR! T does not have 'copy'
}
fun double<T: copy>(x: T) {
(copy x, x) // valid, T has 'copy'
}
public struct NoAbilities {}
fun foo(): (NoAbilities, NoAbilities) {
let r = NoAbilities {};
double<NoAbilities>(r)
// ^ ERROR! NoAbilities does not have 'copy'
}
For more information, see the abilities section on conditional abilities and generic types.
Limitations on Recursions
Recursive Structs
Generic structs can not contain fields of the same type, either directly or indirectly, even with different type arguments. All of the following struct definitions are invalid:
public struct Foo<T> {
x: Foo<u64> // ERROR! 'Foo' containing 'Foo'
}
public struct Bar<T> {
x: Bar<T> // ERROR! 'Bar' containing 'Bar'
}
// ERROR! 'A' and 'B' forming a cycle, which is not allowed either.
public struct A<T> {
x: B<T, u64>
}
public struct B<T1, T2> {
x: A<T1>
y: A<T2>
}
Advanced Topic: Type-level Recursions
Move allows generic functions to be called recursively. However, when used in combination with generic structs, this could create an infinite number of types in certain cases, and allowing this means adding unnecessary complexity to the compiler, vm and other language components. Therefore, such recursions are forbidden.
This restriction might be relaxed in the future, but for now, the following examples should give you an idea of what is allowed and what is not.
module a::m {
public struct A<T> {}
// Finitely many types -- allowed.
// foo<T> -> foo<T> -> foo<T> -> ... is valid
fun foo<T>() {
foo<T>();
}
// Finitely many types -- allowed.
// foo<T> -> foo<A<u64>> -> foo<A<u64>> -> ... is valid
fun foo<T>() {
foo<A<u64>>();
}
}
Not allowed:
module a::m {
public struct A<T> {}
// Infinitely many types -- NOT allowed.
// error!
// foo<T> -> foo<A<T>> -> foo<A<A<T>>> -> ...
fun foo<T>() {
foo<Foo<T>>();
}
}
And similarly, not allowed:
module a::n {
public struct A<T> {}
// Infinitely many types -- NOT allowed.
// error!
// foo<T1, T2> -> bar<T2, T1> -> foo<T2, A<T1>>
// -> bar<A<T1>, T2> -> foo<A<T1>, A<T2>>
// -> bar<A<T2>, A<T1>> -> foo<A<T2>, A<A<T1>>>
// -> ...
fun foo<T1, T2>() {
bar<T2, T1>();
}
fun bar<T1, T2> {
foo<T1, A<T2>>();
}
}
Note, the check for type level recursions is based on a conservative analysis on the call sites and does NOT take control flow or runtime values into account.
module a::m {
public struct A<T> {}
// Infinitely many types -- NOT allowed.
// error!
fun foo<T>(n: u64) {
if (n > 0) foo<A<T>>(n - 1);
}
}
The function in the example above will technically terminate for any given input and therefore only creating finitely many types, but it is still considered invalid by Move's type system.
Abilities
Abilities are a typing feature in Move that control what actions are permissible for values of a given type. This system grants fine grained control over the "linear" typing behavior of values, as well as if and how values are used in storage (as defined by the specific deployment of Move, e.g. the notion of storage for the blockchain). This is implemented by gating access to certain bytecode instructions so that for a value to be used with the bytecode instruction, it must have the ability required (if one is required at all—not every instruction is gated by an ability).
For Sui, key
is used to signify an object. Objects are the basic unit of
storage where each object has a unique, 32-byte ID. store
is then used to both indicate what data
can be stored inside of an object, and is also used to indicate what types can be transferred
outside of their defining module.
The Four Abilities
The four abilities are:
copy
- Allows values of types with this ability to be copied.
drop
- Allows values of types with this ability to be popped/dropped.
store
- Allows values of types with this ability to exist inside a value in storage.
- For Sui,
store
controls what data can be stored inside of an object.store
also controls what types can be transferred outside of their defining module.
key
- Allows the type to serve as a "key" for storage. Ostensibly this means the value can be a top-level value in storage; in other words, it does not need to be contained in another value to be in storage.
- For Sui,
key
is used to signify an object.
copy
The copy
ability allows values of types with that ability to be copied. It gates the ability to
copy values out of local variables with the copy
operator and to
copy values via references with
dereference *e
.
If a value has copy
, all values contained inside of that value have copy
.
drop
The drop
ability allows values of types with that ability to be dropped. By dropped, we mean that
value is not transferred and is effectively destroyed as the Move program executes. As such, this
ability gates the ability to ignore values in a multitude of locations, including:
- not using the value in a local variable or parameter
- not using the value in a sequence via
;
- overwriting values in variables in assignments
- overwriting values via references when
writing
*e1 = e2
.
If a value has drop
, all values contained inside of that value have drop
.
store
The store
ability allows values of types with this ability to exist inside of a value in storage,
but not necessarily as a top-level value in storage. This is the only ability that does not
directly gate an operation. Instead it gates the existence in storage when used in tandem with
key
.
If a value has store
, all values contained inside of that value have store
.
For Sui, store
serves double duty. It controls what values can appear inside of an
object, and what objects can be
transferred outside of their defining module.
key
The key
ability allows the type to serve as a key for storage operations as defined by the
deployment of Move. While it is specific per Move instance, it serves to gates all storage
operations, so in order for a type to be used with storage primitives, the type must have the key
ability.
If a value has key
, all values contained inside of that value have store
. This is the only
ability with this sort of asymmetry.
For Sui, key
is used to signify an object.
Builtin Types
All primitive, builtin types have copy
, drop
, and store
.
bool
,u8
,u16
,u32
,u64
,u128
,u256
, andaddress
all havecopy
,drop
, andstore
.vector<T>
may havecopy
,drop
, andstore
depending on the abilities ofT
.- See Conditional Abilities and Generic Types for more details.
- Immutable references
&
and mutable references&mut
both havecopy
anddrop
.- This refers to copying and dropping the reference itself, not what they refer to.
- References cannot appear in global storage, hence they do not have
store
.
Note that none of the primitive types have key
, meaning none of them can be used directly with
storage operations.
Annotating Structs and Enums
To declare that a struct
or enum
has an ability, it is declared with has <ability>
after the
datatype name and either before or after the fields/variants. For example:
public struct Ignorable has drop { f: u64 }
public struct Pair has copy, drop, store { x: u64, y: u64 }
public struct MyVec(vector<u64>) has copy, drop, store;
public enum IgnorableEnum has drop { Variant }
public enum PairEnum has copy, drop, store { Variant }
public enum MyVecEnum { Variant } has copy, drop, store;
In this case: Ignorable*
has the drop
ability. Pair*
and MyVec*
both have copy
, drop
,
and store
.
All of these abilities have strong guarantees over these gated operations. The operation can be performed on the value only if it has that ability; even if the value is deeply nested inside of some other collection!
As such: when declaring a struct’s abilities, certain requirements are placed on the fields. All fields must satisfy these constraints. These rules are necessary so that structs satisfy the reachability rules for the abilities given above. If a struct is declared with the ability...
copy
, all fields must havecopy
.drop
, all fields must havedrop
.store
, all fields must havestore
.key
, all fields must havestore
.key
is the only ability currently that doesn’t require itself.
An enum can have any of these abilities with the exception of key
, which enums cannot have because
they cannot be top-level values (objects) in storage. The same rules apply to fields of enum
variants as they do for struct fields though. In particular, if an enum is declared with the
ability...
copy
, all fields of all variants must havecopy
.drop
, all fields of all variants must havedrop
.store
, all fields of all variants must havestore
.key
, is not allowed on enums as previously mentioned.
For example:
// A struct without any abilities
public struct NoAbilities {}
public struct WantsCopy has copy {
f: NoAbilities, // ERROR 'NoAbilities' does not have 'copy'
}
public enum WantsCopyEnum has copy {
Variant1
Variant2(NoAbilities), // ERROR 'NoAbilities' does not have 'copy'
}
and similarly:
// A struct without any abilities
public struct NoAbilities {}
public struct MyData has key {
f: NoAbilities, // Error 'NoAbilities' does not have 'store'
}
public struct MyDataEnum has store {
Variant1,
Variant2(NoAbilities), // Error 'NoAbilities' does not have 'store'
}
Conditional Abilities and Generic Types
When abilities are annotated on a generic type, not all instances of that type are guaranteed to have that ability. Consider this struct declaration:
// public struct Cup<T> has copy, drop, store, key { item: T }
It might be very helpful if Cup
could hold any type, regardless of its abilities. The type system
can see the type parameter, so it should be able to remove abilities from Cup
if it sees a
type parameter that would violate the guarantees for that ability.
This behavior might sound a bit confusing at first, but it might be more understandable if we think
about collection types. We could consider the builtin type vector
to have the following type
declaration:
vector<T> has copy, drop, store;
We want vector
s to work with any type. We don't want separate vector
types for different
abilities. So what are the rules we would want? Precisely the same that we would want with the field
rules above. So, it would be safe to copy a vector
value only if the inner elements can be copied.
It would be safe to ignore a vector
value only if the inner elements can be ignored/dropped. And,
it would be safe to put a vector
in storage only if the inner elements can be in storage.
To have this extra expressiveness, a type might not have all the abilities it was declared with
depending on the instantiation of that type; instead, the abilities a type will have depends on both
its declaration and its type arguments. For any type, type parameters are pessimistically
assumed to be used inside of the struct, so the abilities are only granted if the type parameters
meet the requirements described above for fields. Taking Cup
from above as an example:
Cup
has the abilitycopy
only ifT
hascopy
.- It has
drop
only ifT
hasdrop
. - It has
store
only ifT
hasstore
. - It has
key
only ifT
hasstore
.
Here are examples for this conditional system for each ability:
Example: conditional copy
public struct NoAbilities {}
public struct S has copy, drop { f: bool }
public struct Cup<T> has copy, drop, store { item: T }
fun example(c_x: Cup<u64>, c_s: Cup<S>) {
// Valid, 'Cup<u64>' has 'copy' because 'u64' has 'copy'
let c_x2 = copy c_x;
// Valid, 'Cup<S>' has 'copy' because 'S' has 'copy'
let c_s2 = copy c_s;
}
fun invalid(c_account: Cup<signer>, c_n: Cup<NoAbilities>) {
// Invalid, 'Cup<signer>' does not have 'copy'.
// Even though 'Cup' was declared with copy, the instance does not have 'copy'
// because 'signer' does not have 'copy'
let c_account2 = copy c_account;
// Invalid, 'Cup<NoAbilities>' does not have 'copy'
// because 'NoAbilities' does not have 'copy'
let c_n2 = copy c_n;
}
Example: conditional drop
public struct NoAbilities {}
public struct S has copy, drop { f: bool }
public struct Cup<T> has copy, drop, store { item: T }
fun unused() {
Cup<bool> { item: true }; // Valid, 'Cup<bool>' has 'drop'
Cup<S> { item: S { f: false }}; // Valid, 'Cup<S>' has 'drop'
}
fun left_in_local(c_account: Cup<signer>): u64 {
let c_b = Cup<bool> { item: true };
let c_s = Cup<S> { item: S { f: false }};
// Valid return: 'c_account', 'c_b', and 'c_s' have values
// but 'Cup<signer>', 'Cup<bool>', and 'Cup<S>' have 'drop'
0
}
fun invalid_unused() {
// Invalid, Cannot ignore 'Cup<NoAbilities>' because it does not have 'drop'.
// Even though 'Cup' was declared with 'drop', the instance does not have 'drop'
// because 'NoAbilities' does not have 'drop'
Cup<NoAbilities> { item: NoAbilities {} };
}
fun invalid_left_in_local(): u64 {
let n = Cup<NoAbilities> { item: NoAbilities {} };
// Invalid return: 'c_n' has a value
// and 'Cup<NoAbilities>' does not have 'drop'
0
}
Example: conditional store
public struct Cup<T> has copy, drop, store { item: T }
// 'MyInnerData is declared with 'store' so all fields need 'store'
struct MyInnerData has store {
yes: Cup<u64>, // Valid, 'Cup<u64>' has 'store'
// no: Cup<signer>, Invalid, 'Cup<signer>' does not have 'store'
}
// 'MyData' is declared with 'key' so all fields need 'store'
struct MyData has key {
yes: Cup<u64>, // Valid, 'Cup<u64>' has 'store'
inner: Cup<MyInnerData>, // Valid, 'Cup<MyInnerData>' has 'store'
// no: Cup<signer>, Invalid, 'Cup<signer>' does not have 'store'
}
Example: conditional key
public struct NoAbilities {}
public struct MyData<T> has key { f: T }
fun valid(addr: address) acquires MyData {
// Valid, 'MyData<u64>' has 'key'
transfer(addr, MyData<u64> { f: 0 });
}
fun invalid(addr: address) {
// Invalid, 'MyData<NoAbilities>' does not have 'key'
transfer(addr, MyData<NoAbilities> { f: NoAbilities {} })
// Invalid, 'MyData<NoAbilities>' does not have 'key'
borrow<NoAbilities>(addr);
// Invalid, 'MyData<NoAbilities>' does not have 'key'
borrow_mut<NoAbilities>(addr);
}
// Mock storage operation
native public fun transfer<T: key>(addr: address, value: T);
Uses and Aliases
The use
syntax can be used to create aliases to members in other modules. use
can be used to
create aliases that last either for the entire module, or for a given expression block scope.
Syntax
There are several different syntax cases for use
. Starting with the most simple, we have the
following for creating aliases to other modules
use <address>::<module name>;
use <address>::<module name> as <module alias name>;
For example
use std::vector;
use std::option as o;
use std::vector;
introduces an alias vector
for std::vector
. This means that anywhere you
would want to use the module name std::vector
(assuming this use
is in scope), you could use
vector
instead. use std::vector;
is equivalent to use std::vector as vector;
Similarly use std::option as o;
would let you use o
instead of std::option
use std::vector;
use std::option as o;
fun new_vec(): vector<o::Option<u8>> {
let mut v = vector[];
vector::push_back(&mut v, o::some(0));
vector::push_back(&mut v, o::none());
v
}
If you want to import a specific module member (such as a function or struct). You can use the following syntax.
use <address>::<module name>::<module member>;
use <address>::<module name>::<module member> as <member alias>;
For example
use std::vector::push_back;
use std::option::some as s;
This would let you use the function std::vector::push_back
without full qualification. Similarly
for std::option::some
with s
. Instead you could use push_back
and s
respectively. Again,
use std::vector::push_back;
is equivalent to use std::vector::push_back as push_back;
use std::vector::push_back;
use std::option::some as s;
fun new_vec(): vector<std::option::Option<u8>> {
let mut v = vector[];
vector::push_back(&mut v, s(0));
vector::push_back(&mut v, std::option::none());
v
}
Multiple Aliases
If you want to add aliases for multiple module members at once, you can do so with the following syntax
use <address>::<module name>::{<module member>, <module member> as <member alias> ... };
For example
use std::vector::push_back;
use std::option::{some as s, none as n};
fun new_vec(): vector<std::option::Option<u8>> {
let mut v = vector[];
push_back(&mut v, s(0));
push_back(&mut v, n());
v
}
Self aliases
If you need to add an alias to the Module itself in addition to module members, you can do that in a
single use
using Self
. Self
is a member of sorts that refers to the module.
use std::option::{Self, some, none};
For clarity, all of the following are equivalent:
use std::option;
use std::option as option;
use std::option::Self;
use std::option::Self as option;
use std::option::{Self};
use std::option::{Self as option};
Multiple Aliases for the Same Definition
If needed, you can have as many aliases for any item as you like
use std::vector::push_back;
use std::option::{Option, some, none};
fun new_vec(): vector<Option<u8>> {
let mut v = vector[];
push_back(&mut v, some(0));
push_back(&mut v, none());
v
}
Nested imports
In Move, you can also import multiple names with the same use
declaration. This brings all
provided names into scope:
use std::{
vector::{Self as vec, push_back},
string::{String, Self as str}
};
fun example(s: &mut String) {
let mut v = vec::empty();
push_back(&mut v, 0);
push_back(&mut v, 10);
str::append_utf8(s, v);
}
Inside a module
Inside of a module
all use
declarations are usable regardless of the order of declaration.
module a::example {
use std::vector;
fun new_vec(): vector<Option<u8>> {
let mut v = vector[];
vector::push_back(&mut v, 0);
vector::push_back(&mut v, 10);
v
}
use std::option::{Option, some, none};
}
The aliases declared by use
in the module usable within that module.
Additionally, the aliases introduced cannot conflict with other module members. See Uniqueness for more details
Inside an expression
You can add use
declarations to the beginning of any expression block
module a::example {
fun new_vec(): vector<Option<u8>> {
use std::vector::push_back;
use std::option::{Option, some, none};
let mut v = vector[];
push_back(&mut v, some(0));
push_back(&mut v, none());
v
}
}
As with let
, the aliases introduced by use
in an expression block are removed at the end of that
block.
module a::example {
fun new_vec(): vector<Option<u8>> {
let result = {
use std::vector::push_back;
use std::option::{Option, some, none};
let mut v = vector[];
push_back(&mut v, some(0));
push_back(&mut v, none());
v
};
result
}
}
Attempting to use the alias after the block ends will result in an error
fun new_vec(): vector<Option<u8>> {
let mut result = {
use std::vector::push_back;
use std::option::{Option, some, none};
let mut v = vector[];
push_back(&mut v, some(0));
v
};
push_back(&mut result, std::option::none());
// ^^^^^^ ERROR! unbound function 'push_back'
result
}
Any use
must be the first item in the block. If the use
comes after any expression or let
, it
will result in a parsing error
{
let mut v = vector[];
use std::vector; // ERROR!
}
This allows you to shorten your import blocks in many situations. Note that these imports, as the previous ones, are all subject to the naming and uniqueness rules described in the following sections.
Naming rules
Aliases must follow the same rules as other module members. This means that aliases to structs (and
constants) must start with A
to Z
module a::data {
public struct S {}
const FLAG: bool = false;
public fun foo() {}
}
module a::example {
use a::data::{
S as s, // ERROR!
FLAG as fLAG, // ERROR!
foo as FOO, // valid
foo as bar, // valid
};
}
Uniqueness
Inside a given scope, all aliases introduced by use
declarations must be unique.
For a module, this means aliases introduced by use
cannot overlap
module a::example {
use std::option::{none as foo, some as foo}; // ERROR!
// ^^^ duplicate 'foo'
use std::option::none as bar;
use std::option::some as bar; // ERROR!
// ^^^ duplicate 'bar'
}
And, they cannot overlap with any of the module's other members
module a::data {
public struct S {}
}
module example {
use a::data::S;
public struct S { value: u64 } // ERROR!
// ^ conflicts with alias 'S' above
}
}
Inside of an expression block, they cannot overlap with each other, but they can shadow other aliases or names from an outer scope
Shadowing
use
aliases inside of an expression block can shadow names (module members or aliases) from the
outer scope. As with shadowing of locals, the shadowing ends at the end of the expression block;
module a::example {
public struct WrappedVector { vec: vector<u64> }
public fun empty(): WrappedVector {
WrappedVector { vec: std::vector::empty() }
}
public fun push_back(v: &mut WrappedVector, value: u64) {
std::vector::push_back(&mut v.vec, value);
}
fun example1(): WrappedVector {
use std::vector::push_back;
// 'push_back' now refers to std::vector::push_back
let mut vec = vector[];
push_back(&mut vec, 0);
push_back(&mut vec, 1);
push_back(&mut vec, 10);
WrappedVector { vec }
}
fun example2(): WrappedVector {
let vec = {
use std::vector::push_back;
// 'push_back' now refers to std::vector::push_back
let mut v = vector[];
push_back(&mut v, 0);
push_back(&mut v, 1);
v
};
// 'push_back' now refers to Self::push_back
let mut res = WrappedVector { vec };
push_back(&mut res, 10);
res
}
}
Unused Use or Alias
An unused use
will result in a warning
module a::example {
use std::option::{some, none}; // Warning!
// ^^^^ unused alias 'none'
public fun example(): std::option::Option<u8> {
some(0)
}
}
Methods
As a syntactic convenience, some functions in Move can be called as "methods" on a value. This is done
by using the .
operator to call the function, where the value on the left-hand side of the .
is
the first argument to the function (sometimes called the receiver). The type of that value
statically determines which function is called. This is an important difference from some other
languages, where this syntax might indicate a dynamic call, where the function to be called is
determined at runtime. In Move, all function calls are statically determined.
In short, this syntax exists to make it easier to call functions without having to create an alias
with use
, and without having to explicitly borrow the first argument to the function.
Additionally, this can make code more readable, as it reduces the amount of boilerplate needed to
call a function and makes it easier to chain function calls.
Syntax
The syntax for calling a method is as follows:
<expression> . <identifier> <[type_arguments],*> ( <arguments> )
For example
coin.value();
*nums.borrow_mut(i) = 5;
Method Resolution
When a method is called, the compiler will statically determine which function is called based on
the type of the receiver (the argument on the left-hand side of the .
). The compiler maintains a
mapping from type and method name to the module and function name that should be called. This
mapping is created form the use fun
aliases that are currently in scope, and from the appropriate
functions in the receiver type's defining module. In all cases, the receiver type is the first
argument to the function, whether by-value or by-reference.
In this section, when we say a method "resolves" to a function, we mean that the compiler will
statically replace the method with a normal function call. For example if we have
x.foo(e)
with foo
resolving to a::m::foo
, the compiler will replace x.foo(e)
with
a::m::foo(x, e)
, potentially automatically borrowing x
.
Functions in the Defining Module
In a type’s defining module, the compiler will automatically create a method alias for any function declaration for its types when the type is the first argument in the function. For example,
module a::m {
public struct X() has copy, drop, store;
public fun foo(x: &X) { ... }
public fun bar(flag: bool, x: &X) { ... }
}
The function foo
can be called as a method on a value of type X
. However, not the first argument
(and one is not created for bool
since bool
is not defined in that module). For example,
fun example(x: a::m::X) {
x.foo(); // valid
// x.bar(true); ERROR!
}
use fun
Aliases
Like a traditional use
, a use fun
statement creates an alias local to its current
scope. This could be for the current module or the current expression block. However, the alias is
associated to a type.
The syntax for a use fun
statement is as follows:
use fun <function> as <type>.<method alias>;
This creates an alias for the <function>
, which the <type>
can receive as <method alias>
.
For example
module a::cup {
public struct Cup<T>(T) has copy, drop, store;
public fun cup_borrow<T>(c: &Cup<T>): &T {
&c.0
}
public fun cup_value<T>(c: Cup<T>): T {
let Cup(t) = c;
t
}
public fun cup_swap<T: drop>(c: &mut Cup<T>, t: T) {
c.0 = t;
}
}
We can now create use fun
aliases to these functions
module b::example {
use fun a::cup::cup_borrow as Cup.borrow;
use fun a::cup::cup_value as Cup.value;
use fun a::cup::cup_swap as Cup.set;
fun example(c: &mut Cup<u64>) {
let _ = c.borrow(); // resolves to a::cup::cup_borrow
let v = c.value(); // resolves to a::cup::cup_value
c.set(v * 2); // resolves to a::cup::cup_swap
}
}
Note that the <function>
in the use fun
does not have to be a fully resolved path, and an alias
can be used instead, so the declarations in the above example could equivalently be written as
use a::cup::{Self, cup_swap};
use fun cup::cup_borrow as Cup.borrow;
use fun cup::cup_value as Cup.value;
use fun cup_swap as Cup.set;
While these examples are cute for renaming the functions in the current module, the feature is
perhaps more useful for declaring methods on types from other modules. For example, if we wanted to
add a new utility to Cup
, we could do so with a use fun
alias and still use method syntax
module b::example {
fun double(c: &Cup<u64>): Cup<u64> {
let v = c.value();
Cup::new(v * 2)
}
}
Normally, we would be stuck having to call it as double(&c)
because b::example
did not define
Cup
, but instead we can use a use fun
alias
fun double_double(c: Cup<u64>): (Cup<u64>, Cup<u64>) {
use fun b::example::double as Cup.dub;
(c.dub(), c.dub()) // resolves to b::example::double in both calls
}
While use fun
can be made in any scope, the target <function>
of the use fun
must have a first
argument that is the same as the <type>
.
public struct X() has copy, drop, store;
fun new(): X { X() }
fun flag(flag: bool): u8 { if (flag) 1 else 0 }
use fun new as X.new; // ERROR!
use fun flag as X.flag; // ERROR!
// Neither `new` nor `flag` has first argument of type `X`
But any first argument of the <type>
can be used, including references and mutable references
public struct X() has copy, drop, store;
public fun by_val(_: X) {}
public fun by_ref(_: &X) {}
public fun by_mut(_: &mut X) {}
// All 3 valid, in any scope
use fun by_val as X.v;
use fun by_ref as X.r;
use fun by_mut as X.m;
Note for generics, the methods are associated for all instances of the generic type. You cannot overload the method to resolve to different functions depending on the instantiation.
public struct Cup<T>(T) has copy, drop, store;
public fun value<T: copy>(c: &Cup<T>): T {
c.0
}
use fun value as Cup<bool>.flag; // ERROR!
use fun value as Cup<u64>.num; // ERROR!
// In both cases, `use fun` aliases cannot be generic, they must work for all instances of the type
public use fun
Aliases
Unlike a traditional use
, the use fun
statement can be made public
, which allows it
to be used outside of its declared scope. A use fun
can be made public
if it is declared in the
module that defines the receivers type, much like the method aliases that are
automatically created for functions in the defining module. Or
conversely, one can think that an implicit public use fun
is created automatically for every
function in the defining module that has a first argument of the receiver type (if it is defined in
that module). Both of these views are equivalent.
module a::cup {
public struct Cup<T>(T) has copy, drop, store;
public use fun cup_borrow as Cup.borrow;
public fun cup_borrow<T>(c: &Cup<T>): &T {
&c.0
}
}
In this example, a public method alias is created for a::cup::Cup.borrow
and
a::cup::Cup.cup_borrow
. Both resolve to a::cup::cup_borrow
. And both are "public" in the sense
that they can be used outside of a::cup
, without an additional use
or use fun
.
module b::example {
fun example<T: drop>(c: a::cup::Cup<u64>) {
c.borrow(); // resolves to a::cup::cup_borrow
c.cup_borrow(); // resolves to a::cup::cup_borrow
}
}
The public use fun
declarations thus serve as a way of renaming a function if you want to give it
a cleaner name for use with method syntax. This is especially helpful if you have a module with
multiple types, and similarly named functions for each type.
module a::shapes {
public struct Rectangle { base: u64, height: u64 }
public struct Box { base: u64, height: u64, depth: u64 }
// Rectangle and Box can have methods with the same name
public use fun rectangle_base as Rectangle.base;
public fun rectangle_base(rectangle: &Rectangle): u64 {
rectangle.base
}
public use fun box_base as Box.base;
public fun box_base(box: &Box): u64 {
box.base
}
}
Another use for public use fun
is adding methods to types from other modules. This can be helpful
in conjunction with functions spread out across a single package.
module a::cup {
public struct Cup<T>(T) has copy, drop, store;
public fun new<T>(t: T): Cup<T> { Cup(t) }
public fun borrow<T>(c: &Cup<T>): &T {
&c.0
}
// `public use fun` to a function defined in another module
public use fun a::utils::split as Cup.split;
}
module a::utils {
use a::m::{Self, Cup};
public fun split<u64>(c: Cup<u64>): (Cup<u64>, Cup<u64>) {
let Cup(t) = c;
let half = t / 2;
let rem = if (t > 0) t - half else 0;
(cup::new(half), cup::new(rem))
}
}
And note that this public use fun
does not create a circular dependency, as the use fun
is not
present after the module is compiled--all methods are resolved statically.
Interactions with use
Aliases
A small detail to note is that method aliases respect normal use
aliases.
module a::cup {
public struct Cup<T>(T) has copy, drop, store;
public fun cup_borrow<T>(c: &Cup<T>): &T {
&c.0
}
}
module b::other {
use a::cup::{Cup, cup_borrow as borrow};
fun example(c: &Cup<u64>) {
c.borrow(); // resolves to a::cup::cup_borrow
}
}
A helpful way to think about this is that use
creates an implicit use fun
alias for the function
whenever it can. In this case the use a::cup::cup_borrow as borrow
creates an implicit
use fun a::cup::cup_borrow as Cup.borrow
because it would be a valid use fun
alias. Both views
are equivalent. This line of reasoning can inform how specific methods will resolve with shadowing.
See the cases in Scoping for more details.
Scoping
If not public
, a use fun
alias is local to its scope, much like a normal use
. For
example
module a::m {
public struct X() has copy, drop, store;
public fun foo(_: &X) {}
public fun bar(_: &X) {}
}
module b::other {
use a::m::X;
use fun a::m::foo as X.f;
fun example(x: &X) {
x.f(); // resolves to a::m::foo
{
use a::m::bar as f;
x.f(); // resolves to a::m::bar
};
x.f(); // still resolves to a::m::foo
{
use fun a::m::bar as X.f;
x.f(); // resolves to a::m::bar
}
}
Automatic Borrowing
When resolving a method, the compiler will automatically borrow the receiver if the function expects a reference. For example
module a::m {
public struct X() has copy, drop;
public fun by_val(_: X) {}
public fun by_ref(_: &X) {}
public fun by_mut(_: &mut X) {}
fun example(mut x: X) {
x.by_ref(); // resolves to a::m::by_ref(&x)
x.by_mut(); // resolves to a::m::by_mut(&mut x)
}
}
In these examples, x
was automatically borrowed to &x
and &mut x
respectively. This will also
work through field access
module a::m {
public struct X() has copy, drop;
public fun by_val(_: X) {}
public fun by_ref(_: &X) {}
public fun by_mut(_: &mut X) {}
public struct Y has drop { x: X }
fun example(mut y: Y) {
y.x.by_ref(); // resolves to a::m::by_ref(&y.x)
y.x.by_mut(); // resolves to a::m::by_mut(&mut y.x)
}
}
Note that in both examples, the local variable had to be labeled as mut
to allow
for the &mut
borrow. Without this, there would be an error saying that x
(or y
in the second
example) is not mutable.
Keep in mind that without a reference, normal rules for variable and field access come into play. Meaning a value might be moved or copied if it is not borrowed.
module a::m {
public struct X() has copy, drop;
public fun by_val(_: X) {}
public fun by_ref(_: &X) {}
public fun by_mut(_: &mut X) {}
public struct Y has drop { x: X }
public fun drop_y(y: Y) { y }
fun example(y: Y) {
y.x.by_val(); // copies `y.x` since `by_val` is by-value and `X` has `copy`
y.drop_y(); // moves `y` since `drop_y` is by-value and `Y` does _not_ have `copy`
}
}
Chaining
Method calls can be chained, because any expression can be the receiver of the method.
module a::shapes {
public struct Point has copy, drop, store { x: u64, y: u64 }
public struct Line has copy, drop, store { start: Point, end: Point }
public fun x(p: &Point): u64 { p.x }
public fun y(p: &Point): u64 { p.y }
public fun start(l: &Line): &Point { &l.start }
public fun end(l: &Line): &Point { &l.end }
}
module b::example {
use a::shapes::Line;
public fun x_values(l: Line): (u64, u64) {
(l.start().x(), l.end().x())
}
}
In this example for l.start().x()
, the compiler first resolves l.start()
to
a::shapes::start(&l)
. Then .x()
is resolved to a::shapes::x(a::shapes::start(&l))
. Similarly
for l.end().x()
. Keep in mind, this feature is not "special"--the left-hand side of the .
can be
any expression, and the compiler will resolve the method call as normal. We simply draw attention to
this sort of "chaining" because it is a common practice to increase readability.
Index Syntax
Move provides syntax attributes to allow you to define operations that look and feel like native Move code, lowering these operations into your user-provided definitions.
Our first syntax method, index
, allows you to define a group of operations that can be used as
custom index accessors for your datatypes, such as accessing a matrix element as m[i,j]
, by
annotating functions that should be used for these index operations. Moreover, these definitions are
bespoke per-type and available implicitly for any programmer using your type.
Overview and Summary
To start, consider a Matrix
type that uses a vector of vectors to represent its values. You can
write a small library using index
syntax annotations on the borrow
and borrow_mut
functions as
follows:
module matrix {
public struct Matrix<T> { v: vector<vector<T>> }
#[syntax(index)]
public fun borrow<T>(s: &Matrix<T>, i: u64, j: u64): &T {
vector::borrow(vector::borrow(&s.v, i), j)
}
#[syntax(index)]
public fun borrow_mut<T>(s: &mut Matrix<T>, i: u64, j: u64): &mut T {
vector::borrow_mut(vector::borrow_mut(&mut s.v, i), j)
}
public fun make_matrix<T>(v: vector<vector<T>>): Matrix<T> {
Matrix { v }
}
}
Now anyone using this Matrix
type has access to index syntax for it:
let mut m = matrix::make_matrix(vector[
vector[1, 0, 0],
vector[0, 1, 0],
vector[0, 0, 1],
]);
let mut i = 0;
while (i < 3) {
let mut j = 0;
while (j < 3) {
if (i == j) {
assert!(m[i, j] == 1, 1);
} else {
assert!(m[i, j] == 0, 0);
};
*(&mut m[i,j]) = 2;
j = j + 1;
};
i = i + 1;
}
Usage
As the example indicates, if you define a datatype and an associated index syntax method, anyone can invoke that method by writing index syntax on a value of that type:
let mat = matrix::make_matrix(...);
let m_0_0 = mat[0, 0];
During compilation, the compiler translates these into the appropriate function invocations based on the position and mutable usage of the expression:
let mut mat = matrix::make_matrix(...);
let m_0_0 = mat[0, 0];
// translates to `copy matrix::borrow(&mat, 0, 0)`
let m_0_0 = &mat[0, 0];
// translates to `matrix::borrow(&mat, 0, 0)`
let m_0_0 = &mut mat[0, 0];
// translates to `matrix::borrow_mut(&mut mat, 0, 0)`
``
You can also intermix index expressions with field accesses:
```move
public struct V { v: vector<u64> }
public struct Vs { vs: vector<V> }
fun borrow_first(input: &Vs): &u64 {
&input.vs[0].v[0]
// translates to `vector::borrow(&vector::borrow(&input.vs, 0).v, 0)`
}
Index Functions Take Flexible Arguments
Note that, aside from the definition and type limitations described in the rest of this chapter, Move places no restrictions on the values your index syntax method takes as parameters. This allows you to implement intricate programmatic behavior when defining index syntax, such as a data structure that takes a default value if the index is out of bounds:
#[syntax(index)]
public fun borrow_or_set<Key: copy, Value: drop>(
input: &mut MTable<Key, Value>,
key: Key,
default: Value
): &mut Value {
if (contains(input, key)) {
borrow(input, key)
} else {
insert(input, key, default);
borrow(input, key)
}
}
Now, when you index into MTable
, you must also provide a default value:
let string_key: String = ...;
let mut table: MTable<String, u64> = m_table::make_table();
let entry: &mut u64 = &mut table[string_key, 0];
This sort of extensible power allows you to write precise index interfaces for your types, concretely enforcing bespoke behavior.
Defining Index Syntax Functions
This powerful syntax form allows all of your user-defined datatypes to behave in this way, assuming your definitions adhere to the following rules:
- The
#[syntax(index)]
attribute is added to the designated functions defined in the same module as the subject type. - The designated functions have
public
visibility. - The functions take a reference type as its subject type (its first argument) and returns a
matching references type (
mut
if the subject wasmut
). - Each type has only a single mutable and single immutable definition.
- Immutable and mutable versions have type agreement:
- The subject types match, differing only in mutability.
- The return types match the mutability of their subject types.
- Type parameters, if present, have identical constraints between both versions.
- All parameters beyond the subject type are identical.
The following content and additional examples describe these rules in greater detail.
Declaration
To declare an index syntax method, add the #[syntax(index)]
attribute above the relevant function
definition in the same module as the subject type's definition. This signals to the compiler that
the function is an index accessor for the specified type.
Immutable Accessor
The immutable index syntax method is defined for read-only access. It takes an immutable reference
of the subject type and returns an immutable reference to the element type. The borrow
function
defined in std::vector
is an example of this:
#[syntax(index)]
public native fun borrow<Element>(v: &vector<Element>, i: u64): ∈
Mutable Accessor
The mutable index syntax method is the dual of the immutable one, allowing for both read and write
operations. It takes a mutable reference of the subject type and returns a mutable reference to the
element type. The borrow_mut
function defined in std::vector
is an example of this:
#[syntax(index)]
public native fun borrow_mut<Element>(v: &mut vector<Element>, i: u64): &mut Element;
Visibility
To ensure that indexing functions are available anywhere the type is used, all index syntax methods must have public visibility. This ensures ergonomic usage of indexing across modules and packages in Move.
No Duplicates
In addition to the above requirements, we restrict each subject base type to defining a single index syntax method for immutable references and a single index syntax method for mutable references. For example, you cannot define a specialized version for a polymorphic type:
#[syntax(index)]
public fun borrow_matrix_u64(s: &Matrix<u64>, i: u64, j: u64): &u64 { ... }
#[syntax(index)]
public fun borrow_matrix<T>(s: &Matrix<T>, i: u64, j: u64): &T { ... }
// ERROR! Matrix already has a definition
// for its immutable index syntax method
This ensures that you can always tell which method is being invoked, without the need to inspect type instantiation.
Type Constraints
By default, an index syntax method has the following type constraints:
Its subject type (first argument) must be a reference to a single type defined in the same module as the marked function. This means that you cannot define index syntax methods for tuples, type parameters, or values:
#[syntax(index)]
public fun borrow_fst(x: &(u64, u64), ...): &u64 { ... }
// ERROR because the subject type is a tuple
#[syntax(index)]
public fun borrow_tyarg<T>(x: &T, ...): &T { ... }
// ERROR because the subject type is a type parameter
#[syntax(index)]
public fun borrow_value(x: Matrix<u64>, ...): &u64 { ... }
// ERROR because x is not a reference
The subject type must match mutability with the return type. This restriction allows you to
clarify the expected behavior when borrowing an indexed expression as &vec[i]
versus
&mut vec[i]
. The Move compiler uses the mutability marker to determine which borrow form to call
to produce a reference of the appropriate mutability. As a result, we disallow index syntax methods
whose subject and return mutability differ:
#[syntax(index)]
public fun borrow_imm(x: &mut Matrix<u64>, ...): &u64 { ... }
// ERROR! incompatible mutability
// expected a mutable reference '&mut' return type
Type Compatibility
When defining an immutable and mutable index syntax method pair, they are subject to a number of compatibility constraints:
- They must take the same number of type parameters, those type parameters must have the same constraints.
- Type parameters must be used the same by position, not name.
- Their subject types must match exactly except for the mutability.
- Their return types must match exactly except for the mutability.
- All other parameter types must match exactly.
These constraints are to ensure that index syntax behaves identically regardless of being in a mutable or immutable position.
To illustrate some of these errors, recall the previous Matrix
definition:
#[syntax(index)]
public fun borrow<T>(s: &Matrix<T>, i: u64, j: u64): &T {
vector::borrow(vector::borrow(&s.v, i), j)
}
All of the following are type-incompatible definitions of the mutable version:
#[syntax(index)]
public fun borrow_mut<T: drop>(s: &mut Matrix<T>, i: u64, j: u64): &mut T { ... }
// ERROR! `T` has `drop` here, but no in the immutable version
#[syntax(index)]
public fun borrow_mut(s: &mut Matrix<u64>, i: u64, j: u64): &mut u64 { ... }
// ERROR! This takes a different number of type parameters
#[syntax(index)]
public fun borrow_mut<T, U>(s: &mut Matrix<U>, i: u64, j: u64): &mut U { ... }
// ERROR! This takes a different number of type parameters
#[syntax(index)]
public fun borrow_mut<U>(s: &mut Matrix<U>, i_j: (u64, u64)): &mut U { ... }
// ERROR! This takes a different number of arguments
#[syntax(index)]
public fun borrow_mut<U>(s: &mut Matrix<U>, i: u64, j: u32): &mut U { ... }
// ERROR! `j` is a different type
Again, the goal here is to make the usage across the immutable and mutable versions consistent. This allows index syntax methods to work without changing out the behavior or constraints based on mutable versus immutable usage, ultimately ensuring a consistent interface to program against.
Packages
Packages allow Move programmers to more easily re-use code and share it across projects. The Move package system allows programmers to easily:
- Define a package containing Move code;
- Parameterize a package by named addresses;
- Import and use packages in other Move code and instantiate named addresses;
- Build packages and generate associated compilation artifacts from packages; and
- Work with a common interface around compiled Move artifacts.
Package Layout and Manifest Syntax
A Move package source directory contains a Move.toml
package manifest file, a generated
Move.lock
file, and a set of subdirectories:
a_move_package
├── Move.toml (required)
├── Move.lock (generated)
├── sources (required)
├── doc_templates (optional)
├── examples (optional, test & dev mode)
└── tests (optional, test mode)
The directories and files labeled "required" must be present for a directory to be considered a Move
package and built. Optional directories may be present, and if so, they will be included in the
compilation process depending on the mode used to build the package. For instance, when built in
"dev" or "test" modes, the tests
and examples
directories will also be included.
Going through each of these in turn:
- The
Move.toml
file is the package manifest and is required for a directory to be considered a Move package. This file contains metadata about the package, such as name, dependencies, and so on. - The
Move.lock
file is generated by the Move CLI and contains the fixed build versions of the package and its dependencies. It is used to ensure consistent versions are used across different builds and that changes in dependencies are apparent as a change in this file. - The
sources
directory is required and contains the Move modules that make up the package. Modules in this directory will always be included in the compilation process. - The
doc_templates
directory can contain documentation templates that will be used when generating documentation for the package. - The
examples
directory can hold additional code to be used only for development and/or tutorials, this will not be included when compiled outside oftest
ordev
modes. - The
tests
directory can contain Move modules that are only included when compiled intest
mode or when Move unit tests are run.
Move.toml
The Move package manifest is defined within the Move.toml
file and has the following syntax.
Optional fields are marked with *
, +
denotes one or more elements:
[package]
name = <string>
edition* = <string> # e.g., "2024.alpha" to use the Move 2024 edition,
# currently in alpha. Will default to the latest stable edition if not specified.
license* = <string> # e.g., "MIT", "GPL", "Apache 2.0"
authors* = [<string>,+] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"]
# Additional fields may be added to this section by external tools. E.g., on Sui the following sections are added:
published-at* = "<hex-address>" # The address that the package is published at. Should be set after the first publication.
[dependencies] # (Optional section) Paths to dependencies
# One or more lines declaring dependencies in the following format
# ##### Local Dependencies #####
# For local dependencies use `local = path`. Path is relative to the package root
# Local = { local = "../path/to" }
# To resolve a version conflict and force a specific version for dependency
# override you can use `override = true`
# Override = { local = "../conflicting/version", override = true }
# To instantiate address values in a dependency, use `addr_subst`
<string> = {
local = <string>,
override* = <bool>,
addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}
# ##### Git Dependencies #####
# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`.
# Revision must be supplied, it can be a branch, a tag, or a commit hash.
# If no `subdir` is specified, the root of the repository is used.
# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" }
<string> = {
git = <URL ending in .git>,
subdir=<path to dir containing Move.toml inside git repo>,
rev=<git commit hash>,
override* = <bool>,
addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}
[addresses] # (Optional section) Declares named addresses in this package
# One or more lines declaring named addresses in the following format
# Addresses that match the name of the package must be set to `"0x0"` or they will be unable to be published.
<addr_name> = "_" | "<hex_address>" # e.g., std = "_" or my_addr = "0xC0FFEECAFE"
# Named addresses will be accessible in Move as `@name`. They're also exported:
# for example, `std = "0x1"` is exported by the Standard Library.
# alice = "0xA11CE"
[dev-dependencies] # (Optional section) Same as [dependencies] section, but only included in "dev" and "test" modes
# The dev-dependencies section allows overriding dependencies for `--test` and
# `--dev` modes. You can e.g., introduce test-only dependencies here.
# Local = { local = "../path/to/dev-build" }
<string> = {
local = <string>,
override* = <bool>,
addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}
<string> = {
git = <URL ending in .git>,
subdir=<path to dir containing Move.toml inside git repo>,
rev=<git commit hash>,
override* = <bool>,
addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}
[dev-addresses] # (Optional section) Same as [addresses] section, but only included in "dev" and "test" modes
# The dev-addresses section allows overwriting named addresses for the `--test`
# and `--dev` modes.
<addr_name> = "<hex_address>" # e.g., alice = "0xB0B"
An example of a minimal package manifest:
[package]
name = "AName"
An example of a more standard package manifest that also includes the Move standard library and
instantiates the named address std
from the LocalDep
package with the address value 0x1
:
[package]
name = "AName"
license = "Apache 2.0"
[addresses]
address_to_be_filled_in = "_"
specified_address = "0xB0B"
[dependencies]
# Local dependency
LocalDep = { local = "projects/move-awesomeness", addr_subst = { "std" = "0x1" } }
# Git dependency
MoveStdlib = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "framework/mainnet" }
[dev-addresses] # For use when developing this module
address_to_be_filled_in = "0x101010101"
Most of the sections in the package manifest are self explanatory, but named addresses can be a bit difficult to understand so we examine them in more detail in Named Addresses During Compilation.
Named Addresses During Compilation
Recall that Move has named addresses and that named addresses cannot
be declared in Move. Instead they are declared at the package level: in the manifest file
(Move.toml
) for a Move package you declare named addresses in the package, instantiate other named
addresses, and rename named addresses from other packages within the Move package system.
Let's go through each of these actions, and how they are performed in the package's manifest one-by-one:
Declaring Named Addresses
Let's say we have a Move module in example_pkg/sources/A.move
as follows:
module named_addr::a {
public fun x(): address { @named_addr }
}
We could in example_pkg/Move.toml
declare the named address named_addr
in two different ways.
The first:
[package]
name = "example_pkg"
...
[addresses]
named_addr = "_"
Declares named_addr
as a named address in the package example_pkg
and that this address can be
any valid address value. In particular, an importing package can pick the value of the named
address named_addr
to be any address it wishes. Intuitively you can think of this as
parameterizing the package example_pkg
by the named address named_addr
, and the package can then
be instantiated later on by an importing package.
named_addr
can also be declared as:
[package]
name = "example_pkg"
...
[addresses]
named_addr = "0xCAFE"
which states that the named address named_addr
is exactly 0xCAFE
and cannot be changed. This is
useful so other importing packages can use this named address without needing to worry about the
exact value assigned to it.
With these two different declaration methods, there are two ways that information about named addresses can flow in the package graph:
- The former ("unassigned named addresses") allows named address values to flow from the importation site to the declaration site.
- The latter ("assigned named addresses") allows named address values to flow from the declaration site upwards in the package graph to usage sites.
With these two methods for flowing named address information throughout the package graph the rules around scoping and renaming become important to understand.
Scope and Renaming of Named Addresses
A named address N
in a package P
is in scope if:
P
declares a named addressN
; or- A package in one of
P
's transitive dependencies declares the named addressN
and there is a dependency path in the package graph between betweenP
and the declaring package ofN
with no renaming ofN
.
Additionally, every named address in a package is exported. Because of this and the above scoping
rules each package can be viewed as coming with a set of named addresses that will be brought into
scope when the package is imported, e.g., if you import example_pkg
, that import will also bring
the named_addr
named address into scope. Because of this, if P
imports two packages P1
and
P2
both of which declare a named address N
an issue arises in P
: which "N
" is meant when N
is referred to in P
? The one from P1
or P2
? To prevent this ambiguity around which package a
named address is coming from, we enforce that the sets of scopes introduced by all dependencies in a
package are disjoint, and provide a way to rename named addresses when the package that brings
them into scope is imported.
Renaming a named address when importing can be done as follows in our P
, P1
, and P2
example
above:
[package]
name = "P"
...
[dependencies]
P1 = { local = "some_path_to_P1", addr_subst = { "P1N" = "N" } }
P2 = { local = "some_path_to_P2" }
With this renaming N
refers to the N
from P2
and P1N
will refer to N
coming from P1
:
module N::A {
public fun x(): address { @P1N }
}
It is important to note that renaming is not local: once a named address N
has been renamed to
N2
in a package P
all packages that import P
will not see N
but only N2
unless N
is
reintroduced from outside of P
. This is why rule (2) in the scoping rules at the start of this
section specifies a "dependency path in the package graph between between P
and the declaring
package of N
with no renaming of N
."
Instantiating Named Addresses
Named addresses can be instantiated multiple times across the package graph as long as it is always with the same value. It is an error if the same named address (regardless of renaming) is instantiated with differing values across the package graph.
A Move package can only be compiled if all named addresses resolve to a value. This presents issues
if the package wishes to expose an uninstantiated named address. This is what the [dev-addresses]
section solves in part. This section can set values for named addresses, but cannot introduce any
named addresses. Additionally, only the [dev-addresses]
in the root package are included in dev
mode. For example a root package with the following manifest would not compile outside of dev
mode
since named_addr
would be uninstantiated:
[package]
name = "example_pkg"
...
[addresses]
named_addr = "_"
[dev-addresses]
named_addr = "0xC0FFEE"
Usage and Artifacts
The Move package system comes with a command line option as part of the CLI:
sui move <command> <command_flags>
. Unless a particular path is provided, all package commands
will run in the current enclosing Move package. The full list of commands and flags for the Move CLI
can be found by running sui move --help
.
Artifacts
A package can be compiled using CLI commands. This will create a build
directory containing
build-related artifacts (including bytecode binaries, source maps, and documentation). The general
layout of the build
directory is as follows:
a_move_package
├── BuildInfo.yaml
├── bytecode_modules
│ ├── dependencies
│ │ ├── <dep_pkg_name>
│ │ │ └── *.mv
│ │ ...
│ │ └── <dep_pkg_name>
│ │ └── *.mv
│ ...
│ └── *.mv
├── docs
│ ├── dependencies
│ │ ├── <dep_pkg_name>
│ │ │ └── *.md
│ │ ...
│ │ └── <dep_pkg_name>
│ │ └── *.md
│ ...
│ └── *.md
├── source_maps
│ ├── dependencies
│ │ ├── <dep_pkg_name>
│ │ │ └── *.mvsm
│ │ ...
│ │ └── <dep_pkg_name>
│ │ └── *.mvsm
│ ...
│ └── *.mvsm
└── sources
...
└── *.move
├── dependencies
│ ├── <dep_pkg_name>
│ │ └── *.move
│ ...
│ └── <dep_pkg_name>
│ └── *.move
...
└── *.move
Move.lock
The Move.lock
file is generated at the root of the Move package when the package is built. The
Move.lock
file contains information about your package and its build configuration, and acts as a
communication layer between the Move compiler and other tools, like chain-specific command line
interfaces and third-party package managers.
Like the Move.toml
file, the Move.lock
file is a text-based TOML file. Unlike the package
manifest however, the Move.lock
file is not intended for you to edit directly. Processes on the
toolchain, like the Move compiler, access and edit the file to read and append relevant information
to it. You also must not move the file from the root, as it needs to be at the same level as the
Move.toml
manifest in the package.
If you are using source control for your package, it's recommended practice to check in the
Move.lock
file that corresponds with your desired built or published package. This ensures that
every build of your package is an exact replica of the original, and that changes to the build will
be apparent as changes to the Move.lock
file.
The Move.lock
file is a TOML file that currently contains the following fields.
Note: other fields may be added to the lock file either in the future, or by third-party package package managers as well.
The [move]
Section
This section contains the core information needed in the lockfile:
- The version of the lockfile (needed for backwards compatibility checking, and versioning lockfile changes in the future).
- The hash of the
Move.toml
file that was used to generate this lock file. - The hash of the
Move.lock
file of all dependencies. If no dependencies are present, this will be an empty string. - The list of dependencies.
[move]
version = <string> # Lock file version, used for backwards compatibility checking.
manifest_digest = <hash> # Sha3-256 hash of the Move.toml file that was used to generate this lock file.
deps_digest = <hash> # Sha3-256 hash of the Move.lock file of all dependencies. If no dependencies are present, this will be an empty string.
dependencies = { (name = <string>)* } # List of dependencies. Not present if there are no dependencies.
The [move.package]
Sections
After the Move compiler resolves each of the dependencies for the package it writes the location of
the dependency to the Move.lock
file. If a dependency failed to resolve, the compiler will not
write the Move.lock
file and the build fails. If all dependencies resolve, the Move.lock
file
contains the locations (local and remote) of all of the package's transitive dependencies. These
will be stored in the Move.lock
file in the following format:
# ...
[[move.package]]
name = "A"
source = { git = "https://github.com/b/c.git", subdir = "e/f", rev = "a1b2c3" }
[[move.package]]
name = "B"
source = { local = "../local-dep" }
The [move.toolchain-version]
Section
As mentioned above, additional fields may be added to the lock file by external tools. For example, the Sui package manager adds toolchain version information to the lock file that can then be used for on-chain source verification:
# ...
[move.toolchain-version]
compiler-version = <string> # The version of the Move compiler used to build the package, e.g. "1.21.0"
edition = <string> # The edition of the Move language used to build the package, e.g. "2024.alpha"
flavor = <string> # The flavor of the Move compiler used to build the package, e.g. "sui"
Unit Tests
Unit testing for Move uses three annotations in the Move source language:
#[test]
marks a function as a test;#[expected_failure]
marks that a test is expected to fail;#[test_only]
marks a module or module member (use
, function, struct, or constant) as code to be included for testing only.
These annotations can be placed on any appropriate form with any visibility. Whenever a module or
module member is annotated as #[test_only]
or #[test]
, it will not be included in the compiled
bytecode unless it is compiled for testing.
Test Annotations
The #[test]
annotation can only be placed on a function with no parameters. This annotation marks
the function as a test to be run by the unit testing harness.
#[test] // OK
fun this_is_a_test() { ... }
#[test] // Will fail to compile since the test takes an argument
fun this_is_not_correct(arg: u64) { ... }
A test can also be annotated as an #[expected_failure]
. This annotation marks that the test is
expected to raise an error. There are a number of options that can be used with the
#[expected_failure]
annotation to ensure only a failure with the specified condition is marked as
passing, these options are detailed in Expected Failures. Only functions that
have the #[test]
annotation can also be annotated as an #[expected_failure]
.
Some simple examples of using the #[expected_failure]
annotation are shown below:
#[test]
#[expected_failure]
public fun this_test_will_abort_and_pass() { abort 1 }
#[test]
#[expected_failure]
public fun test_will_error_and_pass() { 1/0; }
#[test] // Will pass since test fails with the expected abort code constant.
#[expected_failure(abort_code = ENotFound)] // ENotFound is a constant defined in the module
public fun test_will_error_and_pass_abort_code() { abort ENotFound }
#[test] // Will fail since test fails with a different error than expected.
#[expected_failure(abort_code = my_module::ENotFound)]
public fun test_will_error_and_fail() { 1/0; }
#[test, expected_failure] // Can have multiple in one attribute. This test will pass.
public fun this_other_test_will_abort_and_pass() { abort 1 }
Note:
#[test]
and#[test_only]
functions can also callentry
functions, regardless of their visibility.
Expected Failures
There are a number of different ways that you can use the #[expected_failure]
annotation to
specify different types of error conditions. These are:
1. #[expected_failure(abort_code = <constant>)]
This will pass if the test aborts with the specified constant value in the module that defines the constant and fail otherwise. This is the recommended way of testing for expected test failures.
Note: You can reference constants outside of the current module or package in
expected_failure
annotations.
module pkg_addr::other_module {
const ENotFound: u64 = 1;
public fun will_abort() {
abort ENotFound
}
}
module pkg_addr::my_module {
use pkg_addr::other_module;
const ENotFound: u64 = 1;
#[test]
#[expected_failure(abort_code = ENotFound)]
fun test_will_abort_and_pass() { abort ENotFound }
#[test]
#[expected_failure(abort_code = other_module::ENotFound)]
fun test_will_abort_and_pass() { other_module::will_abort() }
// FAIL: Will not pass since we are expecting the constant from the wrong module.
#[test]
#[expected_failure(abort_code = ENotFound)]
fun test_will_abort_and_pass() { other_module::will_abort() }
}
2. #[expected_failure(arithmetic_error, location = <location>)]
This specifies that the test is expected to fail with an arithmetic error (e.g., integer overflow,
division by zero, etc) at the specified location. The <location>
must be a valid path to a module
location, e.g., Self
, or my_package::my_module
.
module pkg_addr::other_module {
public fun will_arith_error() { 1/0; }
}
module pkg_addr::my_module {
use pkg_addr::other_module;
#[test]
#[expected_failure(arithmetic_error, location = Self)]
fun test_will_arith_error_and_pass1() { 1/0; }
#[test]
#[expected_failure(arithmetic_error, location = pkg_addr::other_module)]
fun test_will_arith_error_and_pass2() { other_module::will_arith_error() }
// FAIL: Will fail since the location we expect it the fail at is different from where the test actually failed.
#[test]
#[expected_failure(arithmetic_error, location = Self)]
fun test_will_arith_error_and_fail() { other_module::will_arith_error() }
}
3. #[expected_failure(out_of_gas, location = <location>)]
This specifies that the test is expected to fail with an out of gas error at the specified location.
The <location>
must be a valid path to a module location, e.g., Self
, or
my_package::my_module
.
module pkg_addr::other_module {
public fun will_oog() { loop {} }
}
module pkg_addr::my_module {
use pkg_addr::other_module;
#[test]
#[expected_failure(out_of_gas, location = Self)]
fun test_will_oog_and_pass1() { loop {} }
#[test]
#[expected_failure(arithmetic_error, location = pkg_addr::other_module)]
fun test_will_oog_and_pass2() { other_module::will_oog() }
// FAIL: Will fail since the location we expect it the fail at is different from where
// the test actually failed.
#[test]
#[expected_failure(out_of_gas, location = Self)]
fun test_will_oog_and_fail() { other_module::will_oog() }
}
4. #[expected_failure(vector_error, minor_status = <u64_opt>, location = <location>)]
This specifies that the test is expected to fail with a vector error at the specified location with
the given minor_status
(if provided). The <location>
must be a valid path to a module module
location, e.g., Self
, or my_package::my_module
. The <u64_opt>
is an optional parameter that
specifies the minor status of the vector error. If it is not specified, the test will pass if the
test fails with any minor status. If it is specified, the test will only pass if the test fails with
a vector error with the specified minor status.
module pkg_addr::other_module {
public fun vector_borrow_empty() {
&vector<u64>[][1];
}
}
module pkg_addr::my_module {
#[test]
#[expected_failure(vector_error, location = Self)]
fun vector_abort_same_module() {
vector::borrow(&vector<u64>[], 1);
}
#[test]
#[expected_failure(vector_error, location = pkg_addr::other_module)]
fun vector_abort_same_module() {
other_module::vector_borrow_empty();
}
// Can specify minor statues (i.e., vector-specific error codes) to expect.
#[test]
#[expected_failure(vector_error, minor_status = 1, location = Self)]
fun native_abort_good_right_code() {
vector::borrow(&vector<u64>[], 1);
}
// FAIL: correct error, but wrong location.
#[test]
#[expected_failure(vector_error, location = pkg_addr::other_module)]
fun vector_abort_same_module() {
other_module::vector_borrow_empty();
}
// FAIL: correct error and location but the minor status differs so this test will fail.
#[test]
#[expected_failure(vector_error, minor_status = 0, location = Self)]
fun vector_abort_wrong_minor_code() {
vector::borrow(&vector<u64>[], 1);
}
}
5. #[expected_failure]
This will pass if the test aborts with any error code. You should be incredibly careful using this to annotate expected tests failures, and always prefer one of the ways described above instead. Examples of these types of annotations are:
#[test]
#[expected_failure]
fun test_will_abort_and_pass1() { abort 1 }
#[test]
#[expected_failure]
fun test_will_arith_error_and_pass2() { 1/0; }
Test Only Annotations
A module and any of its members can be declared as test only. If an item is annotated as
#[test_only]
the item will only be included in the compiled Move bytecode when compiled in test
mode. Additionally, when compiled outside of test mode, any non-test use
s of a #[test_only]
module will raise an error during compilation.
Note: functions that are annotated with
#[test_only]
will only be available to be called from test code, but they themselves are not tests and will not be run as tests by the unit testing framework.
#[test_only] // test only attributes can be attached to modules
module abc { ... }
#[test_only] // test only attributes can be attached to constants
const MY_ADDR: address = @0x1;
#[test_only] // .. to uses
use pkg_addr::some_other_module;
#[test_only] // .. to structs
public struct SomeStruct { ... }
#[test_only] // .. and functions. Can only be called from test code, but this is _not_ a test!
fun test_only_function(...) { ... }
Running Unit Tests
Unit tests for a Move package can be run with the sui move test
command.
When running tests, every test will either PASS
, FAIL
, or TIMEOUT
. If a test case fails, the
location of the failure along with the function name that caused the failure will be reported if
possible. You can see an example of this below.
A test will be marked as timing out if it exceeds the maximum number of instructions that can be executed for any single test. This bound can be changed using the options below. Additionally, while the result of a test is always deterministic, tests are run in parallel by default, so the ordering of test results in a test run is non-deterministic unless running with only one thread, which can be configured via an option.
These aforementioned options are two among many that can fine-tune testing and help debug failing
tests. To see all available options, and a description of what each one does, pass the --help
flag
to the sui move test
command:
$ sui move test --help
Example
A simple module using some of the unit testing features is shown in the following example:
First create an empty package and change directory into it:
$ sui move new test_example; cd test_example
Next add the following module under the sources
directory:
// filename: sources/my_module.move
module test_example::my_module {
public struct Wrapper(u64)
const ECoinIsZero: u64 = 0;
public fun make_sure_non_zero_coin(coin: Wrapper): Wrapper {
assert!(coin.0 > 0, ECoinIsZero);
coin
}
#[test]
fun make_sure_non_zero_coin_passes() {
let coin = Wrapper(1);
let Wrapper(_) = make_sure_non_zero_coin(coin);
}
#[test]
// Or #[expected_failure] if we don't care about the abort code
#[expected_failure(abort_code = ECoinIsZero)]
fun make_sure_zero_coin_fails() {
let coin = Wrapper(0);
let Wrapper(_) = make_sure_non_zero_coin(coin);
}
#[test_only] // test only helper function
fun make_coin_zero(coin: &mut Wrapper) {
coin.0 = 0;
}
#[test]
#[expected_failure(abort_code = ECoinIsZero)]
fun make_sure_zero_coin_fails2() {
let mut coin = Wrapper(10);
coin.make_coin_zero();
let Wrapper(_) = make_sure_non_zero_coin(coin);
}
}
Running Tests
You can then run these tests with the move test
command:
$ sui move test
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ PASS ] 0x0::my_module::make_sure_non_zero_coin_passes
[ PASS ] 0x0::my_module::make_sure_zero_coin_fails
[ PASS ] 0x0::my_module::make_sure_zero_coin_fails2
Test result: OK. Total tests: 3; passed: 3; failed: 0
Using Test Flags
Passing specific tests to run
You can run a specific test, or a set of tests with sui move test <str>
. This will only run tests
whose fully qualified name contains <str>
. For example if we wanted to only run tests with
"non_zero"
in their name:
$ sui move test non_zero
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ PASS ] 0x0::my_module::make_sure_non_zero_coin_passes
Test result: OK. Total tests: 1; passed: 1; failed: 0
-i <bound>
or --gas_used <bound>
This bounds the amount of gas that can be consumed for any one test to <bound>
:
$ sui move test -i 0
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ TIMEOUT ] 0x0::my_module::make_sure_non_zero_coin_passes
[ FAIL ] 0x0::my_module::make_sure_zero_coin_fails
[ FAIL ] 0x0::my_module::make_sure_zero_coin_fails2
Test failures:
Failures in 0x0::my_module:
┌── make_sure_non_zero_coin_passes ──────
│ Test timed out
└──────────────────
┌── make_sure_zero_coin_fails ──────
│ error[E11001]: test failure
│ ┌─ ./sources/my_module.move:22:27
│ │
│ 21 │ fun make_sure_zero_coin_fails() {
│ │ ------------------------- In this function in 0x0::my_module
│ 22 │ let coin = MyCoin(0);
│ │ ^ Test did not error as expected. Expected test to abort with code 0 <SNIP>
│
│
└──────────────────
┌── make_sure_zero_coin_fails2 ──────
│ error[E11001]: test failure
│ ┌─ ./sources/my_module.move:34:31
│ │
│ 33 │ fun make_sure_zero_coin_fails2() {
│ │ -------------------------- In this function in 0x0::my_module
│ 34 │ let mut coin = MyCoin(10);
│ │ ^^ Test did not error as expected. Expected test to abort with code 0 <SNIP>
│
│
└──────────────────
Test result: FAILED. Total tests: 3; passed: 0; failed: 3
-s
or --statistics
With these flags you can gather statistics about the tests run and report the runtime and gas used
for each test. You can additionally add csv
(sui move test -s csv
) to get the gas usage in a csv
output format. For example, if we wanted to see the statistics for the tests in the example above:
$ sui move test -s
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ PASS ] 0x0::my_module::make_sure_non_zero_coin_passes
[ PASS ] 0x0::my_module::make_sure_zero_coin_fails
[ PASS ] 0x0::my_module::make_sure_zero_coin_fails2
Test Statistics:
┌────────────────────────────────────────────────┬────────────┬───────────────────────────┐
│ Test Name │ Time │ Gas Used │
├────────────────────────────────────────────────┼────────────┼───────────────────────────┤
│ 0x0::my_module::make_sure_non_zero_coin_passes │ 0.001 │ 1 │
├────────────────────────────────────────────────┼────────────┼───────────────────────────┤
│ 0x0::my_module::make_sure_zero_coin_fails │ 0.001 │ 1 │
├────────────────────────────────────────────────┼────────────┼───────────────────────────┤
│ 0x0::my_module::make_sure_zero_coin_fails2 │ 0.001 │ 1 │
└────────────────────────────────────────────────┴────────────┴───────────────────────────┘
Test result: OK. Total tests: 3; passed: 3; failed: 0
See Sui's Coding Conventions for Move
DEPRECATED: Friends
NOTE: this feature has been superseded by public(package)
.
The friend
syntax was used to declare modules that are trusted by the current module. A trusted
module is allowed to call any function defined in the current module that have the public(friend)
visibility. For details on function visibilities, refer to the Visibility section in
Functions.
Friend declaration
A module can declare other modules as friends via friend declaration statements, in the format of
-
friend <address::name>
— friend declaration using fully qualified module name like the example below, ormodule 0x42::a { friend 0x42::b; }
-
friend <module-name-alias>
— friend declaration using a module name alias, where the module alias is introduced via theuse
statement.module 0x42::a { use 0x42::b; friend b; }
A module may have multiple friend declarations, and the union of all the friend modules forms the
friend list. In the example below, both 0x42::B
and 0x42::C
are considered as friends of
0x42::A
.
module 0x42::a {
friend 0x42::b;
friend 0x42::c;
}
Unlike use
statements, friend
can only be declared in the module scope and not in the expression
block scope. friend
declarations may be located anywhere a top-level construct (e.g., use
,
function
, struct
, etc.) is allowed. However, for readability, it is advised to place friend
declarations near the beginning of the module definition.
Friend declaration rules
Friend declarations are subject to the following rules:
-
A module cannot declare itself as a friend.
module 0x42::m { friend Self; // ERROR! } // ^^^^ Cannot declare the module itself as a friend module 0x43::m { friend 0x43::M; // ERROR! } // ^^^^^^^ Cannot declare the module itself as a friend
-
Friend modules must be known by the compiler
module 0x42::m { friend 0x42::nonexistent; // ERROR! } // ^^^^^^^^^^^^^^^^^ Unbound module '0x42::nonexistent'
-
Friend modules must be within the same account address.
module 0x42::m {} module 0x42::n { friend 0x42::m; // ERROR! } // ^^^^^^^ Cannot declare modules out of the current address as a friend
-
Friends relationships cannot create cyclic module dependencies.
Cycles are not allowed in the friend relationships, e.g., the relation
0x2::a
friends0x2::b
friends0x2::c
friends0x2::a
is not allowed. More generally, declaring a friend module adds a dependency upon the current module to the friend module (because the purpose is for the friend to call functions in the current module). If that friend module is already used, either directly or transitively, a cycle of dependencies would be created.module 0x2::a { use 0x2::c; friend 0x2::b; public fun a() { c::c() } } module 0x2::b { friend 0x2::c; // ERROR! // ^^^^^^ This friend relationship creates a dependency cycle: '0x2::b' is a friend of '0x2::a' uses '0x2::c' is a friend of '0x2::b' } module 0x2::c { public fun c() {} }
-
The friend list for a module cannot contain duplicates.
module 0x42::a {} module 0x42::m { use 0x42::a as aliased_a; friend 0x42::A; friend aliased_a; // ERROR! // ^^^^^^^^^ Duplicate friend declaration '0x42::a'. Friend declarations in a module must be unique }