blob: 76657bb1043d9e61af6522dcfb87264748469be9 [file] [log] [blame]
//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
// This file has been modified by Microsoft on [12/2016].
/*
* Copyright 2016 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const cliArgs = WScript.Arguments || [];
WScript.Flag("-wasmI64");
if (cliArgs.length < 1) {
print("usage: <exe> spec.js -args <filename.json> [start index] [end index] [-verbose] [-nt] -endargs");
WScript.Quit(0);
}
if (typeof IMPORTS_FROM_OTHER_SCRIPT === "undefined") {
IMPORTS_FROM_OTHER_SCRIPT = {};
}
let passed = 0;
let failed = 0;
// Parse arguments
const iVerbose = cliArgs.indexOf("-verbose");
const verbose = iVerbose !== -1;
if (verbose) {
cliArgs.splice(iVerbose, 1);
}
const iNoTrap = cliArgs.indexOf("-nt");
const noTrap = iNoTrap !== -1;
const trap = !noTrap;
if (noTrap) {
cliArgs.splice(iNoTrap, 1);
}
let file = "";
let iTest = 0;
function getValueStr(value, type) {
if (typeof value === "undefined") {
return "undefined";
}
switch (type) {
case "i64":
if (typeof value === "object") {
const {high, low} = value;
const convert = a => (a >>> 0).toString(16);
return `0x${convert(high)}${convert(low).padStart(8, "0")}`;
}
// Fallthrough
case "i32": return value.toString();
case "f64":
// Fallthrough
case "f32":
if (Object.is(value, -0)) {
return "-0";
}
// Fallthrough
}
// Default case
return value.toString();
}
function getArgsStr(args) {
return args
.map(({type, value}) => `${type}:${getValueStr(mapWasmArg({type, value}), type)}`)
.join(", ");
}
function getActionStr(action) {
const moduleName = action.module || "$$";
switch (action.type) {
case "invoke": return `${moduleName}.${action.field}(${getArgsStr(action.args)})`;
case "get": return `${moduleName}[${action.field}]`;
default: return "Unkown action type";
}
}
function getCommandStr(command) {
const base = `(${iTest}) ${file}:${command.line}`;
switch (command.type) {
case "module": return `${base}: generate module ${command.name ? ` as ${command.name}` : ""}`;
case "register": return `${base}: register module ${command.name || "$$"} as ${command.as}`;
case "assert_malformed":
case "assert_unlinkable":
case "assert_uninstantiable":
case "assert_invalid": return `${base}: ${command.type} module`;
case "assert_return": return `${base}: assert_return(${getActionStr(command.action)} == ${getArgsStr(command.expected)})`;
case "assert_return_canonical_nan": return `${base}: assert_return_canonical_nan(${getActionStr(command.action)}`;
case "assert_return_arithmetic_nan": return `${base}: assert_return_arithmetic_nan(${getActionStr(command.action)})`;
case "action":
case "assert_trap":
case "assert_return_nan":
case "assert_exhaustion":
return `${base}: ${command.type}(${getActionStr(command.action)})`;
}
return base;
}
const CompareType = {
none: Symbol(),
exact: Symbol(),
arithmeticNan: Symbol(),
canonicalNan: Symbol()
};
function run(inPath, iStart, iEnd) {
const lastSlash = Math.max(inPath.lastIndexOf("/"), inPath.lastIndexOf("\\"));
const inDir = lastSlash === -1 ? "." : inPath.slice(0, lastSlash);
file = inPath;
const data = read(inPath);
const {commands} = WebAssembly.wabt.convertWast2Wasm(data, {spec: true});
const registry = Object.assign({
"not wasm": {
overload() {}
},
spectest: {
print,
print_i32: console.log.bind(console),
print_i32_f32: console.log.bind(console),
print_f64_f64: console.log.bind(console),
print_f32: console.log.bind(console),
print_f64: console.log.bind(console),
global_i32: 666,
global_f32: 666,
global_f64: 666,
table: new WebAssembly.Table({initial: 10, maximum: 20, element: "anyfunc"}),
memory: new WebAssembly.Memory({initial: 1, maximum: 2})
}
}, IMPORTS_FROM_OTHER_SCRIPT);
const moduleRegistry = {};
moduleRegistry.currentModule = null;
for (const command of commands) {
++iTest;
if (iTest < iStart) {
// always run module/register commands that happens before iStart
if (command.type !== "module" && command.type !== "register") {
continue;
}
} else if (iTest > iEnd) {
continue;
}
if (verbose) {
print(`Running:${getCommandStr(command)}`);
}
switch (command.type) {
case "module":
moduleCommand(inDir, command, registry, moduleRegistry);
break;
case "assert_malformed":
assertMalformed(inDir, command);
break;
case "assert_unlinkable":
assertUnlinkable(inDir, command, registry);
break;
case "assert_uninstantiable":
assertUninstantiable(inDir, command, registry);
break;
case "assert_invalid":
assertMalformed(inDir, command, registry);
break;
case "register": {
const {as, name = "currentModule"} = command;
if (moduleRegistry[name]) {
registry[as] = moduleRegistry[name].exports;
}
break;
}
case "action":
runSimpleAction(moduleRegistry, command);
break;
case "assert_return":
assertReturn(moduleRegistry, command, CompareType.exact);
break;
case "assert_return_canonical_nan":
assertReturn(moduleRegistry, command, CompareType.canonicalNan);
break;
case "assert_return_arithmetic_nan":
assertReturn(moduleRegistry, command, CompareType.arithmeticNan);
break;
case "assert_exhaustion":
assertStackExhaustion(moduleRegistry, command);
break;
case "assert_trap":
if (trap) {
assertTrap(moduleRegistry, command);
} else {
print(`${getCommandStr(command)} skipped because it has traps`);
}
break;
default:
print(`Unknown command: ${JSON.stringify(command)}`);
}
}
end();
}
function createModule(baseDir, buffer, registry, output) {
if (verbose) {
const u8a = new Uint8Array(buffer);
console.log(u8a);
}
output.module = new WebAssembly.Module(buffer);
// We'll know if an error occurs at instanciation because output.module will be set
output.instance = new WebAssembly.Instance(output.module, registry || undefined);
}
function moduleCommand(baseDir, command, registry, moduleRegistry) {
const {buffer, name} = command;
try {
const output = {};
createModule(baseDir, buffer, registry, output);
if (name) {
moduleRegistry[name] = output.instance;
}
moduleRegistry.currentModule = output.instance;
++passed;
} catch (e) {
++failed;
print(`${getCommandStr(command)} failed. Unexpected Error: ${e}`);
}
return null;
}
function assertMalformed(baseDir, command) {
const {buffer, text} = command;
// Test hook to prevent deferred parsing
WScript.Flag("-off:wasmdeferred");
const output = {};
try {
createModule(baseDir, buffer, null, output);
++failed;
print(`${getCommandStr(command)} failed. Should have had an error`);
} catch (e) {
if (output.module) {
++failed;
print(`${getCommandStr(command)} failed. Had a linking error, expected a compile error: ${e}`);
} else if (e instanceof WebAssembly.CompileError) {
++passed;
if (verbose) {
print(`${getCommandStr(command)} passed. Had compile error: ${e}`);
print(` Spec expected error: ${text}`);
}
} else {
++failed;
print(`${getCommandStr(command)} failed. Expected a compile error: ${e}`);
}
} finally {
// Reset the test hook
WScript.Flag("-on:wasmdeferred");
}
}
function assertUnlinkable(baseDir, command, registry) {
const {buffer, text} = command;
// Test hook to prevent deferred parsing
WScript.Flag("-off:wasmdeferred");
const output = {};
try {
createModule(baseDir, buffer, registry, output);
++failed;
print(`${getCommandStr(command)} failed. Should have had an error`);
} catch (e) {
if (e instanceof WebAssembly.LinkError) {
++passed;
if (verbose) {
print(`${getCommandStr(command)} passed. Had linking error: ${e}`);
print(` Spec expected error: ${text}`);
}
} else {
++failed;
print(`${getCommandStr(command)} failed. Expected a linking error: ${e}`);
}
} finally {
// Reset the test hook
WScript.Flag("-on:wasmdeferred");
}
}
function assertUninstantiable(baseDir, command, registry) {
const {buffer, text} = command;
// Test hook to prevent deferred parsing
WScript.Flag("-off:wasmdeferred");
const output = {};
try {
createModule(baseDir, buffer, registry, output);
++failed;
print(`${getCommandStr(command)} failed. Should have had an error`);
} catch (e) {
if (e instanceof WebAssembly.RuntimeError) {
++passed;
if (verbose) {
print(`${getCommandStr(command)} passed. Had instanciation error: ${e}`);
print(` Spec expected error: ${text}`);
}
} else {
++failed;
print(`${getCommandStr(command)} failed. Expected a instanciation error: ${e}`);
}
} finally {
// Reset the test hook
WScript.Flag("-on:wasmdeferred");
}
}
function genConverters() {
const buffer = WebAssembly.wabt.convertWast2Wasm(`
(module
(func (export "convertI64") (param i64) (result i64) (get_local 0))
(func (export "toF32") (param i32) (result f32) (f32.reinterpret/i32 (get_local 0)))
(func (export "toF64") (param i64) (result f64) (f64.reinterpret/i64 (get_local 0)))
)`);
const module = new WebAssembly.Module(buffer);
const instance = new WebAssembly.Instance(module);
return instance.exports;
}
const {toF32, toF64, convertI64} = genConverters();
function mapWasmArg({type, value}) {
switch (type) {
case "i32": return parseInt(value)|0;
case "i64": return convertI64(value); // will convert string to {high, low}
case "f32": return toF32(parseInt(value));
case "f64": return toF64(value);
}
throw new Error("Unknown argument type");
}
function runCompare(wasmFn, action, expected, compareType) {
let moduleTxt;
const argTypes = action.args.map(({type}) => type);
const params = argTypes.length > 0 ? `(param ${argTypes.join(" ")})` : "";
let returnType = "void";
if (expected.length === 0) {
if (compareType !== CompareType.exact) {
throw new Error("Must have expected type to runCompare if not doing Exact comparison");
}
// Without expected result, we are just making sure we are able to call the function
moduleTxt = `
(module
(import "test" "fn" (func $fn ${params}))
(func (export "res"))
(func (export "compare") (result i32)
${action.args.map(({type, value}) => {
switch (type) {
case "i32": return `(i32.const ${value})`;
case "i64": return `(i64.const ${value})`;
case "f32": return `(f32.reinterpret/i32 (i32.const ${value}))`;
case "f64": return `(f64.reinterpret/i64 (i64.const ${value}))`;
}
throw new Error("Unknown argument type");
}).join("\n")}
(call $fn)
(i32.const 1)
)
)`;
} else {
let compareTxt = "";
const {type: expectedType, value: expectedValue} = expected[0];
returnType = expectedType;
const resultSize = expectedType.endsWith("32") ? 32 : 64;
const matchingIntType = resultSize === 32 ? "i32" : "i64";
switch (compareType) {
case CompareType.exact:
switch (expectedType) {
case "i32":
case "i64":
compareTxt += `(${expectedType}.eq (${expectedType}.const ${expectedValue}))`;
break;
case "f32":
compareTxt += `(i32.reinterpret/f32) (i32.eq (i32.const ${expectedValue}))`;
break;
case "f64":
compareTxt += `(i64.reinterpret/f64) (i64.eq (i64.const ${expectedValue}))`;
break;
}
break;
case CompareType.arithmeticNan: {
const expectedResult = resultSize === 32 ? "0x7f800000" : "0x7ff0000000000000";
compareTxt = `
(${matchingIntType}.reinterpret/${expectedType})
(${matchingIntType}.and (${matchingIntType}.const ${expectedResult}))
(${matchingIntType}.eq (${matchingIntType}.const ${expectedResult}))`;
break;
}
case CompareType.canonicalNan: {
const posNaN = resultSize === 32 ? "0x7fc00000" : "0x7ff8000000000000";
const negNaN = resultSize === 32 ? "0xffc00000" : "0xfff8000000000000";
compareTxt = `
(${matchingIntType}.reinterpret/${expectedType})
(tee_local $intNan)
(${matchingIntType}.eq (${matchingIntType}.const ${posNaN}))
(get_local $intNan)
(${matchingIntType}.eq (${matchingIntType}.const ${negNaN}))
(i32.or)`;
break;
}
default:
throw new Error("runCompare: Unsupported compare type");
}
moduleTxt = `
(module
(import "test" "fn" (func $fn ${params} (result ${expectedType})))
(global $res (mut ${expectedType}) (${expectedType}.const 0))
(func (export "res") (result ${expectedType}) (get_global $res))
(func (export "compare") (result i32)
(local $localRes ${expectedType}) (local $intNan ${matchingIntType})
${action.args.map(({type, value}) => {
switch (type) {
case "i32": return `(i32.const ${value})`;
case "i64": return `(i64.const ${value})`;
case "f32": return `(f32.reinterpret/i32 (i32.const ${value}))`;
case "f64": return `(f64.reinterpret/i64 (i64.const ${value}))`;
}
throw new Error("Unknown argument type");
}).join("\n")}
(call $fn)
(tee_local $localRes)
(set_global $res)
(get_local $localRes)
${compareTxt}
)
)`;
}
if (verbose) {
console.log(moduleTxt);
}
const module = new WebAssembly.Module(WebAssembly.wabt.convertWast2Wasm(moduleTxt));
const {exports: {compare, res}} = new WebAssembly.Instance(module, {test: {fn: wasmFn}});
const hasPassed = compare() === 1;
const original = res();
return {passed: hasPassed, original: {value: original, type: returnType}};
}
function assertReturn(moduleRegistry, command, compareType) {
const {action, expected} = command;
try {
let success = false;
let originalResult;
if (action.type === "invoke") {
const wasmFn = getField(moduleRegistry, action);
const res = runCompare(wasmFn, action, expected, compareType);
originalResult = res.original;
success = res.passed;
} else if (action.type === "get") {
const field = getField(moduleRegistry, action);
originalResult = {value: field, type: expected[0].type};
switch (compareType) {
case CompareType.exact: success = field === mapWasmArg(expected[0]); break;
case CompareType.arithmeticNan:
case CompareType.canonicalNan: success = isNaN(field); break;
default:
throw new Error("assertReturn: Unsupported compare type");
}
}
if (success) {
passed++;
if (verbose) {
print(`${getCommandStr(command)} passed.`);
}
} else {
print(`${getCommandStr(command)} failed. Returned ${getValueStr(originalResult.value, originalResult.type)}`);
failed++;
}
} catch (e) {
print(`${getCommandStr(command)} failed. Unexpectedly threw: ${e}`);
failed++;
}
}
function assertTrap(moduleRegistry, command) {
const {action, text} = command;
try {
runAction(moduleRegistry, action);
print(`${getCommandStr(command)} failed. Should have had an error`);
failed++;
} catch (e) {
if (e instanceof WebAssembly.RuntimeError) {
passed++;
if (verbose) {
print(`${getCommandStr(command)} passed. Error thrown: ${e}`);
print(` Spec error message expected: ${text}`);
}
} else {
failed++;
print(`${getCommandStr(command)} failed. Unexpected error thrown: ${e}`);
}
}
}
let StackOverflow;
function assertStackExhaustion(moduleRegistry, command) {
if (!StackOverflow) {
// Get the stack overflow exception type
// Javascript and WebAssembly must throw the same type of error
try { (function f() { 1 + f() })() } catch (e) { StackOverflow = e.constructor }
}
const {action} = command;
try {
runAction(moduleRegistry, action);
print(`${getCommandStr(command)} failed. Should have exhausted the stack`);
failed++;
} catch (e) {
if (e instanceof StackOverflow) {
passed++;
if (verbose) {
print(`${getCommandStr(command)} passed. Had a StackOverflow`);
}
} else {
failed++;
print(`${getCommandStr(command)} failed. Unexpected error thrown: ${e}`);
}
}
}
function runSimpleAction(moduleRegistry, command) {
try {
const res = runAction(moduleRegistry, command.action);
++passed;
if (verbose) {
print(`${getCommandStr(command)} = ${res}`);
}
} catch (e) {
++failed;
print(`${getCommandStr(command)} failed. Unexpectedly threw: ${e}`);
}
}
function getField(moduleRegistry, action) {
const moduleName = action.module !== undefined ? action.module : "currentModule";
const m = moduleRegistry[moduleName];
if (!m) {
throw new Error("Module unavailable to run action");
}
const {field} = action;
if (!(field in m.exports)) {
throw new Error(`Unable to find field ${field} in ${moduleName}`)
}
return m.exports[field];
}
function runAction(moduleRegistry, action) {
const field = getField(moduleRegistry, action);
switch (action.type) {
case "invoke": {
const {args} = action;
const mappedArgs = args.map(({value}) => value);
const wasmFn = field;
const res = wasmFn(...mappedArgs);
return res;
}
case "get": {
return field;
}
default:
print(`Unknown action: ${JSON.stringify(action)}`);
}
}
function end() {
print(`${passed}/${passed + failed} tests passed.`);
}
run(cliArgs[0], cliArgs[1] | 0, cliArgs[2] === undefined ? undefined : cliArgs[2] | 0);