third_party/move/changes/3-abilities.md
Abilities are a new feature in Move version 1.2 to give more control over what actions are permissible for values of a given type. Previously in Move’s type system, there were copyable values and resource values. In this old system, copyable values were unrestricted, but resource values could not be copied and had to be used. With abilities, Move’s type system grants more fine grained control, allowing types to specifically allow certain operations for their values that were previously implicitly allowed/denied depending on the “kind” (copyable or resource).
Move’s kind system (copyable vs. resource structs) has been great, but it is not expressive enough for certain applications. Resource types are very powerful, but the system bundles up a lot of behavior into one kind, which does not always meet application needs. In the Diem Framework, there has been a need for a “hot potato” type for capabilities—that is some type that cannot be copied or dropped (like a resource), but cannot be stored in global storage. It is then a “hot potato” since you must keep passing it around and it must be consumed before the transaction completes. A type with these restrictions would allow the Move prover to verify the usage of these specific capabilities.
Extending the kind system just to handle this “hot potato” example is likely not the best future-proof solution. A more granular system can give programmers more control to implement not only “hot potato” types but also serve yet unknown needs. As such, we want the new system to be flexible enough to be extended in the future without requiring another large refactoring of the type system.
The kinds copyable and resource are replaced by four new abilities. These abilities gate access to various bytecode instructions. In order 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).
The four added abilities are copy, drop, store, and key. Broken down in detail:
copy
CopyLoc and ReadRefcopy, all values reachable inside of that value have copy.drop
Pop, WriteRef, StLoc, Eq and Neqdrop when the function returns with Retdrop, all values reachable inside of that value have drop.store
key is checkedstore, all values reachable inside of that value have store.key
MoveTo, MoveFrom, BorrowGlobal, BorrowGlobalMut, and Existskey, all values reachable inside of that value have store.Most primitive types all have copy, drop, and store with the exception of signer.
bool, u8, u64, u128, and address all have copy, drop, and store.signer has drop
vector<T> may have copy, drop, and store depending on the abilities of T.
& and mutable references &mut both have copy and drop.
store.To declare that a struct has an ability, it is declared with has <ability> after the struct name but before the fields. For example:
struct Ignorable has drop { f: u64 }
struct Pair has copy, drop, store { x: u64, y: u64 }
In this case Ignorable has the drop ability, and Pair has copy, drop, and store.
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 abilities given above. If a struct is declared with the ability...
copy, all fields must have copydrop, all fields must have dropstore, all fields must have storekey, all fields must have store
key is the only ability currently that doesn’t require itself.For example:
// A struct without any abilities
struct NoAbilities {}
struct WantsCopy has copy {
f: NoAbilities, // ERROR 'NoAbilities' does not have 'copy'
}
and similarly:
// A struct without any abilities
struct NoAbilities {}
struct MyResource has key {
f: NoAbilities, // Error 'NoAbilities' does not have 'store'
}
Copy
struct NoAbilities {}
struct S has copy, drop { f: bool }
fun example(x: u64, s: S) {
let x2 = copy x; // Valid, 'u64' has 'copy'
let s2 = copy s; // Valid, 'S' has 'copy'
}
fun invalid(account: signer, n: NoAbilities) {
let a2 = copy account; // Invalid, 'signer' does not have 'copy'
let n2 = copy n; // Invalid, 'NoAbilities' does not have 'drop'
}
Drop
struct NoAbilities {}
struct S has copy, drop { f: bool }
fun unused() {
true; // Valid, 'bool' has 'drop'
S { f: false }; // Valid, 'S' has 'drop'
}
fun left_in_local(account: signer): u64 {
let b = true;
let s = S { f: false };
// Valid return: 'account', 'b', and 's' have values
// but 'signer', 'bool', and 'S' have 'drop'
0
}
fun invalid_unused() {
NoAbilities {}; // Invalid, Cannot ignore 'NoAbilities' without 'drop'
}
fun invalid_left_in_local(): u64 {
let n = NoAbilities{};
// Invalid return: 'n' has a value and 'NoAbilities' does not have 'drop'
0
}
Store
// 'MyInnerResource' is declared with 'store' so all fields need 'store'
struct MyInnerResource has store {
yes: u64, // Valid, 'u64' has 'store'
// no: signer, Invalid, 'signer' does not have 'store'
}
// 'MyResource' is declared with 'key' so all fields need 'store'
struct MyResource has key {
yes: u64, // Valid, 'u64' has 'store'
inner: MyInnerResource, // Valid, 'MyInnerResource' has 'store'
// no: signer, Invalid, 'signer' does not have 'store'
}
Key
struct NoAbilities {}
struct MyResource has key { f: u64 }
fun valid(account: &signer) acquires MyResource {
let addr = signer::address_of(account);
let has_resource = exists<MyResource>(addr); // Valid, 'MyResource' has 'key'
if (!has_resource) {
move_to(account, MyResource { f: 0 }) // Valid, 'MyResource' has 'key'
};
let r = borrow_global_mut<MyResource>(addr) // Valid, 'MyResource' has 'key'
r.f = r.f + 1;
}
fun invalid(account: &signer) {
let has_it = exists<NoAbilities>(addr); // Invalid, 'NoAbilities' does not have 'key'
let NoAbilities {} = move_from<NoAbilities>(addr); // Invalid, does not have 'key'
move_to(account, NoAbilities {}); // Invalid, 'NoAbilities' does not have 'key'
borrow_global<NoAbilities>(addr); // Invalid, 'NoAbilities' does not have 'key'
}
Abilities can used to constrain generics, meaning that only types with that ability can instantiate that type parameter. This can be used on both function and struct type parameters:
fun foo<T: copy>(x: T): (T, T) { (copy x, x) }
struct CopyCup<T: copy> has copy { item: T }
Type parameters can have more than one constraint, signified with +
fun bar<T: copy + drop>(x: T): T { copy x }
struct AllCup<T: copy + drop + store + key> has copy, drop, store, key { item: T }
When abilities are annotated on a generic type, not all instances of that type are guaranteed to have that ability. Consider this struct declaration:
struct Cup<T> has copy, drop, store, key { item: T }
The type parameter T is 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. That means:
Cup has the ability copy only if T has copy.drop only if T has drop.store only if T has store.key only if T has store.This behavior might be a bit confusing at first, but it is extremely useful for collection-like types. Consider vector: we could consider it to have the following type declaration:
vector<T> has copy, drop, store;
With this, you can copy a vector value only if the inner elements can be copied. You can ignore a vector value only if the inner elements can be ignored/dropped. And, a vector can be in global storage only if the inner elements can be in global storage.
Conditional Copy
struct NoAbilities {}
struct S has copy, drop { f: bool }
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 'drop' because 'S' has 'drop'
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 'drop'
// because 'NoAbilities' does not have 'drop'
let c_n2 = copy c_n;
}
Drop
struct NoAbilities {}
struct S has copy, drop { f: bool }
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
}
Store
struct Cup<T> has copy, drop, store { item: T }
// 'MyInnerResource' is declared with 'store' so all fields need 'store'
struct MyInnerResource has store {
yes: Cup<u64>, // Valid, 'Cup<u64>' has 'store'
// no: Cup<signer>, Invalid, 'Cup<signer>' does not have 'store'
}
// 'MyResource' is declared with 'key' so all fields need 'store'
struct MyResource has key {
yes: Cup<u64>, // Valid, 'Cup<u64>' has 'store'
inner: Cup<MyInnerResource>, // Valid, 'Cup<MyInnerResource>' has 'store'
// no: Cup<signer>, Invalid, 'Cup<signer>' does not have 'store'
}
Key
struct NoAbilities {}
struct MyResource<T> has key { f: T }
fun valid(account: &signer) acquires MyResource {
let addr = signer::address_of(account);
// Valid, 'MyResource<u64>' has 'key'
let has_resource = exists<MyResource<u64>>(addr);
if (!has_resource) {
// Valid, 'MyResource<u64>' has 'key'
move_to(account, MyResource<u64> { f: 0 })
};
// Valid, 'MyResource<u64>' has 'key'
let r = borrow_global_mut<MyResource<u64>>(addr)
r.f = r.f + 1;
}
fun invalid(account: &signer) {
// Invalid, 'MyResource<NoAbilities>' does not have 'key'
let has_it = exists<MyResource<NoAbilities>>(addr);
// Invalid, 'MyResource<NoAbilities>' does not have 'key'
let NoAbilities {} = move_from<NoAbilities>(addr);
// Invalid, 'MyResource<NoAbilities>' does not have 'key'
move_to(account, NoAbilities {});
// Invalid, 'MyResource<NoAbilities>' does not have 'key'
borrow_global<NoAbilities>(addr);
}
The new ability system is backwards compatible with the kind system in nearly all cases. At the bytecode level, old modules and scripts with kinds can be loaded as if they were written with abilities.
For any struct:
resource struct, the struct will be given the abilities copy, drop, and store.resource, the struct will be given the abilities store and key.copyable becomes copy + dropresource becomes keystore is not given as it is not needed in the constraint position. Any usage will still work.For any function:
copyable becomes copy + drop + storeresource becomes key + dropstore is needed as it is not simple to determine if the type parameter will be used with global storage operations.Putting these rules together, the old code
struct S<T: copyable> {}
resource struct R<T1: resource, T2> {}
fun foo<Tc: copyable, Tr: resource, T>() {}
will be loaded as if it was written as:
struct S<T: copy + drop> has copy, drop, store {}
struct R<T1: key, T2> {}
fun foo<Tc: copy + drop + store, Tr: key + store, T: store>() {}
This leads to one spot where there is a breaking change, namely any function instantiated with signer as a type parameter will not now load because the type parameter will have the store constraint—all old function type parameters are given the store constraint—but signer does not have store. Given the restricted usage of signer, this is likely an extreme edge case, and we do not foresee it being an issue in practice for any project.
For the main motivating example for this change, it was considered to add a “hot potato” kind to the system. In the kind system there was:
Copyable which corresponds to copy + drop + store,Resource which corresponds to key + storeAll which sometimes corresponds to store and sometimes to no-abilityOften this was viewed with a sub-kinding system where Copyable <: All and Resource <: All. Adding a HotPotato kind to this system would be bit tricky, possibly giving a hierarchy of Copyable <: All and Resource <: HotPotato <: All. But, this could become a mess if:
AllWithStore kind, giving Copyable <: AllWithStore <: All and Resource <: AllWithStore <: All and Resource <: HotPotato <: All.The complexity around this sub-kinding approach led to the more granular approach described above with abilities. We were particularly worried about needing another kind in a year or two and having the whole thing collapse. With abilities, we can easily add new ones over time if needed.
The current rules around generics being conditional for generic types might be potentially confusing, especially given the keyword has. For instance:
struct Cup<T> has copy, drop, store { item: T }
Despite effectively saying “has copy” and “has drop” and “has store”, Cup<T> may or may not have the ability depending on what T is. This might be rather confusing. It was considered then that for generic types you would write:
struct Cup<T> has ?copy, ?drop, ?store { item: T }
This would mean exactly what it means today, where it may or may not have the ability depending on T and then using has without the question mark ?
struct Ex<T> has copy {}
would be equivalent to:
struct Ex<T: copy> has ?copy {}
The potential problem then comes in that there are a lot of combinations that are meaningless. So, in many cases the compiler would yell at you that there is really just one valid choice.
struct Ex<T: copy> has copy is sort of redundant and could just be struct Ex<T> has copy.struct Ex has ?copy is meaningless in some way, as every instance has the ability, and it is the same as struct Ex has copy.In short, having the option to have the question mark ? caused there to be more cases and possibly more confusion. Furthermore this system was not more expressive, as a programmer could always annotate the generic struct Cup<T: copy> has copy, this would force every instance to have copy in a more explicit manner. In short, just having one option and one rule that might be a bit more confusing in the way it reads at first, but reduces the amount of complexity to consider when declaring a new struct.
Many different names were considered for all aspects of the abilities system.
copyable and dropable and storable were all considered, but they felt too wordy. Shorter names felt more appropriate.mustuse or mustmove were considered for drop, but again, the more concise name felt better even if the others were more informative.resource was considered instead of key. The key ability is very similar to the resource keyword in many cases, but it felt very weird that something like a Coin which might have been resource struct Coin in the old system, would not be struct Coin has store and would not be a “resource”. Thus we are saving the word “resource” to be used in documentation for any time that does not have copy or drop.