diff --git a/.gitignore b/.gitignore
index 1830b6e0e14..592cb8a8f0f 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 d5d3fc4e893..df038c2070b 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 00000000000..dad5157131b
--- /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 00000000000..37143baec6c
--- /dev/null
+++ b/Meta/generate-libwasm-spec-test.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+if [ $# -ne 4 ]; then
+ echo "Usage: $0