Skip to content
Closed
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
139 changes: 139 additions & 0 deletions JSTests/stress/bigint-typed-array-index-of-out-of-range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
function shouldBe(actual, expected, message) {
if (actual !== expected)
throw new Error(message + ": expected " + expected + " but got " + actual);
}

// %TypedArray%.prototype.indexOf / lastIndexOf compare with IsStrictlyEqual and includes with SameValueZero
// against the element's BigInt value. A BigInt that is not representable in the element type can never match,
// even when it is congruent to an element modulo 2^64.

function test(array, needle, expectedIndex) {
let name = array.constructor.name + "(" + array.length + ") needle " + needle + "n";
shouldBe(array.indexOf(needle), expectedIndex, name + " indexOf");
shouldBe(array.lastIndexOf(needle), expectedIndex, name + " lastIndexOf");
shouldBe(array.includes(needle), expectedIndex !== -1, name + " includes");
shouldBe(array.indexOf(needle, 1), expectedIndex >= 1 ? expectedIndex : -1, name + " indexOf fromIndex 1");
shouldBe(array.lastIndexOf(needle, -2), expectedIndex <= array.length - 2 ? expectedIndex : -1, name + " lastIndexOf fromIndex -2");
shouldBe(array.includes(needle, -1), expectedIndex === array.length - 1, name + " includes fromIndex -1");
shouldBe(Array.prototype.indexOf.call(array, needle), expectedIndex, name + " Array.prototype.indexOf");
shouldBe(Array.prototype.includes.call(array, needle), expectedIndex !== -1, name + " Array.prototype.includes");
}

const int64Max = 2n ** 63n - 1n;
const int64Min = -(2n ** 63n);
const uint64Max = 2n ** 64n - 1n;

const int64Cases = [
// [elements, needle, expected index]
[[3n, -1n, 7n], 2n ** 64n - 1n, -1], // wraps to -1n
[[3n, -1n, 7n], 2n ** 64n + 3n, -1], // wraps to 3n
[[3n, -1n, 7n], -(2n ** 64n) + 7n, -1], // wraps to 7n
[[3n, 0n, 7n], 2n ** 64n, -1], // wraps to 0n
[[3n, 0n, 7n], -(2n ** 64n), -1], // wraps to 0n
[[3n, 0n, 7n], 2n ** 128n, -1], // wraps to 0n
[[3n, int64Min, 7n], 2n ** 63n, -1], // wraps to -2^63
[[3n, int64Max, 7n], int64Min - 1n, -1], // wraps to 2^63 - 1
[[3n, 5n, 7n], 2n ** 200n + 5n, -1], // many digits, low 64 bits are 5
[[3n, -5n, 7n], -(2n ** 200n) - 5n, -1],
// Representable needles still match, including the boundaries.
[[3n, -1n, 7n], -1n, 1],
[[3n, -1n, 7n], 3n, 0],
[[3n, -1n, 7n], 7n, 2],
[[3n, 0n, 7n], 0n, 1],
[[3n, 0n, 7n], -0n, 1],
[[3n, int64Max, 7n], int64Max, 1],
[[3n, int64Min, 7n], int64Min, 1],
[[3n, 2n ** 32n, 7n], 2n ** 32n, 1],
[[3n, -(2n ** 32n), 7n], -(2n ** 32n), 1],
[[3n, -1n, 7n], 4n, -1],
];

