llms.txt
Skip to main content

Move 2024 Migration Guide

Move 2024 is the current edition of the Move language maintained by Mysten Labs, and the edition this book teaches. This guide is written for readers migrating code - or knowledge - from the original edition (referred to below as Move 2020): it lists what changed, feature by feature, with a before-and-after example for each.

This guide is a high-level overview. Every feature listed here has a dedicated section in the book, linked from its heading - refer to them for the full story.

Using the 2024 Edition

The edition is specified in the [package] section of the Package Manifest. The stable 2024 edition is the default choice and the one to prefer; the 2024.beta and 2024.alpha editions give early access to features that are still in development and may change:

[package]
name = "my_package"
edition = "2024"

Migration Tool

The Move CLI has a migration tool that updates legacy code to the new edition. To use the migration tool, run the following command in the package directory:

$ sui move migrate

The migration tool handles the mechanical changes: the let mut syntax, the public modifier on structs, and the public(package) visibility in place of friend declarations.

Module Label

See Module.

A module no longer needs to wrap its body in a block: the module label syntax declares the module once, and everything that follows belongs to it - saving a level of indentation in the entire file. The block syntax is still supported, but only useful for declaring multiple modules in one file, which is not a recommended practice:

// Move 2020: module block
module book::my_module {
public struct Book {}
}

// Move 2024: module label
module book::my_module;

public struct Book {}

Mutable Bindings with let mut

See Primitive Types.

Move 2024 requires the mut keyword to declare a variable that can be reassigned or mutably borrowed. The compiler emits an error on an attempt to change a variable declared without mut:

// Move 2020
let x: u64 = 10;
x = 20;

// Move 2024
let mut x: u64 = 10;
x = 20;

Additionally, the mut keyword is used in tuple destructuring and function arguments, placed before the variable name:

// takes by value and mutates
fun takes_by_value_and_mutates(mut v: Value): Value {
v.field = 10;
v
}

// in tuple destructuring
fun destruct() {
let (mut x, y) = point::get_point();
}

// in struct unpack
fun unpack() {
let Point { x, mut y } = point::get_point();
}

Struct Visibility

See Custom Types with Struct.

In Move 2024, struct declarations require a visibility modifier. Currently, the only available visibility is public:

// Move 2020
struct Book {}

// Move 2024
public struct Book {}

Note that public applies to the struct type - the fields stay internal to the module, and only the defining module can pack and unpack the struct, exactly as before.

Friends Are Deprecated

See Visibility Modifiers.

The friend declarations and the public(friend) visibility are deprecated. In their place, the public(package) visibility makes a function callable from any module of the same package - with no declaration required. The friend book::module_name; statements are gone entirely:

// Move 2020
friend book::friend_module;
public(friend) fun protected_function() {}

// Move 2024: no friend declaration needed
public(package) fun protected_function() {}

Method Syntax

See Struct Methods.

Functions whose first argument is a type defined in the same module become methods of that type, callable with the dot syntax anywhere the type is used:

public fun count(c: &Counter): u64 { /* ... */ }

fun use_counter(c: &Counter) {
// Move 2020
let count = counter::count(c);

// Move 2024
let count = c.count();
}

The standard library and the Sui Framework make full use of this: native and standard types come with associated methods out of the box:

// vector to string and ascii string
let str: String = b"Hello, World!".to_string();
let ascii: ascii::String = b"Hello, World!".to_ascii_string();

// address to bytes
let bytes = @0xa11ce.to_bytes();

use fun and Method Aliases

See Struct Methods.

The use fun declaration associates a function with a type under a chosen method name. An alias can be declared for any type locally to the module; or publicly - with public use fun - if the type is defined in the same module:

// Local: the type is foreign to the module
use fun my_custom_function as vector.do_magic;

// Exported: the type is defined in the same module
public use fun kiosk_owner_cap_for as KioskOwnerCap.kiosk;

Index Syntax for Borrowing

See Vector and Index Syntax in the Move Reference.

Square brackets replace explicit borrow and borrow_mut calls on collection types:

