diff options
Diffstat (limited to 'js/src/jit-test/lib/wasm.js')
-rw-r--r-- | js/src/jit-test/lib/wasm.js | 602 |
1 files changed, 602 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..2b3374ebbe --- /dev/null +++ b/js/src/jit-test/lib/wasm.js @@ -0,0 +1,602 @@ +if (!wasmIsSupported()) + quit(); + +load(libdir + "asserts.js"); + +function canRunHugeMemoryTests() { + // 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 (getBuildConfiguration(b)) { + print("Failing canRunHugeMemoryTests() because '" + b + "' is true"); + return false; + } + } + if (getBuildConfiguration("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 (sub (struct))) + (type $a (sub (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()); +} + +let WasmGcObjectValues = WasmStructrefValues.concat(WasmArrayrefValues); + +// Valid values for eqref +let WasmEqrefValues = [...WasmStructrefValues, ...WasmArrayrefValues]; + +// Valid values for i31ref +let MinI31refValue = -1 * Math.pow(2, 30); +let MaxI31refValue = Math.pow(2, 30) - 1; +let WasmI31refValues = [ + // first four 31-bit signed numbers + MinI31refValue, + MinI31refValue + 1, + MinI31refValue + 2, + MinI31refValue + 3, + // five numbers around zero + -2, + -1, + 0, + 1, + 2, + // last four 31-bit signed numbers + MaxI31refValue - 3, + MaxI31refValue - 2, + MaxI31refValue - 1, + MaxI31refValue, +]; + +// Valid and invalid values for anyref +let WasmAnyrefValues = [...WasmEqrefValues, ...WasmI31refValues]; +let WasmNonAnyrefValues = [ + undefined, + true, + false, + {x:1337}, + ["abracadabra"], + 13.37, + -0, + 0x7fffffff + 0.1, + -0x7fffffff - 0.1, + 0x80000000 + 0.1, + -0x80000000 - 0.1, + 0xffffffff + 0.1, + -0xffffffff - 0.1, + Number.EPSILON, + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + Number.MIN_VALUE, + Number.MAX_VALUE, + Number.NaN, + "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,k) creates an Array of length n with values k..k+n-1 +function iota(len, k=0) { + let xs = []; + for ( let i=0 ; i < len ; i++ ) + xs.push(i+k); + 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); + } +} + +// assertEqResults([a,...],[b,...]) asserts that the two results from a wasm +// call are the same. This will compare deeply inside the result array, and +// relax a mismatch around single element arrays. +// +// This predicate is in this file because it is wasm-specific. +function assertEqResults(got, expected) { + if (!Array.isArray(got)) { + got = [got]; + } + if (!Array.isArray(expected)) { + expected = [expected]; + } + assertSame(got, expected); +} + +// TailCallIterations is selected to be large enough to trigger +// "too much recursion", but not to be slow. +var TailCallIterations = getBuildConfiguration("simulator") ? 1000 : 100000; +// TailCallBallast is selected to spill registers as parameters. +var TailCallBallast = 30; |