Skip to main content

Introducing Move 2024

IOTA launches with the "Move 2024.beta" edition, bringing enhanced features to improve both the writing and readability of Move code. These updates refine the source language without impacting the on-chain binary representation, allowing users to enjoy a smoother experience with the latest language improvements.

The primary goal of the "Move 2024.beta" edition is to make Move easier to write and, ideally, easier to read. The minimal breaking changes introduced in the source language are designed to better prepare Move for future advancements.

This document highlights some new features to try out and shows how to use them in your existing modules.

info

Please, provide any feedback or report any issues you encounter via GitHub or Discord.

New features

info

Below are examples of how to rewrite some legacy code in the Move 2024 edition. If you find legacy code in the Move examples and would like to rewrite it based on Move 2024, refer to these examples.

Method Syntax

You can now call certain function methods using the . syntax. For example, the following call:

vector::push_back(&mut v, coin::value(&c));

Can now be written as:

v.push_back(c.value());

Where the receiver of the method (v and c in this example) is automatically borrowed if necessary (as &mut v and &c respectively).

You can call any function defined in the same module as the receiver's type as a method if it takes the receiver as its first argument.

For functions defined outside the module, you can declare methods using public use fun and use fun.

Index Syntax

With method syntax, you can annotate certain functions as being #[syntax(index)] methods. You then call these methods using v[i]-style calls.

For example,

*&mut v[i] = v[j];

resolves to

*vector::borrow_mut(&mut v, i) = *vector::borrow(&v, j);

Macro Functions

Higher-order functions (such as map, filter, fold, for_each, etc.) are useful in many languages for concisely transforming collections. Move does not have lambdas, closures, or function pointers, making defining these operations impossible. Macro functions will allow for Move to mimic these sorts of operations without supporting the behavior at runtime. The body of the macro mimicking the "higher-order function" will be inlined at each call site. And the call site can provide a "lambda" that will be substituted as the macro is expanded. For example:

let v2 = v.map!(|x| x + 1);

Or

v.for_each!(|x| foo(x));

The "lambdas" additionally will support control flow through break and return.

Enums

Enumerations allow you to define a single type that may hold multiple different shapes of data. Unlike structs, which always have the same fields, enums can have different fields depending on the variant of the enum. For example, in enum Option<T> { None, Some(T) }, the variant None has no fields, and the variant Some has a single field of type T. Move allows destructuring enums using match expressions. Some examples of enums in Move are the following:

public enum Color {
RGB { red: u8, green: u8, blue: u8 },
HSL { hue: u16, saturation: u8, lightness: u8 },
Hex(u32)
}

public enum Option<T> {
None,
Some(T),
}

public fun is_rgb_color(color: &Color): bool {
match (color) {
Color::RGB { red: _, green: _, blue: _ } => true,
_ => false,
}
}

const EOptionIsNone: u64 = 0;
public fun unwrap_some<T>(option: Option<T>): T {
match (option) {
Option::Some(x) => x,
Option::None => abort EOptionIsNone,
}
}

Move will add support for basic high-level enums that have similar visibility rules to structs in Move today; the enumeration type is publicly visible, just like struct types, but the variants of the enumeration are not public, much like fields. However, we plan to add public variants in the future. Similarly, enumerations cannot be recursive at release, but we plan to support this in the future.

public(package)

friend declarations, and the associated public(friend) visibility modifiers, are deprecated. In their place is the public(package) visibility modifier, which allows calling functions only within the same package where they are defined.

Positional Fields

You can now define structs with positional fields, which are accessed by zero-based index. For example,

public struct Pair(u64, u64) has copy, drop, store;

then to access each field,

public fun sum(p: &Pair): u64 {
p.0 + p.1
}

And as this example shows, you can now declare abilities after the struct field list.

Postfix has Ability Declarations

With positional fields, it is a bit awkward to read has declarations in the middle of a declaration. As an alternative, has can now be written after the fields. For example, both will be valid:

struct Wrapper1 has copy, drop, store (u64)
struct Wrapper2(u64) has copy, drop, store;

Nested use and Standard Library Defaults

You can now nest use aliases for more conciseness.

use iota::{balance, coin::{Self, Coin}};

Additionally, the following use declarations are now automatically included in every module:

use std::vector;
use std::option::{Self, Option};
use iota::object::{Self, ID, UID};
use iota::transfer;
use iota::tx_context::{Self, TxContext};

Automatic Referencing in Equality

Equality operations, == and !=, now automatically borrow if one side is a reference and the other is not. For example,

fun check(x: u64, r: &u64): bool {
x == r
}

is equivalent to

fun check(x: u64, r: &u64): bool {
&x == r
}

This automatic borrowing can occur on either side of == and !=.

Loop Labels

When nesting loops, it can be convenient to break to the outer loop. For example,

let mut i = 0;
let mut j = 0;
let mut terminate_loop = false;
while (i < 10) {
while (j < 10) {
if (haystack(i, j) == needle) {
terminate_loop = true;
break;
};
j = j + 1;
};
if (terminate_loop) break;
i = i + 1;
}

Now, you can directly name the outer loop (outer in this case) and break it all at once:

let mut i = 0;
let mut j = 0;
'outer: while (i < 10) {
while (j < 10) {
if (haystack(i, j) == needle) break'outer;
j = j + 1;
};
i = i + 1;
}

Type Inference Holes _ On Type Annotations

With type directed programming, often you need to annotate a variable declaration or provide type arguments. But, sometimes you really only need to annotate on specific type, but the other types can be inferred. _ will be added to allow that type to still be inferred, even when other parts of the type are annotated. For example

dynamic_field::borrow_mut<address, Coin<IOTA>>(&mut id, owner)