fun play_vec() {
let mut v = vector[1, 2, 3, 4];
let first = &v[0]; // calls vector::borrow(&v, 0)
let first_mut = &mut v[0]; // calls vector::borrow_mut(&mut v, 0)
let first_copy = v[0]; // calls *vector::borrow(&v, 0)
}

The syntax is supported by vector and the collection types of the Sui Framework: VecMap, Table, Bag, ObjectTable, ObjectBag, and LinkedTable. A custom type can implement it by marking its borrow functions with the #[syntax(index)] attribute:

#[syntax(index)]
public fun borrow<T>(c: &List<T>, key: String): &T { /* ... */ }

#[syntax(index)]
public fun borrow_mut<T>(c: &mut List<T>, key: String): &mut T { /* ... */ }

String Literals

See String.

Move 2020 offered only byte-string literals, and constructing a String required an explicit conversion. The new edition adds the string literal "...", with the type inferred from context - it becomes a String, an ascii::String, or a vector<u8>, whichever is expected:

// Move 2020: bytes, converted at runtime
let str: String = string::utf8(b"Hello");

// Move 2024: the literal is checked and typed at compile time
let str: String = "Hello";
let ascii: std::ascii::String = "ASCII";

The contents are validated at compile time: a literal used as an ascii::String must contain only ASCII characters, or the code will not compile.

Enums and match

See Enums and Match.

Move 2024 introduces enums - user-defined types with multiple variants - and the match expression for handling them. Together they allow expressing varying data structures under a single type, something previously emulated with multiple structs and runtime checks:

/// One type - three different shapes of data.
public enum Segment has copy, drop {
Empty,
String(String),
Special { content: vector<u8>, encoding: u8 },
}

public fun is_empty(s: &Segment): bool {
match (s) {
Segment::Empty => true,
_ => false,
}
}

The match expression is not limited to enums: it works on primitive values and structs as well, requires the arms to be exhaustive, and supports the _ wildcard for the remaining cases.

Macros

See Macro Functions.

Move 2024 introduces macro functions - functions expanded at the call site during compilation, which can take lambdas as arguments. Macro names are followed by the ! mark:

// can be called as `for!(0, 10, |i| call(i));`
macro fun for($start: u64, $stop: u64, $body: |u64|) {
let mut i = $start;
let stop = $stop;
while (i < stop) {
$body(i);
i = i + 1
}
}

The familiar assert! is no longer special-cased compiler magic - it is a regular macro, and its error-code argument is now optional. The standard library ships a rich set of macros which quickly became the idiomatic way to write iteration:

let v = vector[1, 2, 3];

// instead of a hand-written while loop:
let doubled = v.map!(|n| n * 2);
let sum = v.fold!(0, |acc, n| acc + n);
v.do!(|n| std::debug::print(&n));

Abort Without a Code

See Aborting Execution.

The abort code is now optional: a bare abort (and assert! without a second argument) derives the code automatically, encoding the module and source line of the failure. It is a good fit for branches that are not expected to be reachable:

// Move 2020: a code was always required
if (!is_valid) abort 0;

// Move 2024
if (!is_valid) abort;
assert!(is_valid);

Clever Errors

See Aborting Execution.

Error constants marked with the #[error] attribute can carry a human-readable message - a vector<u8> instead of a bare u64. On abort, tooling decodes the constant name, the message, and the source line, removing the need to look up numeric codes:

#[error]
const ENotAuthorized: vector<u8> = "The caller is not authorized to perform this action";

public fun protected_action(/* ... */) {
assert!(is_authorized, ENotAuthorized);
}

Extending Modules in Tests

See Extending Modules.

The extend module declaration adds test-only members to an existing module - including a module from a foreign package - with full access to its private types. It solves the long-standing problem of testing against dependencies that ship no test utilities:

#[test_only]
extend module pyth::price_info;

// Functions defined here can pack and unpack the private
// types of `pyth::price_info` - in tests only.

Module extensions are still in development and currently require the 2024.alpha edition.

Further Reading

llms.txt