From d4bdbc21619178fc7131ede589767ab28418d78a Mon Sep 17 00:00:00 2001 From: Tim Zakian <2895723+tzakian@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:26:51 -0700 Subject: [PATCH] [move docs] Add enums to Move reference book (#68) --- .github/workflows/test.yml | 2 +- packages/reference/Move.lock | 26 ++ packages/reference/Move.toml | 2 +- packages/reference/sources/abilities.move | 8 +- reference/src/abilities.md | 32 +- reference/src/enums.md | 347 ++++++++++++++++++++++ reference/src/structs.md | 6 +- 7 files changed, 410 insertions(+), 13 deletions(-) create mode 100644 packages/reference/Move.lock create mode 100644 reference/src/enums.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 316618cf..f281fdda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: with: repo: MystenLabs/sui platform: ubuntu - version: mainnet + version: main cache: enable # Run the tests in every directory using the latest mainnet binary diff --git a/packages/reference/Move.lock b/packages/reference/Move.lock new file mode 100644 index 00000000..dab39740 --- /dev/null +++ b/packages/reference/Move.lock @@ -0,0 +1,26 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 2 +manifest_digest = "868EC315260CC0A203EABE09C39C72CFDDB66DE119BDF7E24CE5EAABE30EFCC1" +deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "main", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "main", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.27.0" +edition = "2024.alpha" +flavor = "sui" diff --git a/packages/reference/Move.toml b/packages/reference/Move.toml index 5dda7d4d..8519c7fd 100644 --- a/packages/reference/Move.toml +++ b/packages/reference/Move.toml @@ -1,6 +1,6 @@ [package] name = "reference" -edition = "2024.beta" +edition = "2024.alpha" [dependencies.Sui] git = "https://github.com/MystenLabs/sui.git" diff --git a/packages/reference/sources/abilities.move b/packages/reference/sources/abilities.move index ce3b407b..91689b35 100644 --- a/packages/reference/sources/abilities.move +++ b/packages/reference/sources/abilities.move @@ -4,11 +4,15 @@ #[allow(unused_field)] module ref::abilities { -// ANCHOR: annotating_structs +// ANCHOR: annotating_datatypes public struct Ignorable has drop { f: u64 } public struct Pair has copy, drop, store { x: u64, y: u64 } public struct MyVec(vector) has copy, drop, store; -// ANCHOR_END: annotating_structs + +public enum IgnorableEnum has drop { Variant } +public enum PairEnum has copy, drop, store { Variant } +public enum MyVecEnum { Variant } has copy, drop, store; +// ANCHOR_END: annotating_datatypes // ANCHOR: conditional_abilities // public struct Cup has copy, drop, store, key { item: T } diff --git a/reference/src/abilities.md b/reference/src/abilities.md index 490142fa..0c832a3e 100644 --- a/reference/src/abilities.md +++ b/reference/src/abilities.md @@ -98,17 +98,17 @@ All primitive, builtin types have `copy`, `drop`, and `store`. Note that none of the primitive types have `key`, meaning none of them can be used directly with storage operations. -## Annotating Structs +## Annotating Structs and Enums -To declare that a `struct` has an ability, it is declared with `has ` after the struct name -and either before or after the fields. For example: +To declare that a `struct` or `enum` has an ability, it is declared with `has ` after the +datatype name and either before or after the fields/variants. For example: ```move -{{#include ../../packages/reference/sources/abilities.move:annotating_structs}} +{{#include ../../packages/reference/sources/abilities.move:annotating_datatypes}} ``` -In this case: `Ignorable` has the `drop` ability. `Pair` and `MyVec` both have `copy`, `drop`, and -`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 @@ -124,6 +124,16 @@ reachability rules for the abilities given above. If a struct is declared with t - `key`, all fields must have `store`. - `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 have `copy`. +- `drop`, all fields of all variants must have `drop`. +- `store`, all fields of all variants must have `store`. +- `key`, is not allowed on enums as previously mentioned. + For example: ```move @@ -133,6 +143,11 @@ 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: @@ -144,6 +159,11 @@ 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 diff --git a/reference/src/enums.md b/reference/src/enums.md new file mode 100644 index 00000000..f76de8f6 --- /dev/null +++ b/reference/src/enums.md @@ -0,0 +1,347 @@ +# 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: + +```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](./abilities.md) 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: + +```move +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: + +```move +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 { + Leaf(T), + Node { left: BTree, right: BTree }, + // ^ 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](./abilities/object.md) -- you need to grant it additional [abilities](./abilities.md) by +annotating them with `has `: + +```move +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: + +```move +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](./abilities.md#annotating-structs-and-enums). + +## 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`. + +```move +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. + +```move +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: + +```move +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](./pattern_matching.md) 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 `=>`. + +```move +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: + +```move +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: + +```move +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: + +```move +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): + +```move +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. + +```move +module a::m { + public enum X has drop { + A(u64), + B(u64), + } + + public fun overwrite_enum(x: &mut X) { + *x = X::A(10); + } +} +``` + +```move +let mut x = X::B(20); +overwrite_enum(&mut x); +assert!(x == X::A(10)); +``` diff --git a/reference/src/structs.md b/reference/src/structs.md index fc282341..4f3911b6 100644 --- a/reference/src/structs.md +++ b/reference/src/structs.md @@ -70,7 +70,7 @@ must be terminated with a semicolon: ```move module a::m { - public PreNamedAbilities has copy, drop { x: u64, y: bool } + 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 @@ -78,7 +78,7 @@ module a::m { public struct NamedInvalidAbilities has copy { x: u64, y: bool } has drop; // ^ ERROR! duplicate ability declaration - public PrePositionalAbilities has copy, drop (u64, bool) + 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 @@ -88,7 +88,7 @@ module a::m { ``` For more details, see the section on -[annotating a struct's abilities](./abilities.md#annotating-structs). +[annotating a struct's abilities](./abilities.md#annotating-structs-and-enums). ### Naming