Skip to content

Commit

Permalink
[move] Async Move
Browse files Browse the repository at this point in the history
This PR realizes an initial implementation of Asynchronous Move.

- It adds an attribute deriver for the 'async' flavor in `language/move-compiler/src/attr_derivation/async_deriver.rs`.
- It adds a wrapper around the Move VM, called `AsyncVM`, which supports functionality to create actors and to handle messages (`language/async/move-async-vm`). There is one simple test for the new VM in the same crate which illustrates basic usage of this API (more tests to be added). The baseline files in the test illustrates the execution model.
- It extends the VM to allow for an 'environment extension'. The extension allows to access arbitray state passed into session functions, reaching the implementation of native Move functions. This feature allows the AsyncVM to extend the standard ChangeSet by new information of messages being send from Move code. The feature is generic enough to support other use cases (e.g. tables) and is based on Rust `Any` type.
- It adds a number of examples for Async Move at `language/async/examples`. However, the features of continuations and futures used in some of the examples aren't yet supported in the implementation, only core message sending.

Closes: diem#142
  • Loading branch information
wrwg authored and bors-diem committed Mar 29, 2022
1 parent ac094f2 commit 216acc8
Show file tree
Hide file tree
Showing 73 changed files with 2,287 additions and 26 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"language/benchmarks",
"language/evm/exec-utils",
"language/evm/move-to-yul",
"language/extensions/async/move-async-vm",
"language/move-analyzer",
"language/move-binary-format",
"language/move-binary-format/serializer-tests",
Expand Down
4 changes: 4 additions & 0 deletions language/extensions/async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**EXPERIMENTAL**

This is an experimental implementation of an asynchronous flavor of Move. It is work in progress and maybe subject to
arbitrary changes.
15 changes: 15 additions & 0 deletions language/extensions/async/examples/account/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "AccountExample"
version = "0.0.0"

[addresses]
Std = "0x1"
Async = "0x1"
This = "0x3"

[build]
language_flavor = "async"

[dependencies]
MoveAsyncLib = { local = "../../move-async-lib" }
MoveStdlib = { local = "../../../move-stdlib" }
17 changes: 17 additions & 0 deletions language/extensions/async/examples/account/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
This package contains examples for programming patterns in Async Move (code name AMV). We use a simple
account, where accounts live on different actors. One can deposit, withdraw, and transfer
between those accounts. The transfer is the interesting operation because it requires a roundtrip
communication between actors: only if deposit on end is successful, should the money be withdrawn
on the other.

Note that there is a conceptual bug in this solution: since multiple transfers can be initiated simultaneously,
but the money is not withdrawn before one finishes, the account balance could get negative (similar as the
reentrancy problem in Solidity). We chose to ignore this for the sake of illustrating the communication patterns.

Note also that the solutions assume reliable messaging (exactly-once semantics).

There are three versions of this example:

- With continuations.
- With futures.
- With plain message passing and an explicit state machine.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// NOTE: This example is just for illustration of one possible evolution of Async Move.
// Nothing here is implemented.

#[actor]
/// This is an instance of the Account example using continuations.
///
/// In this model each function gets passed an (opaque) representation of a continuation with which to continue
/// executing once it is done. We model the creation of continuations via attribute based code generation.
/// (See end of the module.)
module This::AccountContinuation {

use Async::Unit::Unit;
use Async::Cont::{Cont, done};

const MAX: u64 = 43;

#[state]
struct Account {
value: u64
}

#[init]
fun init(): Account {
Account{value: 0}
}

#[rpc]
fun deposit(this: &mut Account, v: u64, cont: Cont<Unit>) {
this.value = this.value + v;
done(cont)
}

#[rpc]
fun withdraw(this: &mut Account, v: u64, cont: Cont<Unit>) {
this.value = this.value - v;
done(cont)
}

#[rpc]
fun xfer(this: &Account, dest: address, v: u64, cont: Cont<Unit>) {
// Do not initiate the transfer if there are not enough funds.
assert!(this.value >= v, 1);
// Deposit on the other end, and only if this succeeds, withdraw here.
send_deposit(dest, v, cont_xfer_withdraw(v, cont))
}

#[cont]
fun xfer_withdraw(this: &mut Account, _previous: Unit, v: u64, cont: Cont<Unit>) {
withdraw(this, v, cont)
}

// ===============================================================
// The following native function definitions are automatically
// generated by a Move attribute preprocessor, and implemented
// automatically by the runtime environment.

#[_generated_send]
public native fun send_deposit(dest: address, v: u64, cont: Cont<Unit>);

#[_generated_cont]
native fun cont_xfer_withdraw(v: u64, cont: Cont<Unit>): Cont<Unit>;
}

