Skip to content
Open
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
222 changes: 222 additions & 0 deletions JSTests/stress/dataview-jit-bigint64.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Correctness test for DataView BigInt64/BigUint64 DFG/FTL intrinsics.
function assert(cond, msg) {
if (!cond)
throw new Error("FAIL: " + msg);
}

// Reference implementations via byte-level access.
function refGetBigUint64(bytes, offset, littleEndian) {
let v = 0n;
for (let i = 0; i < 8; i++) {
const b = BigInt(bytes[offset + (littleEndian ? 7 - i : i)]);
v = (v << 8n) | b;
}
return v;
}
function refGetBigInt64(bytes, offset, littleEndian) {
const u = refGetBigUint64(bytes, offset, littleEndian);
return u >= (1n << 63n) ? u - (1n << 64n) : u;
}
function refSetBigUint64(bytes, offset, value, littleEndian) {
let v = ((value % (1n << 64n)) + (1n << 64n)) % (1n << 64n);
for (let i = 0; i < 8; i++) {
const shift = BigInt(8 * (littleEndian ? i : 7 - i));
bytes[offset + i] = Number((v >> shift) & 0xffn);
}
}

// --- get tests: constant endianness (true / false / omitted) and variable ---
function testGetLE(view, offset) { return [view.getBigInt64(offset, true), view.getBigUint64(offset, true)]; }
function testGetBE(view, offset) { return [view.getBigInt64(offset, false), view.getBigUint64(offset, false)]; }
function testGetDefault(view, offset) { return [view.getBigInt64(offset), view.getBigUint64(offset)]; }
function testGetVar(view, offset, le) { return [view.getBigInt64(offset, le), view.getBigUint64(offset, le)]; }
noInline(testGetLE);
noInline(testGetBE);
noInline(testGetDefault);
noInline(testGetVar);

function testSetLE(view, offset, v1, v2) { view.setBigInt64(offset, v1, true); view.setBigUint64(offset + 8, v2, true); }
function testSetBE(view, offset, v1, v2) { view.setBigInt64(offset, v1, false); view.setBigUint64(offset + 8, v2, false); }
function testSetDefault(view, offset, v1, v2) { view.setBigInt64(offset, v1); view.setBigUint64(offset + 8, v2); }
function testSetVar(view, offset, v1, v2, le) { view.setBigInt64(offset, v1, le); view.setBigUint64(offset + 8, v2, le); }
noInline(testSetLE);
noInline(testSetBE);
noInline(testSetDefault);
noInline(testSetVar);

const SIZE = 64;
const bytes = new Uint8Array(SIZE);
const view = new DataView(bytes.buffer);

const interestingValues = [
0n, 1n, -1n, 2n, 255n, 256n, 0x7fn,
0x7fffffffn, 0x80000000n, 0xffffffffn, 0x100000000n,
(1n << 63n) - 1n, 1n << 63n, (1n << 64n) - 1n,
-(1n << 63n), -(1n << 63n) + 1n,
0x0102030405060708n, 0xf1f2f3f4f5f6f7f8n,
// multi-digit BigInts (wrap modulo 2^64)
(1n << 64n) + 5n, (1n << 128n) + 7n, -((1n << 64n) + 5n), -((1n << 100n) + 123n),
(1n << 64n), -(1n << 64n),
];

function toInt64(v) {
let u = ((v % (1n << 64n)) + (1n << 64n)) % (1n << 64n);
return u >= (1n << 63n) ? u - (1n << 64n) : u;
}
function toUint64(v) {
return ((v % (1n << 64n)) + (1n << 64n)) % (1n << 64n);
}

