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));