summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--CMakeLists.txt6
-rw-r--r--Meta/generate-libwasm-spec-test.py232
-rw-r--r--Meta/generate-libwasm-spec-test.sh16
-rw-r--r--Tests/LibWasm/test-wasm.cpp148
5 files changed, 396 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore
index 1830b6e0e1..592cb8a8f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ sync-local.sh
.vim/
Userland/Libraries/LibWasm/Tests/Fixtures/SpecTests
+Userland/Libraries/LibWasm/Tests/Spec
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d5d3fc4e89..df038c2070 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -267,11 +267,9 @@ if(INCLUDE_WASM_SPEC_TESTS)
file(GLOB WASM_TESTS "${CMAKE_BINARY_DIR}/testsuite-master/*.wast")
foreach(PATH ${WASM_TESTS})
get_filename_component(NAME ${PATH} NAME_WLE)
- message(STATUS "Compiling WebAssembly test ${NAME}...")
+ message(STATUS "Generating test cases for WebAssembly test ${NAME}...")
execute_process(
- COMMAND wasm-as -n ${PATH} -o "${WASM_SPEC_TEST_PATH}/${NAME}.wasm"
- OUTPUT_QUIET
- ERROR_QUIET)
+ COMMAND bash ${CMAKE_SOURCE_DIR}/Meta/generate-libwasm-spec-test.sh "${PATH}" "${CMAKE_SOURCE_DIR}/Userland/Libraries/LibWasm/Tests/Spec" "${NAME}" "${WASM_SPEC_TEST_PATH}")
endforeach()
file(REMOVE testsuite-master)
endif()
diff --git a/Meta/generate-libwasm-spec-test.py b/Meta/generate-libwasm-spec-test.py
new file mode 100644
index 0000000000..dad5157131
--- /dev/null
+++ b/Meta/generate-libwasm-spec-test.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3
+
+from sys import argv, stderr
+from os import path
+from string import whitespace
+import re
+import math
+from tempfile import NamedTemporaryFile
+from subprocess import call
+import json
+
+atom_end = set('()"' + whitespace)
+
+
+def parse(sexp):
+ sexp = re.sub(r'(?m)\(;.*;\)', '', re.sub(r'(;;.*)', '', sexp))
+ stack, i, length = [[]], 0, len(sexp)
+ while i < length:
+ c = sexp[i]
+ kind = type(stack[-1])
+ if kind == list:
+ if c == '(':
+ stack.append([])
+ elif c == ')':
+ stack[-2].append(stack.pop())
+ elif c == '"':
+ stack.append('')
+ elif c in whitespace:
+ pass
+ else:
+ stack.append((c,))
+ elif kind == str:
+ if c == '"':
+ stack[-2].append(stack.pop())
+ elif c == '\\':
+ i += 1
+ stack[-1] += sexp[i]
+ else:
+ stack[-1] += c
+ elif kind == tuple:
+ if c in atom_end:
+ atom = stack.pop()
+ stack[-1].append(atom)
+ continue
+ else:
+ stack[-1] = ((stack[-1][0] + c),)
+ i += 1
+ return stack.pop()
+
+
+def parse_typed_value(ast):
+ types = {
+ 'i32.const': 'i32',
+ 'i64.const': 'i64',
+ 'f32.const': 'float',
+ 'f64.const': 'double',
+ }
+ if len(ast) == 2 and ast[0][0] in types:
+ return {"type": types[ast[0][0]], "value": ast[1][0]}
+
+ return {"type": "error"}
+
+
+def generate_module_source_for_compilation(entries):
+ s = '('
+ for entry in entries:
+ if type(entry) == tuple and len(entry) == 1 and type(entry[0]) == str:
+ s += entry[0] + ' '
+ elif type(entry) == str:
+ s += json.dumps(entry) + ' '
+ elif type(entry) == list:
+ s += generate_module_source_for_compilation(entry)
+ else:
+ raise Exception("wat? I dunno how to pretty print " + str(type(entry)))
+ while s.endswith(' '):
+ s = s[:len(s) - 1]
+ return s + ')'
+
+
+def generate(ast):
+ if type(ast) != list:
+ return []
+ tests = []
+ for entry in ast:
+ if len(entry) > 0 and entry[0] == ('module',):
+ tests.append({
+ "module": generate_module_source_for_compilation(entry),
+ "tests": []
+ })
+ elif len(entry) in [2, 3] and entry[0][0].startswith('assert_'):
+ if entry[1][0] == ('invoke',):
+ tests[-1]["tests"].append({
+ "kind": entry[0][0][len('assert_'):],
+ "function": {
+ "name": entry[1][1],
+ "args": list(parse_typed_value(x) for x in entry[1][2:])
+ },
+ "result": parse_typed_value(entry[2]) if len(entry) == 3 else None
+ })
+ else:
+ print("Ignoring unknown assertion argument", entry[1][0], file=stderr)
+ elif len(entry) >= 2 and entry[0][0] == 'invoke':
+ # toplevel invoke :shrug:
+ tests[-1]["tests"].append({
+ "kind": "ignore",
+ "function": {
+ "name": entry[1][1],
+ "args": list(parse_typed_value(x) for x in entry[1][2:])
+ },
+ "result": parse_typed_value(entry[2]) if len(entry) == 3 else None
+ })
+ else:
+ print("Ignoring unknown entry", entry, file=stderr)
+ return tests
+
+
+def genarg(spec):
+ if spec['type'] == 'error':
+ return '0'
+
+ def gen():
+ x = spec['value']
+ if x == 'nan':
+ return 'NaN'
+ if x == '-nan':
+ return '-NaN'
+
+ try:
+ x = float.fromhex(x)
+ if math.isnan(x):
+ # FIXME: This is going to mess up the different kinds of nan
+ return '-NaN' if math.copysign(1.0, x) < 0 else 'NaN'
+ if math.isinf(x):
+ return 'Infinity' if x > 0 else '-Infinity'
+ return str(x)
+ except ValueError:
+ try:
+ x = int(x, 0)
+ return str(x)
+ except ValueError:
+ return x
+
+ x = gen()
+ if x.startswith('nan'):
+ return 'NaN'
+ if x.startswith('-nan'):
+ return '-NaN'
+ return x
+
+
+all_names_in_main = {}
+
+
+def genresult(ident, entry):
+ if entry['kind'] == 'return':
+ return_check = f'expect({ident}_result).toBe({genarg(entry["result"])})' if entry["result"] is not None else ''
+ return (
+ f'let {ident}_result ='
+ f' module.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])});\n '
+ f'{return_check};\n '
+ )
+
+ if entry['kind'] == 'trap':
+ return (
+ f'expect(() => module.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])}))'
+ '.toThrow(TypeError, "Execution trapped");\n '
+ )
+
+ if entry['kind'] == 'ignore':
+ return f'module.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])});\n '
+
+ return f'throw Exception("(Test Generator) Unknown test kind {entry["kind"]}");\n '
+
+
+def gentest(entry, main_name):
+ name = entry["function"]["name"]
+ if type(name) != str:
+ print("Unsupported test case (call to", name, ")", file=stderr)
+ return '\n '
+ ident = '_' + re.sub("[^a-zA-Z_0-9]", "_", name)
+ count = all_names_in_main.get(name, 0)
+ all_names_in_main[name] = count + 1
+ test_name = f'execution of {main_name}: {name} (instance {count})'
+ source = (
+ f'test({json.dumps(test_name)}, () => {{\n'
+ f'let {ident} = module.getExport({json.dumps(name)});\n '
+ f'expect({ident}).not.toBeUndefined();\n '
+ f'{genresult(ident, entry)}'
+ '});\n\n '
+ )
+ return source
+
+
+def gen_parse_module(name):
+ return (
+ f'let content;\n '
+ f'try {{\n '
+ f'content = readBinaryWasmFile("Fixtures/SpecTests/{name}.wasm");\n '
+ f'}} catch {{ read_okay = false; }}\n '
+ f'const module = parseWebAssemblyModule(content)\n '
+ )
+
+
+def main():
+ with open(argv[1]) as f:
+ sexp = f.read()
+ name = argv[2]
+ module_output_path = argv[3]
+ ast = parse(sexp)
+ for index, description in enumerate(generate(ast)):
+ testname = f'{name}_{index}'
+ outpath = path.join(module_output_path, f'{testname}.wasm')
+ with NamedTemporaryFile("w+") as temp:
+ temp.write(description["module"])
+ temp.flush()
+ rc = call(["wasm-as", "-n", temp.name, "-o", outpath])
+ if rc != 0:
+ print("Failed to compile", name, "module index", index, "skipping that test", file=stderr)
+ continue
+
+ sep = ""
+ print(f'''{{
+let readOkay = true;
+{gen_parse_module(testname)}
+if (readOkay) {{
+{sep.join(gentest(x, testname) for x in description["tests"])}
+}}}}
+''')
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Meta/generate-libwasm-spec-test.sh b/Meta/generate-libwasm-spec-test.sh
new file mode 100644
index 0000000000..37143baec6
--- /dev/null
+++ b/Meta/generate-libwasm-spec-test.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+if [ $# -ne 4 ]; then
+ echo "Usage: $0 <input spec file> <output path> <name> <module output path>"
+ exit 1
+fi
+
+INPUT_FILE="$1"
+OUTPUT_PATH="$2"
+NAME="$3"
+MODULE_OUTPUT_PATH="$4"
+
+mkdir -p "$OUTPUT_PATH"
+mkdir -p "$MODULE_OUTPUT_PATH"
+
+python3 "$(dirname "$0")/generate-libwasm-spec-test.py" "$INPUT_FILE" "$NAME" "$MODULE_OUTPUT_PATH" | prettier --stdin-filepath "test-$NAME.js" > "$OUTPUT_PATH/$NAME.js"
diff --git a/Tests/LibWasm/test-wasm.cpp b/Tests/LibWasm/test-wasm.cpp
index c8f48a3002..ac2eea0b06 100644
--- a/Tests/LibWasm/test-wasm.cpp
+++ b/Tests/LibWasm/test-wasm.cpp
@@ -27,6 +27,36 @@ TESTJS_GLOBAL_FUNCTION(read_binary_wasm_file, readBinaryWasmFile)
return array;
}
+class WebAssemblyModule final : public JS::Object {
+ JS_OBJECT(WebAssemblyModule, JS::Object);
+
+public:
+ // FIXME: This should only contain an instantiated module, not the entire abstract machine!
+ explicit WebAssemblyModule(JS::Object& prototype)
+ : JS::Object(prototype)
+ {
+ }
+
+ static WebAssemblyModule* create(JS::GlobalObject& global_object, Wasm::Module module)
+ {
+ auto instance = global_object.heap().allocate<WebAssemblyModule>(global_object, *global_object.object_prototype());
+ instance->m_module = move(module);
+ if (auto result = instance->m_machine.instantiate(*instance->m_module, {}); result.is_error())
+ global_object.vm().throw_exception<JS::TypeError>(global_object, result.release_error().error);
+ return instance;
+ }
+ void initialize(JS::GlobalObject&) override;
+
+ ~WebAssemblyModule() override = default;
+
+private:
+ JS_DECLARE_NATIVE_FUNCTION(get_export);
+ JS_DECLARE_NATIVE_FUNCTION(wasm_invoke);
+
+ Wasm::AbstractMachine m_machine;
+ Optional<Wasm::Module> m_module;
+};
+
TESTJS_GLOBAL_FUNCTION(parse_webassembly_module, parseWebAssemblyModule)
{
auto object = vm.argument(0).to_object(global_object);
@@ -43,9 +73,12 @@ TESTJS_GLOBAL_FUNCTION(parse_webassembly_module, parseWebAssemblyModule)
vm.throw_exception<JS::SyntaxError>(global_object, Wasm::parse_error_to_string(result.error()));
return {};
}
- if (stream.handle_any_error())
- return JS::js_undefined();
- return JS::js_null();
+
+ if (stream.handle_any_error()) {
+ vm.throw_exception<JS::SyntaxError>(global_object, "Bianry stream contained errors");
+ return {};
+ }
+ return WebAssemblyModule::create(global_object, result.release_value());
}
TESTJS_GLOBAL_FUNCTION(compare_typed_arrays, compareTypedArrays)
@@ -68,3 +101,112 @@ TESTJS_GLOBAL_FUNCTION(compare_typed_arrays, compareTypedArrays)
auto& rhs_array = static_cast<JS::TypedArrayBase&>(*rhs);
return JS::Value(lhs_array.viewed_array_buffer()->buffer() == rhs_array.viewed_array_buffer()->buffer());
}
+
+void WebAssemblyModule::initialize(JS::GlobalObject& global_object)
+{
+ Base::initialize(global_object);
+ define_native_function("getExport", get_export);
+ define_native_function("invoke", wasm_invoke);
+}
+
+JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::get_export)
+{
+ auto name = vm.argument(0).to_string(global_object);
+ if (vm.exception())
+ return {};
+ auto this_value = vm.this_value(global_object);
+ auto object = this_value.to_object(global_object);
+ if (vm.exception())
+ return {};
+ if (!object || !is<WebAssemblyModule>(object)) {
+ vm.throw_exception<JS::TypeError>(global_object, "Not a WebAssemblyModule");
+ return {};
+ }
+ auto instance = static_cast<WebAssemblyModule*>(object);
+ for (auto& entry : instance->m_machine.module_instance().exports()) {
+ if (entry.name() == name) {
+ auto& value = entry.value();
+ if (auto ptr = value.get_pointer<Wasm::FunctionAddress>())
+ return JS::Value(static_cast<unsigned long>(ptr->value()));
+ vm.throw_exception<JS::TypeError>(global_object, String::formatted("'{}' does not refer to a function", name));
+ return {};
+ }
+ }
+ vm.throw_exception<JS::TypeError>(global_object, String::formatted("'{}' could not be found", name));
+ return {};
+}
+
+JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
+{
+ auto address = static_cast<unsigned long>(vm.argument(0).to_double(global_object));
+ if (vm.exception())
+ return {};
+ auto this_value = vm.this_value(global_object);
+ auto object = this_value.to_object(global_object);
+ if (vm.exception())
+ return {};
+ if (!object || !is<WebAssemblyModule>(object)) {
+ vm.throw_exception<JS::TypeError>(global_object, "Not a WebAssemblyModule");
+ return {};
+ }
+ auto instance = static_cast<WebAssemblyModule*>(object);
+ Wasm::FunctionAddress function_address { address };
+ auto function_instance = instance->m_machine.store().get(function_address);
+ if (!function_instance) {
+ vm.throw_exception<JS::TypeError>(global_object, "Invalid function address");
+ return {};
+ }
+
+ const Wasm::FunctionType* type { nullptr };
+ function_instance->visit([&](auto& value) { type = &value.type(); });
+ if (!type) {
+ vm.throw_exception<JS::TypeError>(global_object, "Invalid function found at given address");
+ return {};
+ }
+
+ Vector<Wasm::Value> arguments;
+ if (type->parameters().size() + 1 > vm.argument_count()) {
+ vm.throw_exception<JS::TypeError>(global_object, String::formatted("Expected {} arguments for call, but found {}", type->parameters().size() + 1, vm.argument_count()));
+ return {};
+ }
+ size_t index = 1;
+ for (auto& param : type->parameters()) {
+ auto value = vm.argument(index++).to_double(global_object);
+ switch (param.kind()) {
+ case Wasm::ValueType::Kind::I32:
+ arguments.append(Wasm::Value(static_cast<i32>(value)));
+ break;
+ case Wasm::ValueType::Kind::I64:
+ arguments.append(Wasm::Value(static_cast<i64>(value)));
+ break;
+ case Wasm::ValueType::Kind::F32:
+ arguments.append(Wasm::Value(static_cast<float>(value)));
+ break;
+ case Wasm::ValueType::Kind::F64:
+ arguments.append(Wasm::Value(static_cast<double>(value)));
+ break;
+ case Wasm::ValueType::Kind::FunctionReference:
+ arguments.append(Wasm::Value(Wasm::FunctionAddress { static_cast<u64>(value) }));
+ break;
+ case Wasm::ValueType::Kind::ExternReference:
+ arguments.append(Wasm::Value(Wasm::ExternAddress { static_cast<u64>(value) }));
+ break;
+ }
+ }
+
+ auto result = instance->m_machine.invoke(function_address, arguments);
+ if (result.is_trap()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Execution trapped");
+ return {};
+ }
+
+ if (result.values().is_empty())
+ return JS::js_null();
+
+ JS::Value return_value;
+ result.values().first().value().visit(
+ [&](const auto& value) { return_value = JS::Value(static_cast<double>(value)); },
+ [&](const Wasm::FunctionAddress& index) { return_value = JS::Value(static_cast<double>(index.value())); },
+ [&](const Wasm::ExternAddress& index) { return_value = JS::Value(static_cast<double>(index.value())); });
+ return return_value;
+}