From 9c5d38b7dbfc4e0cc919f0ab05235d23e499841e Mon Sep 17 00:00:00 2001 From: Ali Mohammad Pur Date: Mon, 21 Jun 2021 20:10:41 +0430 Subject: [PATCH] Meta+LibWasm: Add support for module linking tests This commit makes the linking tests in the wasm spec test run. --- Meta/generate-libwasm-spec-test.py | 185 +++++++++++++++--- Tests/LibWasm/test-wasm.cpp | 67 ++++++- .../LibWasm/Tests/Parser/test-basic-load.js | 2 +- 3 files changed, 218 insertions(+), 36 deletions(-) diff --git a/Meta/generate-libwasm-spec-test.py b/Meta/generate-libwasm-spec-test.py index 1d3399b424a..0cff818d0b3 100644 --- a/Meta/generate-libwasm-spec-test.py +++ b/Meta/generate-libwasm-spec-test.py @@ -34,6 +34,8 @@ def parse(sexp): stack[-2].append(stack.pop()) elif c == '\\': i += 1 + if sexp[i] != '"': + stack[-1] += '\\' stack[-1] += sexp[i] else: stack[-1] += c @@ -77,35 +79,107 @@ def generate_module_source_for_compilation(entries): return s + ')' +def generate_binary_source(chunks): + res = b'' + for chunk in chunks: + i = 0 + while i < len(chunk): + c = chunk[i] + if c == '\\': + res += bytes.fromhex(chunk[i + 1: i + 3]) + i += 3 + continue + res += c.encode('utf-8') + i += 1 + return res + + +named_modules = {} +named_modules_inverse = {} +registered_modules = {} + + def generate(ast): + global named_modules, named_modules_inverse, registered_modules + if type(ast) != list: return [] tests = [] for entry in ast: if len(entry) > 0 and entry[0] == ('module',): + name = None + mode = 'ast' # binary, quote + start_index = 1 + if len(entry) > 1: + if isinstance(entry[1], tuple) and isinstance(entry[1][0], str) and entry[1][0].startswith('$'): + name = entry[1][0] + if len(entry) > 2: + if isinstance(entry[2], tuple) and entry[2][0] in ('binary', 'quote'): + mode = entry[2][0] + start_index = 3 + else: + start_index = 2 + elif isinstance(entry[1][0], str): + mode = entry[1][0] + start_index = 2 + tests.append({ - "module": generate_module_source_for_compilation(entry), + "module": { + 'ast': lambda: ('parse', generate_module_source_for_compilation(entry)), + 'binary': lambda: ('literal', generate_binary_source(entry[start_index:])), + # FIXME: Make this work when we have a WAT parser + 'quote': lambda: ('literal', entry[start_index]), + }[mode](), "tests": [] }) + + if name is not None: + named_modules[name] = len(tests) - 1 + named_modules_inverse[len(tests) - 1] = (name, None) elif len(entry) in [2, 3] and entry[0][0].startswith('assert_'): if entry[1][0] == ('invoke',): + arg, name, module = 0, None, None + if isinstance(entry[1][1], str): + name = entry[1][1] + else: + name = entry[1][2] + module = named_modules[entry[1][1][0]] + arg = 1 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:]) + "module": module, + "name": name, + "args": list(parse_typed_value(x) for x in entry[1][arg + 2:]) }, - "result": parse_typed_value(entry[2]) if len(entry) == 3 else None + "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None + }) + elif entry[1][0] == ('get',): + arg, name, module = 0, None, None + if isinstance(entry[1][1], str): + name = entry[1][1] + else: + name = entry[1][2] + module = named_modules[entry[1][1][0]] + arg = 1 + tests[-1]["tests"].append({ + "kind": entry[0][0][len('assert_'):], + "get": { + "name": name, + "module": module, + }, + "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None }) else: if not len(tests): tests.append({ - "module": "", + "module": ('literal', b""), "tests": [] }) tests[-1]["tests"].append({ "kind": "testgen_fail", "function": { + "module": None, "name": "", "args": [] }, @@ -113,23 +187,41 @@ def generate(ast): }) elif len(entry) >= 2 and entry[0][0] == 'invoke': # toplevel invoke :shrug: + arg, name, module = 0, None, None + if isinstance(entry[1][1], str): + name = entry[1][1] + else: + name = entry[1][2] + module = named_modules[entry[1][1][0]] + arg = 1 tests[-1]["tests"].append({ "kind": "ignore", "function": { - "name": entry[1][1], - "args": list(parse_typed_value(x) for x in entry[1][2:]) + "module": module, + "name": name, + "args": list(parse_typed_value(x) for x in entry[1][arg + 2:]) }, - "result": parse_typed_value(entry[2]) if len(entry) == 3 else None + "result": parse_typed_value(entry[2]) if len(entry) == 3 + arg else None }) + elif len(entry) > 1 and entry[0][0] == 'register': + if len(entry) == 3: + registered_modules[entry[1]] = named_modules[entry[2][0]] + x = named_modules_inverse[named_modules[entry[2][0]]] + named_modules_inverse[named_modules[entry[2][0]]] = (x[0], entry[1]) + else: + index = len(tests) - 1 + registered_modules[entry[1]] = index + named_modules_inverse[index] = (":" + entry[1], entry[1]) else: if not len(tests): tests.append({ - "module": "", + "module": ('literal', b""), "tests": [] }) tests[-1]["tests"].append({ "kind": "testgen_fail", "function": { + "module": None, "name": "", "args": [] }, @@ -190,22 +282,33 @@ all_names_in_main = {} def genresult(ident, entry): + expectation = f'expect().fail("Unknown result structure " + {json.dumps(entry)})' + if "function" in entry: + tmodule = 'module' + if entry['function']['module'] is not None: + tmodule = f'namedModules[{json.dumps(named_modules_inverse[entry["function"]["module"]][0])}]' + expectation = ( + f'{tmodule}.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])})' + ) + elif "get" in entry: + expectation = f'module.getExport({ident})' + 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 ' + f'let {ident}_result = {expectation};\n ' + f'expect({ident}_result).toBe({genarg(entry["result"])})\n ' if entry["result"] is not None else '' ) if entry['kind'] == 'trap': return ( - f'expect(() => module.invoke({ident}, {", ".join(genarg(x) for x in entry["function"]["args"])}))' - '.toThrow(TypeError, "Execution trapped");\n ' + f'expect(() => {expectation}).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 expectation + + if entry['kind'] == 'unlinkable': + return if entry['kind'] == 'testgen_fail': return f'throw Exception("Test Generator Failure: " + {json.dumps(entry["reason"])});\n ' @@ -214,7 +317,8 @@ def genresult(ident, entry): def gentest(entry, main_name): - name = json.dumps(entry["function"]["name"])[1:-1] + isfunction = 'function' in entry + name = json.dumps((entry["function"] if isfunction else entry["get"])["name"])[1:-1] if type(name) != str: print("Unsupported test case (call to", name, ")", file=stderr) return '\n ' @@ -222,9 +326,13 @@ def gentest(entry, main_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})' + tmodule = 'module' + key = "function" if "function" in entry else "get" + if entry[key]['module'] is not None: + tmodule = f'namedModules[{json.dumps(named_modules_inverse[entry[key]["module"]][0])}]' source = ( f'test({json.dumps(test_name)}, () => {{\n' - f'let {ident} = module.getExport({json.dumps(name)});\n ' + f'let {ident} = {tmodule}.getExport({json.dumps(name)});\n ' f'expect({ident}).not.toBeUndefined();\n ' f'{genresult(ident, entry)}' '});\n\n ' @@ -232,33 +340,54 @@ def gentest(entry, main_name): return source -def gen_parse_module(name): +def gen_parse_module(name, index): + export_string = '' + if index in named_modules_inverse: + entry = named_modules_inverse[index] + export_string += f'namedModules[{json.dumps(entry[0])}] = module;\n ' + if entry[1]: + export_string += f'globalImportObject[{json.dumps(entry[1])}] = module;\n ' + return ( f'let content = readBinaryWasmFile("Fixtures/SpecTests/{name}.wasm");\n ' - f'const module = parseWebAssemblyModule(content)\n ' + f'const module = parseWebAssemblyModule(content, globalImportObject)\n ' + f'{export_string}\n ' ) +def nth(a, x, y=None): + if y: + return a[x:y] + return a[x] + + def main(): with open(argv[1]) as f: sexp = f.read() name = argv[2] module_output_path = argv[3] ast = parse(sexp) + print('let globalImportObject = {};') + print('let namedModules = {};\n') 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", "-all", temp.name, "-o", outpath]) - if rc != 0: - print("Failed to compile", name, "module index", index, "skipping that test", file=stderr) - continue + mod = description["module"] + if mod[0] == 'literal': + with open('outpath', 'wb+') as f: + f.write(mod[1]) + elif mod[0] == 'parse': + with NamedTemporaryFile("w+") as temp: + temp.write(mod[1]) + temp.flush() + rc = call(["wasm-as", "-n", "-all", temp.name, "-o", outpath]) + if rc != 0: + print("Failed to compile", name, "module index", index, "skipping that test", file=stderr) + continue sep = "" print(f'''describe({json.dumps(testname)}, () => {{ -{gen_parse_module(testname)} +{gen_parse_module(testname, index)} {sep.join(gentest(x, testname) for x in description["tests"])} }}); ''') diff --git a/Tests/LibWasm/test-wasm.cpp b/Tests/LibWasm/test-wasm.cpp index 7b6f3c6aa5c..24563898c25 100644 --- a/Tests/LibWasm/test-wasm.cpp +++ b/Tests/LibWasm/test-wasm.cpp @@ -40,14 +40,22 @@ public: Wasm::Module& module() { return *m_module; } Wasm::ModuleInstance& module_instance() { return *m_module_instance; } - static WebAssemblyModule* create(JS::GlobalObject& global_object, Wasm::Module module) + static WebAssemblyModule* create(JS::GlobalObject& global_object, Wasm::Module module, HashMap const& imports) { auto instance = global_object.heap().allocate(global_object, *global_object.object_prototype()); instance->m_module = move(module); - if (auto result = machine().instantiate(*instance->m_module, {}); result.is_error()) - global_object.vm().throw_exception(global_object, result.release_error().error); - else - instance->m_module_instance = result.release_value(); + Wasm::Linker linker(*instance->m_module); + linker.link(imports); + linker.link(spec_test_namespace()); + auto link_result = linker.finish(); + if (link_result.is_error()) { + global_object.vm().throw_exception(global_object, "Link failed"); + } else { + if (auto result = machine().instantiate(*instance->m_module, link_result.release_value()); result.is_error()) + global_object.vm().throw_exception(global_object, result.release_error().error); + else + instance->m_module_instance = result.release_value(); + } return instance; } void initialize(JS::GlobalObject&) override; @@ -58,12 +66,31 @@ private: JS_DECLARE_NATIVE_FUNCTION(get_export); JS_DECLARE_NATIVE_FUNCTION(wasm_invoke); + static HashMap const& spec_test_namespace() + { + if (!s_spec_test_namespace.is_empty()) + return s_spec_test_namespace; + Wasm::FunctionType print_i32_type { { Wasm::ValueType(Wasm::ValueType::I32) }, {} }; + + auto address = m_machine.store().allocate(Wasm::HostFunction { + [](auto&, auto&) -> Wasm::Result { + // Noop, this just needs to exist. + return Wasm::Result { Vector {} }; + }, + print_i32_type }); + s_spec_test_namespace.set({ "spectest", "print_i32", print_i32_type }, Wasm::ExternValue { *address }); + + return s_spec_test_namespace; + } + + static HashMap s_spec_test_namespace; static Wasm::AbstractMachine m_machine; Optional m_module; OwnPtr m_module_instance; }; Wasm::AbstractMachine WebAssemblyModule::m_machine; +HashMap WebAssemblyModule::s_spec_test_namespace; TESTJS_GLOBAL_FUNCTION(parse_webassembly_module, parseWebAssemblyModule) { @@ -86,7 +113,24 @@ TESTJS_GLOBAL_FUNCTION(parse_webassembly_module, parseWebAssemblyModule) vm.throw_exception(global_object, "Bianry stream contained errors"); return {}; } - return WebAssemblyModule::create(global_object, result.release_value()); + + HashMap imports; + auto import_value = vm.argument(1); + if (import_value.is_object()) { + auto& import_object = import_value.as_object(); + for (auto& property : import_object.shape().property_table()) { + auto value = import_object.get_own_property(property.key, {}, JS::AllowSideEffects::No); + if (!value.is_object() || !is(value.as_object())) + continue; + auto& module_object = static_cast(value.as_object()); + for (auto& entry : module_object.module_instance().exports()) { + // FIXME: Don't pretend that everything is a function + imports.set({ property.key.as_string(), entry.name(), Wasm::TypeIndex(0) }, entry.value()); + } + } + } + + return WebAssemblyModule::create(global_object, result.release_value(), imports); } TESTJS_GLOBAL_FUNCTION(compare_typed_arrays, compareTypedArrays) @@ -136,7 +180,16 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::get_export) auto& value = entry.value(); if (auto ptr = value.get_pointer()) return JS::Value(static_cast(ptr->value())); - vm.throw_exception(global_object, String::formatted("'{}' does not refer to a function", name)); + if (auto v = value.get_pointer()) { + return m_machine.store().get(*v)->value().value().visit( + [&](const auto& value) -> JS::Value { return JS::Value(static_cast(value)); }, + [&](const Wasm::Reference& reference) -> JS::Value { + return reference.ref().visit( + [&](const Wasm::Reference::Null&) -> JS::Value { return JS::js_null(); }, + [&](const auto& ref) -> JS::Value { return JS::Value(static_cast(ref.address.value())); }); + }); + } + vm.throw_exception(global_object, String::formatted("'{}' does not refer to a function or a global", name)); return {}; } } diff --git a/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js b/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js index 6e8dc253ae6..b2222d5b14a 100644 --- a/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js +++ b/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js @@ -26,7 +26,7 @@ test("parsing can pass", () => { 0x01, 0x05, 0x63, 0x6c, 0x61, 0x6e, 0x67, 0x06, 0x31, 0x31, 0x2e, 0x31, 0x2e, 0x30, ]); // This just checks that the function actually works - parseWebAssemblyModule(binary); + expect(() => parseWebAssemblyModule(binary)).toThrowWithMessage(TypeError, "Link failed"); }); test("parsing can fail", () => {