From 906fa0482236c050e8386f1e1969edf531e5e257 Mon Sep 17 00:00:00 2001 From: Diego <96022404+dzfrias@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:15:12 -0700 Subject: [PATCH] LibWasm: Properly check for indeterminate `NaN`s in SIMD tests Because `nan:arithmetic` and `nan:canonical` aren't bound to a single bit pattern, we cannot check against a float-containing SIMD vector against a single value in the tests. Now, we represent `v128`s as `TypedArray`s in `testjs` (as opposed to using `BigInt`s), allowing us to properly check `NaN` bit patterns. --- Meta/generate-libwasm-spec-test.py | 93 ++++++++++++++++++++---------- Tests/LibWasm/test-wasm.cpp | 84 +++++++++++++++++++++------ 2 files changed, 127 insertions(+), 50 deletions(-) diff --git a/Meta/generate-libwasm-spec-test.py b/Meta/generate-libwasm-spec-test.py index daea99929cb..0542f9ff0de 100644 --- a/Meta/generate-libwasm-spec-test.py +++ b/Meta/generate-libwasm-spec-test.py @@ -16,11 +16,20 @@ class GenerateException(Exception): @dataclass -class WasmValue: - kind: Literal["i32", "i64", "f32", "f64", "externref", "funcref", "v128"] +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 @@ -98,7 +107,13 @@ class CanonicalNan: num_bits: int -GeneratedValue = Union[str, ArithmeticNan, CanonicalNan] +@dataclass +class GeneratedVector: + repr: str + num_bits: int + + +GeneratedValue = Union[str, ArithmeticNan, CanonicalNan, GeneratedVector] @dataclass @@ -117,33 +132,14 @@ def parse_value(arg: dict[str, str]) -> WasmValue: type_ = arg["type"] match type_: case "i32" | "i64" | "f32" | "f64" | "externref" | "funcref": - payload = arg["value"] + return WasmPrimitiveValue(type_, arg["value"]) case "v128": - - def reverse_endianness(hex_str): - if len(hex_str) % 2 != 0: - hex_str = "0" + hex_str - bytes_list = [hex_str[i:i + 2] for i in range(0, len(hex_str), 2)] - reversed_hex_str = "".join(bytes_list[::-1]) - return reversed_hex_str - - size = int(arg["lane_type"][1:]) // 4 - parts = [] - for raw_val in arg["value"]: - match raw_val: - case "nan:canonical": - hex_repr = "7fc".ljust(size, "0") - case "nan:arithmetic": - hex_repr = "7ff".ljust(size, "0") - case "nan:signaling": - hex_repr = "7ff8".ljust(size, "0") - case _: - hex_repr = hex(int(raw_val))[2:].zfill(size) - parts.append(hex_repr) - payload = "0x" + reverse_endianness("".join(reversed(parts))) + "n" + 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_}") - return WasmValue(type_, payload) def parse_args(raw_args: list[dict[str, str]]) -> list[WasmValue]: @@ -212,7 +208,19 @@ def make_description(input_path: Path, name: str, out_path: Path) -> WastDescrip 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): @@ -263,6 +271,9 @@ def gen_value_arg(value: WasmValue) -> str: 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: @@ -319,6 +330,12 @@ expect(() => parseWebAssemblyModule(content, globalImportObject)).toThrow(Error, ) +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, @@ -352,12 +369,26 @@ expect(_field).not.toBeUndefined();""" case str(): print(f"expect(_result).toBe({gen_result});") case ArithmeticNan(): - print( - f"expect(isArithmeticNaN{gen_result.num_bits}(_result)).toBe(true);" + gen_pretty_expect( + f"isArithmeticNaN{gen_result.num_bits}(_result)", + "_result", + "nan:arithmetic", ) case CanonicalNan(): - print( - f"expect(isCanonicalNaN{gen_result.num_bits}(_result)).toBe(true);" + 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: diff --git a/Tests/LibWasm/test-wasm.cpp b/Tests/LibWasm/test-wasm.cpp index d0e6b21e6ae..cfb4b5736ad 100644 --- a/Tests/LibWasm/test-wasm.cpp +++ b/Tests/LibWasm/test-wasm.cpp @@ -199,16 +199,26 @@ TESTJS_GLOBAL_FUNCTION(compare_typed_arrays, compareTypedArrays) return JS::Value(lhs_array.viewed_array_buffer()->buffer() == rhs_array.viewed_array_buffer()->buffer()); } +bool _is_canonical_nan32(u32 value) +{ + return value == 0x7FC00000 || value == 0xFFC00000; +} + +bool _is_canonical_nan64(u64 value) +{ + return value == 0x7FF8000000000000 || value == 0xFFF8000000000000; +} + TESTJS_GLOBAL_FUNCTION(is_canonical_nan32, isCanonicalNaN32) { auto value = TRY(vm.argument(0).to_u32(vm)); - return value == 0x7FC00000 || value == 0xFFC00000; + return _is_canonical_nan32(value); } TESTJS_GLOBAL_FUNCTION(is_canonical_nan64, isCanonicalNaN64) { auto value = TRY(vm.argument(0).to_bigint_uint64(vm)); - return value == 0x7FF8000000000000 || value == 0xFFF8000000000000; + return _is_canonical_nan64(value); } TESTJS_GLOBAL_FUNCTION(is_arithmetic_nan32, isArithmeticNaN32) @@ -223,6 +233,47 @@ TESTJS_GLOBAL_FUNCTION(is_arithmetic_nan64, isArithmeticNaN64) return isnan(value); } +TESTJS_GLOBAL_FUNCTION(test_simd_vector, testSIMDVector) +{ + auto expected = TRY(vm.argument(0).to_object(vm)); + if (!is(*expected)) + return vm.throw_completion("Expected an Array"sv); + auto& expected_array = static_cast(*expected); + auto got = TRY(vm.argument(1).to_object(vm)); + if (!is(*got)) + return vm.throw_completion("Expected a TypedArray"sv); + auto& got_array = static_cast(*got); + auto element_size = 128 / TRY(TRY(expected_array.get("length")).to_u32(vm)); + size_t i = 0; + for (auto it = expected_array.indexed_properties().begin(false); it != expected_array.indexed_properties().end(); ++it) { + auto got_value = TRY(got_array.get(i++)); + u64 got = got_value.is_bigint() ? TRY(got_value.to_bigint_uint64(vm)) : (u64)TRY(got_value.to_index(vm)); + auto expect = TRY(expected_array.get(it.index())); + if (expect.is_string()) { + if (element_size != 32 && element_size != 64) + return vm.throw_completion("Expected element of size 32 or 64"sv); + auto string = expect.as_string().utf8_string(); + if (string == "nan:canonical") { + auto is_canonical = element_size == 32 ? _is_canonical_nan32(got) : _is_canonical_nan64(got); + if (!is_canonical) + return false; + continue; + } + if (string == "nan:arithmetic") { + auto is_arithmetic = element_size == 32 ? isnan(bit_cast((u32)got)) : isnan(bit_cast((u64)got)); + if (!is_arithmetic) + return false; + continue; + } + return vm.throw_completion(ByteString::formatted("Bad SIMD float expectation: {}"sv, string)); + } + u64 expect_value = expect.is_bigint() ? TRY(expect.to_bigint_uint64(vm)) : (u64)TRY(expect.to_index(vm)); + if (got != expect_value) + return false; + } + return true; +} + void WebAssemblyModule::initialize(JS::Realm& realm) { Base::initialize(realm); @@ -281,7 +332,7 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke) for (auto& param : type->parameters()) { auto argument = vm.argument(index++); double double_value = 0; - if (!argument.is_bigint()) + if (!argument.is_bigint() && !argument.is_object()) double_value = TRY(argument.to_double(vm)); switch (param.kind()) { case Wasm::ValueType::Kind::I32: @@ -307,21 +358,14 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke) } break; case Wasm::ValueType::Kind::V128: { - if (!argument.is_bigint()) { - if (argument.is_number()) - argument = JS::BigInt::create(vm, Crypto::SignedBigInteger { TRY(argument.to_double(vm)) }); - else - argument = TRY(argument.to_bigint(vm)); - } - + auto object = MUST(argument.to_object(vm)); + if (!is(*object)) + return vm.throw_completion("Expected typed array"sv); + auto& array = static_cast(*object); u128 bits = 0; - auto bytes = argument.as_bigint().big_integer().unsigned_value().export_data({ bit_cast(&bits), sizeof(bits) }); - VERIFY(!argument.as_bigint().big_integer().is_negative()); - - if constexpr (AK::HostIsLittleEndian) - arguments.append(Wasm::Value(bits << (128 - bytes * 8))); - else - arguments.append(Wasm::Value(bits >> (128 - bytes * 8))); + auto* ptr = bit_cast(&bits); + memcpy(ptr, array.viewed_array_buffer()->buffer().data(), 16); + arguments.append(Wasm::Value(bits)); break; } case Wasm::ValueType::Kind::FunctionReference: @@ -359,8 +403,10 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke) [](i32 value) { return JS::Value(static_cast(value)); }, [&](i64 value) { return JS::Value(JS::BigInt::create(vm, Crypto::SignedBigInteger { value })); }, [&](u128 value) { - auto unsigned_bigint_value = Crypto::UnsignedBigInteger::import_data(bit_cast(&value), sizeof(value)); - return JS::Value(JS::BigInt::create(vm, Crypto::SignedBigInteger(move(unsigned_bigint_value), false))); + // FIXME: remove the MUST here + auto buf = MUST(JS::ArrayBuffer::create(*vm.current_realm(), 16)); + memcpy(buf->buffer().data(), value.bytes().data(), 16); + return JS::Value(buf); }, [](Wasm::Reference const& reference) { return reference.ref().visit(