From 0855ba4b47b827d51534f7cd1d4d9971324dc91d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:34:45 +0000 Subject: [PATCH] FFI: convert Number arguments of i64/u64 parameters modulo 2^64 writeInt64Slot converted a double-encoded Number with the CPU's truncating double -> int64 instruction (FFI::doubleToInt64). For u64 / u64_fast that has no representation for values in [2^63, 2^64): every such Number arrived in the callee as 9223372036854775808 on x86-64 (cvttsd2si's indefinite value) or 9223372036854775807 on arm64 (fcvtzs saturates). NaN and values outside the int64 range likewise got a different answer on each CPU. Use JSC::toInt64 instead: truncate toward zero and keep the low 64 bits of the two's complement result, with NaN and the infinities becoming 0. That is the ToInt32 conversion writeIntegerSlot already applies to the narrower integer types, widened to 64 bits, and it is what the int32 encoding and the BigInt branch of the same function already produce, so a Number now reaches an i64 / u64 parameter (or leaves an i64 / u64 callback) with the same bits however the engine happened to box it and as the BigInt of the same value. The signed and unsigned types share that bit pattern, so the isUnsigned split goes away, and doubleToUInt64 has no remaining user. doubleToInt64 itself stays the pointer conversion, where the IC stub and DFG inline the same instruction. testFFI pins the slot bits for the new cases; the JSTests batteries gain the Numbers that used to be CPU-specific, and the fuzzer's reference model now covers every double edge instead of excluding |d| >= 2^63. --- JSTests/stress/ffi-fuzz-signatures.js | 26 +++++++---- JSTests/stress/ffi-host-path.js | 8 ++++ JSTests/stress/ffi-tier-differential.js | 8 ++++ JSTests/stress/ffi-types-echo.js | 12 +++++ Source/JavaScriptCore/ffi/FFIConversions.cpp | 12 +++-- Source/JavaScriptCore/ffi/FFIConversions.h | 9 ++-- Source/JavaScriptCore/ffi/tests/testFFI.cpp | 48 ++++++++++++++++---- 7 files changed, 96 insertions(+), 27 deletions(-) diff --git a/JSTests/stress/ffi-fuzz-signatures.js b/JSTests/stress/ffi-fuzz-signatures.js index b4d8e87e31b7f..b41dcfac1c57d 100644 --- a/JSTests/stress/ffi-fuzz-signatures.js +++ b/JSTests/stress/ffi-fuzz-signatures.js @@ -62,9 +62,16 @@ function main() { // normalize (r is always an integer here, so `+ 0` only flips -0 to +0). return r + 0; } - // Both hardware truncations agree exactly when |d| < 2^63; the fuzzer only - // generates such doubles (the arch-specific saturation edges live in - // testFFI's doubleToInt64 corpus). + // i64 / u64 take a Number as ToInt64 / ToUint64: truncate, then the callers + // below keep the low 64 bits; NaN and the infinities become 0. Identical on + // every CPU, so the generators feed it every double edge, 2^63 and up + // included. + function numberToInt64(d) { + return Number.isFinite(d) ? BigInt(Math.trunc(d)) : 0n; + } + // ptr uses the CPU's truncating conversion instead; the ptr generator stays + // inside (-2^63, 2^63), where every CPU agrees with plain truncation (the + // arch-specific edges live in testFFI's doubleToInt64 checks). function doubleToInt64(d) { return BigInt(Math.trunc(d)); } @@ -82,14 +89,14 @@ function main() { return BigInt.asIntN(64, v); if (Number.isInteger(v) && Math.abs(v) <= 2147483647) return BigInt(v); // int32 -> sign-extend - return BigInt.asIntN(64, doubleToInt64(v)); + return BigInt.asIntN(64, numberToInt64(v)); }, "u64": v => { if (typeof v === "bigint") return BigInt.asUintN(64, v); if (Number.isInteger(v) && Math.abs(v) <= 2147483647) return BigInt.asUintN(64, BigInt(v)); // int32 -> sign-extend then reinterpret - return BigInt.asUintN(64, doubleToInt64(v)); + return BigInt.asUintN(64, numberToInt64(v)); }, "i64_fast": v => { const r = reference["i64"](v); @@ -122,6 +129,9 @@ function main() { const int32Edges = [0, 1, -1, 2147483647, -2147483648, 2147483646, -2147483647, 65535, 65536, -65536, 255, 256, 127, 128, -128, -129, 32767, 32768, -32768]; const doubleEdges = [0, -0, 0.5, -0.5, 1.5, -1.5, 2.5, 0.999999, -0.999999, 2 ** 31, -(2 ** 31), 2 ** 32 + 5, -(2 ** 32) - 5, 2 ** 52, 2 ** 53, 2 ** 53 - 1, -(2 ** 53), 2 ** 62, -(2 ** 62), 1e15 + 0.75, -1e15 - 0.75, NaN, Infinity, -Infinity, Number.MAX_VALUE, Number.MIN_VALUE, Number.EPSILON]; const bigIntEdges = [0n, 1n, -1n, twoTo63 - 1n, -twoTo63, twoTo63, twoTo64 - 1n, twoTo64, twoTo64 + 12345n, -twoTo64, 2n ** 100n + 7n, -(2n ** 90n), 9007199254740993n, 4611686018427387904n]; + // The doubles a 64-bit integer parameter has to wrap or zero: negatives (two's complement), + // [2^63, 2^64) (in range for u64, wraps for i64), 2^64 and beyond, non-finite. + const int64DoubleEdges = doubleEdges.concat([-7.5, -4294967297, 2 ** 63, -(2 ** 63), 2 ** 63 + 2 ** 62, 2 ** 64 - 2048, 2 ** 64, 2 ** 64 + 4096, -(2 ** 64), -(2 ** 64) - 4096, 1e300]); const oddballs = [true, false, undefined, null]; function genFor(type) { switch (type) { @@ -137,11 +147,7 @@ function main() { case 0: return pick(int32Edges); case 1: return pick(bigIntEdges); case 2: return BigInt.asIntN(64, BigInt(randomIntBits()) * BigInt(randomIntBits()) * 4294967311n); - default: { - // doubles strictly inside (-2^63, 2^63) so both hardware truncations agree - const d = pick(doubleEdges.filter(x => Number.isFinite(x) && Math.abs(x) < 9007199254740992 * 512)); - return d; - } + default: return pick(int64DoubleEdges); } } case "f64": diff --git a/JSTests/stress/ffi-host-path.js b/JSTests/stress/ffi-host-path.js index 3f1ed87aba672..753b98d144540 100644 --- a/JSTests/stress/ffi-host-path.js +++ b/JSTests/stress/ffi-host-path.js @@ -116,6 +116,12 @@ function main() { [echoU64, [-1], 18446744073709551615n, "u64(-1)"], [echoU64, [-2147483648], 18446744071562067968n, "u64(-2^31)"], [echoU64, [2n ** 64n + 3n], 3n, "u64(2^64+3)"], + [echoI64, [2 ** 63], -9223372036854775808n, "i64(2^63 as number)"], + [echoI64, [NaN], 0n, "i64(NaN)"], + [echoU64, [-1.5], 18446744073709551615n, "u64(-1.5)"], + [echoU64, [2 ** 63 + 2 ** 62], 13835058055282163712n, "u64(2^63+2^62 as number)"], + [echoU64, [2 ** 64], 0n, "u64(2^64 as number)"], + [echoU64, [NaN], 0n, "u64(NaN)"], [echoI64Fast, [9007199254740991], 9007199254740991, "i64_fast(2^53-1)"], [echoI64Fast, [-9007199254740991], -9007199254740991, "i64_fast(-(2^53-1))"], [echoI64Fast, [2n ** 53n], 9007199254740992n, "i64_fast(2^53)"], @@ -124,6 +130,8 @@ function main() { [echoU64Fast, [9007199254740990], 9007199254740990, "u64_fast(2^53-2)"], [echoU64Fast, [2n ** 53n - 1n], 9007199254740991n, "u64_fast(2^53-1)"], [echoU64Fast, [-1], 18446744073709551615n, "u64_fast(-1)"], + [echoU64Fast, [-1.5], 18446744073709551615n, "u64_fast(-1.5)"], + [echoU64Fast, [2 ** 63 + 2 ** 62], 13835058055282163712n, "u64_fast(2^63+2^62 as number)"], [echoF32, [1.1], 1.100000023841858, "f32(1.1)"], [echoF32, [-0], -0, "f32(-0)"], [echoF32, [NaN], NaN, "f32(NaN)"], diff --git a/JSTests/stress/ffi-tier-differential.js b/JSTests/stress/ffi-tier-differential.js index fa79abb178ffd..228edc6245d09 100644 --- a/JSTests/stress/ffi-tier-differential.js +++ b/JSTests/stress/ffi-tier-differential.js @@ -116,6 +116,12 @@ function main() { [echoU64, [-1], 18446744073709551615n, "u64(-1)"], [echoU64, [-2147483648], 18446744071562067968n, "u64(-2^31)"], [echoU64, [2n ** 64n + 3n], 3n, "u64(2^64+3)"], + [echoI64, [2 ** 63], -9223372036854775808n, "i64(2^63 as number)"], + [echoI64, [NaN], 0n, "i64(NaN)"], + [echoU64, [-1.5], 18446744073709551615n, "u64(-1.5)"], + [echoU64, [2 ** 63 + 2 ** 62], 13835058055282163712n, "u64(2^63+2^62 as number)"], + [echoU64, [2 ** 64], 0n, "u64(2^64 as number)"], + [echoU64, [NaN], 0n, "u64(NaN)"], [echoI64Fast, [9007199254740991], 9007199254740991, "i64_fast(2^53-1)"], [echoI64Fast, [-9007199254740991], -9007199254740991, "i64_fast(-(2^53-1))"], [echoI64Fast, [2n ** 53n], 9007199254740992n, "i64_fast(2^53)"], @@ -124,6 +130,8 @@ function main() { [echoU64Fast, [9007199254740990], 9007199254740990, "u64_fast(2^53-2)"], [echoU64Fast, [2n ** 53n - 1n], 9007199254740991n, "u64_fast(2^53-1)"], [echoU64Fast, [-1], 18446744073709551615n, "u64_fast(-1)"], + [echoU64Fast, [-1.5], 18446744073709551615n, "u64_fast(-1.5)"], + [echoU64Fast, [2 ** 63 + 2 ** 62], 13835058055282163712n, "u64_fast(2^63+2^62 as number)"], [echoF32, [1.1], 1.100000023841858, "f32(1.1)"], [echoF32, [-0], -0, "f32(-0)"], [echoF32, [NaN], NaN, "f32(NaN)"], diff --git a/JSTests/stress/ffi-types-echo.js b/JSTests/stress/ffi-types-echo.js index 54204acd8c42e..2ed588f8e319b 100644 --- a/JSTests/stress/ffi-types-echo.js +++ b/JSTests/stress/ffi-types-echo.js @@ -93,21 +93,33 @@ function main() { [123n, 123n], [-123n, -123n], [2n ** 63n - 1n, 9223372036854775807n], [-(2n ** 63n), -9223372036854775808n], [2n ** 64n + 7n, 7n], [2n ** 63n, -9223372036854775808n], [-1.5, -1n], [2.9, 2n], [-0, 0n], [2147483647, 2147483647n], [-2147483648, -2147483648n], [4294967296, 4294967296n], + // A Number converts like the BigInt of its truncated value: modulo 2^64, non-finite -> 0. + [-(2 ** 63), -9223372036854775808n], [2 ** 63, -9223372036854775808n], [2 ** 63 + 2 ** 62, -4611686018427387904n], + [2 ** 64, 0n], [2 ** 64 + 4096, 4096n], [NaN, 0n], [Infinity, 0n], [-Infinity, 0n], ]], [echoU64, "u64", [ [0, 0n], [1, 1n], [-1, 18446744073709551615n], [4294967295, 4294967295n], [2n ** 64n - 1n, 18446744073709551615n], [2n ** 64n + 7n, 7n], [-2n, 18446744073709551614n], [2.5, 2n], [2 ** 53, 9007199254740992n], [-2147483648, 18446744071562067968n], [9007199254740991, 9007199254740991n], + // Double-encoded negatives wrap exactly like the int32-encoded -1 and the BigInt -2n above. + [-1.5, 18446744073709551615n], [-7.5, 18446744073709551609n], [-(2 ** 53), 18437736874454810624n], + [-4294967297, 18446744069414584319n], + // [2^63, 2^64) is in range for u64 and must arrive exactly; 2^64 and above wrap, non-finite -> 0. + [2 ** 63, 9223372036854775808n], [2 ** 63 + 2 ** 62, 13835058055282163712n], [2 ** 64 - 2048, 18446744073709549568n], + [2 ** 64, 0n], [2 ** 64 + 4096, 4096n], [NaN, 0n], [Infinity, 0n], [-Infinity, 0n], ]], [echoI64Fast, "i64_fast", [ [0, 0], [-1, -1], [42, 42], [2 ** 53 - 1, 9007199254740991], [-(2 ** 53 - 1), -9007199254740991], [2n ** 53n, 9007199254740992n], [-(2n ** 53n), -9007199254740992n], [1n << 62n, 4611686018427387904n], [2n ** 63n - 1n, 9223372036854775807n], [-2, -2], [3.7, 3], [-3.7, -3], + [2 ** 63, -9223372036854775808n], [2 ** 63 + 2 ** 62, -4611686018427387904n], [NaN, 0], [Infinity, 0], ]], [echoU64Fast, "u64_fast", [ [0, 0], [123, 123], [2 ** 53 - 2, 9007199254740990], [2n ** 53n - 1n, 9007199254740991n], [2n ** 53n, 9007199254740992n], [-1, 18446744073709551615n], [2n ** 64n - 1n, 18446744073709551615n], [4.9, 4], + [-1.5, 18446744073709551615n], [-4294967297, 18446744069414584319n], [2 ** 63 + 2 ** 62, 13835058055282163712n], + [2 ** 64, 0], [NaN, 0], [-Infinity, 0], ]], [echoF32, "f32", [ [0, 0], [-0, -0], [1.5, 1.5], [1.1, Math.fround(1.1)], [-1.1, Math.fround(-1.1)], [NaN, NaN], diff --git a/Source/JavaScriptCore/ffi/FFIConversions.cpp b/Source/JavaScriptCore/ffi/FFIConversions.cpp index 82d7a9d5ff5e9..cfc5aa17c82c5 100644 --- a/Source/JavaScriptCore/ffi/FFIConversions.cpp +++ b/Source/JavaScriptCore/ffi/FFIConversions.cpp @@ -44,6 +44,7 @@ #include "JSFFICallback.h" #include "JSGlobalObject.h" #include "JSString.h" +#include "MathCommon.h" #include "PureNaN.h" #include #include @@ -170,22 +171,27 @@ static bool writeIntegerSlot(JSGlobalObject* globalObject, Type type, JSValue va return true; } +// Every branch stores the low 64 bits of the value's two's complement representation, which is the +// same bit pattern for the signed and the unsigned types (ToBigInt64 and ToBigUint64 only differ in +// how the callee reads it), so the type only matters for the error message. static bool writeInt64Slot(JSGlobalObject* globalObject, Type type, JSValue value, uint64_t& slotOut) { VM& vm = getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - bool isUnsigned = type == Type::Uint64 || type == Type::Uint64Fast; if (value.isInt32()) { slotOut = static_cast(static_cast(value.asInt32())); return true; } if (value.isDouble()) { - slotOut = isUnsigned ? doubleToUInt64(value.asDouble()) : static_cast(doubleToInt64(value.asDouble())); + // Modular like writeIntegerSlot's ToInt32, not doubleToInt64's hardware truncation: that has no + // representation for u64 values in [2^63, 2^64) and gives NaN and out-of-range values a + // different answer on x86-64 (INT64_MIN) than on arm64 (0 / saturated). + slotOut = static_cast(toInt64(value.asDouble())); return true; } if (value.isBigInt()) { - slotOut = isUnsigned ? JSBigInt::toBigUInt64(value) : static_cast(JSBigInt::toBigInt64(value)); + slotOut = JSBigInt::toBigUInt64(value); return true; } return throwCannotConvert(globalObject, scope, type); diff --git a/Source/JavaScriptCore/ffi/FFIConversions.h b/Source/JavaScriptCore/ffi/FFIConversions.h index a9d8826f85324..c518e769e7942 100644 --- a/Source/JavaScriptCore/ffi/FFIConversions.h +++ b/Source/JavaScriptCore/ffi/FFIConversions.h @@ -35,7 +35,6 @@ #include "JSCJSValue.h" #include "JSExportMacros.h" #include "OperationResult.h" -#include namespace JSC { @@ -49,13 +48,11 @@ JS_EXPORT_PRIVATE JSValue jsValueFromSlot(JSGlobalObject*, FFIContext&, Type, ui JS_EXPORT_PRIVATE JSValue pointerToJSValue(JSGlobalObject*, uint64_t address); +// The pointer-typed argument conversion for a Number: the CPU's truncating double -> int64 instruction, +// which is also what the IC stub and the DFG emit inline (truncateDoubleToInt64) for those types. +// i64 / u64 arguments use the modular JSC::toInt64 instead; see writeInt64Slot. JS_EXPORT_PRIVATE int64_t doubleToInt64(double); -inline uint64_t doubleToUInt64(double value) -{ - return std::bit_cast(doubleToInt64(value)); -} - } // namespace FFI JSC_DECLARE_JIT_OPERATION(operationFFIBoxSlot, EncodedJSValue, (JSGlobalObject*, uint32_t typeTag, uint64_t slot, int32_t exitArena)); diff --git a/Source/JavaScriptCore/ffi/tests/testFFI.cpp b/Source/JavaScriptCore/ffi/tests/testFFI.cpp index b4972d45a7a9f..8e424c9133422 100644 --- a/Source/JavaScriptCore/ffi/tests/testFFI.cpp +++ b/Source/JavaScriptCore/ffi/tests/testFFI.cpp @@ -918,10 +918,6 @@ static void testDoubleToInt64() FFI_CHECK_EQ(FFI::doubleToInt64(twoTo64), std::numeric_limits::max()); FFI_CHECK_EQ(FFI::doubleToInt64(-twoTo64), std::numeric_limits::min()); #endif - - static const double corpus[] = { 0.0, -0.0, 1.0, -1.0, -1.5, 1.5, 255.75, -255.75, twoTo63, -twoTo63, twoTo64, nan, inf, -inf, 4294967295.5, -4294967295.5, 1e300, -1e300, 4.9e-324 }; - for (double value : corpus) - FFI_CHECK_EQ_HEX(FFI::doubleToUInt64(value), std::bit_cast(FFI::doubleToInt64(value))); } enum class ExpectThrow : bool { No, Yes }; @@ -1103,20 +1099,56 @@ static void testConversions() expectSlot(T::Int64, jsNumber(-2147483647 - 1), 0xffffffff80000000ull); expectSlot(T::Int64, jsNumber(4294967296.0), 4294967296ull); expectSlot(T::Int64, jsNumber(1e18), 1000000000000000000ull); - expectSlot(T::Int64, jsNumber(-1.5), std::bit_cast(FFI::doubleToInt64(-1.5))); - expectSlot(T::Int64, jsNumber(9223372036854775808.0), std::bit_cast(FFI::doubleToInt64(9223372036854775808.0))); - expectSlot(T::Int64, jsNumber(nan), std::bit_cast(FFI::doubleToInt64(nan))); + expectSlot(T::Int64, jsNumber(-1.5), allOnes); + expectSlot(T::Int64, JSValue(JSValue::EncodeAsDouble, -1.0), allOnes); // the same number as jsNumber(-1), boxed as a double + expectSlot(T::Int64, jsNumber(-4294967297.0), 0xfffffffeffffffffull); + expectSlot(T::Int64, jsNumber(-9007199254740992.0), 0xffe0000000000000ull); // -2^53 + expectSlot(T::Int64, jsNumber(9223372036854774784.0), 0x7ffffffffffffc00ull); // largest double below 2^63 + expectSlot(T::Int64, jsNumber(-9223372036854775808.0), 0x8000000000000000ull); // INT64_MIN exactly + // Out of range: the low 64 bits of the truncated value, like Int32 above, on every CPU. + expectSlot(T::Int64, jsNumber(9223372036854775808.0), 0x8000000000000000ull); // 2^63 wraps to INT64_MIN + expectSlot(T::Int64, jsNumber(13835058055282163712.0), 0xc000000000000000ull); // 2^63 + 2^62 + expectSlot(T::Int64, jsNumber(18446744073709551616.0), 0); // 2^64 + expectSlot(T::Int64, jsNumber(18446744073709555712.0), 4096); // 2^64 + 2^12 (the next double after 2^64) + expectSlot(T::Int64, jsNumber(-18446744073709555712.0), static_cast(-4096)); + expectSlot(T::Int64, jsNumber(1e300), 0); // no mantissa bit lands in the low 64 bits + expectSlot(T::Int64, jsNumber(nan), 0); + expectSlot(T::Int64, jsNumber(inf), 0); + expectSlot(T::Int64, jsNumber(-inf), 0); + expectSlot(T::Int64, jsNumber(-0.0), 0); + expectSlot(T::Int64, jsNumber(4.9e-324), 0); expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast(std::numeric_limits::max())), 0x7fffffffffffffffull); expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast(std::numeric_limits::min())), 0x8000000000000000ull); expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast(-1)), allOnes); expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast(0xffffffffffffffffull)), allOnes); // 2^64-1 mod 2^64 expectSlot(T::Uint64, jsNumber(-1), allOnes); - expectSlot(T::Uint64, jsNumber(2.5), std::bit_cast(FFI::doubleToInt64(2.5))); + expectSlot(T::Uint64, jsNumber(2.5), 2); + // A Number reaches a u64 with the same bits however it is boxed (int32 / double) and as the BigInt of the same value. + expectSlot(T::Uint64, JSValue(JSValue::EncodeAsDouble, -1.0), allOnes); + expectSlot(T::Uint64, jsNumber(-1.5), allOnes); + expectSlot(T::Uint64, jsNumber(-7.5), allOnes - 6); + expectSlot(T::Uint64, jsNumber(-4294967297.0), 0xfffffffeffffffffull); + expectSlot(T::Uint64, JSBigInt::createFrom(globalObject, static_cast(-4294967297ll)), 0xfffffffeffffffffull); + expectSlot(T::Uint64, jsNumber(9223372036854775808.0), 0x8000000000000000ull); // 2^63 is a plain in-range u64 + expectSlot(T::Uint64, jsNumber(13835058055282163712.0), 0xc000000000000000ull); // 2^63 + 2^62 + expectSlot(T::Uint64, jsNumber(18446744073709549568.0), 0xfffffffffffff800ull); // largest double below 2^64 + expectSlot(T::Uint64, jsNumber(18446744073709551616.0), 0); // 2^64 wraps like Uint32(2^32) above + expectSlot(T::Uint64, jsNumber(18446744073709555712.0), 4096); // 2^64 + 2^12 + expectSlot(T::Uint64, jsNumber(nan), 0); + expectSlot(T::Uint64, jsNumber(inf), 0); + expectSlot(T::Uint64, jsNumber(-inf), 0); + expectSlot(T::Uint64, jsNumber(-0.5), 0); expectSlot(T::Uint64, JSBigInt::createFrom(globalObject, static_cast(0xdeadbeefcafebabeull)), 0xdeadbeefcafebabeull); expectSlot(T::Uint64, JSBigInt::createFrom(globalObject, static_cast(-2)), allOnes - 1); // BigInt(-2) mod 2^64 expectSlot(T::Int64Fast, jsNumber(-42), std::bit_cast(static_cast(-42))); + expectSlot(T::Int64Fast, jsNumber(-1.5), allOnes); + expectSlot(T::Int64Fast, jsNumber(9223372036854775808.0), 0x8000000000000000ull); + expectSlot(T::Int64Fast, jsNumber(nan), 0); expectSlot(T::Int64Fast, JSBigInt::createFrom(globalObject, static_cast(1) << 60), static_cast(1) << 60); expectSlot(T::Uint64Fast, jsNumber(9007199254740992.0), 9007199254740992ull); + expectSlot(T::Uint64Fast, jsNumber(-1.5), allOnes); + expectSlot(T::Uint64Fast, jsNumber(13835058055282163712.0), 0xc000000000000000ull); + expectSlot(T::Uint64Fast, jsNumber(nan), 0); expectSlot(T::Uint64Fast, JSBigInt::createFrom(globalObject, static_cast(1) << 63), static_cast(1) << 63); expectSlot(T::Double, jsNumber(1.5), std::bit_cast(1.5));