mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-25 00:50:22 +00:00
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.
This commit is contained in:
parent
524e09dda1
commit
906fa04822
Notes:
sideshowbarker
2024-07-17 00:16:31 +09:00
Author: https://github.com/dzfrias Commit: https://github.com/LadybirdBrowser/ladybird/commit/906fa04822 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/591
2 changed files with 127 additions and 50 deletions
|
@ -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:
|
||||
|
|
|
@ -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<JS::Array>(*expected))
|
||||
return vm.throw_completion<JS::TypeError>("Expected an Array"sv);
|
||||
auto& expected_array = static_cast<JS::Array&>(*expected);
|
||||
auto got = TRY(vm.argument(1).to_object(vm));
|
||||
if (!is<JS::TypedArrayBase>(*got))
|
||||
return vm.throw_completion<JS::TypeError>("Expected a TypedArray"sv);
|
||||
auto& got_array = static_cast<JS::TypedArrayBase&>(*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<JS::TypeError>("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<float>((u32)got)) : isnan(bit_cast<double>((u64)got));
|
||||
if (!is_arithmetic)
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
return vm.throw_completion<JS::TypeError>(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<JS::TypedArrayBase>(*object))
|
||||
return vm.throw_completion<JS::TypeError>("Expected typed array"sv);
|
||||
auto& array = static_cast<JS::TypedArrayBase&>(*object);
|
||||
u128 bits = 0;
|
||||
auto bytes = argument.as_bigint().big_integer().unsigned_value().export_data({ bit_cast<u8*>(&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<u8*>(&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<double>(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<u8 const*>(&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(
|
||||
|
|
Loading…
Reference in a new issue