const ITERATIONS = 20000;
for (let i = 0; i < ITERATIONS; i++) {
const value = interestingValues[i % interestingValues.length];
const offset = (i * 3) % (SIZE - 16);

for (const le of [true, false]) {
// set via intrinsic path, check bytes against reference
testSetVar(view, offset, value, value, le);
const expected = new Uint8Array(16);
refSetBigUint64(expected, 0, value, le);
refSetBigUint64(expected, 8, value, le);
for (let j = 0; j < 16; j++)
assert(bytes[offset + j] === expected[j], `setVar le=${le} value=${value} byte ${j}: ${bytes[offset + j]} != ${expected[j]}`);

// get via intrinsic path, check against reference
const [i64v, u64v] = testGetVar(view, offset, le);
assert(i64v === toInt64(value), `getVar i64 le=${le} value=${value}: ${i64v}`);
assert(u64v === toUint64(value), `getVar u64 le=${le} value=${value}: ${u64v}`);
}

// constant-endianness paths
testSetLE(view, offset, value, value);
let [a, b] = testGetLE(view, offset);
assert(a === toInt64(value), `LE i64 ${value}: ${a}`);
assert(b === toUint64(value), `LE u64 ${value}: ${b}`);

testSetBE(view, offset, value, value);
[a, b] = testGetBE(view, offset);
assert(a === toInt64(value), `BE i64 ${value}: ${a}`);
assert(b === toUint64(value), `BE u64 ${value}: ${b}`);

// default (big-endian per spec)
testSetDefault(view, offset, value, value);
[a, b] = testGetDefault(view, offset);
assert(a === toInt64(value), `default i64 ${value}: ${a}`);
assert(b === toUint64(value), `default u64 ${value}: ${b}`);
assert(view.getBigUint64(offset, false) === toUint64(value), `default is big-endian ${value}`);
}

// cross-check against two-uint32 decomposition
for (let i = 0; i < ITERATIONS; i++) {
const value = interestingValues[i % interestingValues.length];
testSetLE(view, 0, value, value);
const lo = BigInt(view.getUint32(0, true));
const hi = BigInt(view.getUint32(4, true));
assert(((hi << 32n) | lo) === toUint64(value), `uint32 cross-check ${value}`);
}

// --- out-of-bounds must throw RangeError even once optimized ---
function oobGet(view, offset) { return view.getBigUint64(offset, true); }
function oobSet(view, offset) { view.setBigUint64(offset, 1n, true); }
noInline(oobGet);
noInline(oobSet);
for (let i = 0; i < ITERATIONS; i++) {
oobGet(view, 0);
oobSet(view, 0);
}
for (const badOffset of [SIZE - 7, SIZE, -1, 0x7fffffff]) {
let threw = false;
try { oobGet(view, badOffset); } catch (e) { threw = e instanceof RangeError; }
assert(threw, `getBigUint64(${badOffset}) should throw RangeError`);
threw = false;
try { oobSet(view, badOffset); } catch (e) { threw = e instanceof RangeError; }
assert(threw, `setBigUint64(${badOffset}) should throw RangeError`);
}

// --- non-BigInt value to set must throw TypeError after optimization ---
function setAny(view, offset, v) { view.setBigUint64(offset, v, true); }
noInline(setAny);
for (let i = 0; i < ITERATIONS; i++)
setAny(view, 0, 42n);
for (const bad of [42, 1.5, null, undefined, Symbol("x")]) {
let threw = false;
try { setAny(view, 0, bad); } catch (e) { threw = e instanceof TypeError; }
assert(threw, `setBigUint64 with ${String(bad)} should throw TypeError`);
}
{
let threw = false;
try { setAny(view, 0, {}); } catch (e) { threw = e instanceof SyntaxError; }
assert(threw, "setBigUint64 with {} should throw SyntaxError");
}
// string and boolean convert via ToBigInt without throwing
setAny(view, 0, "42");
assert(view.getBigUint64(0, true) === 42n, "string value converts");
setAny(view, 0, true);
assert(view.getBigUint64(0, true) === 1n, "boolean value converts");
// after the exits, bigint values must still work
setAny(view, 0, 7n);
assert(view.getBigUint64(0, true) === 7n, "set after exits");

// --- number value to get offset coercion & detached buffer ---
{
const buf = new ArrayBuffer(16);
const v = new DataView(buf);
v.setBigUint64(0, 0x1122334455667788n, true);
transferArrayBuffer(buf);
let threw = false;
try { oobGet(v, 0); } catch (e) { threw = e instanceof TypeError || e instanceof RangeError; }
assert(threw, "get on detached buffer should throw");
threw = false;
try { oobSet(v, 0); } catch (e) { threw = e instanceof TypeError || e instanceof RangeError; }
assert(threw, "set on detached buffer should throw");
}
Comment on lines +160 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Comment promises offset-coercion coverage that isn't tested.

