From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../jit-test/tests/wasm/ref-types/directives.txt | 1 + .../tests/wasm/ref-types/externref-boxing.js | 140 ++++++ .../tests/wasm/ref-types/externref-fastpaths.js | 100 +++++ .../wasm/ref-types/externref-global-object.js | 91 ++++ .../wasm/ref-types/externref-global-postbarrier.js | 89 ++++ .../wasm/ref-types/externref-global-prebarrier.js | 61 +++ .../tests/wasm/ref-types/externref-val-tracing.js | 10 + js/src/jit-test/tests/wasm/ref-types/externref.js | 482 +++++++++++++++++++++ .../tests/wasm/ref-types/funcref-fastpaths.js | 76 ++++ js/src/jit-test/tests/wasm/ref-types/funcref.js | 135 ++++++ .../ref-types/fuzz-gc-while-allocating-global.js | 3 + js/src/jit-test/tests/wasm/ref-types/ref-func.js | 290 +++++++++++++ js/src/jit-test/tests/wasm/ref-types/stackmaps1.js | 85 ++++ js/src/jit-test/tests/wasm/ref-types/stackmaps2.js | 132 ++++++ js/src/jit-test/tests/wasm/ref-types/stackmaps3.js | 201 +++++++++ .../wasm/ref-types/stackmaps4-params-n-locals.js | 143 ++++++ js/src/jit-test/tests/wasm/ref-types/tables-api.js | 93 ++++ .../jit-test/tests/wasm/ref-types/tables-fill.js | 212 +++++++++ .../tests/wasm/ref-types/tables-generalized.js | 432 ++++++++++++++++++ .../tests/wasm/ref-types/tables-multiple.js | 465 ++++++++++++++++++++ .../wasm/ref-types/tables-postbarrier-grow.js | 21 + .../jit-test/tests/wasm/ref-types/tables-stress.js | 46 ++ 22 files changed, 3308 insertions(+) create mode 100644 js/src/jit-test/tests/wasm/ref-types/directives.txt create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref-boxing.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref-fastpaths.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref-global-object.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref-global-postbarrier.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref-global-prebarrier.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref-val-tracing.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/externref.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/funcref-fastpaths.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/funcref.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/fuzz-gc-while-allocating-global.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/ref-func.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/stackmaps1.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/stackmaps2.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/stackmaps3.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/stackmaps4-params-n-locals.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/tables-api.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/tables-fill.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/tables-generalized.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/tables-multiple.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/tables-postbarrier-grow.js create mode 100644 js/src/jit-test/tests/wasm/ref-types/tables-stress.js (limited to 'js/src/jit-test/tests/wasm/ref-types') diff --git a/js/src/jit-test/tests/wasm/ref-types/directives.txt b/js/src/jit-test/tests/wasm/ref-types/directives.txt new file mode 100644 index 0000000000..ed90b6bd5f --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/directives.txt @@ -0,0 +1 @@ +|jit-test| test-also=--wasm-compiler=optimizing; test-also=--wasm-compiler=baseline; test-also=--disable-wasm-huge-memory; skip-variant-if: --disable-wasm-huge-memory, !wasmHugeMemorySupported(); test-also=--wasm-test-serialization; include:wasm.js diff --git a/js/src/jit-test/tests/wasm/ref-types/externref-boxing.js b/js/src/jit-test/tests/wasm/ref-types/externref-boxing.js new file mode 100644 index 0000000000..854f3e7815 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref-boxing.js @@ -0,0 +1,140 @@ +// Moving a JS value through a wasm externref is a pair of boxing/unboxing +// conversions that leaves the value unchanged. There are many cases, +// along these axes: +// +// - global variables +// - tables +// - function parameters and returns +// - struct fields [for the gc feature], see externref-boxing-struct.js + +// Global variables can receive values via several mechanisms: +// +// - on initialization when created from JS +// - on initialization when created in Wasm, from an imported global +// - through the "value" property if the value is mutable +// - through the global.set wasm instruction, ditto +// +// Their values can be obtained in several ways: +// +// - through the "value" property +// - through the global.get wasm instruction +// - read when other globals are initialized from them + +// Set via initialization and read via 'value' + +for (let v of WasmExternrefValues) +{ + let g = new WebAssembly.Global({value: "externref"}, v); + assertEq(g.value, v); +} + +// Set via 'value' and read via 'value' + +for (let v of WasmExternrefValues) +{ + let g = new WebAssembly.Global({value: "externref", mutable: true}); + g.value = v; + assertEq(g.value, v); +} + +// Set via initialization, then read via global.get and returned + +for (let v of WasmExternrefValues) +{ + let g = new WebAssembly.Global({value: "externref"}, v); + let ins = wasmEvalText( + `(module + (import "m" "g" (global $glob externref)) + (func (export "f") (result externref) + (global.get $glob)))`, + {m:{g}}); + assertEq(ins.exports.f(), v); +} + +// Set via global.set, then read via 'value' + +for (let v of WasmExternrefValues) +{ + let g = new WebAssembly.Global({value: "externref", mutable: true}); + let ins = wasmEvalText( + `(module + (import "m" "g" (global $glob (mut externref))) + (func (export "f") (param $v externref) + (global.set $glob (local.get $v))))`, + {m:{g}}); + ins.exports.f(v); + assertEq(g.value, v); +} + +// Tables of externref can receive values via several mechanisms: +// +// - through WebAssembly.Table.prototype.set() +// - through the table.set, table.copy, and table.grow instructions +// - through table.fill +// - through WebAssembly.Table.prototype.grow() +// +// Their values can be read in several ways: +// +// - through WebAssembly.Table.prototype.get() +// - through the table.get and table.copy instructions +// +// (Note, tables are always initialized to null when created from JS) + +// .set() and .get() + +for (let v of WasmExternrefValues) +{ + let t = new WebAssembly.Table({element: "externref", initial: 10}); + t.set(3, v); + assertEq(t.get(3), v); +} + +// write with table.set, read with .get() + +for (let v of WasmExternrefValues) +{ + let t = new WebAssembly.Table({element: "externref", initial: 10}); + let ins = wasmEvalText( + `(module + (import "m" "t" (table $t 10 externref)) + (func (export "f") (param $v externref) + (table.set $t (i32.const 3) (local.get $v))))`, + {m:{t}}); + ins.exports.f(v); + assertEq(t.get(3), v); +} + +// write with .set(), read with table.get + +for (let v of WasmExternrefValues) +{ + let t = new WebAssembly.Table({element: "externref", initial: 10}); + let ins = wasmEvalText( + `(module + (import "m" "t" (table $t 10 externref)) + (func (export "f") (result externref) + (table.get $t (i32.const 3))))`, + {m:{t}}); + t.set(3, v); + assertEq(ins.exports.f(), v); +} + +// Imported JS functions can receive externref values as parameters and return +// them. + +for (let v of WasmExternrefValues) +{ + let returner = function () { return v; }; + let receiver = function (w) { assertEq(w, v); }; + let ins = wasmEvalText( + `(module + (import "m" "returner" (func $returner (result externref))) + (import "m" "receiver" (func $receiver (param externref))) + (func (export "test_returner") (result externref) + (call $returner)) + (func (export "test_receiver") (param $v externref) + (call $receiver (local.get $v))))`, + {m:{returner, receiver}}); + assertEq(ins.exports.test_returner(), v); + ins.exports.test_receiver(v); +} diff --git a/js/src/jit-test/tests/wasm/ref-types/externref-fastpaths.js b/js/src/jit-test/tests/wasm/ref-types/externref-fastpaths.js new file mode 100644 index 0000000000..749a7c788b --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref-fastpaths.js @@ -0,0 +1,100 @@ +setJitCompilerOption("baseline.warmup.trigger", 5); +setJitCompilerOption("ion.warmup.trigger", 10); + +var iter = 1000; +let objs = WasmExternrefValues; +var index = 0; + +// Tests that: +// +// * wasm->js calls along the fast path unbox externref correctly on entry to JS. +// This is tested by JS observing that the value that it receives is the same +// that was passed into wasm in the first place. +// +// * js->wasm calls along the fast path box JS values into externref correctly on +// entry into wasm. This is tested by the precise same test as the case +// above. +// +// * js->wasm calls along the fast path unbox externref correctly on return from +// wasm. This is tested by returning the passed-in value and observing in +// JS that the value is the same as the value that was passed in. + +// `p` will be a register argument on most platforms. + +function js_externref_regarg(p) { + if (p !== objs[index]) + throw new Error("Bad argument at " + index); +} + +// `p` will be a stack argument on all platforms. In the internal ABI, the +// externref parameter ends up in a stack slot in the call to js_externref_stackarg, +// thus we're testing a different path in the stub code. (This holds for +// architectures with up to 10 register arguments, which covers all tier-1 +// platforms as of 2019.) + +function js_externref_stackarg(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, p) { + if (p !== objs[index]) + throw new Error("Bad argument at " + index); +} + +var ins1 = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (import "" "f" (func $f (param externref))) + (import "" "g" (func $g (param i32) (param i32) (param i32) (param i32) (param i32) + (param i32) (param i32) (param i32) (param i32) (param i32) + (param externref))) + (func (export "run1") (param externref) (result externref) + (call $f (local.get 0)) + (local.get 0)) + (func (export "run2") (param externref) (result externref) + (call $g (i32.const 0) (i32.const 1) (i32.const 2) (i32.const 3) (i32.const 4) + (i32.const 5) (i32.const 6) (i32.const 7) (i32.const 8) (i32.const 9) + (local.get 0)) + (local.get 0)))`)), + {"":{f: js_externref_regarg, g: js_externref_stackarg}}); + +index = 0; +for ( let i=0; i < iter; i++ ) { + let res = ins1.exports.run1(objs[index]); + if (res !== objs[index]) + throw new Error("Bad return at " + index); + index = (index + 1) % objs.length; +} + +index = 0; +for ( let i=0; i < iter; i++ ) { + let res = ins1.exports.run2(objs[index]); + if (res !== objs[index]) + throw new Error("Bad return at " + index); + index = (index + 1) % objs.length; +} + +// Test that: +// +// * wasm->js calls along the fast path box JS values into externref correctly on +// return from JS. This is tested by returing a value from JS through wasm +// back into JS and then checking that the object is the same as the one that +// was returned. + +function js_returns_externref(p) { + return objs[index]; +} + +var ins2 = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (import "" "f" (func $f (result externref))) + (import "" "g" (func $g (param externref))) + (func (export "run1") (result externref) + (local $tmp externref) + (local.set $tmp (call $f)) + (call $g (local.get $tmp)) + (local.get $tmp)))`)), + {"":{f: js_returns_externref, g: js_externref_regarg}}); + +index = 0; +for ( let i=0; i < iter; i++ ) { + let res = ins2.exports.run1(); + if (res !== objs[index]) + throw new Error("Bad return at " + index); + index = (index + 1) % objs.length; +} diff --git a/js/src/jit-test/tests/wasm/ref-types/externref-global-object.js b/js/src/jit-test/tests/wasm/ref-types/externref-global-object.js new file mode 100644 index 0000000000..806896e9b8 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref-global-object.js @@ -0,0 +1,91 @@ +// |jit-test| skip-if: typeof WebAssembly.Global !== 'function' + +// Dummy object. +function Baguette(calories) { + this.calories = calories; +} + +assertEq(new WebAssembly.Global({value: "externref"}) instanceof WebAssembly.Global, true); + +(function() { + // Test initialization without a value. + let g = new WebAssembly.Global({value: "externref"}); + assertEq(g.value, void 0); + assertErrorMessage(() => g.value = 42, TypeError, /immutable global/); +})(); + +(function() { + // Test initialization with a value. + let g = new WebAssembly.Global({value: "externref"}, null); + assertEq(g.value, null); + assertErrorMessage(() => g.value = 42, TypeError, /immutable global/); + + let obj = {}; + g = new WebAssembly.Global({value: "externref"}, obj); + assertEq(g.value, obj); + assertErrorMessage(() => g.value = 42, TypeError, /immutable global/); + + g = new WebAssembly.Global({value: "externref"}, 1337); + assertEq(typeof g.value, "number"); + assertEq(+g.value, 1337); + + g = new WebAssembly.Global({value: "externref"}, 13.37); + assertEq(typeof g.value, "number"); + assertEq(+g.value, 13.37); + + g = new WebAssembly.Global({value: "externref"}, "string"); + assertEq(typeof g.value, "string"); + assertEq(g.value.toString(), "string"); + + g = new WebAssembly.Global({value: "externref"}, true); + assertEq(typeof g.value, "boolean"); + assertEq(!!g.value, true); + + g = new WebAssembly.Global({value: "externref"}, Symbol("status")); + assertEq(typeof g.value, "symbol"); + assertEq(g.value.toString(), "Symbol(status)"); + + g = new WebAssembly.Global({value: "externref"}, undefined); + assertEq(g.value, undefined); +})(); + +(function() { + // Test mutable property and assignment. + let g = new WebAssembly.Global({value: "externref", mutable: true}, null); + assertEq(g.value, null); + + let obj = { x: 42 }; + g.value = obj; + assertEq(g.value, obj); + assertEq(g.value.x, 42); + + obj = null; + assertEq(g.value.x, 42); + + let otherObj = { y : 35 }; + g.value = otherObj; + assertEq(g.value, otherObj); +})(); + +(function() { + // Test tracing. + let nom = new Baguette(1); + let g = new WebAssembly.Global({value: "externref"}, nom); + nom = null; + gc(); + assertEq(g.value.calories, 1); +})(); + +var global = new WebAssembly.Global({ value: "externref", mutable: true }, null); + +// GCZeal mode 2 implies that every allocation (second parameter = every single +// allocation) will trigger a full GC. +gczeal(2, 1); + +{ + let nomnom = new Baguette(42); + global.value = nomnom; + nomnom = null; +} +new Baguette(); +assertEq(global.value.calories, 42); diff --git a/js/src/jit-test/tests/wasm/ref-types/externref-global-postbarrier.js b/js/src/jit-test/tests/wasm/ref-types/externref-global-postbarrier.js new file mode 100644 index 0000000000..06d9521504 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref-global-postbarrier.js @@ -0,0 +1,89 @@ +const { startProfiling, endProfiling, assertEqPreciseStacks, isSingleStepProfilingEnabled } = WasmHelpers; + +// Dummy constructor. +function Baguette(calories) { + this.calories = calories; +} + +// Ensure the baseline compiler sync's before the postbarrier. +(function() { + wasmEvalText(`(module + (global (mut externref) (ref.null extern)) + (func (export "f") + global.get 0 + ref.null extern + global.set 0 + global.set 0 + ) + )`).exports.f(); +})(); + +let exportsPlain = wasmEvalText(`(module + (global i32 (i32.const 42)) + (global $g (mut externref) (ref.null extern)) + (func (export "set") (param externref) local.get 0 global.set $g) + (func (export "get") (result externref) global.get $g) +)`).exports; + +let exportsObj = wasmEvalText(`(module + (global $g (export "g") (mut externref) (ref.null extern)) + (func (export "set") (param externref) local.get 0 global.set $g) + (func (export "get") (result externref) global.get $g) +)`).exports; + +// 7 => Generational GC zeal. +gczeal(7, 1); + +for (var i = 0; i < 100; i++) { + new Baguette(i); +} + +function test(exports) { + // Test post-write barrier in wasm code. + { + let nomnom = new Baguette(15); + exports.set(nomnom); + nomnom = null; + } + new Baguette(); + assertEq(exports.get().calories, 15); +} + +test(exportsPlain); +test(exportsObj); + +// Test stacks reported in profiling mode in a separate way, to not perturb +// the behavior of the tested functions. +if (!isSingleStepProfilingEnabled) + quit(0); + +enableGeckoProfiling(); + +const EXPECTED_STACKS = [ + // Expected output for (simulator+baseline). + ['', '!>', '0,!>', '<,0,!>', 'GC postbarrier,0,!>', + '<,0,!>', '0,!>', '!>', ''], + + // Expected output for (simulator+via-Ion). + ['', '!>', '0,!>', '<,0,!>', 'filtering GC postbarrier,0,!>', + '<,0,!>', '0,!>', '!>', ''], + + // Expected output for other configurations. + ['', '!>', '0,!>', '!>', ''], +]; + +function testStacks(exports) { + // Test post-write barrier in wasm code. + { + let nomnom = new Baguette(15); + startProfiling(); + exports.set(nomnom); + assertEqPreciseStacks(endProfiling(), EXPECTED_STACKS); + nomnom = null; + } + new Baguette(); + assertEq(exports.get().calories, 15); +} + +testStacks(exportsPlain); +testStacks(exportsObj); diff --git a/js/src/jit-test/tests/wasm/ref-types/externref-global-prebarrier.js b/js/src/jit-test/tests/wasm/ref-types/externref-global-prebarrier.js new file mode 100644 index 0000000000..139f71e758 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref-global-prebarrier.js @@ -0,0 +1,61 @@ +// Do not run the test if we're jit-compiling JS, since it's the wasm frames +// we're interested in and eager JS compilation can upset the test. + +opts = getJitCompilerOptions(); +if (opts['ion.enable'] || opts['baseline.enable']) + quit(); + +const { startProfiling, endProfiling, assertEqPreciseStacks, isSingleStepProfilingEnabled } = WasmHelpers; + +let e = wasmEvalText(`(module + (global $g (mut externref) (ref.null extern)) + (func (export "set") (param externref) local.get 0 global.set $g) +)`).exports; + +let obj = { field: null }; + +// GCZeal mode 4 implies that prebarriers are being verified at many +// locations in the interpreter, during interrupt checks, etc. It can be ultra +// slow, so disable it with gczeal(0) when it's not strictly needed. +gczeal(4, 1); +e.set(obj); +e.set(null); +gczeal(0); + +if (!isSingleStepProfilingEnabled) { + quit(0); +} + +enableGeckoProfiling(); +startProfiling(); +gczeal(4, 1); +e.set(obj); +gczeal(0); +assertEqPreciseStacks( + endProfiling(), + [ + // Expected output for (simulator+via-Ion/baseline). + ['', '!>', '0,!>', '<,0,!>', 'GC postbarrier,0,!>', + '<,0,!>', '0,!>', '!>', ''], + + // Expected output for other configurations. + ['', '!>', '0,!>', '!>', ''], + ]); + +startProfiling(); +gczeal(4, 1); +e.set(null); +gczeal(0); + +// We're losing stack info in the prebarrier code. +assertEqPreciseStacks( + endProfiling(), + [ + // Expected output for (simulator+via-Ion/baseline). + ['', '!>', '0,!>', '', '0,!>', '<,0,!>', 'GC postbarrier,0,!>', + '<,0,!>', '0,!>', '!>', ''], + + // Expected output for other configurations. + ['', '!>', '0,!>', '', '0,!>', '!>', ''], + ]); + diff --git a/js/src/jit-test/tests/wasm/ref-types/externref-val-tracing.js b/js/src/jit-test/tests/wasm/ref-types/externref-val-tracing.js new file mode 100644 index 0000000000..3c7e596d12 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref-val-tracing.js @@ -0,0 +1,10 @@ +gczeal(14, 1); +let { exports } = wasmEvalText(`(module + (global $externref (import "glob" "externref") externref) + (func (export "get") (result externref) global.get $externref) +)`, { + glob: { + externref: { sentinel: "lol" }, + } +}); +assertEq(exports.get().sentinel, "lol"); diff --git a/js/src/jit-test/tests/wasm/ref-types/externref.js b/js/src/jit-test/tests/wasm/ref-types/externref.js new file mode 100644 index 0000000000..695279da73 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/externref.js @@ -0,0 +1,482 @@ +// Dummy constructor. +function Baguette(calories) { + this.calories = calories; +} + +// Type checking. + +const { validate, CompileError } = WebAssembly; + +assertErrorMessage(() => wasmEvalText(`(module + (func (result externref) + i32.const 42 + ) +)`), CompileError, mismatchError('i32', 'externref')); + +assertErrorMessage(() => wasmEvalText(`(module + (func (result externref) + i32.const 0 + ref.null extern + i32.const 42 + select (result externref) + ) +)`), CompileError, /type mismatch/); + +assertErrorMessage(() => wasmEvalText(`(module + (func (result i32) + ref.null extern + if + i32.const 42 + end + ) +)`), CompileError, mismatchError('externref', 'i32')); + + +// Basic compilation tests. + +let simpleTests = [ + "(module (func (drop (ref.null extern))))", + "(module (func $test (local externref)))", + "(module (func $test (param externref)))", + "(module (func $test (result externref) (ref.null extern)))", + "(module (func $test (block (result externref) (unreachable)) unreachable))", + "(module (func $test (result i32) (local externref) (ref.is_null (local.get 0))))", + `(module (import "a" "b" (func (param externref))))`, + `(module (import "a" "b" (func (result externref))))`, + `(module (global externref (ref.null extern)))`, + `(module (global (mut externref) (ref.null extern)))`, +]; + +for (let src of simpleTests) { + wasmEvalText(src, {a:{b(){}}}); + assertEq(validate(wasmTextToBinary(src)), true); +} + +// Basic behavioral tests. + +let { exports } = wasmEvalText(`(module + (func (export "is_null") (result i32) + ref.null extern + ref.is_null + ) + + (func $sum (param i32) (result i32) + local.get 0 + i32.const 42 + i32.add + ) + + (func (export "is_null_spill") (result i32) + ref.null extern + i32.const 58 + call $sum + drop + ref.is_null + ) + + (func (export "is_null_local") (result i32) (local externref) + ref.null extern + local.set 0 + i32.const 58 + call $sum + drop + local.get 0 + ref.is_null + ) + )`); + +assertEq(exports.is_null(), 1); +assertEq(exports.is_null_spill(), 1); +assertEq(exports.is_null_local(), 1); + +// ExternRef param and result in wasm functions. + +exports = wasmEvalText(`(module + (func (export "is_null") (param $ref externref) (result i32) + local.get $ref + ref.is_null + ) + + (func (export "ref_or_null") (param $ref externref) (param $selector i32) (result externref) + local.get $ref + ref.null extern + local.get $selector + select (result externref) + ) + + (func $recursive (export "nested") (param $ref externref) (param $i i32) (result externref) + ;; i == 10 => ret $ref + local.get $i + i32.const 10 + i32.eq + if + local.get $ref + return + end + + local.get $ref + + local.get $i + i32.const 1 + i32.add + + call $recursive + ) +)`).exports; + +assertEq(exports.is_null(undefined), 0); +assertEq(exports.is_null(null), 1); +assertEq(exports.is_null({}), 0); +assertEq(exports.is_null("hi"), 0); +assertEq(exports.is_null(3), 0); +assertEq(exports.is_null(3.5), 0); +assertEq(exports.is_null(true), 0); +assertEq(exports.is_null(Symbol("croissant")), 0); +assertEq(exports.is_null(new Baguette(100)), 0); + +let baguette = new Baguette(42); +assertEq(exports.ref_or_null(null, 0), null); +assertEq(exports.ref_or_null(baguette, 0), null); + +let ref = exports.ref_or_null(baguette, 1); +assertEq(ref, baguette); +assertEq(ref.calories, baguette.calories); + +ref = exports.nested(baguette, 0); +assertEq(ref, baguette); +assertEq(ref.calories, baguette.calories); + +// Make sure grow-memory isn't blocked by the lack of gc. +(function() { + assertEq(wasmEvalText(`(module + (memory 0 64) + (func (export "f") (param externref) (result i32) + i32.const 10 + memory.grow + drop + memory.size + ) +)`).exports.f({}), 10); +})(); + +// More interesting use cases about control flow joins. + +function assertJoin(body) { + let val = { i: -1 }; + assertEq(wasmEvalText(`(module + (func (export "test") (param $ref externref) (param $i i32) (result externref) + ${body} + ) + )`).exports.test(val), val); + assertEq(val.i, -1); +} + +assertJoin("(block (result externref) local.get $ref)"); +assertJoin("(block $out (result externref) local.get $ref br $out)"); +assertJoin("(loop (result externref) local.get $ref)"); + +assertJoin(`(block $out (result externref) (loop $top (result externref) + local.get $i + i32.const 1 + i32.add + tee_local $i + i32.const 10 + i32.eq + if + local.get $ref + return + end + br $top)) +`); + +assertJoin(`(block $out (loop $top + local.get $i + i32.const 1 + i32.add + tee_local $i + i32.const 10 + i32.le_s + if + br $top + else + local.get $ref + return + end + )) unreachable +`); + +assertJoin(`(block $out (result externref) (loop $top + local.get $ref + local.get $i + i32.const 1 + i32.add + tee_local $i + i32.const 10 + i32.eq + br_if $out + br $top + ) unreachable) +`); + +assertJoin(`(block $out (result externref) (block $unreachable (result externref) (loop $top + local.get $ref + local.get $i + i32.const 1 + i32.add + tee_local $i + br_table $unreachable $out + ) unreachable)) +`); + +let x = { i: 42 }, y = { f: 53 }; +exports = wasmEvalText(`(module + (func (export "test") (param $lhs externref) (param $rhs externref) (param $i i32) (result externref) + local.get $lhs + local.get $rhs + local.get $i + select (result externref) + ) +)`).exports; + +let result = exports.test(x, y, 0); +assertEq(result, y); +assertEq(result.i, undefined); +assertEq(result.f, 53); +assertEq(x.i, 42); + +result = exports.test(x, y, 1); +assertEq(result, x); +assertEq(result.i, 42); +assertEq(result.f, undefined); +assertEq(y.f, 53); + +// ExternRef in params/result of imported functions. + +let firstBaguette = new Baguette(13), + secondBaguette = new Baguette(37); + +let imports = { + i: 0, + myBaguette: null, + funcs: { + param(x) { + if (this.i === 0) { + assertEq(x, firstBaguette); + assertEq(x.calories, 13); + assertEq(secondBaguette !== null, true); + } else if (this.i === 1 || this.i === 2) { + assertEq(x, secondBaguette); + assertEq(x.calories, 37); + assertEq(firstBaguette !== null, true); + } else if (this.i === 3) { + assertEq(x, null); + } else { + firstBaguette = null; + secondBaguette = null; + gc(); // evil mode + } + this.i++; + }, + ret() { + return imports.myBaguette; + } + } +}; + +exports = wasmEvalText(`(module + (import "funcs" "ret" (func $ret (result externref))) + (import "funcs" "param" (func $param (param externref))) + + (func (export "param") (param $x externref) (param $y externref) + local.get $y + local.get $x + call $param + call $param + ) + + (func (export "ret") (result externref) + call $ret + ) +)`, imports).exports; + +exports.param(firstBaguette, secondBaguette); +exports.param(secondBaguette, null); +exports.param(firstBaguette, secondBaguette); + +imports.myBaguette = null; +assertEq(exports.ret(), null); + +imports.myBaguette = new Baguette(1337); +assertEq(exports.ret(), imports.myBaguette); + +// Check lazy stubs generation. + +exports = wasmEvalText(`(module + (import "funcs" "mirror" (func $mirror (param externref) (result externref))) + (import "funcs" "augment" (func $augment (param externref) (result externref))) + + (global $count_f (mut i32) (i32.const 0)) + (global $count_g (mut i32) (i32.const 0)) + + (func $f (param $param externref) (result externref) + i32.const 1 + global.get $count_f + i32.add + global.set $count_f + + local.get $param + call $augment + ) + + (func $g (param $param externref) (result externref) + i32.const 1 + global.get $count_g + i32.add + global.set $count_g + + local.get $param + call $mirror + ) + + (table (export "table") 10 funcref) + (elem (i32.const 0) $f $g $mirror $augment) + (type $table_type (func (param externref) (result externref))) + + (func (export "call_indirect") (param $i i32) (param $ref externref) (result externref) + local.get $ref + local.get $i + call_indirect (type $table_type) + ) + + (func (export "count_f") (result i32) global.get $count_f) + (func (export "count_g") (result i32) global.get $count_g) +)`, { + funcs: { + mirror(x) { + return x; + }, + augment(x) { + x.i++; + x.newProp = "hello"; + return x; + } + } +}).exports; + +x = { i: 19 }; +assertEq(exports.table.get(0)(x), x); +assertEq(x.i, 20); +assertEq(x.newProp, "hello"); +assertEq(exports.count_f(), 1); +assertEq(exports.count_g(), 0); + +x = { i: 21 }; +assertEq(exports.table.get(1)(x), x); +assertEq(x.i, 21); +assertEq(typeof x.newProp, "undefined"); +assertEq(exports.count_f(), 1); +assertEq(exports.count_g(), 1); + +x = { i: 22 }; +assertEq(exports.table.get(2)(x), x); +assertEq(x.i, 22); +assertEq(typeof x.newProp, "undefined"); +assertEq(exports.count_f(), 1); +assertEq(exports.count_g(), 1); + +x = { i: 23 }; +assertEq(exports.table.get(3)(x), x); +assertEq(x.i, 24); +assertEq(x.newProp, "hello"); +assertEq(exports.count_f(), 1); +assertEq(exports.count_g(), 1); + +// Globals. + +// ExternRef globals in wasm modules. + +assertErrorMessage(() => wasmEvalText(`(module (global (import "glob" "externref") externref))`, { glob: { externref: new WebAssembly.Global({ value: 'i32' }, 42) } }), + WebAssembly.LinkError, + /imported global type mismatch/); + +assertErrorMessage(() => wasmEvalText(`(module (global (import "glob" "i32") i32))`, { glob: { i32: {} } }), + WebAssembly.LinkError, + /import object field 'i32' is not a Number/); + +imports = { + constants: { + imm_null: null, + imm_bread: new Baguette(321), + mut_null: new WebAssembly.Global({ value: "externref", mutable: true }, null), + mut_bread: new WebAssembly.Global({ value: "externref", mutable: true }, new Baguette(123)) + } +}; + +exports = wasmEvalText(`(module + (global $g_imp_imm_null (import "constants" "imm_null") externref) + (global $g_imp_imm_bread (import "constants" "imm_bread") externref) + + (global $g_imp_mut_null (import "constants" "mut_null") (mut externref)) + (global $g_imp_mut_bread (import "constants" "mut_bread") (mut externref)) + + (global $g_imm_null externref (ref.null extern)) + (global $g_imm_getglob externref (global.get $g_imp_imm_bread)) + (global $g_mut (mut externref) (ref.null extern)) + + (func (export "imm_null") (result externref) global.get $g_imm_null) + (func (export "imm_getglob") (result externref) global.get $g_imm_getglob) + + (func (export "imp_imm_null") (result externref) global.get $g_imp_imm_null) + (func (export "imp_imm_bread") (result externref) global.get $g_imp_imm_bread) + (func (export "imp_mut_null") (result externref) global.get $g_imp_mut_null) + (func (export "imp_mut_bread") (result externref) global.get $g_imp_mut_bread) + + (func (export "set_imp_null") (param externref) local.get 0 global.set $g_imp_mut_null) + (func (export "set_imp_bread") (param externref) local.get 0 global.set $g_imp_mut_bread) + + (func (export "set_mut") (param externref) local.get 0 global.set $g_mut) + (func (export "get_mut") (result externref) global.get $g_mut) +)`, imports).exports; + +assertEq(exports.imp_imm_null(), imports.constants.imm_null); +assertEq(exports.imp_imm_bread(), imports.constants.imm_bread); + +assertEq(exports.imm_null(), null); +assertEq(exports.imm_getglob(), imports.constants.imm_bread); + +assertEq(exports.imp_mut_null(), imports.constants.mut_null.value); +assertEq(exports.imp_mut_bread(), imports.constants.mut_bread.value); + +let brandNewBaguette = new Baguette(1000); +exports.set_imp_null(brandNewBaguette); +assertEq(exports.imp_mut_null(), brandNewBaguette); +assertEq(exports.imp_mut_bread(), imports.constants.mut_bread.value); + +exports.set_imp_bread(null); +assertEq(exports.imp_mut_null(), brandNewBaguette); +assertEq(exports.imp_mut_bread(), null); + +assertEq(exports.get_mut(), null); +let glutenFreeBaguette = new Baguette("calories-free bread"); +exports.set_mut(glutenFreeBaguette); +assertEq(exports.get_mut(), glutenFreeBaguette); +assertEq(exports.get_mut().calories, "calories-free bread"); + +// Make sure that dead code doesn't prevent compilation. +wasmEvalText( + `(module + (func + (return) + (ref.null extern) + (drop) + ) + )`); + +wasmEvalText( + `(module + (func (param externref) + (return) + (ref.is_null (get_local 0)) + (drop) + ) + )`); diff --git a/js/src/jit-test/tests/wasm/ref-types/funcref-fastpaths.js b/js/src/jit-test/tests/wasm/ref-types/funcref-fastpaths.js new file mode 100644 index 0000000000..1650deda3f --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/funcref-fastpaths.js @@ -0,0 +1,76 @@ +// Test the fast paths along which funcref values flow between wasm and JS. +// Currently, these paths always go from wasm into JS; never from JS into wasm. + +setJitCompilerOption("baseline.warmup.trigger", 5); +setJitCompilerOption("ion.warmup.trigger", 10); + +var iter = 10000; +var objs = []; +var index = 0; + +var ins0 = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (func $f (export "f") (result i32) (i32.const 37)) + (func $g (export "g") (result i32) (i32.const 42)))`))); +objs.push(ins0.exports.f); +objs.push(ins0.exports.g); + +// Tests that: +// +// * wasm->js calls along the fast path pass funcref correctly on entry to JS. +// This is tested by JS observing that the value that it receives is the same +// that was passed into wasm in the first place. +// +// * js->wasm calls along the fast path pass funcref correctly on return from +// wasm. This is tested by returning the passed-in value and observing in +// JS that the value is the same as the value that was passed in. + +// `p` will be a register argument on most platforms. + +function js_funcref_regarg(p) { + if (p !== objs[index]) + throw new Error("Bad argument at " + index); +} + +// `p` will be a stack argument on all platforms. In the internal ABI, the +// funcref parameter ends up in a stack slot in the call to js_funcref_stackarg, +// thus we're testing a different path in the stub code. (This holds for +// architectures with up to 10 register arguments, which covers all tier-1 +// platforms as of 2019.) + +function js_funcref_stackarg(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, p) { + if (p !== objs[index]) + throw new Error("Bad argument at " + index); +} + +var ins1 = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (import "" "f" (func $f (param funcref))) + (import "" "g" (func $g (param i32) (param i32) (param i32) (param i32) (param i32) + (param i32) (param i32) (param i32) (param i32) (param i32) + (param funcref))) + (func (export "run1") (param funcref) (result funcref) + (call $f (local.get 0)) + (local.get 0)) + (func (export "run2") (param funcref) (result funcref) + (call $g (i32.const 0) (i32.const 1) (i32.const 2) (i32.const 3) (i32.const 4) + (i32.const 5) (i32.const 6) (i32.const 7) (i32.const 8) (i32.const 9) + (local.get 0)) + (local.get 0)))`)), + {"":{f: js_funcref_regarg, g: js_funcref_stackarg}}); + +index = 0; +for ( let i=0; i < iter; i++ ) { + let res = ins1.exports.run1(objs[index]); + if (res !== objs[index]) + throw new Error("Bad return at " + index); + index = (index + 1) % objs.length; +} + +index = 0; +for ( let i=0; i < iter; i++ ) { + let res = ins1.exports.run2(objs[index]); + if (res !== objs[index]) + throw new Error("Bad return at " + index); + index = (index + 1) % objs.length; +} diff --git a/js/src/jit-test/tests/wasm/ref-types/funcref.js b/js/src/jit-test/tests/wasm/ref-types/funcref.js new file mode 100644 index 0000000000..21ce95c0cb --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/funcref.js @@ -0,0 +1,135 @@ +const {Module,Instance,Global,RuntimeError} = WebAssembly; + +const badWasmFunc = /can only pass WebAssembly exported functions to funcref/; +const typeErr = /type mismatch/; + + +// Validation: + +wasmFailValidateText(`(module (func (local externref funcref) (local.set 0 (local.get 1))))`, typeErr); +wasmEvalText(`(module (func (local funcref funcref) (local.set 0 (local.get 1))))`); +wasmEvalText(`(module (func (local funcref) (local.set 0 (ref.null func))))`); +wasmFailValidateText(`(module (func (local funcref externref) (local.set 0 (local.get 1))))`, typeErr); +wasmEvalText(`(module (global (mut funcref) (ref.null func)) (func (param funcref) (global.set 0 (local.get 0))))`); +wasmFailValidateText(`(module (global (mut externref) (ref.null extern)) (func (param funcref) (global.set 0 (local.get 0))))`, typeErr); +wasmFailValidateText(`(module (global (mut funcref) (ref.null func)) (func (param externref) (global.set 0 (local.get 0))))`, typeErr); +wasmEvalText(`(module (func (param funcref)) (func (param funcref) (call 0 (local.get 0))))`); +wasmFailValidateText(`(module (func (param externref)) (func (param funcref) (call 0 (local.get 0))))`, typeErr); +wasmFailValidateText(`(module (func (param funcref)) (func (param externref) (call 0 (local.get 0))))`, typeErr); +wasmEvalText(`(module (func (param funcref) (result funcref) (block (result funcref) (local.get 0) (br 0))))`); +wasmFailValidateText(`(module (func (param funcref) (result externref) (block (result externref) (local.get 0) (br 0))))`, typeErr); +wasmFailValidateText(`(module (func (param externref) (result externref) (block (result funcref) (local.get 0) (br 0))))`, typeErr); +wasmEvalText(`(module (func (param funcref funcref) (result funcref) (select (result funcref) (local.get 0) (local.get 1) (i32.const 0))))`); +wasmFailValidateText(`(module (func (param externref funcref) (result externref) (select (result externref) (local.get 0) (local.get 1) (i32.const 0))))`, typeErr); +wasmFailValidateText(`(module (func (param funcref externref) (result externref) (select (result externref)(local.get 0) (local.get 1) (i32.const 0))))`, typeErr); +wasmFailValidateText(`(module (func (param externref funcref) (result funcref) (select (result funcref) (local.get 0) (local.get 1) (i32.const 0))))`, typeErr); +wasmFailValidateText(`(module (func (param funcref externref) (result funcref) (select (result funcref) (local.get 0) (local.get 1) (i32.const 0))))`, typeErr); + + +// Runtime: + +var m = new Module(wasmTextToBinary(`(module (func (export "wasmFun")))`)); +const wasmFun1 = new Instance(m).exports.wasmFun; +const wasmFun2 = new Instance(m).exports.wasmFun; +const wasmFun3 = new Instance(m).exports.wasmFun; + +var run = wasmEvalText(`(module + (global (mut funcref) (ref.null func)) + (func (param $x funcref) (param $test i32) (result funcref) + local.get $x + global.get 0 + local.get $test + select (result funcref) + ) + (func (export "run") (param $a funcref) (param $b funcref) (param $c funcref) (param $test1 i32) (param $test2 i32) (result funcref) + local.get $a + global.set 0 + block (result funcref) + local.get $b + local.get $test1 + br_if 0 + drop + local.get $c + end + local.get $test2 + call 0 + ) +)`).exports.run; +assertEq(run(wasmFun1, wasmFun2, wasmFun3, false, false), wasmFun1); +assertEq(run(wasmFun1, wasmFun2, wasmFun3, true, false), wasmFun1); +assertEq(run(wasmFun1, wasmFun2, wasmFun3, true, true), wasmFun2); +assertEq(run(wasmFun1, wasmFun2, wasmFun3, false, true), wasmFun3); + +var run = wasmEvalText(`(module + (type $t0 (func (param externref) (result externref))) + (type $t1 (func (param funcref) (result externref))) + (type $t2 (func (param funcref funcref) (result externref))) + (func $f0 (type $t0) ref.null extern) + (func $f1 (type $t1) ref.null extern) + (func $f2 (type $t2) ref.null extern) + (table funcref (elem $f0 $f1 $f2)) + (func (export "run") (param i32 i32) (result externref) + block $b2 block $b1 block $b0 + local.get 0 + br_table $b0 $b1 $b2 + end $b0 + ref.null extern + local.get 1 + call_indirect (type $t0) + return + end $b1 + ref.null func + local.get 1 + call_indirect (type $t1) + return + end $b2 + ref.null func + ref.null func + local.get 1 + call_indirect (type $t2) + return + ) +)`).exports.run; + +for (var i = 0; i < 3; i++) { + for (var j = 0; j < 3; j++) { + if (i == j) + assertEq(run(i, j), null); + else + assertErrorMessage(() => run(i, j), RuntimeError, /indirect call signature mismatch/); + } +} + + +// JS API: + +const wasmFun = wasmEvalText(`(module (func (export "x")))`).exports.x; + +var run = wasmEvalText(`(module (func (export "run") (param funcref) (result funcref) (local.get 0)))`).exports.run; +assertEq(run(wasmFun), wasmFun); +assertEq(run(null), null); +assertErrorMessage(() => run(() => {}), TypeError, badWasmFunc); + +var importReturnValue; +var importFun = () => importReturnValue; +var run = wasmEvalText(`(module (func (import "" "i") (result funcref)) (func (export "run") (result funcref) (call 0)))`, {'':{i:importFun}}).exports.run; +importReturnValue = wasmFun; +assertEq(run(), wasmFun); +importReturnValue = null; +assertEq(run(), null); +importReturnValue = undefined; +assertErrorMessage(() => run(), TypeError, badWasmFunc); +importReturnValue = () => {}; +assertErrorMessage(() => run(), TypeError, badWasmFunc); + +var g = new Global({value:'anyfunc', mutable:true}, wasmFun); +assertEq(g.value, wasmFun); +g.value = null; +assertEq(g.value, null); +Math.sin(); +assertErrorMessage(() => g.value = () => {}, TypeError, badWasmFunc); +var g = new Global({value:'anyfunc', mutable:true}, null); +assertEq(g.value, null); +g.value = wasmFun; +assertEq(g.value, wasmFun); +assertErrorMessage(() => new Global({value:'anyfunc'}, () => {}), TypeError, badWasmFunc); diff --git a/js/src/jit-test/tests/wasm/ref-types/fuzz-gc-while-allocating-global.js b/js/src/jit-test/tests/wasm/ref-types/fuzz-gc-while-allocating-global.js new file mode 100644 index 0000000000..0b51622397 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/fuzz-gc-while-allocating-global.js @@ -0,0 +1,3 @@ +enableShellAllocationMetadataBuilder(); +gczeal(9, 1); +new WebAssembly.Global({ value: 'i32' }, 42); diff --git a/js/src/jit-test/tests/wasm/ref-types/ref-func.js b/js/src/jit-test/tests/wasm/ref-types/ref-func.js new file mode 100644 index 0000000000..de874669b3 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/ref-func.js @@ -0,0 +1,290 @@ +load(libdir + "wasm-binary.js"); + +const v2vSig = {args:[], ret:VoidCode}; +const v2vSigSection = sigSection([v2vSig]); + +// 'ref.func' parses, validates and returns a non-null value +wasmFullPass(` + (module + (elem declare $run) + (func $run (result i32) + ref.func $run + ref.is_null + ) + (export "run" (func $run)) + ) +`, 0); + +// function returning reference to itself +{ + let {f1} = wasmEvalText(` + (module + (elem declare $f1) + (func $f1 (result funcref) ref.func $f1) + (export "f1" (func $f1)) + ) + `).exports; + assertEq(f1(), f1); +} + +// function returning reference to a different function +{ + let {f1, f2} = wasmEvalText(` + (module + (elem declare $f1) + (func $f1) + (func $f2 (result funcref) ref.func $f1) + (export "f1" (func $f1)) + (export "f2" (func $f2)) + ) + `).exports; + assertEq(f2(), f1); +} + +// function returning reference to function in a different module +{ + let i1 = wasmEvalText(` + (module + (elem declare $f1) + (func $f1) + (export "f1" (func $f1)) + ) + `); + let i2 = wasmEvalText(` + (module + (import "" "f1" (func $f1)) + (elem declare $f1) + (func $f2 (result funcref) ref.func $f1) + (export "f1" (func $f1)) + (export "f2" (func $f2)) + ) + `, {"": i1.exports}); + + let f1 = i1.exports.f1; + let f2 = i2.exports.f2; + assertEq(f2(), f1); +} + +// function index must be valid +assertErrorMessage(() => { + wasmEvalText(` + (module + (func (result funcref) ref.func 10) + ) + `); +}, WebAssembly.CompileError, /(function index out of range)|(function index out of bounds)/); + +function validFuncRefText(forwardDeclare, tbl_type) { + return wasmEvalText(` + (module + (table 1 ${tbl_type}) + (func $test (result funcref) ref.func $referenced) + (func $referenced) + ${forwardDeclare} + ) + `); +} + +// referenced function must be forward declared somehow +assertErrorMessage(() => validFuncRefText('', 'funcref'), WebAssembly.CompileError, /(function index is not declared in a section before the code section)|(undeclared function reference)/); + +// referenced function can be forward declared via segments +assertEq(validFuncRefText('(elem 0 (i32.const 0) func $referenced)', 'funcref') instanceof WebAssembly.Instance, true); +assertEq(validFuncRefText('(elem func $referenced)', 'funcref') instanceof WebAssembly.Instance, true); +assertEq(validFuncRefText('(elem declare $referenced)', 'funcref') instanceof WebAssembly.Instance, true); + +// also when the segment is passive or active 'funcref' +assertEq(validFuncRefText('(elem 0 (i32.const 0) funcref (ref.func $referenced))', 'funcref') instanceof WebAssembly.Instance, true); +assertEq(validFuncRefText('(elem funcref (ref.func $referenced))', 'funcref') instanceof WebAssembly.Instance, true); + +// reference function can be forward declared via globals +assertEq(validFuncRefText('(global funcref (ref.func $referenced))', 'externref') instanceof WebAssembly.Instance, true); + +// reference function can be forward declared via export +assertEq(validFuncRefText('(export "referenced" (func $referenced))', 'externref') instanceof WebAssembly.Instance, true); + +// reference function cannot be forward declared via start +assertErrorMessage(() => validFuncRefText('(start $referenced)', 'externref'), WebAssembly.CompileError, /(function index is not declared in a section before the code section)|(undeclared function reference)/); + +// Tests not expressible in the text format. + +// element segment with elemexpr can carry non-reference type, but this must be +// rejected. + +assertErrorMessage(() => new WebAssembly.Module( + moduleWithSections([generalElemSection([{ flag: PassiveElemExpr, + typeCode: I32Code, + elems: [] }])])), + WebAssembly.CompileError, + /bad type/); + +// Test case for bug 1596026: when taking the ref.func of an imported function, +// the value obtained should not be the JS function. This would assert (even in +// a release build), so the test is merely that the code runs. + +var ins = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(` + (module + (import "m" "f" (func $f (param i32) (result i32))) + (elem declare $f) + (table 1 funcref) + (func (export "f") + (table.set 0 (i32.const 0) (ref.func $f))))`)), + {m:{f:(x) => 37+x}}); +ins.exports.f(); + +// Verification that we can handle encoding errors for passive element segments +// properly. + +function checkPassiveElemSegment(mangle, err) { + let bin = moduleWithSections( + [v2vSigSection, declSection([0]), // One function + tableSection(1), // One table + { name: elemId, // One passive segment + body: (function () { + let body = []; + body.push(1); // 1 element segment + body.push(0x1 | 0x4); // Flags: Passive and uses element expression + body.push(AnyFuncCode + (mangle == "type" ? 1 : 0)); // always anyfunc + body.push(1); // Element count + body.push(RefFuncCode + (mangle == "ref.func" ? 1 : 0)); // always ref.func + body.push(0); // func index + body.push(EndCode + (mangle == "end" ? 1 : 0)); + return body; + })() }, + bodySection( // Empty function + [funcBody( + {locals:[], + body:[]})]) + ]); + if (err) { + assertErrorMessage(() => new WebAssembly.Module(bin), + WebAssembly.CompileError, + err); + } else { + new WebAssembly.Module(bin); + } +} + +checkPassiveElemSegment(""); +checkPassiveElemSegment("type", /bad type/); +checkPassiveElemSegment("ref.func", /failed to read initializer operation/); +checkPassiveElemSegment("end", /failed to read end of initializer expression/); + +// Passive element segments can contain literal null values. + +{ + let txt = + `(module + (table (export "t") 10 funcref) + (elem (i32.const 1) $m) + (elem (i32.const 3) $m) + (elem (i32.const 6) $m) + (elem (i32.const 8) $m) + (elem funcref (ref.func $f) (ref.null func) (ref.func $g) (ref.null func) (ref.func $h)) + (func $m) + (func $f) + (func $g) + (func $h) + (func (export "doit") (param $idx i32) + (table.init 4 (local.get $idx) (i32.const 0) (i32.const 5))))`; + let ins = wasmEvalText(txt); + ins.exports.doit(0); + ins.exports.doit(5); + assertEq(typeof ins.exports.t.get(0), "function"); + assertEq(ins.exports.t.get(1), null); + assertEq(typeof ins.exports.t.get(2), "function"); + assertEq(ins.exports.t.get(0) == ins.exports.t.get(2), false); + assertEq(ins.exports.t.get(3), null); + assertEq(typeof ins.exports.t.get(4), "function"); + assertEq(typeof ins.exports.t.get(5), "function"); + assertEq(ins.exports.t.get(6), null); + assertEq(typeof ins.exports.t.get(7), "function"); + assertEq(ins.exports.t.get(8), null); + assertEq(typeof ins.exports.t.get(9), "function"); +} + +// Test ref.func in global initializer expressions + +for (let mutable of [true, false]) { + for (let imported of [true, false]) { + for (let exported of [true, false]) { + let globalType = mutable ? `(mut funcref)` : `funcref`; + + let imports = {}; + + if (imported) { + imports = wasmEvalText(` + (module + (global $g (export "g") ${globalType} (ref.func $f)) + (func $f (export "f") (result i32) i32.const 42) + ) + `).exports; + } + + let exports = wasmEvalText(` + (module + (global $g ${exported ? `(export "g")` : ``} ${imported ? `(import "" "g")` : ``} ${globalType} ${imported ? `` : `(ref.func $f)`}) + ${exported ? `` : `(func (export "get_g") (result funcref) global.get $g)`} + (func $f (export "f") (result i32) i32.const 42) + ) + `, { "": imports }).exports; + + let targetFunc = imported ? imports.f : exports.f; + let globalVal = exported ? exports.g.value : exports.get_g(); + assertEq(targetFunc(), 42); + assertEq(globalVal(), 42); + assertEq(targetFunc, globalVal); + if (imported && exported) { + assertEq(imports.g, exports.g); + } + } + } +} + +// Test invalid ref.func indices + +function testCodeRefFuncIndex(index) { + assertErrorMessage(() => { + new WebAssembly.Module(moduleWithSections( + [v2vSigSection, + declSection([0]), // One function + bodySection( + [funcBody( + {locals:[], + body:[ + RefFuncCode, + ...varU32(index), + DropCode + ]})]) + ])) + }, + WebAssembly.CompileError, + /(function index out of range)|(function index out of bounds)/); +} + +testCodeRefFuncIndex(1); +testCodeRefFuncIndex(2); +testCodeRefFuncIndex(10); +testCodeRefFuncIndex(1000); + +function testGlobalRefFuncIndex(index) { + assertErrorMessage(() => { + new WebAssembly.Module(moduleWithSections( + [v2vSigSection, + globalSection([ + { + valType: AnyFuncCode, + flags: 0, + initExpr: [RefFuncCode, ...varU32(index), EndCode], + } + ]) + ])) + }, + WebAssembly.CompileError, + /(function index out of range)|(function index out of bounds)/); +} + +testGlobalRefFuncIndex(1); +testGlobalRefFuncIndex(2); +testGlobalRefFuncIndex(10); +testGlobalRefFuncIndex(1000); diff --git a/js/src/jit-test/tests/wasm/ref-types/stackmaps1.js b/js/src/jit-test/tests/wasm/ref-types/stackmaps1.js new file mode 100644 index 0000000000..0776b490b2 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/stackmaps1.js @@ -0,0 +1,85 @@ +// Tests wasm frame tracing. Only tests for direct and indirect call chains +// in wasm that lead to JS allocation. Does not test any timeout or interrupt +// related aspects. The structure is +// +// test top level: call fn2 +// fn2: call fn1 +// fn1: do 100k times { call-direct fn0; call-indirect fn0; } +// fn0: call out to JS that does allocation +// +// Eventually fn0 will trigger GC and we expect the chain of resulting frames +// to be traced correctly. fn2, fn1 and fn0 have some ref-typed args, so +// there will be traceable stack words to follow, in the sequence of frames. + +const {Module,Instance} = WebAssembly; + +let t = + `(module + (import "" "check3" (func $check3 (param externref) (param externref) (param externref))) + (import "" "alloc" (func $alloc (result externref))) + (type $typeOfFn0 + (func (param i32) (param externref) (param i32) + (param externref) (param externref) (param i32) (result i32))) + (table 1 1 funcref) + (elem (i32.const 0) $fn0) + + ;; -- fn 0 + (func $fn0 (export "fn0") + (param $arg1 i32) (param $arg2 externref) (param $arg3 i32) + (param $arg4 externref) (param $arg5 externref) (param $arg6 i32) (result i32) + (call $alloc) + drop + (i32.add (i32.add (local.get $arg1) (local.get $arg3)) (local.get $arg6)) + + ;; Poke the ref-typed arguments, to be sure that they got kept alive + ;; properly across any GC that the |alloc| call might have done. + (call $check3 (local.get $arg2) (local.get $arg4) (local.get $arg5)) + ) + + ;; -- fn 1 + (func $fn1 (export "fn1") (param $arg1 externref) (result i32) + (local $i i32) + + (loop (result i32) + ;; call direct 0 + (call $fn0 (i32.const 10) (local.get $arg1) (i32.const 12) + (local.get $arg1) (local.get $arg1) (i32.const 15)) + + ;; call indirect 0 + (call_indirect (type $typeOfFn0) + (i32.const 10) (local.get $arg1) (i32.const 12) + (local.get $arg1) (local.get $arg1) (i32.const 15) + (i32.const 0)) ;; table index + + i32.add + + ;; Do 60k iterations of this loop, to get a good amount of allocation + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if 0 (i32.lt_s (local.get $i) (i32.const 60000))) + ) + ) + + ;; -- fn 2 + (func $fn2 (export "fn2") (param $arg1 externref) (result i32) + (call $fn1 (local.get $arg1)) + ) + )`; + +function Croissant(chocolate, number) { + this.chocolate = chocolate; + this.number = number; +} + +function allocates() { + return new Croissant(true, 271828); +} + +function check3(a1, a2, a3) { + assertEq(a1.number, 31415927); + assertEq(a2.number, 31415927); + assertEq(a3.number, 31415927); +} + +let i = wasmEvalText(t, {"":{alloc: allocates, check3: check3}}); + +print(i.exports.fn2( new Croissant(false, 31415927) )); diff --git a/js/src/jit-test/tests/wasm/ref-types/stackmaps2.js b/js/src/jit-test/tests/wasm/ref-types/stackmaps2.js new file mode 100644 index 0000000000..986d378a75 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/stackmaps2.js @@ -0,0 +1,132 @@ +// Tests wasm frame tracing in the presence of interrupt handlers that perform +// allocation. The structure is +// +// test top level: call fn2 +// fn2: call fn1 +// fn1: repeat { call-direct fn0; call-indirect fn0; } +// fn0: a 100-iteration loop that does nothing except waste time +// +// At the same time we are asynchronously runnning handler(), which does a lot +// of allocation. At some point that will trigger a GC. Assuming that +// handler() runs whilst fn0 is running (the most likely scenario, since fn0 +// consumes the majority of the wasm running time), then the runtime will walk +// the stack from the wasm exit frame, through fn0, fn1 and finally fn2. As +// with stackmaps1.js, there are some ref-typed args in use so as provide +// traceable stack slots to follow. +// +// The test runs until the loop in fn1 determines that handler() has allocated +// sufficient memory as to have caused at least three collections. This helps +// keep the test effective in the face of wide variations in the rate of +// progress of the handler()'s loop (eg x86+native is fast, arm64+simulator is +// slow). + +const {Module,Instance} = WebAssembly; + +const DEBUG = false; + +let t = + `(module + (type $typeOfFn0 + (func (param i32) (param externref) (param i32) + (param externref) (param externref) (param i32) (result i32))) + + (import "" "alloc" (func $alloc (result externref))) + (import "" "quitp" (func $quitp (result i32))) + (import "" "check3" (func $check3 (param externref) (param externref) (param externref))) + + (table 1 1 funcref) + (elem (i32.const 0) $fn0) + + ;; -- fn 0 + (func $fn0 (export "fn0") + (param $arg1 i32) (param $arg2 externref) (param $arg3 i32) + (param $arg4 externref) (param $arg5 externref) (param $arg6 i32) (result i32) + (local $i i32) + + ;; spinloop to waste time + (loop + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if 0 (i32.lt_s (local.get $i) (i32.const 100))) + ) + + (i32.add (i32.add (local.get $arg1) (local.get $arg3)) (local.get $arg6)) + + ;; Poke the ref-typed arguments, to be sure that they got kept alive + ;; properly across any GC that might have happened. + (call $check3 (local.get $arg2) (local.get $arg4) (local.get $arg5)) + ) + + ;; -- fn 1 + (func $fn1 (export "fn1") (param $arg1 externref) (result i32) + (loop (result i32) + ;; call direct to $fn0 + (call $fn0 (i32.const 10) (local.get $arg1) (i32.const 12) + (local.get $arg1) (local.get $arg1) (i32.const 15)) + + ;; call indirect to table index 0, which is $fn0 + (call_indirect (type $typeOfFn0) + (i32.const 10) (local.get $arg1) (i32.const 12) + (local.get $arg1) (local.get $arg1) (i32.const 15) + (i32.const 0)) ;; table index + + i32.add + + ;; Continue iterating until handler() has allocated enough + (br_if 0 (i32.eq (call $quitp) (i32.const 0))) + ) + ) + + ;; -- fn 2 + (func $fn2 (export "fn2") (param $arg1 externref) (result i32) + (call $fn1 (local.get $arg1)) + ) + )`; + +function Croissant(chocolate, number) { + this.chocolate = chocolate; + this.number = number; +} + +function allocates() { + return new Croissant(true, 271828); +} + +let totAllocs = 0; + +function handler() { + if (DEBUG) { + print('XXXXXXXX icallback: START'); + } + let q = allocates(); + let sum = 0; + let iters = 15000; + for (let i = 0; i < iters; i++) { + let x = allocates(); + // Without this hoop jumping to create an apparent use of |x|, Ion + // will remove the allocation call and make the test pointless. + if (x == q) { sum++; } + } + totAllocs += iters; + // Artificial use of |sum|. See comment above. + if (sum == 133713371337) { print("unlikely!"); } + timeout(0.5, handler); + if (DEBUG) { + print('XXXXXXXX icallback: END'); + } + return true; +} + +function quitp() { + return totAllocs > 200000 ? 1 : 0; +} + +function check3(a1, a2, a3) { + assertEq(a1.number, 31415927); + assertEq(a2.number, 31415927); + assertEq(a3.number, 31415927); +} + +let i = wasmEvalText(t, {"":{alloc: allocates, quitp: quitp, check3: check3}}); + +timeout(0.5, handler); +print(i.exports.fn2( new Croissant(false, 31415927) )); diff --git a/js/src/jit-test/tests/wasm/ref-types/stackmaps3.js b/js/src/jit-test/tests/wasm/ref-types/stackmaps3.js new file mode 100644 index 0000000000..1b4c15abd2 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/stackmaps3.js @@ -0,0 +1,201 @@ +// Generates a bunch of numbers-on-the-heap, and tries to ensure that they are +// held live -- at least for a short while -- only by references from the wasm +// evaluation stack. Then assembles them in a list and checks that the list +// is as expected (and we don't segfault). While all this is running we also +// have an regular interrupt whose handler does a bunch of allocation, so as +// to cause as much disruption as possible. + +// Note this makes an assumption about how the wasm compiler works. There's +// no particular reason that the wasm compiler needs to keep the results of +// the $mkBoxedInt calls on the machine stack. It could equally cache them in +// registers or even reorder the call sequences so as to interleave +// construction of the list elements with construction of the list itself. It +// just happens that our baseline compiler will behave as described. That +// said, however, it's hard to imagine how an implementation could complete +// the list construction without having at least one root in a register or on +// the stack, so the test still has value regardless of how the underlying +// implementation works. + +const {Module,Instance} = WebAssembly; + +const DEBUG = false; + +let t = + `(module + (import "" "mkCons" (func $mkCons (param externref) (param externref) (result externref))) + (import "" "mkBoxedInt" (func $mkBoxedInt (result externref))) + + (func $mkNil (result externref) + ref.null extern + ) + + (func $mkConsIgnoringScalar + (param $hd externref) (param i32) (param $tl externref) + (result externref) + (local.get $hd) + (local.get $tl) + call $mkCons + ) + + (func $mkList (export "mkList") (result externref) + call $mkList20 + ) + + (func $mkList20 (result externref) + ;; create 20 pointers to boxed ints on the stack, plus a few + ;; scalars for added confusion + (local $scalar99 i32) + (local $scalar97 i32) + (local.set $scalar99 (i32.const 99)) + (local.set $scalar97 (i32.const 97)) + + call $mkBoxedInt + local.get $scalar99 + call $mkBoxedInt + call $mkBoxedInt + local.get $scalar97 + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkBoxedInt + call $mkNil + ;; Now we have (pointers to) 20 boxed ints and a NIL on the stack, and + ;; nothing else holding them live. Build a list from the elements. + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkCons + call $mkConsIgnoringScalar + call $mkCons + call $mkConsIgnoringScalar + ) + )`; + +let boxedIntCounter = 0; + +function BoxedInt() { + this.theInt = boxedIntCounter; + boxedIntCounter++; +} + +function mkBoxedInt() { + return new BoxedInt(); +} + +function printBoxedInt(bi) { + print(bi.theInt); +} + +function Cons(hd, tl) { + this.hd = hd; + this.tl = tl; +} + +function mkCons(hd, tl) { + return new Cons(hd, tl); +} + +function showList(list) { + print("["); + while (list) { + printBoxedInt(list.hd); + print(","); + list = list.tl; + } + print("]"); +} + +function checkList(list, expectedHdValue, expectedLength) { + while (list) { + if (expectedLength <= 0) + return false; + if (list.hd.theInt !== expectedHdValue) { + return false; + } + list = list.tl; + expectedHdValue++; + expectedLength--; + } + if (expectedLength == 0) { + return true; + } else { + return false; + } +} + +let i = wasmEvalText(t, {"":{mkCons: mkCons, mkBoxedInt: mkBoxedInt}}); + + +function Croissant(chocolate) { + this.chocolate = chocolate; +} + +function allocates() { + return new Croissant(true); +} + +function handler() { + if (DEBUG) { + print('XXXXXXXX icallback: START'); + } + let q = allocates(); + let sum = 0; + for (let i = 0; i < 15000; i++) { + let x = allocates(); + // Without this hoop jumping to create an apparent use of |x|, Ion + // will remove the allocation call and make the test pointless. + if (x == q) { sum++; } + } + // Artificial use of |sum|. See comment above. + if (sum == 133713371337) { print("unlikely!"); } + timeout(1, handler); + if (DEBUG) { + print('XXXXXXXX icallback: END'); + } + return true; +} + +timeout(1, handler); + +for (let n = 0; n < 10000; n++) { + let listLowest = boxedIntCounter; + + // Create the list in wasm land, possibly inducing GC on the way + let aList = i.exports.mkList(); + + // Check it is as we expect + let ok = checkList(aList, listLowest, 20/*expected length*/); + if (!ok) { + print("Failed on list: "); + showList(aList); + } + assertEq(ok, true); +} + +// If we get here, the test finished successfully. diff --git a/js/src/jit-test/tests/wasm/ref-types/stackmaps4-params-n-locals.js b/js/src/jit-test/tests/wasm/ref-types/stackmaps4-params-n-locals.js new file mode 100644 index 0000000000..2b95ea14a6 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/stackmaps4-params-n-locals.js @@ -0,0 +1,143 @@ +// A stress test for stackmap creation as it relates to params and locals. + +function Stuff(n) { + this.n = n; +} +function allocate(n) { + const res = new Stuff(n); + // The webassembly loop below will provide us with 254 as an arg after not + // very long. + if (n == 254) { + gc(); + } + return res; +} + +function visit(aStuff) { + return aStuff.n; +} + +let bin = wasmTextToBinary( +` +(module + (import "" "allocate" (func $allocate (param i32) (result externref))) + (import "" "visit" (func $visit (param externref) (result i32))) + + ;; A function with many params and locals, most of which are ref-typed. + ;; The purpose of having so many is to defeat any reasonable attempt at + ;; allocating them all in registers. The asymmetrically-placed i32s are + ;; an attempt to expose any misalignment or inversion of the stack layout + ;; vs what the stackmap claims the layout to be. + + (func $manyParamsAndLocals (export "manyParamsAndLocals") + (param $p1 externref) (param $p2 i32) (param $p3 externref) + (param $p4 externref) (param $p5 externref) (param $p6 externref) + (param $p7 externref) (param $p8 externref) (param $p9 i32) + (result i32) + (local $l1 externref) (local $l2 externref) (local $l3 externref) + (local $l4 i32) (local $l5 externref) (local $l6 i32) + (local $l7 externref) (local $l8 externref) (local $l9 externref) + + (local $i i32) + (local $runningTotal i32) + + ;; Bind some objects to l1 .. l9. The JS harness will already + ;; have done the same for p1 .. p9. + (local.set $l1 (call $allocate (i32.const 1))) + (local.set $l2 (call $allocate (i32.const 3))) + (local.set $l3 (call $allocate (i32.const 5))) + (local.set $l4 (i32.const 7)) + (local.set $l5 (call $allocate (i32.const 9))) + (local.set $l6 (i32.const 11)) + (local.set $l7 (call $allocate (i32.const 13))) + (local.set $l8 (call $allocate (i32.const 15))) + (local.set $l9 (call $allocate (i32.const 17))) + + ;; Now loop, allocating as we go, and forcing GC every 256 iterations. + ;; Also in each iteration, visit all the locals and params, in the hope + ;; of exposing any cases where they are not held live across GC. + (loop $CONT + ;; Allocate, and hold on to the resulting value, so that Ion can't + ;; delete the allocation. + (local.set $l9 (call $allocate (i32.and (local.get $i) (i32.const 255)))) + + ;; Visit all params and locals + + local.get $runningTotal + + (call $visit (local.get $p1)) + i32.add + local.get $p2 + i32.add + (call $visit (local.get $p3)) + i32.add + (call $visit (local.get $p4)) + i32.add + (call $visit (local.get $p5)) + i32.add + (call $visit (local.get $p6)) + i32.add + (call $visit (local.get $p7)) + i32.add + (call $visit (local.get $p8)) + i32.add + local.get $p9 + i32.add + + (call $visit (local.get $l1)) + i32.add + (call $visit (local.get $l2)) + i32.add + (call $visit (local.get $l3)) + i32.add + local.get $l4 + i32.add + (call $visit (local.get $l5)) + i32.add + local.get $l6 + i32.add + (call $visit (local.get $l7)) + i32.add + (call $visit (local.get $l8)) + i32.add + (call $visit (local.get $l9)) + i32.add + + local.set $runningTotal + + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $CONT (i32.lt_s (local.get $i) (i32.const 10000))) + ) ;; loop + + local.get $runningTotal + ) ;; func +) +`); + +let mod = new WebAssembly.Module(bin); +let ins = new WebAssembly.Instance(mod, {"":{allocate, visit}}); + +let p1 = allocate(97); +let p2 = 93; +let p3 = allocate(91); +let p4 = allocate(83); +let p5 = allocate(79); +let p6 = allocate(73); +let p7 = allocate(71); +let p8 = allocate(67); +let p9 = 61; + +let res = ins.exports.manyParamsAndLocals(p1, p2, p3, p4, p5, p6, p7, p8, p9); + +// Check that GC didn't mess up p1 .. p9 +res += visit(p1); +res += p2; +res += visit(p3); +res += visit(p4); +res += visit(p5); +res += visit(p6); +res += visit(p7); +res += visit(p8); +res += p9; + +assertEq(res, 9063795); diff --git a/js/src/jit-test/tests/wasm/ref-types/tables-api.js b/js/src/jit-test/tests/wasm/ref-types/tables-api.js new file mode 100644 index 0000000000..a2ae3c45f0 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/tables-api.js @@ -0,0 +1,93 @@ +// Test changes to the WebAssembly.Table API by reference-types + +function assertTableFilled(table, length, element) { + assertEq(table.length, length); + for (let i = 0; i < length; i++) { + assertEq(table.get(i), element); + } +} + +const tableLength = 10; + +// Test table constructor can accept a default value to fill elements to +for (let value of WasmExternrefValues) { + let table = new WebAssembly.Table({'element': 'externref', initial: tableLength}, value); + assertTableFilled(table, tableLength, value); +} +for (let value of WasmFuncrefValues) { + let table = new WebAssembly.Table({'element': 'anyfunc', initial: tableLength}, value); + assertTableFilled(table, tableLength, value); +} + +// Test not specifying default value in constructor yields 'undefined' for externref +{ + let table = new WebAssembly.Table({'element': 'externref', initial: tableLength}); + assertTableFilled(table, tableLength, undefined); +} + +// Test not specifying default value in constructor yields 'null' for funcref +{ + let table = new WebAssembly.Table({'element': 'anyfunc', initial: tableLength}); + assertTableFilled(table, tableLength, null); +} + +// Test omitting the value in table set defaults to undefined for externref and +// null for funcref. +{ + let t = new WebAssembly.Table({element:"externref", initial: 1}); + // Clear out initial value + t.set(0, ''); + // Set with an omitted value + t.set(0); + // Assert the omitted value is undefined + assertEq(t.get(0), undefined); +} +{ + let t = new WebAssembly.Table({element:"anyfunc", initial: 1}); + // Clear out initial value + t.set(0, WasmFuncrefValues[0]); + // Set with an omitted value + t.set(0); + // Assert the omitted value is null + assertEq(t.get(0), null); +} + +// Test table grow. There is an optional fill argument that defaults to +// undefined with externref, and null with funcref. +{ + let t = new WebAssembly.Table({element:"externref", initial:0}); + t.grow(1); + assertEq(t.get(t.length-1), undefined); + let prev = undefined; + for (let v of WasmExternrefValues) { + t.grow(2, v); + assertEq(t.get(t.length-3), prev); + assertEq(t.get(t.length-2), v); + + assertEq(t.get(t.length-1), v); + prev = v; + } +} + +{ + let t = new WebAssembly.Table({element:"anyfunc", initial:0}); + t.grow(1); + assertEq(t.get(t.length-1), null); + let prev = null; + for (let v of WasmFuncrefValues) { + t.grow(2, v); + assertEq(t.get(t.length-3), prev); + assertEq(t.get(t.length-2), v); + + assertEq(t.get(t.length-1), v); + prev = v; + } +} + +// If growing by zero elements there are no spurious writes +{ + let t = new WebAssembly.Table({element:"externref", initial:1}); + t.set(0, 1337); + t.grow(0, 1789); + assertEq(t.get(0), 1337); +} diff --git a/js/src/jit-test/tests/wasm/ref-types/tables-fill.js b/js/src/jit-test/tests/wasm/ref-types/tables-fill.js new file mode 100644 index 0000000000..b6b821f1d0 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/tables-fill.js @@ -0,0 +1,212 @@ +const N = 8; + +function testTableFill(tbl_type, val_type, obj) { + assertEq(obj.length, N); + + let ins + = wasmEvalText( + `(module + (table 8 ${tbl_type}) ;; table 0 + (table $t 10 ${tbl_type}) ;; table 1 + + ;; fill/get for table 0, referenced implicitly + (func (export "fill0") (param $i i32) (param $r ${val_type}) (param $n i32) + (table.fill (local.get $i) (local.get $r) (local.get $n)) + ) + (func (export "get0") (param $i i32) (result ${tbl_type}) + (table.get (local.get $i)) + ) + + ;; fill/get for table 1, referenced explicitly + (func (export "fill1") (param $i i32) (param $r ${val_type}) (param $n i32) + (table.fill $t (local.get $i) (local.get $r) (local.get $n)) + ) + (func (export "get1") (param $i i32) (result ${tbl_type}) + (table.get $t (local.get $i)) + ) + )`); + + // An initial test to ascertain that tables 0 and 1 are independent + + // Fill in table 0, then check it. + assertEq(ins.exports.fill0(2, obj[6], 5), undefined) + assertEq(ins.exports.fill0(1, obj[7], 3), undefined); + + function check_table0() { + assertEq(ins.exports.get0(0), null); + assertEq(ins.exports.get0(1), obj[7]); + assertEq(ins.exports.get0(2), obj[7]); + assertEq(ins.exports.get0(3), obj[7]); + assertEq(ins.exports.get0(4), obj[6]); + assertEq(ins.exports.get0(5), obj[6]); + assertEq(ins.exports.get0(6), obj[6]); + assertEq(ins.exports.get0(7), null); + } + + // Check that table 0 has the expected content. + check_table0(); + + // Check that messing with table 0 above hasn't changed table 1. + for (let i = 0; i < 10; i++) { + assertEq(ins.exports.get1(i), null); + } + + // Now a bunch of tests involving only table 1. + + // Partly outside the table + assertErrorMessage(() => ins.exports.fill1(8, obj[5], 3), + WebAssembly.RuntimeError, /index out of bounds/); + + assertEq(ins.exports.get1(7), null); + assertEq(ins.exports.get1(8), null); + assertEq(ins.exports.get1(9), null); + + // Within the table + assertEq(ins.exports.fill1(2, obj[0], 3), undefined); + assertEq(ins.exports.get1(1), null); + assertEq(ins.exports.get1(2), obj[0]); + assertEq(ins.exports.get1(3), obj[0]); + assertEq(ins.exports.get1(4), obj[0]); + assertEq(ins.exports.get1(5), null); + + // Within the table + assertEq(ins.exports.fill1(4, obj[1], 2), undefined); + assertEq(ins.exports.get1(3), obj[0]); + assertEq(ins.exports.get1(4), obj[1]); + assertEq(ins.exports.get1(5), obj[1]); + assertEq(ins.exports.get1(6), null); + + // Within the table + assertEq(ins.exports.fill1(4, obj[2], 0), undefined); + assertEq(ins.exports.get1(3), obj[0]); + assertEq(ins.exports.get1(4), obj[1]); + assertEq(ins.exports.get1(5), obj[1]); + + // Within the table + assertEq(ins.exports.fill1(8, obj[3], 2), undefined); + assertEq(ins.exports.get1(7), null); + assertEq(ins.exports.get1(8), obj[3]); + assertEq(ins.exports.get1(9), obj[3]); + + // Within the table + assertEq(ins.exports.fill1(9, null, 1), undefined); + assertEq(ins.exports.get1(8), obj[3]); + assertEq(ins.exports.get1(9), null); + + // Within the table + assertEq(ins.exports.fill1(10, obj[4], 0), undefined); + assertEq(ins.exports.get1(9), null); + + // Boundary tests on table 1: at the edge of the table. + + // Length-zero fill1 at the edge of the table must succeed + assertEq(ins.exports.fill1(10, null, 0), undefined); + + // Length-one fill1 at the edge of the table fails + assertErrorMessage(() => ins.exports.fill1(10, null, 1), + WebAssembly.RuntimeError, /index out of bounds/); + + // Length-more-than-one fill1 at the edge of the table fails + assertErrorMessage(() => ins.exports.fill1(10, null, 2), + WebAssembly.RuntimeError, /index out of bounds/); + + + // Boundary tests on table 1: beyond the edge of the table: + + // Length-zero fill1 beyond the edge of the table fails + assertErrorMessage(() => ins.exports.fill1(11, null, 0), + WebAssembly.RuntimeError, /index out of bounds/); + + // Length-one fill1 beyond the edge of the table fails + assertErrorMessage(() => ins.exports.fill1(11, null, 1), + WebAssembly.RuntimeError, /index out of bounds/); + + // Length-more-than-one fill1 beyond the edge of the table fails + assertErrorMessage(() => ins.exports.fill1(11, null, 2), + WebAssembly.RuntimeError, /index out of bounds/); + + // Following all the above tests on table 1, check table 0 hasn't changed. + check_table0(); +} + +var objs = []; +for (var i = 0; i < N; i++) + objs[i] = {n:i}; +testTableFill('externref', 'externref', objs); + +var funcs = []; +for (var i = 0; i < N; i++) + funcs[i] = wasmEvalText(`(module (func (export "x") (result i32) (i32.const ${i})))`).exports.x; +testTableFill('funcref', 'funcref', funcs); + + +// Type errors. Required sig is: (i32, externref, i32) -> void + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $expected-3-args-got-0 + (table.fill $t) + ))`), + WebAssembly.CompileError, /(popping value from empty stack)|(nothing on stack)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $expected-3-args-got-1 + (table.fill $t (i32.const 0)) + ))`), + WebAssembly.CompileError, /(popping value from empty stack)|(nothing on stack)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $expected-3-args-got-2 + (table.fill $t (ref.null extern) (i32.const 0)) + ))`), + WebAssembly.CompileError, /(popping value from empty stack)|(nothing on stack)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $argty-1-wrong + (table.fill $t (i32.const 0) (ref.null extern) (f64.const 0)) + ))`), + WebAssembly.CompileError, + /(type mismatch: expression has type f64 but expected i32)|(type mismatch: expected i32, found f64)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $argty-2-wrong + (table.fill $t (i32.const 0) (f32.const 0) (i32.const 0)) + ))`), + WebAssembly.CompileError, + /(type mismatch: expression has type f32 but expected externref)|(type mismatch: expected externref, found f32)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $argty-3-wrong + (table.fill $t (i64.const 0) (ref.null extern) (i32.const 0)) + ))`), + WebAssembly.CompileError, + /(type mismatch: expression has type i64 but expected i32)|(type mismatch: expected i32, found i64)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t 10 externref) + (func $retty-wrong (result i32) + (table.fill $t (i32.const 0) (ref.null extern) (i32.const 0)) + ))`), + WebAssembly.CompileError, + /(popping value from empty stack)|(nothing on stack)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table 8 funcref) + (func (param $v externref) + (table.fill (i32.const 0) (local.get $v) (i32.const 0))) + )`), + WebAssembly.CompileError, + /(expression has type externref but expected funcref)|(type mismatch: expected funcref, found externref)/); diff --git a/js/src/jit-test/tests/wasm/ref-types/tables-generalized.js b/js/src/jit-test/tests/wasm/ref-types/tables-generalized.js new file mode 100644 index 0000000000..19e530baae --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/tables-generalized.js @@ -0,0 +1,432 @@ +/////////////////////////////////////////////////////////////////////////// +// +// General table management in wasm + +// Wasm: Create table-of-externref + +new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 externref))`)); + +// Wasm: Import table-of-externref +// JS: create table-of-externref + +new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (table (import "m" "t") 10 externref))`)), + {m:{t: new WebAssembly.Table({element:"externref", initial:10})}}); + +new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (import "m" "t" (table 10 externref)))`)), + {m:{t: new WebAssembly.Table({element:"externref", initial:10})}}); + +// Wasm: Export table-of-externref, initial values shall be null + +{ + let ins = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 externref))`))); + let t = ins.exports.t; + assertEq(t.length, 10); + for (let i=0; i < t.length; i++) + assertEq(t.get(0), null); +} + +// JS: Exported table can be grown, and values are preserved + +{ + let ins = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 externref))`))); + let t = ins.exports.t; + let objs = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]; + for (let i in objs) + t.set(i, objs[i]); + ins.exports.t.grow(10); + assertEq(ins.exports.t.length, 20); + for (let i in objs) + assertEq(t.get(i), objs[i]); +} + +// Wasm: table.copy between tables of externref (currently source and destination +// are both table zero) + +{ + let ins = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 externref) + (func (export "f") + (table.copy (i32.const 5) (i32.const 0) (i32.const 3))))`))); + let t = ins.exports.t; + let objs = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]; + for (let i in objs) + t.set(i, objs[i]); + ins.exports.f(); + assertEq(t.get(0), objs[0]); + assertEq(t.get(1), objs[1]); + assertEq(t.get(2), objs[2]); + assertEq(t.get(3), objs[3]); + assertEq(t.get(4), objs[4]); + assertEq(t.get(5), objs[0]); + assertEq(t.get(6), objs[1]); + assertEq(t.get(7), objs[2]); + assertEq(t.get(8), objs[8]); + assertEq(t.get(9), objs[9]); +} + +// Wasm: table.copy from table(funcref) to table(externref) should not work + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 externref) + (func $f1) + (func $f2) + (func $f3) + (func $f4) + (func $f5) + (table 5 funcref) + (elem (table 1) (i32.const 0) func $f1 $f2 $f3 $f4 $f5) + (func (export "f") + (table.copy 0 1 (i32.const 5) (i32.const 0) (i32.const 5))))`)), + WebAssembly.CompileError, + /(expression has type funcref but expected externref)|(type mismatch)/); + +// Wasm: table.copy from table(externref) to table(funcref) should not work + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 funcref) + (table 10 externref) + (func (export "f") + (table.copy 0 1 (i32.const 0) (i32.const 0) (i32.const 5))))`)), + WebAssembly.CompileError, + /(expression has type externref but expected funcref)|(type mismatch)/); + +// Wasm: Element segments of funcref can't target tables of externref + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (func $f1 (result i32) (i32.const 0)) + (table (export "t") 10 externref) + (elem 0 (i32.const 0) funcref (ref.func $f1)))`)), + WebAssembly.CompileError, + /type mismatch/); + + +// Wasm: Element segments of externref can't target tables of funcref + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 funcref) + (elem 0 (i32.const 0) externref (ref.null extern)))`)), + WebAssembly.CompileError, + /type mismatch/); + +// Wasm: table.init on table-of-externref is not allowed when the segment has +// funcref. + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (func $f1 (result i32) (i32.const 0)) + (table 10 externref) + (elem funcref (ref.func $f1)) + (func + (table.init 0 (i32.const 0) (i32.const 0) (i32.const 0))))`)), + WebAssembly.CompileError, + /(expression has type funcref but expected externref)|(type mismatch)/); + + +// Wasm: table.init on table-of-funcref is not allowed when the segment has +// externref. + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 funcref) + (elem externref (ref.null extern)) + (func + (table.init 0 (i32.const 0) (i32.const 0) (i32.const 0))))`)), + WebAssembly.CompileError, + /(expression has type externref but expected funcref)|(type mismatch)/); + +// Wasm: table types must match at link time + +assertErrorMessage( + () => new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (import "m" "t" (table 10 externref)))`)), + {m:{t: new WebAssembly.Table({element:"anyfunc", initial:10})}}), + WebAssembly.LinkError, + /imported table type mismatch/); + +// call_indirect cannot reference table-of-externref + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 externref) + (type $t (func (param i32) (result i32))) + (func (result i32) + (call_indirect (type $t) (i32.const 37))))`)), + WebAssembly.CompileError, + /(indirect calls must go through a table of 'funcref')|(indirect calls must go through a table of funcref)/); + +/////////////////////////////////////////////////////////////////////////// +// +// additional js api tests + +{ + let tbl = new WebAssembly.Table({element:"externref", initial:10}); + + // Initial value is undefined. This is different from when tables are + // created inside wasm. + assertEq(tbl.get(0), undefined); + + // Identity preserving. + let x = {hi: 48}; + tbl.set(0, x); + assertEq(tbl.get(0), x); + tbl.set(2, dummy); + assertEq(tbl.get(2), dummy); + tbl.set(2, null); + assertEq(tbl.get(2), null); + + // Temporary semantics is to convert to object and leave as object; once we + // have a better wrapped externref this will change, we won't be able to + // observe the boxing. + tbl.set(1, 42); + let y = tbl.get(1); + assertEq(typeof y, "number"); + assertEq(y, 42); +} + +function dummy() { return 37 } + +/////////////////////////////////////////////////////////////////////////// +// +// table.get and table.set + +const wasmFun = wasmEvalText(`(module (func (export "x")))`).exports.x; + +// table.get in bounds - returns right value type & value +// table.get out of bounds - fails + +function testTableGet(type, x) { + let ins = wasmEvalText( + `(module + (table (export "t") 10 ${type}) + (func (export "f") (param i32) (result ${type}) + (table.get (local.get 0))))`); + ins.exports.t.set(0, x); + assertEq(ins.exports.f(0), x); + assertEq(ins.exports.f(1), null); + assertErrorMessage(() => ins.exports.f(10), WebAssembly.RuntimeError, /index out of bounds/); + assertErrorMessage(() => ins.exports.f(-5), WebAssembly.RuntimeError, /index out of bounds/); +} +testTableGet('externref', {}); +testTableGet('funcref', wasmFun); + +// table.get with non-i32 index - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 externref) + (func (export "f") (param f64) (result externref) + (table.get (local.get 0))))`)), + WebAssembly.CompileError, + /type mismatch/); + +// table.get when there are no tables - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (func (export "f") (param i32) + (drop (table.get (local.get 0)))))`)), + WebAssembly.CompileError, + /(table index out of range for table.get)|(table index out of bounds)/); + +// table.set in bounds with i32 x externref - works, no value generated +// table.set with null - works +// table.set out of bounds - fails + +function testTableSet(lhs_type, rhs_type, rhs_reftype, x) { + let ins = wasmEvalText( + `(module + (table (export "t") 10 ${lhs_type}) + (func (export "set_ref") (param i32) (param ${rhs_type}) + (table.set (local.get 0) (local.get 1))) + (func (export "set_null") (param i32) + (table.set (local.get 0) (ref.null ${rhs_reftype}))))`); + ins.exports.set_ref(3, x); + assertEq(ins.exports.t.get(3), x); + ins.exports.set_null(3); + assertEq(ins.exports.t.get(3), null); + + assertErrorMessage(() => ins.exports.set_ref(10, x), WebAssembly.RuntimeError, /index out of bounds/); + assertErrorMessage(() => ins.exports.set_ref(-1, x), WebAssembly.RuntimeError, /index out of bounds/); +} +testTableSet('externref', 'externref', 'extern', {}); +testTableSet('funcref', 'funcref', 'func', wasmFun); + +// Wasm: table.set on table(funcref) with externref value should fail + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 funcref) + (func (export "set_ref") (param i32) (param externref) + (table.set (local.get 0) (local.get 1))))`)), + WebAssembly.CompileError, + /(type mismatch: expression has type externref but expected funcref)|(type mismatch: expected funcref, found externref)/); + +// table.set with non-i32 index - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 externref) + (func (export "f") (param f64) + (table.set (local.get 0) (ref.null extern))))`)), + WebAssembly.CompileError, + /type mismatch/); + +// table.set with non-ref value - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 externref) + (func (export "f") (param f64) + (table.set (i32.const 0) (local.get 0))))`)), + WebAssembly.CompileError, + /type mismatch/); +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 funcref) + (func (export "f") (param f64) + (table.set (i32.const 0) (local.get 0))))`)), + WebAssembly.CompileError, + /type mismatch/); + +// table.set when there are no tables - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (func (export "f") (param externref) + (table.set (i32.const 0) (local.get 0))))`)), + WebAssembly.CompileError, + /(table index out of range for table.set)|(table index out of bounds)/); + +function testTableGrow(lhs_type, lhs_reftype, rhs_type, x) { + let ins = wasmEvalText( + `(module + (table (export "t") 10 20 ${lhs_type}) + (func (export "grow") (param i32) (result i32) + (table.grow (ref.null ${lhs_reftype}) (local.get 0))) + (func (export "grow2") (param i32) (param ${rhs_type}) (result i32) + (table.grow (local.get 1) (local.get 0))))`); + + // we can grow table of references + // table.grow with zero delta - always works even at maximum + // table.grow with delta - works and returns correct old value + // table.grow with delta at upper limit - fails + // table.grow with negative delta - fails + assertEq(ins.exports.grow(0), 10); + assertEq(ins.exports.t.length, 10); + assertEq(ins.exports.grow(1), 10); + assertEq(ins.exports.t.length, 11); + assertEq(ins.exports.t.get(10), null); + assertEq(ins.exports.grow2(9, x), 11); + assertEq(ins.exports.t.length, 20); + for (var i = 11; i < 20; i++) + assertEq(ins.exports.t.get(i), x); + assertEq(ins.exports.grow(0), 20); + + // The JS API throws if it can't grow + assertErrorMessage(() => ins.exports.t.grow(1), RangeError, /failed to grow table/); + assertErrorMessage(() => ins.exports.t.grow(-1), TypeError, /bad [Tt]able grow delta/); + + // The wasm API does not throw if it can't grow, but returns -1 + assertEq(ins.exports.grow(1), -1); + assertEq(ins.exports.t.length, 20); + assertEq(ins.exports.grow(-1), -1); + assertEq(ins.exports.t.length, 20) +} +testTableGrow('externref', 'extern', 'externref', 42); +testTableGrow('funcref', 'func', 'funcref', wasmFun); + +// Wasm: table.grow on table(funcref) with externref initializer should fail + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table (export "t") 10 20 funcref) + (func (export "grow2") (param i32) (param externref) (result i32) + (table.grow (local.get 1) (local.get 0))))`)), + WebAssembly.CompileError, + /(type mismatch: expression has type externref but expected funcref)|(type mismatch: expected funcref, found externref)/); + +// Special case for private tables without a maximum + +{ + let ins = wasmEvalText( + `(module + (table 10 externref) + (func (export "grow") (param i32) (result i32) + (table.grow (ref.null extern) (local.get 0))))`); + assertEq(ins.exports.grow(0), 10); + assertEq(ins.exports.grow(1), 10); + assertEq(ins.exports.grow(9), 11); + assertEq(ins.exports.grow(0), 20); +} + +// table.grow with non-i32 argument - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (table 10 externref) + (func (export "f") (param f64) + (table.grow (ref.null extern) (local.get 0))))`)), + WebAssembly.CompileError, + /type mismatch/); + +// table.grow when there are no tables - fails validation + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary( + `(module + (func (export "f") (param i32) + (table.grow (ref.null extern) (local.get 0))))`)), + WebAssembly.CompileError, + /(table index out of range for table.grow)|(table index out of bounds)/); + +// table.size on table of externref + +for (let visibility of ['', '(export "t")', '(import "m" "t")']) { + let exp = {m:{t: new WebAssembly.Table({element:"externref", + initial: 10, + maximum: 20})}}; + let ins = wasmEvalText( + `(module + (table ${visibility} 10 20 externref) + (func (export "grow") (param i32) (result i32) + (table.grow (ref.null extern) (local.get 0))) + (func (export "size") (result i32) + (table.size)))`, + exp); + assertEq(ins.exports.grow(0), 10); + assertEq(ins.exports.size(), 10); + assertEq(ins.exports.grow(1), 10); + assertEq(ins.exports.size(), 11); + assertEq(ins.exports.grow(9), 11); + assertEq(ins.exports.size(), 20); + assertEq(ins.exports.grow(0), 20); + assertEq(ins.exports.size(), 20); +} + +// table.size on table of funcref + +{ + let ins = wasmEvalText( + `(module + (table (export "t") 2 funcref) + (func (export "f") (result i32) + (table.size)))`); + assertEq(ins.exports.f(), 2); + ins.exports.t.grow(1); + assertEq(ins.exports.f(), 3); +} diff --git a/js/src/jit-test/tests/wasm/ref-types/tables-multiple.js b/js/src/jit-test/tests/wasm/ref-types/tables-multiple.js new file mode 100644 index 0000000000..ca93824170 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/tables-multiple.js @@ -0,0 +1,465 @@ +// Note that negative tests not having to do with table indices have been taken +// care of by tables-generalized.js. + +/////////////////////////////////////////////////////////////////////////// +// +// Positive tests + +// - multiple local tables of misc type: syntax, validation, instantiation +// - element segments can point to a table +// - call-indirect can specify a table and will use it + +var ins = wasmEvalText( + `(module + (table $t1 2 funcref) + (table $t2 2 funcref) + (type $ftype (func (param i32) (result i32))) + (elem (table $t1) (i32.const 0) func $f1 $f2) + (elem (table $t2) (i32.const 0) func $f3 $f4) + (func $f1 (param $n i32) (result i32) + (i32.add (local.get $n) (i32.const 1))) + (func $f2 (param $n i32) (result i32) + (i32.add (local.get $n) (i32.const 2))) + (func $f3 (param $n i32) (result i32) + (i32.add (local.get $n) (i32.const 3))) + (func $f4 (param $n i32) (result i32) + (i32.add (local.get $n) (i32.const 4))) + (func (export "f") (param $fn i32) (param $n i32) (result i32) + (call_indirect $t1 (type $ftype) (local.get $n) (local.get $fn))) + (func (export "g") (param $fn i32) (param $n i32) (result i32) + (call_indirect $t2 (type $ftype) (local.get $n) (local.get $fn))))`).exports; + +assertEq(ins.f(0, 10), 11); +assertEq(ins.f(1, 10), 12); +assertEq(ins.g(0, 10), 13); +assertEq(ins.g(1, 10), 14); + +// - export multiple tables. +// note the first and third tables make the export list not start at zero, +// and make it sparse + +var ins = wasmEvalText( + `(module + (table $t0 (import "m" "t") 2 funcref) + (table $t1 (export "t1") 2 funcref) + (table 1 externref) + (table $t2 (export "t2") 3 funcref))`, + {m:{t: new WebAssembly.Table({element:"anyfunc", initial:2})}}).exports; + +assertEq(ins.t1 instanceof WebAssembly.Table, true); +assertEq(ins.t1.length, 2); +assertEq(ins.t2 instanceof WebAssembly.Table, true); +assertEq(ins.t2.length, 3); + +// - multiple imported tables of misc type +// - table.get and table.set can point to a table + +var exp = {m:{t0: new WebAssembly.Table({element:"anyfunc", initial:2}), + t1: new WebAssembly.Table({element:"externref", initial:3}), + t2: new WebAssembly.Table({element:"anyfunc", initial:4}), + t3: new WebAssembly.Table({element:"externref", initial:5})}}; +var ins = wasmEvalText( + `(module + (table $t0 (import "m" "t0") 2 funcref) + (table $t1 (import "m" "t1") 3 externref) + (table $t2 (import "m" "t2") 4 funcref) + (table $t3 (import "m" "t3") 5 externref) + + (type $id_i32_t (func (param i32) (result i32))) + (func $id_i32 (param i32) (result i32) (local.get 0)) + (elem (table $t0) (i32.const 1) func $id_i32) + + (type $id_f64_t (func (param f64) (result f64))) + (func $id_f64 (param f64) (result f64) (local.get 0)) + (elem (table $t2) (i32.const 3) func $id_f64) + + (func (export "f0") (param i32) (result i32) + (call_indirect $t0 (type $id_i32_t) (local.get 0) (i32.const 1))) + + (func (export "f1") (param externref) + (table.set $t1 (i32.const 2) (local.get 0))) + + (func (export "f2") (param f64) (result f64) + (call_indirect $t2 (type $id_f64_t) (local.get 0) (i32.const 3))) + + (func (export "f3") + (table.set $t3 (i32.const 4) (table.get $t1 (i32.const 2)))))`, + + exp).exports; + +assertEq(ins.f0(37), 37); + +var x = {} +ins.f1(x); +assertEq(exp.m.t1.get(2), x); + +assertEq(ins.f2(3.25), 3.25); + +ins.f3(); +assertEq(exp.m.t3.get(4), x); + +// - table.grow can point to a table +// - growing a table grows the right table but not the others +// - table.size on tables other than table 0 + +var exp = {m:{t0: new WebAssembly.Table({element:"externref", initial:2}), + t1: new WebAssembly.Table({element:"externref", initial:3})}}; +var ins = wasmEvalText( + `(module + (table $t0 (import "m" "t0") 2 externref) + (table $t1 (import "m" "t1") 3 externref) + (func (export "f") (result i32) + (table.grow $t1 (ref.null extern) (i32.const 5))) + (func (export "size0") (result i32) + (table.size $t0)) + (func (export "size1") (result i32) + (table.size $t1)))`, + exp); + +assertEq(ins.exports.f(), 3); +assertEq(exp.m.t1.length, 8); +assertEq(ins.exports.size0(), 2); +assertEq(ins.exports.size1(), 8); + +// - table.copy can point to tables + +var exp = {m:{t0: new WebAssembly.Table({element:"externref", initial:2}), + t1: new WebAssembly.Table({element:"externref", initial:3})}}; +var ins = wasmEvalText( + `(module + (table $t0 (import "m" "t0") 2 externref) + (table $t1 (import "m" "t1") 3 externref) + (func (export "f") (param $dest i32) (param $src i32) (param $len i32) + (table.copy $t1 $t0 (local.get $dest) (local.get $src) (local.get $len))))`, + exp); + +exp.m.t0.set(0, {x:0}); +exp.m.t0.set(1, {x:1}); +ins.exports.f(1, 0, 2); +assertEq(exp.m.t1.get(1), exp.m.t0.get(0)); +assertEq(exp.m.t1.get(2), exp.m.t0.get(1)); + +// - the table.copy syntax makes sense even in the non-parenthesized case + +var ins = wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func (export "copy") (param $dest i32) (param $src i32) (param $len i32) + local.get $dest + local.get $src + local.get $len + table.copy $t1 $t0) + (func (export "set") (param $n i32) (param $v externref) + (table.set $t0 (local.get $n) (local.get $v))) + (func (export "get") (param $n i32) (result externref) + (table.get $t1 (local.get $n))))`, + exp); + +var values = [{x:0}, {x:1}]; +ins.exports.set(0, values[0]); +ins.exports.set(1, values[1]); +ins.exports.copy(0, 0, 2); +assertEq(ins.exports.get(0), values[0]); +assertEq(ins.exports.get(1), values[1]); + +// Copy beween an external table and a local table. These cases are interesting +// mostly because the representations of the tables must be compatible; the copy +// in effect forces a representation for the function in the local table that +// captures the function's instance, at the latest at the time the copy is +// executed. +// +// In the case where the function is imported, it comes from a different module. +// +// Also tests: +// - local tables can be exported and re-imported in another module + +var arg = 4; +for (let [a,b,x,y,result,init] of [['$t0', '$t1', '(export "t")', '', arg*13, true], + ['$t0', '$t1', '', '(export "t")', arg*13, true], + ['$t0', '$t1', '(import "m" "t")', '', arg*13, true], + ['$t1', '$t0', '(import "m" "t")', '', arg-11, false]]) +{ + var otherins = wasmEvalText( + `(module + (table $t (export "t") 2 funcref) + (type $fn1 (func (param i32) (result i32))) + (func $f1 (param $n i32) (result i32) + (i32.sub (local.get $n) (i32.const 11))) + (elem (table $t) (i32.const 1) func $f1))`); + + let text = + `(module + (table ${a} ${x} 2 funcref) + + (table ${b} ${y} 2 funcref) + (type $fn1 (func (param i32) (result i32))) + (func $f1 (param $n i32) (result i32) + (i32.mul (local.get $n) (i32.const 13))) + ${init ? "(elem (table $t1) (i32.const 1) func $f1)" : ""} + + (func (export "f") (param $n i32) (result i32) + (table.copy $t0 $t1 (i32.const 0) (i32.const 0) (i32.const 2)) + (call_indirect $t0 (type $fn1) (local.get $n) (i32.const 1))))`; + var ins = wasmEvalText(text, {m: otherins.exports}); + + assertEq(ins.exports.f(arg), result); +} + +// - a table can be imported multiple times, and when it is, and one of them is grown, +// they are all grown. +// - test the (import "m" "t" (table ...)) syntax +// - if table is grown from JS, wasm can observe the growth + +var tbl = new WebAssembly.Table({element:"externref", initial:1}); +var exp = {m: {t0: tbl, t1:tbl}}; + +var ins = wasmEvalText( + `(module + (import "m" "t0" (table $t0 1 externref)) + (import "m" "t1" (table $t1 1 externref)) + (table $t2 (export "t2") 1 funcref) + (func (export "f") (result i32) + (table.grow $t0 (ref.null extern) (i32.const 1))) + (func (export "g") (result i32) + (table.grow $t1 (ref.null extern) (i32.const 1))) + (func (export "size") (result i32) + (table.size $t2)))`, + exp); + +assertEq(ins.exports.f(), 1); +assertEq(ins.exports.g(), 2); +assertEq(ins.exports.f(), 3); +assertEq(ins.exports.g(), 4); +assertEq(tbl.length, 5); +ins.exports.t2.grow(3); +assertEq(ins.exports.size(), 4); + +// - table.init on tables other than table 0 + +var ins = wasmEvalText( + `(module + (table $t0 2 funcref) + (table $t1 2 funcref) + (elem func $f0 $f1) ;; 0 + (type $ftype (func (param i32) (result i32))) + (func $f0 (param i32) (result i32) + (i32.mul (local.get 0) (i32.const 13))) + (func $f1 (param i32) (result i32) + (i32.sub (local.get 0) (i32.const 11))) + (func (export "call") (param i32) (param i32) (result i32) + (call_indirect $t1 (type $ftype) (local.get 1) (local.get 0))) + (func (export "init") + (table.init $t1 0 (i32.const 0) (i32.const 0) (i32.const 2))))`); + +ins.exports.init(); +assertEq(ins.exports.call(0, 10), 130); +assertEq(ins.exports.call(1, 10), -1); + +// - [white-box] if a multi-imported table of funcref is grown and the grown +// part is properly initialized with functions then calls through both tables +// in the grown area should succeed, ie, bounds checks should pass. this is +// an interesting case because we cache the table bounds for the benefit of +// call_indirect, so here we're testing that the caches are updated properly +// even when a table is observed multiple times (also by multiple modules). +// there's some extra hair here because a table of funcref can be grown only +// from JS at the moment. +// - also test that bounds checking continues to catch OOB calls + +var tbl = new WebAssembly.Table({element:"anyfunc", initial:2}); +var exp = {m:{t0: tbl, t1: tbl}}; +var ins = wasmEvalText( + `(module + (import "m" "t0" (table $t0 2 funcref)) + (import "m" "t1" (table $t1 2 funcref)) + (type $ftype (func (param f64) (result f64))) + (func (export "f") (param $n f64) (result f64) + (f64.mul (local.get $n) (f64.const 3.25))) + (func (export "do0") (param $i i32) (param $n f64) (result f64) + (call_indirect $t0 (type $ftype) (local.get $n) (local.get $i))) + (func (export "do1") (param $i i32) (param $n f64) (result f64) + (call_indirect $t1 (type $ftype) (local.get $n) (local.get $i))))`, + exp); +var ins2 = wasmEvalText( + `(module + (import "m" "t0" (table $t0 2 funcref)) + (import "m" "t1" (table $t1 2 funcref)) + (type $ftype (func (param f64) (result f64))) + (func (export "do0") (param $i i32) (param $n f64) (result f64) + (call_indirect $t0 (type $ftype) (local.get $n) (local.get $i))) + (func (export "do1") (param $i i32) (param $n f64) (result f64) + (call_indirect $t1 (type $ftype) (local.get $n) (local.get $i))))`, + exp); + +assertEq(tbl.grow(10), 2); +tbl.set(11, ins.exports.f); +assertEq(ins.exports.do0(11, 2.0), 6.5); +assertEq(ins.exports.do1(11, 4.0), 13.0); +assertEq(ins2.exports.do0(11, 2.0), 6.5); +assertEq(ins2.exports.do1(11, 4.0), 13.0); +assertErrorMessage(() => ins.exports.do0(12, 2.0), + WebAssembly.RuntimeError, + /index out of bounds/); +assertErrorMessage(() => ins2.exports.do0(12, 2.0), + WebAssembly.RuntimeError, + /index out of bounds/); +assertErrorMessage(() => ins.exports.do1(12, 2.0), + WebAssembly.RuntimeError, + /index out of bounds/); +assertErrorMessage(() => ins2.exports.do1(12, 2.0), + WebAssembly.RuntimeError, + /index out of bounds/); + +/////////////////////////////////////////////////////////////////////////// +// +// Negative tests + +// Table index (statically) out of bounds + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func $f (result externref) + (table.get 2 (i32.const 0))))`), + WebAssembly.CompileError, + /(table index out of range for table.get)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func $f (param externref) + (table.set 2 (i32.const 0) (local.get 0))))`), + WebAssembly.CompileError, + /(table index out of range for table.set)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func $f (param externref) + (table.copy 0 2 (i32.const 0) (i32.const 0) (i32.const 2))))`), + WebAssembly.CompileError, + /(table index out of range for table.copy)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func $f (param externref) + (table.copy 2 0 (i32.const 0) (i32.const 0) (i32.const 2))))`), + WebAssembly.CompileError, + /(table index out of range for table.copy)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func $f (result i32) + (table.size 2)))`), + WebAssembly.CompileError, + /(table index out of range for table.size)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 externref) + (table $t1 2 externref) + (func $f (result i32) + (table.grow 2 (ref.null extern) (i32.const 1))))`), + WebAssembly.CompileError, + /(table index out of range for table.grow)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 funcref) + (elem func) ;; 0 + (func $f (result i32) + (table.init 2 0 (i32.const 0) (i32.const 0) (i32.const 0))))`), + WebAssembly.CompileError, + /(table index out of range for table.init)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 funcref) + (elem 2 (i32.const 0) func))`), + WebAssembly.CompileError, + /(table index out of range for element segment)|(table index out of bounds)/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 funcref) + (type $ft (func (param f64) (result i32))) + (func $f (result i32) + (call_indirect 2 (type $ft) (f64.const 3.14) (i32.const 0))))`), + WebAssembly.CompileError, + /(table index out of range for call_indirect)|(table index out of bounds)/); + +// Syntax errors when parsing text + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 funcref) + (elem func) ;; 0 + (func $f (result i32) + (table.init $t0 (i32.const 0) (i32.const 0) (i32.const 0))))`), // no segment + SyntaxError, + /failed to find name/); + +assertErrorMessage(() => wasmEvalText( + `(module + (table $t0 2 funcref) + (table $t1 2 funcref) + (func $f + (table.copy 0 (i32.const 0) (i32.const 0) (i32.const 2))))`), // target without source + SyntaxError, + /unexpected token, expected an identifier or u32/); + +// Make sure that dead code doesn't prevent compilation. +wasmEvalText( + `(module + (table (export "t") 10 externref) + (func (param i32) + (return) + (table.get (get_local 0)) + (drop) + ) + )`); + +wasmEvalText( + `(module + (table (export "t") 10 externref) + (func (param i32) (param i32) + (return) + (table.grow (get_local 1)) + (drop) + ) + )`); + +wasmEvalText( + `(module + (table (export "t") 10 externref) + (func (param i32) (param externref) + (return) + (table.set (get_local 0) (get_local 1)) + ) + )`); + +wasmEvalText( + `(module + (table (export "t") 10 externref) + (func + (return) + (table.size) + (drop) + ) + )`); + +wasmEvalText( + `(module + (table (export "t") 10 externref) + (func + (return) + (table.copy (i32.const 0) (i32.const 1)) + ) + )`); diff --git a/js/src/jit-test/tests/wasm/ref-types/tables-postbarrier-grow.js b/js/src/jit-test/tests/wasm/ref-types/tables-postbarrier-grow.js new file mode 100644 index 0000000000..20086e6579 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/tables-postbarrier-grow.js @@ -0,0 +1,21 @@ +// Add test for issue with a post-write barrier that doesn't remove +// store buffer entries when used on a table that may grow. + +let {set, table} = wasmEvalText(`(module + (table (export "table") 1 externref) + (func (export "set") (param externref) + i32.const 0 + local.get 0 + table.set + ) +)`).exports; + +let tenured = {}; +gc(tenured); +assertEq(isNurseryAllocated(tenured), false); +let nursery = {}; +assertEq(isNurseryAllocated(nursery), true); +set(nursery); +set(null); +assertEq(table.grow(1000), 1, 'table grows'); +gc(); diff --git a/js/src/jit-test/tests/wasm/ref-types/tables-stress.js b/js/src/jit-test/tests/wasm/ref-types/tables-stress.js new file mode 100644 index 0000000000..2e13e76ba0 --- /dev/null +++ b/js/src/jit-test/tests/wasm/ref-types/tables-stress.js @@ -0,0 +1,46 @@ +for ( let prefix of ['', '(table $prefix 0 32 funcref)']) { + let mod = new WebAssembly.Module(wasmTextToBinary( + `(module + (import "m" "item" (func $item (result externref))) + ${prefix} + (table $tbl 0 externref) + (func (export "run") (param $numiters i32) + (local $i i32) + (local $j i32) + (local $last i32) + (local $iters i32) + (local $tmp externref) + (local.set $last (table.grow $tbl (ref.null extern) (i32.const 1))) + (table.set $tbl (local.get $last) (call $item)) + (loop $iter_continue + (local.set $i (i32.const 0)) + (local.set $j (local.get $last)) + (block $l_break + (loop $l_continue + (br_if $l_break (i32.ge_s (local.get $j) (local.get $i))) + (local.set $tmp (table.get $tbl (local.get $i))) + (if (i32.eqz (i32.rem_s (local.get $i) (i32.const 3))) + (local.set $tmp (call $item))) + (table.set $tbl (local.get $i) (table.get $tbl (local.get $j))) + (table.set $tbl (local.get $j) (local.get $tmp)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (local.set $j (i32.sub (local.get $j) (i32.const 1))) + (br $l_continue)) + (local.set $iters (i32.add (local.get $iters) (i32.const 1))) + (br_if $iter_continue (i32.lt_s (local.get $iters) (local.get $numiters)))))))`)); + + for (let [mode,freq] of [[14,100], // Compact every 100 allocations + [2,12], // Collect every 12 allocations + [7,100], // Minor gc every 100 allocations + [15,100]]) // Verify heap integrity + { + if (this.gczeal) + this.gczeal(mode,freq); + let k = 0; + let ins = new WebAssembly.Instance(mod, {m:{item:() => { return { x: k++ } }}}).exports; + for ( let i=0; i < 1000; i++ ) + ins.run(1000); + } +} + + -- cgit v1.2.3