123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- import json
- import sys
- import struct
- import subprocess
- from dataclasses import dataclass
- from pathlib import Path
- from typing import Union, Literal, Any
- class ParseException(Exception):
- pass
- class GenerateException(Exception):
- pass
- @dataclass
- class WasmPrimitiveValue:
- kind: Literal["i32", "i64", "f32", "f64", "externref", "funcref"]
- value: str
- @dataclass
- class WasmVector:
- lanes: list[str]
- num_bits: int
- WasmValue = Union[WasmPrimitiveValue, WasmVector]
- @dataclass
- class ModuleCommand:
- line: int
- file_name: Path
- name: str | None
- @dataclass
- class Invoke:
- field: str
- args: list[WasmValue]
- module: str | None
- @dataclass
- class Get:
- field: str
- module: str | None
- Action = Union[Invoke, Get]
- @dataclass
- class Register:
- line: int
- name: str | None
- as_: str
- @dataclass
- class AssertReturn:
- line: int
- action: Action
- expected: WasmValue | None
- @dataclass
- class AssertTrap:
- line: int
- messsage: str
- action: Action
- @dataclass
- class ActionCommand:
- line: int
- action: Action
- @dataclass
- class AssertInvalid:
- line: int
- filename: str
- message: str
- Command = Union[
- ModuleCommand,
- AssertReturn,
- AssertTrap,
- ActionCommand,
- AssertInvalid,
- Register,
- ]
- @dataclass
- class ArithmeticNan:
- num_bits: int
- @dataclass
- class CanonicalNan:
- num_bits: int
- @dataclass
- class GeneratedVector:
- repr: str
- num_bits: int
- GeneratedValue = Union[str, ArithmeticNan, CanonicalNan, GeneratedVector]
- @dataclass
- class WastDescription:
- source_filename: str
- commands: list[Command]
- @dataclass
- class Context:
- current_module_name: str
- has_unclosed: bool
- def parse_value(arg: dict[str, str]) -> WasmValue:
- type_ = arg["type"]
- match type_:
- case "i32" | "i64" | "f32" | "f64" | "externref" | "funcref":
- return WasmPrimitiveValue(type_, arg["value"])
- case "v128":
- if not isinstance(arg["value"], list):
- raise ParseException("Got unknown type for Wasm value")
- num_bits = int(arg["lane_type"][1:])
- return WasmVector(arg["value"], num_bits)
- case _:
- raise ParseException(f"Unknown value type: {type_}")
- def parse_args(raw_args: list[dict[str, str]]) -> list[WasmValue]:
- return [parse_value(arg) for arg in raw_args]
- def parse_action(action: dict[str, Any]) -> Action:
- match action["type"]:
- case "invoke":
- return Invoke(
- action["field"], parse_args(action["args"]), action.get("module")
- )
- case "get":
- return Get(action["field"], action.get("module"))
- case _:
- raise ParseException(f"Action not implemented: {action['type']}")
- def parse(raw: dict[str, Any]) -> WastDescription:
- commands: list[Command] = []
- for raw_cmd in raw["commands"]:
- line = raw_cmd["line"]
- cmd: Command
- match raw_cmd["type"]:
- case "module":
- cmd = ModuleCommand(
- line, Path(raw_cmd["filename"]), raw_cmd.get("name")
- )
- case "action":
- cmd = ActionCommand(line, parse_action(raw_cmd["action"]))
- case "register":
- cmd = Register(line, raw_cmd.get("name"), raw_cmd["as"])
- case "assert_return":
- cmd = AssertReturn(
- line,
- parse_action(raw_cmd["action"]),
- parse_value(raw_cmd["expected"][0])
- if len(raw_cmd["expected"]) == 1
- else None,
- )
- case "assert_trap" | "assert_exhaustion":
- cmd = AssertTrap(line, raw_cmd["text"], parse_action(raw_cmd["action"]))
- case "assert_invalid" | "assert_malformed" | "assert_uninstantiable" | "assert_unlinkable":
- if raw_cmd.get("module_type") == "text":
- continue
- cmd = AssertInvalid(line, raw_cmd["filename"], raw_cmd["text"])
- case _:
- raise ParseException(f"Unknown command type: {raw_cmd['type']}")
- commands.append(cmd)
- return WastDescription(raw["source_filename"], commands)
- def escape(s: str) -> str:
- return s.replace('"', '\\"')
- def make_description(input_path: Path, name: str, out_path: Path) -> WastDescription:
- out_json_path = out_path / f"{name}.json"
- result = subprocess.run(
- ["wast2json", input_path, f"--output={out_json_path}", "--no-check"],
- )
- result.check_returncode()
- with open(out_json_path, "r") as f:
- description = json.load(f)
- return parse(description)
- def gen_vector(vec: WasmVector, *, array=False) -> str:
- addition = "n" if vec.num_bits == 64 else ""
- vals = ", ".join(v + addition if v.isdigit() else f'"{v}"' for v in vec.lanes)
- if not array:
- type_ = "BigUint64Array" if vec.num_bits == 64 else f"Uint{vec.num_bits}Array"
- return f"new {type_}([{vals}])"
- return f"[{vals}]"
- def gen_value_arg(value: WasmValue) -> str:
- if isinstance(value, WasmVector):
- return gen_vector(value)
- def unsigned_to_signed(uint: int, bits: int) -> int:
- max_value = 2**bits
- if uint >= 2 ** (bits - 1):
- signed_int = uint - max_value
- else:
- signed_int = uint
- return signed_int
- def int_to_float_bitcast(uint: int) -> float:
- b = struct.pack("I", uint)
- f = struct.unpack("f", b)[0]
- return f
- def int_to_float64_bitcast(uint: int) -> float:
- uint64 = uint & 0xFFFFFFFFFFFFFFFF
- b = struct.pack("Q", uint64)
- f = struct.unpack("d", b)[0]
- return f
- def float_to_str(bits: int, *, double=False) -> str:
- f = int_to_float64_bitcast(bits) if double else int_to_float_bitcast(bits)
- return str(f)
- if value.value.startswith("nan"):
- raise GenerateException("Should not get indeterminate nan value as an argument")
- if value.value == "inf":
- return "Infinity"
- if value.value == "-inf":
- return "-Infinity"
- match value.kind:
- case "i32":
- return str(unsigned_to_signed(int(value.value), 32))
- case "i64":
- return str(unsigned_to_signed(int(value.value), 64)) + "n"
- case "f32":
- return str(int(value.value)) + f" /* {float_to_str(int(value.value))} */"
- case "f64":
- return (
- str(int(value.value))
- + f"n /* {float_to_str(int(value.value), double=True)} */"
- )
- case "externref" | "funcref" | "v128":
- return value.value
- case _:
- raise GenerateException(f"Not implemented: {value.kind}")
- def gen_value_result(value: WasmValue) -> GeneratedValue:
- if isinstance(value, WasmVector):
- return GeneratedVector(gen_vector(value, array=True), value.num_bits)
- if (value.kind == "f32" or value.kind == "f64") and value.value.startswith("nan"):
- num_bits = int(value.kind[1:])
- match value.value:
- case "nan:canonical":
- return CanonicalNan(num_bits)
- case "nan:arithmetic":
- return ArithmeticNan(num_bits)
- case _:
- raise GenerateException(f"Unknown indeterminate nan: {value.value}")
- return gen_value_arg(value)
- def gen_args(args: list[WasmValue]) -> str:
- return ",".join(gen_value_arg(arg) for arg in args)
- def gen_module_command(command: ModuleCommand, ctx: Context):
- if ctx.has_unclosed:
- print("});")
- print(
- f"""describe("{command.file_name.stem}", () => {{
- let _test = test;
- let content, module;
- try {{
- content = readBinaryWasmFile("Fixtures/SpecTests/{command.file_name}");
- module = parseWebAssemblyModule(content, globalImportObject);
- }} catch (e) {{
- _test("parse", () => expect().fail(e));
- _test = test.skip;
- _test.skip = test.skip;
- }}
- """
- )
- if command.name is not None:
- print(f'namedModules["{command.name}"] = module;')
- ctx.current_module_name = command.file_name.stem
- ctx.has_unclosed = True
- def gen_invalid(invalid: AssertInvalid, ctx: Context):
-
-
-
- if invalid.message == "multiple memories":
- return
- if ctx.has_unclosed:
- print("});")
- ctx.has_unclosed = False
- stem = Path(invalid.filename).stem
- print(
- f"""
- describe("{stem}", () => {{
- let _test = test;
- _test("parse of {stem} (line {invalid.line})", () => {{
- content = readBinaryWasmFile("Fixtures/SpecTests/{invalid.filename}");
- expect(() => parseWebAssemblyModule(content, globalImportObject)).toThrow(Error, "{invalid.message}");
- }});
- }});"""
- )
- def gen_pretty_expect(expr: str, got: str, expect: str):
- print(
- f"if (!{expr}) {{ expect().fail(`Failed with ${{{got}}}, expected {expect}`); }}"
- )
- def gen_invoke(
- line: int,
- invoke: Invoke,
- result: WasmValue | None,
- ctx: Context,
- *,
- fail_msg: str | None = None,
- ):
- if not ctx.has_unclosed:
- print(f'describe("inline (line {line}))", () => {{\nlet _test = test;\n')
- module = "module"
- if invoke.module is not None:
- module = f'namedModules["{invoke.module}"]'
- utf8 = (
- str(invoke.field.encode("utf8"))[2:-1]
- .replace("\\'", "'")
- .replace("`", "${'`'}")
- )
- print(
- f"""_test(`execution of {ctx.current_module_name}: {utf8} (line {line})`, () => {{
- let _field = {module}.getExport(decodeURIComponent(escape(`{utf8}`)));
- expect(_field).not.toBeUndefined();"""
- )
- if fail_msg is not None:
- print(f'expect(() => {module}.invoke(_field)).toThrow(Error, "{fail_msg}");')
- else:
- print(f"let _result = {module}.invoke(_field, {gen_args(invoke.args)});")
- if result is not None:
- gen_result = gen_value_result(result)
- match gen_result:
- case str():
- print(f"expect(_result).toBe({gen_result});")
- case ArithmeticNan():
- gen_pretty_expect(
- f"isArithmeticNaN{gen_result.num_bits}(_result)",
- "_result",
- "nan:arithmetic",
- )
- case CanonicalNan():
- gen_pretty_expect(
- f"isCanonicalNaN{gen_result.num_bits}(_result)",
- "_result",
- "nan:canonical",
- )
- case GeneratedVector():
- if gen_result.num_bits == 64:
- array = "new BigUint64Array(_result)"
- else:
- array = f"new Uint{gen_result.num_bits}Array(_result)"
- gen_pretty_expect(
- f"testSIMDVector({gen_result.repr}, {array})",
- array,
- gen_result.repr,
- )
- print("});")
- if not ctx.has_unclosed:
- print("});")
- def gen_get(line: int, get: Get, result: WasmValue | None, ctx: Context):
- module = "module"
- if get.module is not None:
- module = f'namedModules["{get.module}"]'
- print(
- f"""_test("execution of {ctx.current_module_name}: get-{get.field} (line {line})", () => {{
- let _field = {module}.getExport("{get.field}");"""
- )
- if result is not None:
- print(f"expect(_field).toBe({gen_value_result(result)});")
- print("});")
- def gen_register(register: Register, _: Context):
- module = "module"
- if register.name is not None:
- module = f'namedModules["{register.name}"]'
- print(f'globalImportObject["{register.as_}"] = {module};')
- def gen_command(command: Command, ctx: Context):
- match command:
- case ModuleCommand():
- gen_module_command(command, ctx)
- case ActionCommand():
- if isinstance(command.action, Invoke):
- gen_invoke(command.line, command.action, None, ctx)
- else:
- raise GenerateException(
- f"Not implemented: top-level {type(command.action)}"
- )
- case AssertInvalid():
- gen_invalid(command, ctx)
- case Register():
- gen_register(command, ctx)
- case AssertReturn():
- match command.action:
- case Invoke():
- gen_invoke(command.line, command.action, command.expected, ctx)
- case Get():
- gen_get(command.line, command.action, command.expected, ctx)
- case AssertTrap():
- if not isinstance(command.action, Invoke):
- raise GenerateException(f"Not implemented: {type(command.action)}")
- gen_invoke(
- command.line, command.action, None, ctx, fail_msg=command.messsage
- )
- def generate(description: WastDescription):
- print("let globalImportObject = {};\nlet namedModules = {};\n")
- ctx = Context("", False)
- for command in description.commands:
- gen_command(command, ctx)
- if ctx.has_unclosed:
- print("});")
- def clean_up(path: Path):
- for file in path.iterdir():
- if file.suffix in ("wat", "json"):
- file.unlink()
- def main():
- input_path = Path(sys.argv[1])
- name = sys.argv[2]
- out_path = Path(sys.argv[3])
- description = make_description(input_path, name, out_path)
- generate(description)
- clean_up(out_path)
- if __name__ == "__main__":
- main()
|