summaryrefslogtreecommitdiffstats
path: root/js/src/jit-test/lib/wasm.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/jit-test/lib/wasm.js')
-rw-r--r--js/src/jit-test/lib/wasm.js546
1 files changed, 546 insertions, 0 deletions
diff --git a/js/src/jit-test/lib/wasm.js b/js/src/jit-test/lib/wasm.js
new file mode 100644
index 0000000000..215cf0e421
--- /dev/null
+++ b/js/src/jit-test/lib/wasm.js
@@ -0,0 +1,546 @@
+if (!wasmIsSupported())
+ quit();
+
+load(libdir + "asserts.js");
+
+function canRunHugeMemoryTests() {
+ let conf = getBuildConfiguration();
+ // We're aiming for 64-bit desktop builds with no interesting analysis
+ // running that might inflate memory consumption unreasonably. It's OK if
+ // they're debug builds, though.
+ //
+ // The build configuration object may be extended at any time with new
+ // properties, so neither an allowlist of properties that can be true or a
+ // blocklist of properties that can't be true is great. But the latter is
+ // probably better.
+ let blocked = ['rooting-analysis','simulator',
+ 'android','wasi','asan','tsan','ubsan','dtrace','valgrind'];
+ for ( let b of blocked ) {
+ if (conf[b]) {
+ print("Failing canRunHugeMemoryTests() because '" + b + "' is true");
+ return false;
+ }
+ }
+ if (conf['pointer-byte-size'] != 8) {
+ print("Failing canRunHugeMemoryTests() because the build is not 64-bit");
+ return false;
+ }
+ return true;
+}
+
+// On 64-bit systems with explicit bounds checking, ion and baseline can handle
+// 65536 pages.
+
+var PageSizeInBytes = 65536;
+var MaxBytesIn32BitMemory = 0;
+if (largeArrayBufferSupported()) {
+ MaxBytesIn32BitMemory = 65536*PageSizeInBytes;
+} else {
+ // This is an overestimate twice: first, the max byte value is divisible by
+ // the page size; second, it must be a valid bounds checking immediate. But
+ // INT32_MAX is fine for testing.
+ MaxBytesIn32BitMemory = 0x7FFF_FFFF;
+}
+var MaxPagesIn32BitMemory = Math.floor(MaxBytesIn32BitMemory / PageSizeInBytes);
+
+function wasmEvalText(str, imports) {
+ let binary = wasmTextToBinary(str);
+ let valid = WebAssembly.validate(binary);
+
+ let m;
+ try {
+ m = new WebAssembly.Module(binary);
+ assertEq(valid, true, "failed WebAssembly.validate but still compiled successfully");
+ } catch(e) {
+ if (!e.toString().match(/out of memory/)) {
+ assertEq(valid, false, `passed WebAssembly.validate but failed to compile: ${e}`);
+ }
+ throw e;
+ }
+
+ return new WebAssembly.Instance(m, imports);
+}
+
+function wasmValidateText(str) {
+ let binary = wasmTextToBinary(str);
+ let valid = WebAssembly.validate(binary);
+ if (!valid) {
+ new WebAssembly.Module(binary);
+ throw new Error("module failed WebAssembly.validate but compiled successfully");
+ }
+ assertEq(valid, true, "wasm module was invalid");
+}
+
+function wasmFailValidateText(str, pattern) {
+ let binary = wasmTextToBinary(str);
+ assertEq(WebAssembly.validate(binary), false, "module passed WebAssembly.validate when it should not have");
+ assertErrorMessage(() => new WebAssembly.Module(binary), WebAssembly.CompileError, pattern, "module failed WebAssembly.validate but did not fail to compile as expected");
+}
+
+// Expected compilation failure can happen in a couple of ways:
+//
+// - The compiler can be available but not capable of recognizing some opcodes:
+// Compilation will start, but will fail with a CompileError. This is what
+// happens without --wasm-gc if opcodes enabled by --wasm-gc are used.
+//
+// - The compiler can be unavailable: Compilation will not start at all but will
+// throw an Error. This is what happens with "--wasm-gc --wasm-compiler=X" if
+// X does not support the features enabled by --wasm-gc.
+
+function wasmCompilationShouldFail(bin, compile_error_regex) {
+ try {
+ new WebAssembly.Module(bin);
+ } catch (e) {
+ if (e instanceof WebAssembly.CompileError) {
+ assertEq(compile_error_regex.test(e), true);
+ } else if (e instanceof Error) {
+ assertEq(/can't use wasm debug\/gc without baseline/.test(e), true);
+ } else {
+ throw new Error("Unexpected exception value:\n" + e);
+ }
+ }
+}
+
+function mismatchError(actual, expect) {
+ var str = `(type mismatch: expression has type ${actual} but expected ${expect})|` +
+ `(type mismatch: expected ${expect}, found ${actual}\)`;
+ return RegExp(str);
+}
+
+const emptyStackError = /(from empty stack)|(nothing on stack)/;
+const unusedValuesError = /(unused values not explicitly dropped by end of block)|(values remaining on stack at end of block)/;
+
+function jsify(wasmVal) {
+ if (wasmVal === 'nan')
+ return NaN;
+ if (wasmVal === 'inf')
+ return Infinity;
+ if (wasmVal === '-inf')
+ return Infinity;
+ if (wasmVal === '-0')
+ return -0;
+ return wasmVal;
+}
+
+function _augmentSrc(src, assertions) {
+ let i = 0;
+ let newSrc = src.substr(0, src.lastIndexOf(')'));
+ for (let { func, args, expected, type } of assertions) {
+ newSrc += `
+ (func (export "assert_${i++}") (result i32)
+ ${ args ? args.join('\n') : '' }
+ call ${func}`;
+
+ if (typeof expected !== 'undefined') {
+ switch (type) {
+ case 'f32':
+ newSrc += `
+ i32.reinterpret/f32
+ ${(function () {
+ if (expected == 'nan:arithmetic') {
+ expected = '0x7FC00000';
+ return '(i32.const 0x7FC00000) i32.and';
+ }
+ return '';
+ })()}
+ i32.const ${expected}
+ i32.eq`;
+ break;
+ case 'f64':
+ newSrc += `
+ i64.reinterpret/f64
+ ${(function () {
+ if (expected == 'nan:arithmetic') {
+ expected = '0x7FF8000000000000';
+ return '(i64.const 0x7FF8000000000000) i64.and';
+ }
+ return '';
+ })()}
+ i64.const ${expected}
+ i64.eq`;
+ break;
+ case 'i32':
+ newSrc += `
+ i32.const ${expected}
+ i32.eq`;
+ break;
+ case 'i64':
+ newSrc += `
+ i64.const ${expected}
+ i64.eq`;
+ break;
+ case 'v128':
+ newSrc += `
+ v128.const ${expected}
+ i8x16.eq
+ i8x16.all_true`;
+ break;
+ default:
+ throw new Error("unexpected usage of wasmAssert");
+ }
+ } else {
+ // Always true when there's no expected return value.
+ newSrc += "\ni32.const 1";
+ }
+
+ newSrc += ')\n';
+ }
+ newSrc += ')';
+ return newSrc;
+}
+
+function wasmAssert(src, assertions, maybeImports = {}, exportBox = null) {
+ let { exports } = wasmEvalText(_augmentSrc(src, assertions), maybeImports);
+ if (exportBox !== null)
+ exportBox.exports = exports;
+ for (let i = 0; i < assertions.length; i++) {
+ let { func, expected, params } = assertions[i];
+ let paramText = params ? params.join(', ') : '';
+ assertEq(exports[`assert_${i}`](), 1,
+ `Unexpected value when running ${func}(${paramText}), expecting ${expected}.`);
+ }
+}
+
+// Fully test a module:
+// - ensure it validates.
+// - ensure it compiles and produces the expected result.
+// - ensure textToBinary(binaryToText(binary)) = binary
+// Preconditions:
+// - the binary module must export a function called "run".
+function wasmFullPass(text, expected, maybeImports, ...args) {
+ let binary = wasmTextToBinary(text);
+ assertEq(WebAssembly.validate(binary), true, "Must validate.");
+
+ let module = new WebAssembly.Module(binary);
+ let instance = new WebAssembly.Instance(module, maybeImports);
+ assertEq(typeof instance.exports.run, 'function', "A 'run' function must be exported.");
+ assertEq(instance.exports.run(...args), expected, "Initial module must return the expected result.");
+}
+
+// Ditto, but expects a function named '$run' instead of exported with this name.
+function wasmFullPassI64(text, expected, maybeImports, ...args) {
+ let binary = wasmTextToBinary(text);
+ assertEq(WebAssembly.validate(binary), true, "Must validate.");
+
+ let augmentedSrc = _augmentSrc(text, [ { type: 'i64', func: '$run', args, expected } ]);
+ let augmentedBinary = wasmTextToBinary(augmentedSrc);
+
+ let module = new WebAssembly.Module(augmentedBinary);
+ let instance = new WebAssembly.Instance(module, maybeImports);
+ assertEq(instance.exports.assert_0(), 1);
+}
+
+function wasmRunWithDebugger(wast, lib, init, done) {
+ let g = newGlobal({newCompartment: true});
+ let dbg = new Debugger(g);
+
+ g.eval(`
+var wasm = wasmTextToBinary(\`${wast}\`);
+var lib = ${lib || 'undefined'};
+var m = new WebAssembly.Instance(new WebAssembly.Module(wasm), lib);`);
+
+ var wasmScript = dbg.findScripts().filter(s => s.format == 'wasm')[0];
+
+ init({dbg, wasmScript, g,});
+ let result = undefined, error = undefined;
+ try {
+ result = g.eval("m.exports.test()");
+ } catch (ex) {
+ error = ex;
+ }
+ done({dbg, result, error, wasmScript, g,});
+}
+
+const WasmHelpers = {};
+
+(function() {
+ let enabled = false;
+ try {
+ enableSingleStepProfiling();
+ disableSingleStepProfiling();
+ enabled = true;
+ } catch (e) {}
+ WasmHelpers.isSingleStepProfilingEnabled = enabled;
+})();
+
+// The cache of matched and unmatched strings seriously speeds up matching on
+// the emulators and makes tests time out less often.
+
+var matched = {};
+var unmatched = {};
+
+WasmHelpers._normalizeStack = (stack, preciseStacks) => {
+ var wasmFrameTypes = [
+ {re:/^jit call to int64(?: or v128)? wasm function$/, sub:"i64>"},
+ {re:/^out-of-line coercion for jit entry arguments \(in wasm\)$/, sub:"ool>"},
+ {re:/^wasm-function\[(\d+)\] \(.*\)$/, sub:"$1"},
+ {re:/^(fast|slow) exit trampoline (?:to native )?\(in wasm\)$/, sub:"<"},
+ {re:/^call to(?: asm.js)? native (.*) \(in wasm\)$/, sub:"$1"},
+ {re:/ \(in wasm\)$/, sub:""}
+ ];
+
+ let entryRegexps;
+ if (preciseStacks) {
+ entryRegexps = [
+ {re:/^slow entry trampoline \(in wasm\)$/, sub:"!>"},
+ {re:/^fast entry trampoline \(in wasm\)$/, sub:">"},
+ ];
+ } else {
+ entryRegexps = [
+ {re:/^(fast|slow) entry trampoline \(in wasm\)$/, sub:">"}
+ ];
+ }
+ wasmFrameTypes = entryRegexps.concat(wasmFrameTypes);
+
+ var framesIn = stack.split(',');
+ var framesOut = [];
+ outer:
+ for (let frame of framesIn) {
+ if (unmatched[frame])
+ continue;
+ let probe = matched[frame];
+ if (probe !== undefined) {
+ framesOut.push(probe);
+ continue;
+ }
+ for (let {re, sub} of wasmFrameTypes) {
+ if (re.test(frame)) {
+ let repr = frame.replace(re, sub);
+ framesOut.push(repr);
+ matched[frame] = repr;
+ continue outer;
+ }
+ }
+ unmatched[frame] = true;
+ }
+
+ return framesOut.join(',');
+};
+
+WasmHelpers._removeAdjacentDuplicates = array => {
+ if (array.length < 2)
+ return;
+ let i = 0;
+ for (let j = 1; j < array.length; j++) {
+ if (array[i] !== array[j])
+ array[++i] = array[j];
+ }
+ array.length = i + 1;
+}
+
+WasmHelpers.normalizeStacks = (stacks, preciseStacks = false) => {
+ let observed = [];
+ for (let i = 0; i < stacks.length; i++)
+ observed[i] = WasmHelpers._normalizeStack(stacks[i], preciseStacks);
+ WasmHelpers._removeAdjacentDuplicates(observed);
+ return observed;
+};
+
+WasmHelpers._compareStacks = (got, expect) => {
+ if (got.length != expect.length) {
+ return false;
+ }
+ for (let i = 0; i < got.length; i++) {
+ if (got[i] !== expect[i])
+ return false;
+ }
+ return true;
+}
+
+WasmHelpers.assertEqImpreciseStacks = (got, expect) => {
+ let observed = WasmHelpers.normalizeStacks(got, /* precise */ false);
+ let same = WasmHelpers._compareStacks(observed, expect);
+ if (!same) {
+ if (observed.length != expect.length) {
+ print(`Got:\n${observed.toSource()}\nExpect:\n${expect.toSource()}`);
+ assertEq(observed.length, expect.length);
+ }
+ for (let i = 0; i < observed.length; i++) {
+ if (observed[i] !== expect[i]) {
+ print(`On stack ${i}, Got:\n${observed[i]}\nExpect:\n${expect[i]}`);
+ assertEq(observed[i], expect[i]);
+ }
+ }
+ }
+}
+
+WasmHelpers.extractStackFrameFunction = (frameString) => {
+ var [_, name, filename, line, column] = frameString.match(/^(.*)@(.*):(.*):(.*)$/);
+ if (name)
+ return name;
+ if (/wasm-function/.test(line))
+ return line;
+ return "";
+};
+
+WasmHelpers.assertStackTrace = (exception, expected) => {
+ let callsites = exception.stack.trim().split('\n').map(WasmHelpers.extractStackFrameFunction);
+ assertEq(callsites.length, expected.length);
+ for (let i = 0; i < callsites.length; i++) {
+ assertEq(callsites[i], expected[i]);
+ }
+};
+
+WasmHelpers.nextLineNumber = (n=1) => {
+ return +(new Error().stack).split('\n')[1].split(':')[1] + n;
+}
+
+WasmHelpers.startProfiling = () => {
+ if (!WasmHelpers.isSingleStepProfilingEnabled)
+ return;
+ enableSingleStepProfiling();
+}
+
+WasmHelpers.endProfiling = () => {
+ if (!WasmHelpers.isSingleStepProfilingEnabled)
+ return;
+ return disableSingleStepProfiling();
+}
+
+WasmHelpers.assertEqPreciseStacks = (observed, expectedStacks) => {
+ if (!WasmHelpers.isSingleStepProfilingEnabled)
+ return null;
+
+ observed = WasmHelpers.normalizeStacks(observed, /* precise */ true);
+
+ for (let i = 0; i < expectedStacks.length; i++) {
+ if (WasmHelpers._compareStacks(observed, expectedStacks[i]))
+ return i;
+ }
+
+ throw new Error(`no plausible stacks found, observed: ${observed.join('/')}
+Expected one of:
+${expectedStacks.map(stacks => stacks.join("/")).join('\n')}`);
+}
+
+function fuzzingSafe() {
+ return typeof getErrorNotes == 'undefined';
+}
+
+// Common instantiations of wasm values for dynamic type check testing
+
+// Valid values for funcref
+let WasmFuncrefValues = [
+ wasmEvalText(`(module (func (export "")))`).exports[''],
+];
+
+// Valid values for structref/arrayref
+let WasmStructrefValues = [];
+let WasmArrayrefValues = [];
+if (wasmGcEnabled()) {
+ let { newStruct, newArray } = wasmEvalText(`
+ (module
+ (type $s (struct))
+ (type $a (array i32))
+ (func (export "newStruct") (result anyref)
+ struct.new $s)
+ (func (export "newArray") (result anyref)
+ i32.const 0
+ i32.const 0
+ array.new $a)
+ )`).exports;
+ WasmStructrefValues.push(newStruct());
+ WasmArrayrefValues.push(newArray());
+}
+
+// Valid values for eqref
+let WasmEqrefValues = [...WasmStructrefValues, ...WasmArrayrefValues];
+
+// Valid and invalid values for anyref
+let WasmAnyrefValues = [...WasmEqrefValues];
+let WasmNonAnyrefValues = [
+ undefined,
+ true,
+ false,
+ {x:1337},
+ ["abracadabra"],
+ 1337,
+ 13.37,
+ "hi",
+ 37n,
+ new Number(42),
+ new Boolean(true),
+ Symbol("status"),
+ () => 1337,
+ ...WasmFuncrefValues,
+];
+
+// Valid externref values
+let WasmNonNullExternrefValues = [
+ ...WasmNonAnyrefValues,
+ ...WasmAnyrefValues
+];
+let WasmExternrefValues = [null, ...WasmNonNullExternrefValues];
+
+// Common array utilities
+
+// iota(n) creates an Array of length n with values 0..n-1
+function iota(len) {
+ let xs = [];
+ for ( let i=0 ; i < len ; i++ )
+ xs.push(i);
+ return xs;
+}
+
+// cross(A) where A is an array of length n creates an Array length n*n of
+// two-element Arrays representing all pairs of elements of A.
+function cross(xs) {
+ let results = [];
+ for ( let x of xs )
+ for ( let y of xs )
+ results.push([x,y]);
+ return results;
+}
+
+// Remove all values equal to v from an array xs, comparing equal for NaN.
+function remove(v, xs) {
+ let result = [];
+ for ( let w of xs ) {
+ if (v === w || isNaN(v) && isNaN(w))
+ continue;
+ result.push(w);
+ }
+ return result;
+}
+
+// permute(A) where A is an Array returns an Array of Arrays, each inner Array a
+// distinct permutation of the elements of A. A is assumed not to have any
+// elements that are pairwise equal in the sense of remove().
+function permute(xs) {
+ if (xs.length == 1)
+ return [xs];
+ let results = [];
+ for (let v of xs)
+ for (let tail of permute(remove(v, xs)))
+ results.push([v, ...tail]);
+ return results;
+}
+
+// interleave([a,b,c,...],[0,1,2,...]) => [a,0,b,1,c,2,...]
+function interleave(xs, ys) {
+ assertEq(xs.length, ys.length);
+ let res = [];
+ for ( let i=0 ; i < xs.length; i++ ) {
+ res.push(xs[i]);
+ res.push(ys[i]);
+ }
+ return res;
+}
+
+// assertSame([a,...],[b,...]) asserts that the two arrays have the same length
+// and that they element-wise assertEq IGNORING Number/BigInt differences. This
+// predicate is in this file because it is wasm-specific.
+function assertSame(got, expected) {
+ assertEq(got.length, expected.length);
+ for ( let i=0; i < got.length; i++ ) {
+ let g = got[i];
+ let e = expected[i];
+ if (typeof g != typeof e) {
+ if (typeof g == "bigint")
+ e = BigInt(e);
+ else if (typeof e == "bigint")
+ g = BigInt(g);
+ }
+ assertEq(g, e);
+ }
+}