Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions JSTests/stress/ffi-fuzz-signatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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":
Expand Down
8 changes: 8 additions & 0 deletions JSTests/stress/ffi-host-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)"],
Expand All @@ -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)"],
Expand Down
8 changes: 8 additions & 0 deletions JSTests/stress/ffi-tier-differential.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)"],
Expand All @@ -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)"],
Expand Down
12 changes: 12 additions & 0 deletions JSTests/stress/ffi-types-echo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
12 changes: 9 additions & 3 deletions Source/JavaScriptCore/ffi/FFIConversions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "JSFFICallback.h"
#include "JSGlobalObject.h"
#include "JSString.h"
#include "MathCommon.h"
#include "PureNaN.h"
#include <bit>
#include <cmath>
Expand Down Expand Up @@ -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<uint64_t>(static_cast<int64_t>(value.asInt32()));
return true;
}
if (value.isDouble()) {
slotOut = isUnsigned ? doubleToUInt64(value.asDouble()) : static_cast<uint64_t>(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<uint64_t>(toInt64(value.asDouble()));
return true;
}
if (value.isBigInt()) {
slotOut = isUnsigned ? JSBigInt::toBigUInt64(value) : static_cast<uint64_t>(JSBigInt::toBigInt64(value));
slotOut = JSBigInt::toBigUInt64(value);
return true;
}
return throwCannotConvert(globalObject, scope, type);
Expand Down
9 changes: 3 additions & 6 deletions Source/JavaScriptCore/ffi/FFIConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
#include "JSCJSValue.h"
#include "JSExportMacros.h"
#include "OperationResult.h"
#include <bit>

namespace JSC {

Expand All @@ -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<uint64_t>(doubleToInt64(value));
}

} // namespace FFI

JSC_DECLARE_JIT_OPERATION(operationFFIBoxSlot, EncodedJSValue, (JSGlobalObject*, uint32_t typeTag, uint64_t slot, int32_t exitArena));
Expand Down
48 changes: 40 additions & 8 deletions Source/JavaScriptCore/ffi/tests/testFFI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -918,10 +918,6 @@ static void testDoubleToInt64()
FFI_CHECK_EQ(FFI::doubleToInt64(twoTo64), std::numeric_limits<int64_t>::max());
FFI_CHECK_EQ(FFI::doubleToInt64(-twoTo64), std::numeric_limits<int64_t>::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<uint64_t>(FFI::doubleToInt64(value)));
}

enum class ExpectThrow : bool { No, Yes };
Expand Down Expand Up @@ -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<uint64_t>(FFI::doubleToInt64(-1.5)));
expectSlot(T::Int64, jsNumber(9223372036854775808.0), std::bit_cast<uint64_t>(FFI::doubleToInt64(9223372036854775808.0)));
expectSlot(T::Int64, jsNumber(nan), std::bit_cast<uint64_t>(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<uint64_t>(-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<int64_t>(std::numeric_limits<int64_t>::max())), 0x7fffffffffffffffull);
expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast<int64_t>(std::numeric_limits<int64_t>::min())), 0x8000000000000000ull);
expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast<int64_t>(-1)), allOnes);
expectSlot(T::Int64, JSBigInt::createFrom(globalObject, static_cast<uint64_t>(0xffffffffffffffffull)), allOnes); // 2^64-1 mod 2^64
expectSlot(T::Uint64, jsNumber(-1), allOnes);
expectSlot(T::Uint64, jsNumber(2.5), std::bit_cast<uint64_t>(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<int64_t>(-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<uint64_t>(0xdeadbeefcafebabeull)), 0xdeadbeefcafebabeull);
expectSlot(T::Uint64, JSBigInt::createFrom(globalObject, static_cast<int64_t>(-2)), allOnes - 1); // BigInt(-2) mod 2^64
expectSlot(T::Int64Fast, jsNumber(-42), std::bit_cast<uint64_t>(static_cast<int64_t>(-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<int64_t>(1) << 60), static_cast<uint64_t>(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<uint64_t>(1) << 63), static_cast<uint64_t>(1) << 63);

expectSlot(T::Double, jsNumber(1.5), std::bit_cast<uint64_t>(1.5));
Expand Down
Loading