could be rewritten as

dynamic_field::borrow_mut<_, Coin<IOTA>>(&mut id, owner)

where the _ would be inferred as address

break With Value

It's now possible to break with a value from a loop. For example,

let mut i = 0;
let x: u64 = loop {
if (v[i] > 10) break i;
i = i + 1;
};

You can achieve this with labels, as well. For example,

let mut i = 0;
let mut j = 0;
let item = 'outer: loop {
while (j < 10) {
let item = haystack(i, j);
if (item == needle) break'outer option::some(item);
j = j + 1;
};
i = i + 1;
if (i == 10) break option::none();
};

Named Blocks With Enhanced Control Flow Operations

Move 2024 supports naming loop, while, and normal blocks, allowing for more-complex control flow.

Previous code with nested while loops (such as this simplified excerpt from deepbook) would need to set a flag to break both loops:

let mut terminate_loop = false;

while (...loop_condition...) {
while (...inner_condition...) {
...
if (...break_condition...) {
terminate_loop = true;
}
...
if (terminate_loop) {
break;
}
}
if (terminate_loop) {
break;
}
}

Now, you can directly name the outer loop and break it all at once:

let mut terminate_loop = false;

while (...loop_condition...) 'outer: {
while (...inner_condition...) {
...
if (...break_condition...) {
terminate_loop = true;
}
...
if (terminate_loop) {
break 'outer;
}
}
}

This will immediately break to the outer loop, allowing more precise control flow when you'd like to escape from loops.

This feature also works with normal loop forms, including breaks with values:

let y = loop 'outer: {
let _x = loop 'inner: {
if (true) {
break 'outer 10;
} else {
break 'inner 20
}
};
};

In this toy example, y will take on the value 10 because the first break will break the 'outer` loop with that value.

Finally, this feature can be applied to normal blocks in Move, but instead utilizes the return keyword. This can be useful when sequencing a block of code that may need early returns with values.

public fun auth_user(auth_one: EasyAuth, auth_two: AuthTwo): u64 {
let auth_token = 'auth: {
let maybe_auth_token = try_auth(auth_one);
if (valid_auth(maybe_auth_token)) {
return 'auth unpack_auth(maybe_auth_token);
}
// ... more complicated code involving auth_two
};
// ... code using the auth_token
}

While we do not expect programmers to use named blocks with return in everyday cases, we anticipate that they will ease the development and usage of macros significantly.

Breaking Changes

Datatype Visibility Requirements

Currently, all structs in Move are, by convention, public: any other module or package can import them and refer to them by type. To make this clearer, Move 2024 requires that all structs be declared with the public keyword. For example:

// legacy code
struct S { x: u64 }

// Move 2024 code
public struct S { x: u64 }

Any non-public struct produces an error at this time, though the Move team is working on new visibility options for future releases.

Mutability Requirements

Previously, all variables in Move were implicitly mutable. For example:

fun f(s: S, y: u64): u64 {
let a = 0;
let S { x } = s;
a = 1;
x = 10;
y = 5;
x + y
}

Now, you must declare mutable variables explicitly:

fun f(s: S, mut y: u64): u64 {
let mut a = 0;
let S { mut x } = 5;
a = 1;
x = 10;
y = 5;
x + y
}

The compiler now produces an error if you attempt to reassign or borrow a variable mutably without this explicit declaration.

Removing Friends and public(friend)

Friends and the public(friend) visibilities were introduced early in Move's development, predating even the package system. As indicated in the public(package) section, public(package) deprecates public(friend) in Move 2024.

The following declaration now produces an error:

module pkg::m {
friend pkg::a;
public(friend) fun f() { ... }
}

module pkg::a {
fun calls_f() { ... pkg::m::f() ... }
}

Instead, if you want your function to be visible only in the package, write:

module pkg::m {
public(package) fun f() { ... }
}

module pkg::a {
// this now works directly
fun calls_f() { ... pkg::m::f() ... }
}

New Keywords

Looking toward the future, Move 2024 Beta adds the following keywords to the language: enum, for, match, mut, and type. Unfortunately, the compiler now produces parsing errors when it finds these in other positions. This is a necessary change as the language matures.

Revised Paths and Namespaces

Move 2024 revises how paths and namespaces work compared to legacy Move, toward easing enum aliasing in the future. Consider the following snippet from a test annotation in the iota_system library:

use iota_system::iota_system;
...
#[expected_failure(abort_code = iota_system::validator_set::EInvalidCap)]

Legacy Move would always treat a three-part name as an address(iota_system), module(validator_set), and module member (EInvalidCap). Move 2024 respects scope for use, so iota_system in the attribute resolves to the module, producing a name resolution error overall.

To avoid cases where this is the intended behavior, Move 2024 introduces a prefix operation for global qualification. To use, you can rewrite this annotation as:

use iota_system::iota_system;
...
#[expected_failure(abort_code = ::iota_system::validator_set::EInvalidCap)]
// ^ note `::` here

Move 2024 Editions

The beta release of Move 2024 comes with some powerful new features in addition to the breaking changes described here. There are also more on the horizon, such as syntactic macros, enums with pattern matching, and other user-defined syntax extensions.

beta Edition

  • beta (specified via edition = "2024.beta") is the recommended edition. It includes all the new features mentioned above and all breaking changes. While there is the risk of breaking changes or bugs in beta, you should feel comfortable using it in your projects. As new features are added and tested, they will be included in the beta edition. The beta edition will end after all features for the year have been added and finalized.

    alpha Edition

  • alpha (specified via edition = "2024.alpha") will get new features and changes as they are developed. Breaking changes to features in alpha should be expected. As such, take caution when usingalpha in your projects.