diff options
author | Alon Zakai <azakai@google.com> | 2019-06-16 11:01:29 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-06-16 11:01:29 -0700 |
commit | 3d3a5a6c28e9266eebcad7315cce96fba6e6dc09 (patch) | |
tree | bfc31c2683514f28e1664936e3d86e2e1dbd1da1 | |
parent | 1cd34c211dffa66fa2f2e45f3f9291e8ad836e07 (diff) | |
download | binaryen-3d3a5a6c28e9266eebcad7315cce96fba6e6dc09.tar.gz binaryen-3d3a5a6c28e9266eebcad7315cce96fba6e6dc09.tar.bz2 binaryen-3d3a5a6c28e9266eebcad7315cce96fba6e6dc09.zip |
Bysyncify: bysyncify_stop_unwind (#2173)
Add a method to note the stopping of an unwind. This is enough to implement coroutines. Includes an example of coroutine usage in the test suite.
-rw-r--r-- | src/passes/Bysyncify.cpp | 43 | ||||
-rw-r--r-- | test/passes/bysyncify.txt | 89 | ||||
-rw-r--r-- | test/passes/bysyncify.wast | 5 | ||||
-rw-r--r-- | test/passes/bysyncify_optimize-level=1.txt | 10 | ||||
-rw-r--r-- | test/passes/bysyncify_pass-arg=bysyncify-imports@env.import,env.import2.txt (renamed from test/passes/bysyncify_pass-arg=bysyncify@env.import,env.import2.txt) | 58 | ||||
-rw-r--r-- | test/passes/bysyncify_pass-arg=bysyncify-imports@env.import,env.import2.wast (renamed from test/passes/bysyncify_pass-arg=bysyncify@env.import,env.import2.wast) | 0 | ||||
-rw-r--r-- | test/unit/input/bysyncify-coroutine.wast | 44 | ||||
-rw-r--r-- | test/unit/input/bysyncify-sleep.wast (renamed from test/unit/input/bysyncify.wast) | 0 | ||||
-rw-r--r-- | test/unit/input/bysyncify.js | 352 | ||||
-rw-r--r-- | test/unit/test_bysyncify.py | 3 |
10 files changed, 439 insertions, 165 deletions
diff --git a/src/passes/Bysyncify.cpp b/src/passes/Bysyncify.cpp index ffbf6db1c..f9f679f2e 100644 --- a/src/passes/Bysyncify.cpp +++ b/src/passes/Bysyncify.cpp @@ -126,13 +126,21 @@ // proper place, and the end to the proper end based on how much memory // you reserved. Note that bysyncify will grow the stack up. // -// The pass will also create three functions that let you control unwinding +// The pass will also create four functions that let you control unwinding // and rewinding: // // * bysyncify_start_unwind(data : i32): call this to start unwinding the // stack from the current location. "data" must point to a data // structure as described above (with fields containing valid data). // +// * bysyncify_stop_unwind(): call this to note that unwinding has +// concluded. If no other code will run before you start to rewind, +// this is not strictly necessary, however, if you swap between +// coroutines, or even just want to run some normal code during a +// "sleep", then you must call this at the proper time. Otherwise, +// the code will think it is still unwinding when it should not be, +// which means it will keep unwinding in a meaningless way. +// // * bysyncify_start_rewind(data : i32): call this to start rewinding the // stack vack up to the location stored in the provided data. This prepares // for the rewind; to start it, you must call the first function in the @@ -141,17 +149,18 @@ // * bysyncify_stop_rewind(): call this to note that rewinding has // concluded, and normal execution can resume. // -// These three functions are exported so that you can call them from the +// These four functions are exported so that you can call them from the // outside. If you want to manage things from inside the wasm, then you // couldn't have called them before they were created by this pass. To work // around that, you can create imports to bysyncify.start_unwind, -// bysyncify.start_rewind, and bysyncify.stop_rewind; if those exist when -// this pass runs then it will turn those into direct calls to the functions -// that it creates. +// bysyncify.stop_unwind, bysyncify.start_rewind, and bysyncify.stop_rewind; +// if those exist when this pass runs then it will turn those into direct +// calls to the functions that it creates. // // To use this API, call bysyncify_start_unwind when you want to. The call // stack will then be unwound, and so execution will resume in the JS or -// other host environment on the outside that called into wasm. Then when +// other host environment on the outside that called into wasm. When you +// return there after unwinding, call bysyncify_stop_unwind. Then when // you want to rewind, call bysyncify_start_rewind, and then call the same // initial function you called before, so that unwinding can begin. The // unwinding will reach the same function from which you started, since @@ -163,15 +172,16 @@ // To customize this, you can provide an argument to wasm-opt (or another // tool that can run this pass), // -// --pass-arg=bysyncify@module1.base1,module2.base2,module3.base3 +// --pass-arg=bysyncify-imports@module1.base1,module2.base2,module3.base3 // // Each module.base in that comma-separated list will be considered to // be an import that can unwind/rewind, and all others are assumed not to // (aside from the bysyncify.* imports, which are always assumed to). To // say that no import (aside from bysyncify.*) can do so (that is, the // opposite of the default behavior), you can simply provide an import -// that doesn't exist (say, -// --pass-arg=bysyncify@no.imports +// that doesn't exist, for example: + +// --pass-arg=bysyncify-imports@no.imports // #include "ir/effects.h" @@ -190,11 +200,13 @@ namespace { static const Name BYSYNCIFY_STATE = "__bysyncify_state"; static const Name BYSYNCIFY_DATA = "__bysyncify_data"; static const Name BYSYNCIFY_START_UNWIND = "bysyncify_start_unwind"; +static const Name BYSYNCIFY_STOP_UNWIND = "bysyncify_stop_unwind"; static const Name BYSYNCIFY_START_REWIND = "bysyncify_start_rewind"; static const Name BYSYNCIFY_STOP_REWIND = "bysyncify_stop_rewind"; static const Name BYSYNCIFY_UNWIND = "__bysyncify_unwind"; static const Name BYSYNCIFY = "bysyncify"; static const Name START_UNWIND = "start_unwind"; +static const Name STOP_UNWIND = "stop_unwind"; static const Name START_REWIND = "start_rewind"; static const Name STOP_REWIND = "stop_rewind"; static const Name BYSYNCIFY_GET_CALL_INDEX = "__bysyncify_get_call_index"; @@ -234,10 +246,10 @@ public: ModuleUtils::ParallelFunctionMap<Info> scanner( module, [&](Function* func, Info& info) { if (func->imported()) { - // The bysyncify imports to start an unwind or rewind can definitely - // change the state. + // The bysyncify imports can definitely change the state. if (func->module == BYSYNCIFY && - (func->base == START_UNWIND || func->base == START_REWIND)) { + (func->base == START_UNWIND || func->base == STOP_UNWIND || + func->base == START_REWIND || func->base == STOP_REWIND)) { info.canChangeState = true; } else { info.canChangeState = @@ -252,6 +264,8 @@ public: // Redirect the imports to the functions we'll add later. if (target->base == START_UNWIND) { curr->target = BYSYNCIFY_START_UNWIND; + } else if (target->base == STOP_UNWIND) { + curr->target = BYSYNCIFY_STOP_UNWIND; } else if (target->base == START_REWIND) { curr->target = BYSYNCIFY_START_REWIND; } else if (target->base == STOP_REWIND) { @@ -328,7 +342,9 @@ public: // We only implement these at the very end, but we know that they // definitely change the state. if (curr->target == BYSYNCIFY_START_UNWIND || + curr->target == BYSYNCIFY_STOP_UNWIND || curr->target == BYSYNCIFY_START_REWIND || + curr->target == BYSYNCIFY_STOP_REWIND || curr->target == BYSYNCIFY_GET_CALL_INDEX || curr->target == BYSYNCIFY_CHECK_CALL_INDEX) { canChangeState = true; @@ -775,7 +791,7 @@ struct Bysyncify : public Pass { // Find which imports can change the state. const char* ALL_IMPORTS_CAN_CHANGE_STATE = "__bysyncify_all_imports"; auto stateChangingImports = runner->options.getArgumentOrDefault( - "bysyncify", ALL_IMPORTS_CAN_CHANGE_STATE); + "bysyncify-imports", ALL_IMPORTS_CAN_CHANGE_STATE); bool allImportsCanChangeState = stateChangingImports == ALL_IMPORTS_CAN_CHANGE_STATE; std::string separator = ","; @@ -895,6 +911,7 @@ private: true, State::Unwinding, builder.makeGlobalSet(BYSYNCIFY_DATA, builder.makeLocalGet(0, i32))); + makeFunction(BYSYNCIFY_STOP_UNWIND, false, State::Normal, nullptr); makeFunction( BYSYNCIFY_START_REWIND, true, diff --git a/test/passes/bysyncify.txt b/test/passes/bysyncify.txt index 1ec35057e..2ab682149 100644 --- a/test/passes/bysyncify.txt +++ b/test/passes/bysyncify.txt @@ -6,6 +6,7 @@ (global $__bysyncify_state (mut i32) (i32.const 0)) (global $__bysyncify_data (mut i32) (i32.const 0)) (export "bysyncify_start_unwind" (func $bysyncify_start_unwind)) + (export "bysyncify_stop_unwind" (func $bysyncify_stop_unwind)) (export "bysyncify_start_rewind" (func $bysyncify_start_rewind)) (export "bysyncify_stop_rewind" (func $bysyncify_stop_rewind)) (func $do_sleep (; 0 ;) (type $FUNCSIG$v) @@ -151,16 +152,40 @@ (i32.const 2) ) ) - (if - (i32.eq - (global.get $__bysyncify_state) - (i32.const 0) - ) - (block + (block + (if + (i32.eq + (global.get $__bysyncify_state) + (i32.const 0) + ) (global.set $sleeping (i32.const 0) ) - (call $bysyncify_stop_rewind) + ) + (if + (if (result i32) + (i32.eq + (global.get $__bysyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $3) + (i32.const 1) + ) + ) + (block + (call $bysyncify_stop_rewind) + (if + (i32.eq + (global.get $__bysyncify_state) + (i32.const 1) + ) + (br $__bysyncify_unwind + (i32.const 1) + ) + ) + ) ) ) ) @@ -458,6 +483,31 @@ ) ) (block + (call $bysyncify_stop_unwind) + (if + (i32.eq + (global.get $__bysyncify_state) + (i32.const 1) + ) + (br $__bysyncify_unwind + (i32.const 0) + ) + ) + ) + ) + (if + (if (result i32) + (i32.eq + (global.get $__bysyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $1) + (i32.const 1) + ) + ) + (block (call $bysyncify_start_rewind (i32.const 4) ) @@ -467,7 +517,7 @@ (i32.const 1) ) (br $__bysyncify_unwind - (i32.const 0) + (i32.const 1) ) ) ) @@ -481,7 +531,7 @@ (i32.const 1) (i32.eq (local.get $1) - (i32.const 1) + (i32.const 2) ) ) (block @@ -492,7 +542,7 @@ (i32.const 1) ) (br $__bysyncify_unwind - (i32.const 1) + (i32.const 2) ) ) ) @@ -546,7 +596,12 @@ (local.get $0) ) ) - (func $bysyncify_start_rewind (; 7 ;) (param $0 i32) + (func $bysyncify_stop_unwind (; 7 ;) + (global.set $__bysyncify_state + (i32.const 0) + ) + ) + (func $bysyncify_start_rewind (; 8 ;) (param $0 i32) (if (i32.gt_u (i32.load @@ -565,7 +620,7 @@ (local.get $0) ) ) - (func $bysyncify_stop_rewind (; 8 ;) + (func $bysyncify_stop_rewind (; 9 ;) (global.set $__bysyncify_state (i32.const 0) ) @@ -583,6 +638,7 @@ (global $__bysyncify_state (mut i32) (i32.const 0)) (global $__bysyncify_data (mut i32) (i32.const 0)) (export "bysyncify_start_unwind" (func $bysyncify_start_unwind)) + (export "bysyncify_stop_unwind" (func $bysyncify_stop_unwind)) (export "bysyncify_start_rewind" (func $bysyncify_start_rewind)) (export "bysyncify_stop_rewind" (func $bysyncify_stop_rewind)) (func $calls-import (; 3 ;) (type $FUNCSIG$v) @@ -2683,7 +2739,12 @@ (local.get $0) ) ) - (func $bysyncify_start_rewind (; 20 ;) (param $0 i32) + (func $bysyncify_stop_unwind (; 20 ;) + (global.set $__bysyncify_state + (i32.const 0) + ) + ) + (func $bysyncify_start_rewind (; 21 ;) (param $0 i32) (if (i32.gt_u (i32.load @@ -2702,7 +2763,7 @@ (local.get $0) ) ) - (func $bysyncify_stop_rewind (; 21 ;) + (func $bysyncify_stop_rewind (; 22 ;) (global.set $__bysyncify_state (i32.const 0) ) diff --git a/test/passes/bysyncify.wast b/test/passes/bysyncify.wast index 7c92bd86f..00ecc7fde 100644 --- a/test/passes/bysyncify.wast +++ b/test/passes/bysyncify.wast @@ -2,6 +2,7 @@ (module (memory 1 2) (import "bysyncify" "start_unwind" (func $bysyncify_start_unwind (param i32))) + (import "bysyncify" "stop_unwind" (func $bysyncify_stop_unwind)) (import "bysyncify" "start_rewind" (func $bysyncify_start_rewind (param i32))) (import "bysyncify" "stop_rewind" (func $bysyncify_stop_rewind)) (global $sleeping (mut i32) (i32.const 0)) @@ -34,8 +35,10 @@ ;; work will sleep, so we exit through here while it is paused ) ;; the second event called from the main event loop: to resume $work, - ;; initiate a rewind, and then do the call to start things back up + ;; stop the unwind, then prepare a rewind, and initiate it by doing + ;; the call to rewind the call stack back up to where it was (func $second_event + (call $bysyncify_stop_unwind) (call $bysyncify_start_rewind (i32.const 4)) (call $work) ) diff --git a/test/passes/bysyncify_optimize-level=1.txt b/test/passes/bysyncify_optimize-level=1.txt index 6eb7217e4..73456a7ae 100644 --- a/test/passes/bysyncify_optimize-level=1.txt +++ b/test/passes/bysyncify_optimize-level=1.txt @@ -10,6 +10,7 @@ (global $__bysyncify_state (mut i32) (i32.const 0)) (global $__bysyncify_data (mut i32) (i32.const 0)) (export "bysyncify_start_unwind" (func $bysyncify_start_unwind)) + (export "bysyncify_stop_unwind" (func $bysyncify_stop_unwind)) (export "bysyncify_start_rewind" (func $bysyncify_start_rewind)) (export "bysyncify_stop_rewind" (func $bysyncify_stop_rewind)) (func $calls-import (; 3 ;) (type $FUNCSIG$v) @@ -1514,7 +1515,12 @@ (local.get $0) ) ) - (func $bysyncify_start_rewind (; 20 ;) (param $0 i32) + (func $bysyncify_stop_unwind (; 20 ;) + (global.set $__bysyncify_state + (i32.const 0) + ) + ) + (func $bysyncify_start_rewind (; 21 ;) (param $0 i32) (if (i32.gt_u (i32.load @@ -1533,7 +1539,7 @@ (local.get $0) ) ) - (func $bysyncify_stop_rewind (; 21 ;) + (func $bysyncify_stop_rewind (; 22 ;) (global.set $__bysyncify_state (i32.const 0) ) diff --git a/test/passes/bysyncify_pass-arg=bysyncify@env.import,env.import2.txt b/test/passes/bysyncify_pass-arg=bysyncify-imports@env.import,env.import2.txt index e850da6cf..81a37a643 100644 --- a/test/passes/bysyncify_pass-arg=bysyncify@env.import,env.import2.txt +++ b/test/passes/bysyncify_pass-arg=bysyncify-imports@env.import,env.import2.txt @@ -6,6 +6,7 @@ (global $__bysyncify_state (mut i32) (i32.const 0)) (global $__bysyncify_data (mut i32) (i32.const 0)) (export "bysyncify_start_unwind" (func $bysyncify_start_unwind)) + (export "bysyncify_stop_unwind" (func $bysyncify_stop_unwind)) (export "bysyncify_start_rewind" (func $bysyncify_start_rewind)) (export "bysyncify_stop_rewind" (func $bysyncify_stop_rewind)) (func $do_sleep (; 0 ;) (type $FUNCSIG$v) @@ -151,16 +152,40 @@ (i32.const 2) ) ) - (if - (i32.eq - (global.get $__bysyncify_state) - (i32.const 0) - ) - (block + (block + (if + (i32.eq + (global.get $__bysyncify_state) + (i32.const 0) + ) (global.set $sleeping (i32.const 0) ) - (call $bysyncify_stop_rewind) + ) + (if + (if (result i32) + (i32.eq + (global.get $__bysyncify_state) + (i32.const 0) + ) + (i32.const 1) + (i32.eq + (local.get $3) + (i32.const 1) + ) + ) + (block + (call $bysyncify_stop_rewind) + (if + (i32.eq + (global.get $__bysyncify_state) + (i32.const 1) + ) + (br $__bysyncify_unwind + (i32.const 1) + ) + ) + ) ) ) ) @@ -546,7 +571,12 @@ (local.get $0) ) ) - (func $bysyncify_start_rewind (; 7 ;) (param $0 i32) + (func $bysyncify_stop_unwind (; 7 ;) + (global.set $__bysyncify_state + (i32.const 0) + ) + ) + (func $bysyncify_start_rewind (; 8 ;) (param $0 i32) (if (i32.gt_u (i32.load @@ -565,7 +595,7 @@ (local.get $0) ) ) - (func $bysyncify_stop_rewind (; 8 ;) + (func $bysyncify_stop_rewind (; 9 ;) (global.set $__bysyncify_state (i32.const 0) ) @@ -583,6 +613,7 @@ (global $__bysyncify_state (mut i32) (i32.const 0)) (global $__bysyncify_data (mut i32) (i32.const 0)) (export "bysyncify_start_unwind" (func $bysyncify_start_unwind)) + (export "bysyncify_stop_unwind" (func $bysyncify_stop_unwind)) (export "bysyncify_start_rewind" (func $bysyncify_start_rewind)) (export "bysyncify_stop_rewind" (func $bysyncify_stop_rewind)) (func $calls-import (; 3 ;) (type $FUNCSIG$v) @@ -1971,7 +2002,12 @@ (local.get $0) ) ) - (func $bysyncify_start_rewind (; 20 ;) (param $0 i32) + (func $bysyncify_stop_unwind (; 20 ;) + (global.set $__bysyncify_state + (i32.const 0) + ) + ) + (func $bysyncify_start_rewind (; 21 ;) (param $0 i32) (if (i32.gt_u (i32.load @@ -1990,7 +2026,7 @@ (local.get $0) ) ) - (func $bysyncify_stop_rewind (; 21 ;) + (func $bysyncify_stop_rewind (; 22 ;) (global.set $__bysyncify_state (i32.const 0) ) diff --git a/test/passes/bysyncify_pass-arg=bysyncify@env.import,env.import2.wast b/test/passes/bysyncify_pass-arg=bysyncify-imports@env.import,env.import2.wast index 7c92bd86f..7c92bd86f 100644 --- a/test/passes/bysyncify_pass-arg=bysyncify@env.import,env.import2.wast +++ b/test/passes/bysyncify_pass-arg=bysyncify-imports@env.import,env.import2.wast diff --git a/test/unit/input/bysyncify-coroutine.wast b/test/unit/input/bysyncify-coroutine.wast new file mode 100644 index 000000000..ace97c6e6 --- /dev/null +++ b/test/unit/input/bysyncify-coroutine.wast @@ -0,0 +1,44 @@ +(module + (memory 1 2) + ;; import a "yield" function that receives the current value, + ;; then pauses execution until it is resumed later. + (import "env" "yield" (func $yield (param i32))) + (export "memory" (memory 0)) + ;; simple linear progression in a loop + (func "linear" (result i32) + (local $x i32) + (loop $l + (call $yield (local.get $x)) + (local.set $x + (i32.add (local.get $x) (i32.const 10)) + ) + (br $l) + ) + ) + ;; exponential in a loop + (func "exponential" (result i32) + (local $x i32) + (local.set $x + (i32.const 1) + ) + (loop $l + (call $yield (local.get $x)) + (local.set $x + (i32.mul (local.get $x) (i32.const 2)) + ) + (br $l) + ) + ) + ;; just some weird numbers, no loop + (func "weird" (result i32) + (call $yield (i32.const 42)) + (call $yield (i32.const 1337)) + (call $yield (i32.const 0)) + (call $yield (i32.const -1000)) + (call $yield (i32.const 42)) + (call $yield (i32.const 314159)) + (call $yield (i32.const 21828)) + (unreachable) + ) +) + diff --git a/test/unit/input/bysyncify.wast b/test/unit/input/bysyncify-sleep.wast index 91fb5a327..91fb5a327 100644 --- a/test/unit/input/bysyncify.wast +++ b/test/unit/input/bysyncify-sleep.wast diff --git a/test/unit/input/bysyncify.js b/test/unit/input/bysyncify.js index 97d7a0da5..4fd6f4b84 100644 --- a/test/unit/input/bysyncify.js +++ b/test/unit/input/bysyncify.js @@ -5,151 +5,257 @@ function assert(x, y) { var fs = require('fs'); -// Get and compile the wasm. - -var binary = fs.readFileSync('a.wasm'); - -var module = new WebAssembly.Module(binary); - -var DATA_ADDR = 4; - -var sleeps = 0; - -var sleeping = false; - -var instance = new WebAssembly.Instance(module, { - env: { - sleep: function() { - logMemory(); -assert(view[0] == 0); - if (!sleeping) { - // We are called in order to start a sleep/unwind. - console.log('sleep...'); - sleeps++; - // Unwinding. - exports.bysyncify_start_unwind(DATA_ADDR); - // Fill in the data structure. The first value has the stack location, - // which for simplicity we can start right after the data structure itself. - view[DATA_ADDR >> 2] = DATA_ADDR + 8; - // The end of the stack will not be reached here anyhow. - view[DATA_ADDR + 4 >> 2] = 1024; - sleeping = true; - } else { - // We are called as part of a resume/rewind. Stop sleeping. - console.log('resume...'); - exports.bysyncify_stop_rewind(); - // The stack should have been all used up, and so returned to the original state. - assert(view[DATA_ADDR >> 2] == DATA_ADDR + 8); - assert(view[DATA_ADDR + 4 >> 2] == 1024); - sleeping = false; +function sleepTests() { + console.log('\nsleep tests\n\n'); + + // Get and compile the wasm. + + var binary = fs.readFileSync('a.wasm'); + + var module = new WebAssembly.Module(binary); + + var DATA_ADDR = 4; + + var sleeps = 0; + + var sleeping = false; + + var instance = new WebAssembly.Instance(module, { + env: { + sleep: function() { + logMemory(); + if (!sleeping) { + // We are called in order to start a sleep/unwind. + console.log('sleep...'); + sleeps++; + // Unwinding. + exports.bysyncify_start_unwind(DATA_ADDR); + // Fill in the data structure. The first value has the stack location, + // which for simplicity we can start right after the data structure itself. + view[DATA_ADDR >> 2] = DATA_ADDR + 8; + // The end of the stack will not be reached here anyhow. + view[DATA_ADDR + 4 >> 2] = 1024; + sleeping = true; + } else { + // We are called as part of a resume/rewind. Stop sleeping. + console.log('resume...'); + exports.bysyncify_stop_rewind(); + // The stack should have been all used up, and so returned to the original state. + assert(view[DATA_ADDR >> 2] == DATA_ADDR + 8); + assert(view[DATA_ADDR + 4 >> 2] == 1024); + sleeping = false; + } + logMemory(); + }, + tunnel: function(x) { + console.log('tunneling, sleep == ' + sleeping); + return exports.end_tunnel(x); } - logMemory(); - }, - tunnel: function(x) { - console.log('tunneling, sleep == ' + sleeping); - return exports.end_tunnel(x); } - } -}); + }); -var exports = instance.exports; -var view = new Int32Array(exports.memory.buffer); + var exports = instance.exports; + var view = new Int32Array(exports.memory.buffer); -function logMemory() { - // Log the relevant memory locations for debugging purposes. - console.log('memory: ', view[0 >> 2], view[4 >> 2], view[8 >> 2], view[12 >> 2], view[16 >> 2], view[20 >> 2], view[24 >> 2]); -} + function logMemory() { + // Log the relevant memory locations for debugging purposes. + console.log('memory: ', view[0 >> 2], view[4 >> 2], view[8 >> 2], view[12 >> 2], view[16 >> 2], view[20 >> 2], view[24 >> 2]); + } -function runTest(name, expectedSleeps, expectedResult, params) { - params = params || []; + function runTest(name, expectedSleeps, expectedResult, params) { + params = params || []; - console.log('\n==== testing ' + name + ' ===='); + console.log('\n==== testing ' + name + ' ===='); - sleeps = 0; + sleeps = 0; - logMemory(); + logMemory(); - // Run until the sleep. - var result = exports[name].apply(null, params); - logMemory(); + // Run until the sleep. + var result = exports[name].apply(null, params); + logMemory(); - if (expectedSleeps > 0) { - assert(!result, 'results during sleep are meaningless, just 0'); + if (expectedSleeps > 0) { + assert(!result, 'results during sleep are meaningless, just 0'); + exports.bysyncify_stop_unwind(DATA_ADDR); + + for (var i = 0; i < expectedSleeps - 1; i++) { + console.log('rewind, run until the next sleep'); + exports.bysyncify_start_rewind(DATA_ADDR); + result = exports[name](); // no need for params on later times + assert(!result, 'results during sleep are meaningless, just 0'); + logMemory(); + exports.bysyncify_stop_unwind(DATA_ADDR); + } - for (var i = 0; i < expectedSleeps - 1; i++) { - console.log('rewind, run until the next sleep'); + console.log('rewind and run til the end.'); exports.bysyncify_start_rewind(DATA_ADDR); - result = exports[name](); // no need for params on later times - assert(!result, 'results during sleep are meaningless, just 0'); - assert(!result, 'bad first sleep result'); - logMemory(); + result = exports[name](); } - console.log('rewind and run til the end.'); - exports.bysyncify_start_rewind(DATA_ADDR); - result = exports[name](); - } + console.log('final result: ' + result); + assert(result == expectedResult, 'bad final result'); + logMemory(); - console.log('final result: ' + result); - assert(result == expectedResult, 'bad final result'); - logMemory(); + assert(sleeps == expectedSleeps, 'expectedSleeps'); + } - assert(sleeps == expectedSleeps, 'expectedSleeps'); + //================ + // Tests + //================ + + // A minimal single sleep. + runTest('minimal', 1, 21); + + // Two sleeps. + runTest('repeat', 2, 42); + + // A value in a local is preserved across a sleep. + runTest('local', 1, 10); + + // A local with more operations done on it. + runTest('local2', 1, 22); + + // A local with more operations done on it. + runTest('params', 1, 18); + runTest('params', 1, 21, [1, 2]); + + // Calls to multiple other functions, only one of whom + // sleeps, and keep locals and globals valid throughout. + runTest('deeper', 0, 27, [0]); + runTest('deeper', 1, 3, [1]); + + // A recursive factorial, that sleeps on each iteration + // above 1. + runTest('factorial-recursive', 0, 1, [1]); + runTest('factorial-recursive', 1, 2, [2]); + runTest('factorial-recursive', 2, 6, [3]); + runTest('factorial-recursive', 3, 24, [4]); + runTest('factorial-recursive', 4, 120, [5]); + + // A looping factorial, that sleeps on each iteration + // above 1. + runTest('factorial-loop', 0, 1, [1]); + runTest('factorial-loop', 1, 2, [2]); + runTest('factorial-loop', 2, 6, [3]); + runTest('factorial-loop', 3, 24, [4]); + runTest('factorial-loop', 4, 120, [5]); + + // Test calling into JS in the middle (which can work if + // the JS just forwards the call and has no side effects or + // state of its own that needs to be saved). + runTest('do_tunnel', 2, 72, [1]); + + // Test indirect function calls. + runTest('call_indirect', 3, 432, [1, 2]); + + // Test indirect function calls. + runTest('if_else', 3, 1460, [1, 1000]); + runTest('if_else', 3, 2520, [2, 2000]); } -//================ -// Tests -//================ - -// A minimal single sleep. -runTest('minimal', 1, 21); - -// Two sleeps. -runTest('repeat', 2, 42); - -// A value in a local is preserved across a sleep. -runTest('local', 1, 10); - -// A local with more operations done on it. -runTest('local2', 1, 22); - -// A local with more operations done on it. -runTest('params', 1, 18); -runTest('params', 1, 21, [1, 2]); - -// Calls to multiple other functions, only one of whom -// sleeps, and keep locals and globals valid throughout. -runTest('deeper', 0, 27, [0]); -runTest('deeper', 1, 3, [1]); - -// A recursive factorial, that sleeps on each iteration -// above 1. -runTest('factorial-recursive', 0, 1, [1]); -runTest('factorial-recursive', 1, 2, [2]); -runTest('factorial-recursive', 2, 6, [3]); -runTest('factorial-recursive', 3, 24, [4]); -runTest('factorial-recursive', 4, 120, [5]); +function coroutineTests() { + console.log('\ncoroutine tests\n\n'); + + // Get and compile the wasm. + + var binary = fs.readFileSync('b.wasm'); + + var module = new WebAssembly.Module(binary); + + // Create a coroutine, for a specific export to + // call, and whose unwind/rewind data is in + // a specific range. + function Coroutine(name, dataStart, dataEnd) { + this.name = name; + this.start = function() { + exports[name](); + }; + this.startUnwind = function() { + // Initialize the data. + view[dataStart >> 2] = dataStart + 8; + view[dataStart + 4 >> 2] = dataEnd; + exports.bysyncify_start_unwind(dataStart); + // (With C etc. coroutines we would also have + // a C stack to pause and resume here.) + }; + this.stopUnwind = function() { + exports.bysyncify_stop_unwind(); + }; + this.startRewind = function() { + exports.bysyncify_start_rewind(dataStart); + exports[name](); + }; + this.stopRewind = function() { + exports.bysyncify_stop_rewind(); + }; + } -// A looping factorial, that sleeps on each iteration -// above 1. -runTest('factorial-loop', 0, 1, [1]); -runTest('factorial-loop', 1, 2, [2]); -runTest('factorial-loop', 2, 6, [3]); -runTest('factorial-loop', 3, 24, [4]); -runTest('factorial-loop', 4, 120, [5]); + var Runtime = { + coroutines: [ + new Coroutine('linear', 1000, 2000), + new Coroutine('exponential', 2000, 3000), + new Coroutine('weird', 3000, 4000) + ], + active: null, + rewinding: false, + run: function(iters) { + Runtime.coroutines.forEach(function(coroutine) { + console.log('starting ' + coroutine.name); + Runtime.active = coroutine; + coroutine.start(); + coroutine.stopUnwind(); + Runtime.active = null; + }); + for (var i = 0; i < iters; i++) { + Runtime.coroutines.forEach(function(coroutine) { + console.log('resuming ' + coroutine.name); + Runtime.active = coroutine; + Runtime.rewinding = true; + coroutine.startRewind(); + Runtime.active = null; + }); + } + }, + values: [], + yield: function(value) { + console.log('yield reached', Runtime.rewinding, value); + var coroutine = Runtime.active; + if (Runtime.rewinding) { + coroutine.stopRewind(); + Runtime.rewinding = false; + } else { + Runtime.values.push(value); + coroutine.startUnwind(); + console.log('pausing ' + coroutine.name); + } + }, + }; -// Test calling into JS in the middle (which can work if -// the JS just forwards the call and has no side effects or -// state of its own that needs to be saved). -runTest('do_tunnel', 2, 72, [1]); + var instance = new WebAssembly.Instance(module, { + env: { + yield: Runtime.yield + } + }); + + var exports = instance.exports; + var view = new Int32Array(exports.memory.buffer); + + Runtime.run(4); + console.log(Runtime.values); + assert(JSON.stringify(Runtime.values) === JSON.stringify([ + 0, 1, 42, + 10, 2, 1337, + 20, 4, 0, + 30, 8, -1000, + 40, 16, 42 + ]), 'check yielded values') +} -// Test indirect function calls. -runTest('call_indirect', 3, 432, [1, 2]); +// Main -// Test indirect function calls. -runTest('if_else', 3, 1460, [1, 1000]); -runTest('if_else', 3, 2520, [2, 2000]); +sleepTests(); +coroutineTests(); -// All done. console.log('\ntests completed successfully'); diff --git a/test/unit/test_bysyncify.py b/test/unit/test_bysyncify.py index 5373a4def..29a8ae7cb 100644 --- a/test/unit/test_bysyncify.py +++ b/test/unit/test_bysyncify.py @@ -8,7 +8,8 @@ class BysyncifyTest(BinaryenTestCase): def test_bysyncify(self): def test(args): print(args) - run_process(WASM_OPT + args + [self.input_path('bysyncify.wast'), '--bysyncify', '-o', 'a.wasm']) + run_process(WASM_OPT + args + [self.input_path('bysyncify-sleep.wast'), '--bysyncify', '-o', 'a.wasm']) + run_process(WASM_OPT + args + [self.input_path('bysyncify-coroutine.wast'), '--bysyncify', '-o', 'b.wasm']) print(' file size: %d' % os.path.getsize('a.wasm')) run_process([NODEJS, self.input_path('bysyncify.js')]) |