const uint64Cases = [
[[3n, uint64Max, 7n], -1n, -1], // wraps to 2^64 - 1
[[3n, int64Max, 7n], int64Min - 1n, -1], // wraps to 2^63 - 1
[[3n, 2n ** 63n, 7n], int64Min, -1], // wraps to 2^63
[[3n, 0n, 7n], 2n ** 64n, -1], // wraps to 0n
[[3n, 0n, 7n], -(2n ** 64n), -1], // wraps to 0n
[[3n, 0n, 7n], 2n ** 128n, -1], // wraps to 0n
[[3n, 5n, 7n], 2n ** 64n + 5n, -1], // wraps to 5n
[[3n, 5n, 7n], -(2n ** 64n) + 5n, -1], // wraps to 5n
[[3n, 5n, 7n], 2n ** 200n + 5n, -1],
[[3n, uint64Max - 4n, 7n], -5n, -1], // wraps to 2^64 - 5
// Representable needles still match, including the boundaries.
[[3n, uint64Max, 7n], uint64Max, 1],
[[3n, 0n, 7n], 0n, 1],
[[3n, 0n, 7n], -0n, 1],
[[3n, 2n ** 63n, 7n], 2n ** 63n, 1],
[[3n, int64Max, 7n], int64Max, 1],
[[3n, 2n ** 32n, 7n], 2n ** 32n, 1],
[[3n, 5n, 7n], 3n, 0],
[[3n, 5n, 7n], 7n, 2],
[[3n, 5n, 7n], 4n, -1],
];

function makeViews(constructor, elements) {
let views = [];

views.push(new constructor(elements));

let padded = new constructor(elements.length + 2);
padded.set(elements, 1);
views.push(padded.subarray(1, elements.length + 1));

let resizable = new ArrayBuffer(elements.length * 8, { maxByteLength: elements.length * 8 + 64 });
let tracking = new constructor(resizable);
tracking.set(elements);
views.push(tracking);

let fixedWindow = new constructor(resizable, 0, elements.length);
views.push(fixedWindow);

let shared = new constructor(new SharedArrayBuffer(elements.length * 8));
shared.set(elements);
views.push(shared);

return views;
}

let rows = [];
for (let [elements, needle, expected] of int64Cases) {
for (let view of makeViews(BigInt64Array, elements))
rows.push([view, needle, expected]);
}
for (let [elements, needle, expected] of uint64Cases) {
for (let view of makeViews(BigUint64Array, elements))
rows.push([view, needle, expected]);
}

for (let i = 0; i < Math.max(testLoopCount, rows.length); ++i) {
let [view, needle, expected] = rows[i % rows.length];
test(view, needle, expected);
}

// A repeated element: indexOf finds the first match, lastIndexOf the last, and a wrapped needle neither.
for (let constructor of [BigInt64Array, BigUint64Array]) {
for (let view of makeViews(constructor, [7n, 3n, 7n, 7n, 3n])) {
let name = constructor.name + " repeated 7n";
shouldBe(view.indexOf(7n), 0, name + " indexOf");
shouldBe(view.lastIndexOf(7n), 3, name + " lastIndexOf");
shouldBe(view.indexOf(7n, 1), 2, name + " indexOf fromIndex 1");
shouldBe(view.lastIndexOf(7n, -3), 2, name + " lastIndexOf fromIndex -3");
shouldBe(view.indexOf(7n + 2n ** 64n), -1, name + " wrapped indexOf");
shouldBe(view.lastIndexOf(7n + 2n ** 64n), -1, name + " wrapped lastIndexOf");
shouldBe(view.includes(7n + 2n ** 64n), false, name + " wrapped includes");
}
}