/// Continuation support -- currently not in the Async library
module Async::Cont {
use Async::Unit::{Unit, unit};

/// A type which represents a continuation.
native struct Cont<T> has drop;

/// Continues execution with the given continuation, passing in the result from the previous step.
public native fun next<T>(cont: Cont<T>, result: T);

/// Shortcut for `next(cont, unit())`
public fun done(cont: Cont<Unit>) {
next(cont, unit())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// NOTE: This example is just for illustration of one possible evolution of Async Move.
// Nothing here is implemented.#[actor]

/// This is an instance of the Account example using futures. See the README for semantics.
///
/// This model is similar as continuations, but uses a more intuitive way to chain execution steps, which is
/// also the dominant one in most modern programming languages.
module This::AccountFuture {

use Async::Unit::Unit;
use Async::Future::{Future, Cont, done, followed_by};

#[state]
struct Account {
value: u64,
}

#[init]
fun init(): Account {
Account{value: 0}
}

#[rpc]
fun deposit(this: &mut Account, v: u64): Future<Unit> {
this.value = this.value + v;
done()
}

#[rpc]
fun withdraw(this: &mut Account, v: u64): Future<Unit> {
this.value = this.value - v;
done()
}

#[rpc]
fun xfer(this: &Account, dest: address, v: u64): Future<Unit> {
// Do not initiate the transfer if there are not enough funds.
assert!(this.value >= v, 1);
// Deposit on the other end, and only if this succeeds, withdraw here.
followed_by(rpc_deposit(dest, v), cont_xfer_withdraw(v))
}

#[cont]
fun xfer_withdraw(this: &mut Account, _previous: Unit, v: u64): Future<Unit> {
withdraw(this, v)
}

// ===============================================================
// The following native function definitions are automatically
// generated by a Move attribute preprocessor, and implemented
// automatically by the runtime environment.

#[_generated_rpc]
public native fun rpc_deposit(dest: address, v: u64): Future<Unit>;

#[_generated_cont]
native fun cont_xfer_withdraw(v: u64): Cont<Unit, Unit>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#[actor]
/// This is an instance of the Account example using an explicit state machine and one-way message passing.
///
/// In this example, we need to manage rpc state explicitly, remembering any outstanding transfers in the
/// actors state. This creates a little more code, but also is somehow more transparent and true to the
/// transactional semantics of Move. This version implements an additional `cleanup` message which cancels
/// pending transactions over a certain age.
module This::AccountStateMachine {

use Async::Actor::{self, epoch_time};
use Std::Vector;

const MAX: u64 = 43;
const MAX_TRANSFER_AGE: u128 = 100000000;

#[state]
struct Account {
value: u64,
xfer_id_counter: u64,
pending: vector<PendingTransfer>
}

struct PendingTransfer has drop {
xfer_id: u64,
amount: u64,
initiated_at: u128,
}

#[init]
fun init(): Account {
Account{value: 0, xfer_id_counter: 0, pending: Vector::empty()}
}

#[message]
fun deposit(this: &mut Account, v: u64) {
assert!(MAX - this.value >= v, 1);
this.value = this.value + v;
}

#[message]
fun withdraw(this: &mut Account, v: u64) {
assert!(this.value >= v, 2);
this.value = this.value - v;
}

#[message]
fun xfer(this: &mut Account, dest: address, v: u64) {
// Do not initiate the transfer if there are not enough funds.
assert!(this.value >= v, 1);
let xfer_id = new_xfer_id(this);
Vector::push_back(&mut this.pending, PendingTransfer{xfer_id, amount: v, initiated_at: epoch_time()});
// Call into a special version of deposit which calls us back once done.
send_xfer_deposit(dest, v, self(), xfer_id);
}

fun new_xfer_id(this: &mut Account): u64 {
let counter = &mut this.xfer_id_counter;
let xfer_id = *counter;
*counter = *counter + 1;
xfer_id
}

#[message]
fun xfer_deposit(this: &mut Account, v: u64, caller: address, xfer_id: u64) {
deposit(this, v);
send_xfer_finish(caller, xfer_id);
}

#[message]
fun xfer_finish(this: &mut Account, xfer_id: u64) {
let i = find_xfer(this, xfer_id);
let amount = Vector::borrow(&this.pending, i).amount;
Vector::remove(&mut this.pending, i);
withdraw(this, amount)
}

fun find_xfer(this: &Account, xfer_id: u64): u64 {
let pending = &this.pending;
let i = 0;
while (i < Vector::length(pending) && Vector::borrow(pending, i).xfer_id != xfer_id) {
i = i + 1;
};
assert!(i < Vector::length(pending), 3);
i
}

#[message]
/// A periodical cleanup which removes dated pending transfers.
fun cleanup(this: &mut Account) {
let pending = &mut this.pending;
let i = 0;
while (i < Vector::length(pending)) {
let p = Vector::borrow(pending, i);
if (epoch_time() - p.initiated_at >= MAX_TRANSFER_AGE) {
Vector::remove(pending, i);
} else {
i = i + 1;
}
}
}
}
14 changes: 14 additions & 0 deletions language/extensions/async/move-async-lib/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "MoveAsyncLib"
version = "0.0.0"

[addresses]
Std = "_"
Async = "_"

[dev-addresses]
Std = "0x1"
Async = "0x1"

[dependencies]
MoveStdlib = { local = "../../../move-stdlib" }
9 changes: 9 additions & 0 deletions language/extensions/async/move-async-lib/sources/Actor.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Actor support functions.
module Async::Actor {
/// Returns the address of the executing actor.
public native fun self(): address;

/// Returns the current virtual time, in micro seconds. This time does not increase during handling
/// of a message. On blockchains, this might be for example the block timestamp.
public native fun virtual_time(): u128;
}
21 changes: 21 additions & 0 deletions language/extensions/async/move-async-lib/sources/Future.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// NOTE: this module is not implemented yet
module Async::Future {
use Async::Unit::{Unit, Self};

/// A type which represents a future.
native struct Future<phantom T>;

/// A type which represents a continuation.
native struct Cont<phantom T, phantom R>;

/// Yield execution to the given continuation.
public native fun yield<T>(result: T): Future<T>;

/// Yield a future which first executes `f`, then continues with `cont`.
public native fun followed_by<T, R>(f: Future<T>, c: Cont<T, R>): Future<R>;

/// Shortcut for yield(unit())
public fun done(): Future<Unit> {
yield(Unit::unit())
}
}
19 changes: 19 additions & 0 deletions language/extensions/async/move-async-lib/sources/Runtime.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// Async runtime functions. Supporting the compiler.
module Async::Runtime {
public native fun send__0(actor: address, message_hash: u64);
public native fun send__1(actor: address, message_hash: u64, arg1: vector<u8>);
public native fun send__2(actor: address, message_hash: u64, arg1: vector<u8>, arg2: vector<u8>);
public native fun send__3(actor: address, message_hash: u64, arg1: vector<u8>, arg2: vector<u8>, arg3: vector<u8>);
public native fun send__4(actor: address, message_hash: u64,
arg1: vector<u8>, arg2: vector<u8>, arg3: vector<u8>, arg4: vector<u8>);
public native fun send__5(actor: address, message_hash: u64,
arg1: vector<u8>, arg2: vector<u8>, arg3: vector<u8>, arg4: vector<u8>, arg5: vector<u8>);
public native fun send__6(actor: address, message_hash: u64,
arg1: vector<u8>, arg2: vector<u8>, arg3: vector<u8>, arg4: vector<u8>, arg5: vector<u8>, arg6: vector<u8>);
public native fun send__7(actor: address, message_hash: u64,
arg1: vector<u8>, arg2: vector<u8>, arg3: vector<u8>, arg4: vector<u8>, arg5: vector<u8>, arg6: vector<u8>,
arg7: vector<u8>);
public native fun send__8(actor: address, message_hash: u64,
arg1: vector<u8>, arg2: vector<u8>, arg3: vector<u8>, arg4: vector<u8>, arg5: vector<u8>, arg6: vector<u8>,
arg7: vector<u8>, arg8: vector<u8>);
}
5 changes: 5 additions & 0 deletions language/extensions/async/move-async-lib/sources/Unit.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// The `Unit` type, a type which contains a singleton element.
module Async::Unit {
native struct Unit has drop, copy, store;
public native fun unit(): Unit;
}
Loading

0 comments on commit 216acc8

Please sign in to comment.