The block comment says "number value to get offset coercion & detached buffer" but only exercises the detached-buffer path; offset/byteOffset is always a plain integer everywhere in this file (main loop offsets, OOB offsets, resizable-buffer offsets). No test passes a non-Int32 offset (a float like 16.0, a numeric string, or an object with valueOf) to getBigInt64/setBigUint64, so ToIndex coercion on the offset operand of the new intrinsic is untested. Given this PR adds new DFG/FTL type speculation for the offset operand of DataViewGetInt/DataViewSet, an offset that isn't a plain small integer is exactly the kind of input likely to hit a different type-check/OSR-exit path.

Suggested addition
 {
     const buf = new ArrayBuffer(16);
     const v = new DataView(buf);
     v.setBigUint64(0, 0x1122334455667788n, true);
+    // offset coercion via ToIndex
+    assert(v.getBigUint64(0.0, true) === 0x1122334455667788n, "float offset coerces");
+    assert(v.getBigUint64("0", true) === 0x1122334455667788n, "string offset coerces");
+    assert(v.getBigUint64({ valueOf() { return 0; } }, true) === 0x1122334455667788n, "object offset coerces via valueOf");
     transferArrayBuffer(buf);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// --- number value to get offset coercion & detached buffer ---
{
const buf = new ArrayBuffer(16);
const v = new DataView(buf);
v.setBigUint64(0, 0x1122334455667788n, true);
transferArrayBuffer(buf);
let threw = false;
try { oobGet(v, 0); } catch (e) { threw = e instanceof TypeError || e instanceof RangeError; }
assert(threw, "get on detached buffer should throw");
threw = false;
try { oobSet(v, 0); } catch (e) { threw = e instanceof TypeError || e instanceof RangeError; }
assert(threw, "set on detached buffer should throw");
}
// --- number value to get offset coercion & detached buffer ---
{
const buf = new ArrayBuffer(16);
const v = new DataView(buf);
v.setBigUint64(0, 0x1122334455667788n, true);
// offset coercion via ToIndex
assert(v.getBigUint64(0.0, true) === 0x1122334455667788n, "float offset coerces");
assert(v.getBigUint64("0", true) === 0x1122334455667788n, "string offset coerces");
assert(v.getBigUint64({ valueOf() { return 0; } }, true) === 0x1122334455667788n, "object offset coerces via valueOf");
transferArrayBuffer(buf);
let threw = false;
try { oobGet(v, 0); } catch (e) { threw = e instanceof TypeError || e instanceof RangeError; }
assert(threw, "get on detached buffer should throw");
threw = false;
try { oobSet(v, 0); } catch (e) { threw = e instanceof TypeError || e instanceof RangeError; }
assert(threw, "set on detached buffer should throw");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@JSTests/stress/dataview-jit-bigint64.js` around lines 160 - 172, Update the
offset-coercion coverage block around oobGet and oobSet to invoke the BigInt64
DataView accessors with non-plain-integer offsets, such as a fractional number,
numeric string, and object implementing valueOf, while preserving the
detached-buffer assertions. Ensure these cases exercise ToIndex coercion for
both get and set rather than only passing integer offsets.


// --- resizable ArrayBuffer ---
{
const rab = new ArrayBuffer(32, { maxByteLength: 64 });
const v = new DataView(rab);
function rget(v, o) { return v.getBigUint64(o, true); }
function rset(v, o, val) { v.setBigUint64(o, val, true); }
noInline(rget);
noInline(rset);
for (let i = 0; i < ITERATIONS; i++) {
rset(v, 16, BigInt(i));
assert(rget(v, 16) === BigInt(i), "resizable basic");
}
rab.resize(16);
let threw = false;
try { rget(v, 16); } catch (e) { threw = true; }
assert(threw, "get past shrunk resizable buffer should throw");
rab.resize(64);
rset(v, 48, 99n);
assert(rget(v, 48) === 99n, "grown resizable buffer");
}

// --- GC stress: results are real, independent BigInts ---
function gcGet(view, offset) { return view.getBigUint64(offset, true); }
noInline(gcGet);
view.setBigUint64(0, 0xdeadbeefcafebaben, true);
{
const keep = [];
for (let i = 0; i < 50000; i++) {
keep.push(gcGet(view, 0));
if (keep.length > 64)
keep.shift();
if ((i % 10000) === 0)
fullGC();
}
for (const k of keep)
assert(k === 0xdeadbeefcafebaben, "gc stress value");
}

// --- negative zero-adjacent and zero BigInt handling in set fast path ---
function setZero(view) { view.setBigUint64(0, 0n, true); view.setBigInt64(8, 0n, true); }
noInline(setZero);
view.setBigUint64(0, ~0n & ((1n << 64n) - 1n), true);
view.setBigUint64(8, ~0n & ((1n << 64n) - 1n), true);
for (let i = 0; i < ITERATIONS; i++)
setZero(view);
assert(view.getBigUint64(0, true) === 0n, "zero set u64");
assert(view.getBigInt64(8, true) === 0n, "zero set i64");

print("PASS");
4 changes: 3 additions & 1 deletion Source/JavaScriptCore/b3/B3AbstractHeapRepository.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ namespace JSC::B3 {
macro(JSArrayBufferView_length, JSArrayBufferView::offsetOfLength(), Mutability::Mutable) \
macro(JSArrayBufferView_mode, JSArrayBufferView::offsetOfMode(), Mutability::Mutable) \
macro(JSArrayBufferView_vector, JSArrayBufferView::offsetOfVector(), Mutability::Mutable) \
macro(JSBigInt_length, JSBigInt::offsetOfLength(), Mutability::Immutable) \
macro(JSBigInt_length, JSBigInt::offsetOfLength(), Mutability::Mutable) \
macro(JSBigInt_hash, JSBigInt::offsetOfHash(), Mutability::Mutable) \
macro(JSBigInt_data0, JSBigInt::offsetOfData(), Mutability::Mutable) \
macro(JSBoundFunction_targetFunction, JSBoundFunction::offsetOfTargetFunction(), Mutability::Mutable) \
macro(JSBoundFunction_boundThis, JSBoundFunction::offsetOfBoundThis(), Mutability::Mutable) \
macro(JSBoundFunction_boundArg0, JSBoundFunction::offsetOfBoundArgs() + sizeof(WriteBarrier<Unknown>) * 0, Mutability::Mutable) \
Expand Down
6 changes: 4 additions & 2 deletions Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
Original file line number Diff line number Diff line change
Expand Up @@ -5974,12 +5974,14 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
DataViewData data = node->dataViewData();
if (data.byteSize < 4)
setNonCellTypeForNode(node, SpecInt32Only);
else {
ASSERT(data.byteSize == 4);
else if (data.byteSize == 4) {
if (data.isSigned)
setNonCellTypeForNode(node, SpecInt32Only);
else
setNonCellTypeForNode(node, SpecInt52Any);
} else {
ASSERT(data.byteSize == 8);
setTypeForNode(node, SpecBigInt);
}
break;
}
Expand Down
27 changes: 25 additions & 2 deletions Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4863,7 +4863,9 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca
case DataViewGetUint32:
case DataViewGetFloat16:
case DataViewGetFloat32:
case DataViewGetFloat64: {
case DataViewGetFloat64:
case DataViewGetBigInt64:
case DataViewGetBigUint64: {
if (!is64Bit())
return CallOptimizationResult::DidNothing;

Expand Down Expand Up @@ -4919,6 +4921,13 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca
byteSize = 8;
op = DataViewGetFloat;
break;

case DataViewGetBigInt64:
isSigned = true;
[[fallthrough]];
case DataViewGetBigUint64:
byteSize = 8;
break;
default:
RELEASE_ASSERT_NOT_REACHED();
}
Expand Down Expand Up @@ -4966,9 +4975,17 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca
case DataViewSetUint32:
case DataViewSetFloat16:
case DataViewSetFloat32:
case DataViewSetFloat64: {
case DataViewSetFloat64:
case DataViewSetBigInt64:
case DataViewSetBigUint64: {
if (!is64Bit())
return CallOptimizationResult::DidNothing;
#if USE(BIGINT32)
// The value child is speculated as a heap BigInt, which would always
// exit when small BigInts are boxed as BigInt32.
if (intrinsic == DataViewSetBigInt64 || intrinsic == DataViewSetBigUint64)
return CallOptimizationResult::DidNothing;
#endif

if (argumentCountIncludingThis < 3)
return CallOptimizationResult::DidNothing;
Expand Down Expand Up @@ -5019,6 +5036,12 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca
isFloatingPoint = true;
byteSize = 8;
break;
case DataViewSetBigInt64:
isSigned = true;
[[fallthrough]];
case DataViewSetBigUint64:
byteSize = 8;
break;
default:
RELEASE_ASSERT_NOT_REACHED();
}
Expand Down
5 changes: 4 additions & 1 deletion Source/JavaScriptCore/dfg/DFGDoesGC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ bool doesGC(Graph& graph, Node* node)
case FilterSetPrivateBrandStatus:
case DateGetInt32OrNaN:
case DateGetTime:
case DataViewGetInt:
case DataViewGetFloat:
case DataViewSet:
case PutByOffset:
Expand Down Expand Up @@ -534,6 +533,10 @@ bool doesGC(Graph& graph, Node* node)
case GlobalIsNaN:
return node->child1().useKind() == UntypedUse;

case DataViewGetInt:
// getBigInt64/getBigUint64 allocate a BigInt for the result.
return node->dataViewData().byteSize == 8;

case CallNumberConstructor:
switch (node->child1().useKind()) {
case BigInt32Use:
Expand Down
9 changes: 9 additions & 0 deletions Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3668,6 +3668,10 @@ class FixupPhase : public Phase {
else
node->setResult(NodeResultInt52);
break;
case 8:
// getBigInt64/getBigUint64: the result is a BigInt JSValue.
ASSERT(node->result() == NodeResultJS);
break;
default:
RELEASE_ASSERT_NOT_REACHED();
}
Expand Down Expand Up @@ -3697,6 +3701,11 @@ class FixupPhase : public Phase {
else
fixEdge<Int52RepUse>(valueToStore);
break;
case 8:
// setBigInt64/setBigUint64: the value is wrapped modulo 2^64,
// so the heap BigInt's low digit (with sign applied) suffices.
fixEdge<HeapBigIntUse>(valueToStore);
break;
}
}
break;
Expand Down
9 changes: 9 additions & 0 deletions Source/JavaScriptCore/dfg/DFGOperations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6387,6 +6387,15 @@ JSC_DEFINE_JIT_OPERATION(operationInt64ToBigInt, EncodedJSValue, (JSGlobalObject
OPERATION_RETURN(scope, JSValue::encode(JSBigInt::makeHeapBigIntOrBigInt32(globalObject, value)));
}

JSC_DEFINE_JIT_OPERATION(operationUint64ToBigInt, EncodedJSValue, (JSGlobalObject* globalObject, uint64_t value))
{
VM& vm = globalObject->vm();
CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
OPERATION_RETURN(scope, JSValue::encode(JSBigInt::makeHeapBigIntOrBigInt32(globalObject, value)));
}

JSC_DEFINE_JIT_OPERATION(operationThrowDFG, void, (JSGlobalObject* globalObject, EncodedJSValue valueToThrow))
{
VM& vm = globalObject->vm();
Expand Down
1 change: 1 addition & 0 deletions Source/JavaScriptCore/dfg/DFGOperations.h
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ JSC_DECLARE_JIT_OPERATION(operationDateGetTimezoneOffset, EncodedJSValue, (VM*,
JSC_DECLARE_JIT_OPERATION(operationDateGetYear, EncodedJSValue, (VM*, DateInstance*));

JSC_DECLARE_JIT_OPERATION(operationInt64ToBigInt, EncodedJSValue, (JSGlobalObject*, int64_t));
JSC_DECLARE_JIT_OPERATION(operationUint64ToBigInt, EncodedJSValue, (JSGlobalObject*, uint64_t));

JSC_DECLARE_JIT_OPERATION(operationProcessTypeProfilerLogDFG, void, (VM*));

Expand Down
Loading
Loading