// A non-BigInt needle never matches a BigInt element, and a BigInt needle never matches a Number element.
{
let i64 = new BigInt64Array([0n, 1n, -1n]);
shouldBe(i64.indexOf(1), -1, "BigInt64Array indexOf Number");
shouldBe(i64.includes(-1), false, "BigInt64Array includes Number");
shouldBe(i64.lastIndexOf("1"), -1, "BigInt64Array lastIndexOf String");
let u64 = new BigUint64Array([0n, 1n, uint64Max]);
shouldBe(u64.indexOf(2 ** 64), -1, "BigUint64Array indexOf Number 2^64");
shouldBe(u64.includes(18446744073709551615), false, "BigUint64Array includes Number 2^64-1");
let f64 = new Float64Array([0, 1, 2 ** 64]);
shouldBe(f64.indexOf(2n ** 64n), -1, "Float64Array indexOf BigInt");
shouldBe(f64.includes(1n), false, "Float64Array includes BigInt");
}
4 changes: 4 additions & 0 deletions Source/JavaScriptCore/runtime/JSBigInt.h
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,12 @@ class JSBigInt final : public JSCell {
static JSValue asUintN(JSGlobalObject*, uint64_t numberOfBits, int32_t bigIntAsInt32);
#endif

// ToBigUint64 / ToBigInt64: the value modulo 2^64.
inline static uint64_t toBigUInt64(JSValue); // Defined in JSBigIntInlines.h
inline static int64_t toBigInt64(JSValue); // Defined in JSBigIntInlines.h
// nullopt instead of wrapping when the value is not representable.
inline static std::optional<uint64_t> tryGetAsUint64(JSValue); // Defined in JSBigIntInlines.h
inline static std::optional<int64_t> tryGetAsInt64(JSValue); // Defined in JSBigIntInlines.h

Digit digit(unsigned) const;
void setDigit(unsigned, Digit); // Use only when initializing.
Expand Down
34 changes: 34 additions & 0 deletions Source/JavaScriptCore/runtime/JSBigIntInlines.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,40 @@ inline int64_t JSBigInt::toBigInt64(JSValue bigInt)
return static_cast<int64_t>(toBigUInt64Heap(bigInt.asHeapBigInt()));
}

inline std::optional<uint64_t> JSBigInt::tryGetAsUint64(JSValue bigInt)
{
ASSERT(bigInt.isBigInt());
#if USE(BIGINT32)
if (bigInt.isBigInt32()) {
int32_t value = bigInt.bigInt32AsInt32();
if (value < 0)
return std::nullopt;
return static_cast<uint64_t>(value);
}
#endif
JSBigInt* heapBigInt = bigInt.asHeapBigInt();
if (heapBigInt->sign() || heapBigInt->length() > sizeof(uint64_t) / sizeof(Digit))
return std::nullopt;
return toBigUInt64Heap(heapBigInt);
}

inline std::optional<int64_t> JSBigInt::tryGetAsInt64(JSValue bigInt)
{
ASSERT(bigInt.isBigInt());
#if USE(BIGINT32)
if (bigInt.isBigInt32())
return static_cast<int64_t>(bigInt.bigInt32AsInt32());
#endif
JSBigInt* heapBigInt = bigInt.asHeapBigInt();
if (heapBigInt->length() > sizeof(uint64_t) / sizeof(Digit))
return std::nullopt;
// The magnitude is below 2^64 here, so the wrapped value has the BigInt's sign exactly when the BigInt is in [-2^63, 2^63 - 1].
int64_t value = static_cast<int64_t>(toBigUInt64Heap(heapBigInt));
if ((value < 0) != heapBigInt->sign())
return std::nullopt;
return value;
}

ALWAYS_INLINE std::optional<double> JSBigInt::tryExtractDouble(JSValue value)
{
if (value.isNumber())
Expand Down
5 changes: 3 additions & 2 deletions Source/JavaScriptCore/runtime/ToNativeFromValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ std::optional<typename Adaptor::Type> toNativeFromValueWithoutCoercion(JSValue v
if constexpr (std::is_same_v<Adaptor, BigInt64Adaptor> || std::is_same_v<Adaptor, BigUint64Adaptor>) {
if (!value.isBigInt())
return std::nullopt;
// Not toBigInt64 / toBigUInt64: a BigInt the element type cannot represent matches no element, even one it is congruent to modulo 2^64.
if constexpr (std::is_same_v<Adaptor, BigInt64Adaptor>)
return JSBigInt::toBigInt64(value);
return JSBigInt::tryGetAsInt64(value);
else
return JSBigInt::toBigUInt64(value);
return JSBigInt::tryGetAsUint64(value);
} else {
if (!value.isNumber())
return std::nullopt;
Expand Down
Loading