| function assert(x, y) { | |
| if (!x) throw (y || 'assertion failed') + '\n' + new Error().stack; | |
| } | |
| // This is the reason we have a package.json file alongside us, to tell node | |
| // that we are not an ES6 module (as a parent directory might have one with | |
| // type: "module" which would then cause breakage). | |
| var fs = require('fs'); | |
| 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++; | |
| sleeping = true; | |
| // Unwinding. | |
| // 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; | |
| exports.asyncify_start_unwind(DATA_ADDR); | |
| } else { | |
| // We are called as part of a resume/rewind. Stop sleeping. | |
| console.log('resume...'); | |
| exports.asyncify_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); | |
| } | |
| } | |
| }); | |
| 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 runTest(name, expectedSleeps, expectedResult, params) { | |
| params = params || []; | |
| console.log('\n==== testing ' + name + ' ===='); | |
| sleeps = 0; | |
| 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'); | |
| exports.asyncify_stop_unwind(); | |
| for (var i = 0; i < expectedSleeps - 1; i++) { | |
| console.log('rewind, run until the next sleep'); | |
| exports.asyncify_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.asyncify_stop_unwind(); | |
| } | |
| console.log('rewind and run til the end.'); | |
| exports.asyncify_start_rewind(DATA_ADDR); | |
| result = exports[name](); | |
| } | |
| console.log('final result: ' + result); | |
| assert(result == expectedResult, 'bad final result'); | |
| logMemory(); | |
| 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]); | |
| } | |
| 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.asyncify_start_unwind(dataStart); | |
| // (With C etc. coroutines we would also have | |
| // a C stack to pause and resume here.) | |
| }; | |
| this.stopUnwind = function() { | |
| exports.asyncify_stop_unwind(); | |
| }; | |
| this.startRewind = function() { | |
| exports.asyncify_start_rewind(dataStart); | |
| exports[name](); | |
| }; | |
| this.stopRewind = function() { | |
| exports.asyncify_stop_rewind(); | |
| }; | |
| } | |
| 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); | |
| } | |
| }, | |
| }; | |
| 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') | |
| } | |
| function stackOverflowAssertTests() { | |
| console.log('\nstack overflow assertion tests\n\n'); | |
| // Get and compile the wasm. | |
| var binary = fs.readFileSync('c.wasm'); | |
| var module = new WebAssembly.Module(binary); | |
| var DATA_ADDR = 4; | |
| var instance = new WebAssembly.Instance(module, { | |
| env: { | |
| sleep: function() { | |
| console.log('sleep...'); | |
| exports.asyncify_start_unwind(DATA_ADDR); | |
| view[DATA_ADDR >> 2] = DATA_ADDR + 8; | |
| // The end of the stack will be reached as the stack is tiny. | |
| view[DATA_ADDR + 4 >> 2] = view[DATA_ADDR >> 2] + 1; | |
| } | |
| } | |
| }); | |
| var exports = instance.exports; | |
| var view = new Int32Array(exports.memory.buffer); | |
| exports.many_locals(); | |
| assert(view[DATA_ADDR >> 2] > view[DATA_ADDR + 4 >> 2], 'should have wrote past the end of the stack'); | |
| // All API calls should now fail, since we wrote past the end of the | |
| // stack | |
| var fails = 0; | |
| ['asyncify_stop_unwind', 'asyncify_start_rewind', 'asyncify_stop_rewind', 'asyncify_start_unwind'].forEach(function(name) { | |
| try { | |
| exports[name](DATA_ADDR); | |
| console.log('no fail on', name); | |
| } catch (e) { | |
| console.log('expected fail on', name); | |
| fails++; | |
| } | |
| }); | |
| assert(fails == 4, 'all 4 should have failed'); | |
| } | |
| // Main | |
| sleepTests(); | |
| coroutineTests(); | |
| stackOverflowAssertTests(); | |
| console.log('\ntests completed successfully'); | |