Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
72 changes: 72 additions & 0 deletions JSTests/stress/buffer-accessor-jit-bigint-write.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//@ requireOptions("--useDollarVM=1")

// writeBigInt64* / writeBigUInt64*: a BufferWrite node with a heap-BigInt value edge and an inline
// "fits in 64 bits" check; anything wider (or negative, for the unsigned writers) OSR-exits to the
// host function, which throws.

function shouldBe(actual, expected, message) {
if (actual !== expected) throw new Error(message + ": expected " + expected + " but got " + actual);
}
function shouldThrow(f, expected, message) {
let error = null;
try {
f();
} catch (e) {
error = e;
}
if (!(error instanceof expected)) throw new Error(message + ": expected a " + expected.name + " but got " + error);
}

const accessors = $vm.createBufferAccessors();
class Buffer extends Uint8Array {}
Object.assign(Buffer.prototype, accessors);

const buf = new Buffer(64);
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);

function writeBigInt64LE(b, v, o) {
return b.writeBigInt64LE(v, o);
}
noInline(writeBigInt64LE);
function writeBigInt64BE(b, v, o) {
return b.writeBigInt64BE(v, o);
}
noInline(writeBigInt64BE);
function writeBigUInt64LE(b, v, o) {
return b.writeBigUInt64LE(v, o);
}
noInline(writeBigUInt64LE);
function writeBigUInt64BE(b, v, o) {
return b.writeBigUInt64BE(v, o);
}
noInline(writeBigUInt64BE);

const values = [0n, 1n, -1n, 42n, -42n, 2n ** 31n, -(2n ** 31n), 2n ** 32n + 7n, 2n ** 63n - 1n, -(2n ** 63n)];
for (let i = 0; i < 2e4; ++i) {
const o = (i & 7) * 8;
const v = values[i % values.length];
shouldBe(writeBigInt64LE(buf, v, o), o + 8, "writeBigInt64LE result");
shouldBe(dv.getBigInt64(o, true), v, "writeBigInt64LE store");
shouldBe(writeBigInt64BE(buf, v, o), o + 8, "writeBigInt64BE result");
shouldBe(dv.getBigInt64(o, false), v, "writeBigInt64BE store");
if (v >= 0n) {
shouldBe(writeBigUInt64LE(buf, v, o), o + 8, "writeBigUInt64LE result");
shouldBe(dv.getBigUint64(o, true), v, "writeBigUInt64LE store");
shouldBe(writeBigUInt64BE(buf, v, o), o + 8, "writeBigUInt64BE result");
shouldBe(dv.getBigUint64(o, false), v, "writeBigUInt64BE store");
}
}

// unsigned max, and the out-of-range / wrong-type exits.
for (let i = 0; i < 2e4; ++i) {
shouldBe(writeBigUInt64LE(buf, 2n ** 64n - 1n, 0), 8, "unsigned max");
shouldBe(dv.getBigUint64(0, true), 2n ** 64n - 1n, "unsigned max store");
shouldThrow(() => writeBigUInt64LE(buf, -1n, 0), RangeError, "unsigned negative");
shouldThrow(() => writeBigUInt64LE(buf, 2n ** 64n, 0), RangeError, "unsigned too big");
shouldThrow(() => writeBigInt64LE(buf, 2n ** 63n, 0), RangeError, "signed too big");
shouldThrow(() => writeBigInt64LE(buf, -(2n ** 63n) - 1n, 0), RangeError, "signed too small");
shouldThrow(() => writeBigInt64LE(buf, 2n ** 100n, 0), RangeError, "way too big");
shouldThrow(() => writeBigInt64LE(buf, 5, 0), TypeError, "a number is not a BigInt");
shouldThrow(() => writeBigInt64LE(buf, 0n, 57), RangeError, "out of bounds");
shouldBe(dv.getBigInt64(0, true), -1n, "the failed writes stored nothing");
}
137 changes: 137 additions & 0 deletions JSTests/stress/buffer-accessor-jit-exits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//@ requireOptions("--useDollarVM=1")

// The Buffer accessor nodes speculate: a Uint8Array receiver, an int32 in-bounds offset, an in-range
// value. Everything else must OSR-exit to the host function and behave exactly as it does in the
// interpreter -- in particular an out-of-bounds access must always throw, never return undefined,
// and no store may happen (or happen twice) around an exit.

function shouldThrow(f, expected, message) {
let error = null;
try {
f();
} catch (e) {
error = e;
}
if (!error) throw new Error(message + ": expected a " + expected.name + " but got no throw");
if (!(error instanceof expected)) throw new Error(message + ": expected a " + expected.name + " but got " + error);
}
function shouldBe(actual, expected, message) {
if (actual !== expected) throw new Error(message + ": expected " + expected + " but got " + actual);
}

const accessors = $vm.createBufferAccessors();
class Buffer extends Uint8Array {}
Object.assign(Buffer.prototype, accessors);

const buf = new Buffer(16);
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);

// Offsets that leave the fast path.
function readInt32LE(b, o) {
return b.readInt32LE(o);
}
noInline(readInt32LE);
function readUInt8(b, o) {
return b.readUInt8(o);
}
noInline(readUInt8);
for (let i = 0; i < 1e4; ++i) {
dv.setInt32(12, i, true);
shouldBe(readInt32LE(buf, 12), i, "last valid offset");
shouldThrow(() => readInt32LE(buf, 13), RangeError, "one past the last valid offset");
shouldThrow(() => readInt32LE(buf, 16), RangeError, "offset === length");
shouldThrow(() => readInt32LE(buf, -1), RangeError, "negative offset");
shouldThrow(() => readInt32LE(buf, 1.5), RangeError, "fractional offset");
shouldThrow(() => readInt32LE(buf, NaN), RangeError, "NaN offset");
shouldThrow(() => readInt32LE(buf, Infinity), RangeError, "Infinity offset");
shouldThrow(() => readInt32LE(buf, "0"), RangeError, "string offset");
shouldBe(readInt32LE(buf, 4.0), dv.getInt32(4, true), "integral double offset");
shouldBe(readUInt8(buf, 15), buf[15], "last byte");
shouldThrow(() => readUInt8(buf, 16), RangeError, "one-byte read one past the end");
}

// Values that leave the fast path: the narrow writers range-check.
function writeInt8(b, v, o) {
return b.writeInt8(v, o);
}
noInline(writeInt8);
function writeUInt16BE(b, v, o) {
return b.writeUInt16BE(v, o);
}
noInline(writeUInt16BE);
function writeUInt32LE(b, v, o) {
return b.writeUInt32LE(v, o);
}
noInline(writeUInt32LE);
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeInt8(buf, 127, 3), 4, "writeInt8 max");
shouldBe(dv.getInt8(3), 127, "writeInt8 max store");
shouldBe(writeInt8(buf, -128, 3), 4, "writeInt8 min");
shouldBe(dv.getInt8(3), -128, "writeInt8 min store");
shouldThrow(() => writeInt8(buf, 128, 3), RangeError, "writeInt8 too big");
shouldThrow(() => writeInt8(buf, -129, 3), RangeError, "writeInt8 too small");
shouldBe(dv.getInt8(3), -128, "a throwing writeInt8 stores nothing");
shouldThrow(() => writeUInt16BE(buf, -1, 2), RangeError, "writeUInt16BE negative");
shouldThrow(() => writeUInt16BE(buf, 65536, 2), RangeError, "writeUInt16BE too big");
shouldBe(writeUInt16BE(buf, 65535, 2), 4, "writeUInt16BE max");
shouldBe(dv.getUint16(2, false), 65535, "writeUInt16BE max store");
shouldThrow(() => writeUInt32LE(buf, -1, 4), RangeError, "writeUInt32LE negative");
shouldThrow(() => writeUInt32LE(buf, 4294967296, 4), RangeError, "writeUInt32LE too big");
shouldThrow(() => writeUInt32LE(buf, 4294967295, 14), RangeError, "writeUInt32LE out of bounds");
shouldBe(writeUInt32LE(buf, 4294967295, 4), 8, "writeUInt32LE max");
shouldBe(dv.getUint32(4, true), 4294967295, "writeUInt32LE max store");
}

// Receivers other than a Uint8Array (Buffer) exit the CheckArray and take the host path, which
// (unlike the JIT'd form) accepts any ArrayBufferView with byte-length semantics and rejects the rest.
{
const floats = new Float64Array(4);
const dataView = new DataView(new ArrayBuffer(8));
const otherView = new Uint32Array(4);
function readOnAnything(b, o) {
return accessors.readInt32LE.call(b, o);
}
noInline(readOnAnything);
for (let i = 0; i < 1e4; ++i) {
floats[0] = i;
shouldBe(readOnAnything(buf, 0), dv.getInt32(0, true), "Buffer receiver");
shouldBe(readOnAnything(floats, 0), new DataView(floats.buffer).getInt32(0, true), "Float64Array receiver");
shouldBe(readOnAnything(dataView, 4), 0, "DataView receiver");
shouldBe(readOnAnything(otherView, 12), 0, "Uint32Array receiver (byte semantics)");
shouldThrow(() => readOnAnything({}, 0), TypeError, "plain object receiver");
shouldThrow(() => readOnAnything(null, 0), TypeError, "null receiver");
}
}

// A detached receiver has length 0: always the host path, always a RangeError.
{
const detached = new Buffer(16);
function readDetached(b) {
return b.readUInt16LE(0);
}
noInline(readDetached);
for (let i = 0; i < 1e3; ++i) shouldBe(readDetached(detached), 0, "before detach");
transferArrayBuffer(detached.buffer);
for (let i = 0; i < 1e3; ++i) shouldThrow(() => readDetached(detached), RangeError, "after detach");
}

// A write's value coercion happens exactly once even when the offset then fails (the JIT exits
// before any effect; the host coerces first and validates the offset second).
{
let calls = 0;
const value = {
valueOf() {
calls++;
return 5;
},
};
function writeWithBadOffset(b, o) {
return b.writeInt32LE(value, o);
}
noInline(writeWithBadOffset);
for (let i = 0; i < 1e3; ++i) {
shouldBe(writeWithBadOffset(buf, 0), 4, "good offset");
shouldThrow(() => writeWithBadOffset(buf, 100), RangeError, "bad offset");
}
shouldBe(calls, 2000, "valueOf calls");
}
99 changes: 99 additions & 0 deletions JSTests/stress/buffer-accessor-jit-resizable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//@ requireOptions("--useDollarVM=1")

// Buffer accessors on views over a resizable ArrayBuffer / a growable SharedArrayBuffer: the length
// load must track the current (byte) length, the first resizable receiver at a site OSR-exits
// (UnexpectedResizableArrayBufferView) and the recompile keeps working, and out-of-bounds after a
// shrink must throw.

function shouldBe(actual, expected, message) {
if (actual !== expected) throw new Error(message + ": expected " + expected + " but got " + actual);
}
function shouldThrow(f, expected, message) {
let error = null;
try {
f();
} catch (e) {
error = e;
}
if (!(error instanceof expected)) throw new Error(message + ": expected a " + expected.name + " but got " + error);
}

const accessors = $vm.createBufferAccessors();
class Buffer extends Uint8Array {}
Object.assign(Buffer.prototype, accessors);

function readUInt16LE(b, o) {
return b.readUInt16LE(o);
}
noInline(readUInt16LE);
function writeUInt16LE(b, v, o) {
return b.writeUInt16LE(v, o);
}
noInline(writeUInt16LE);

// Warm up on fixed-length views first, so the resizable ones arrive after the code is optimized.
{
const fixed = new Buffer(64);
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeUInt16LE(fixed, i & 0xffff, i & 62), (i & 62) + 2, "fixed write");
shouldBe(readUInt16LE(fixed, i & 62), i & 0xffff, "fixed read");
}
}

// A length-tracking view over a resizable ArrayBuffer.
{
const rab = new ArrayBuffer(16, { maxByteLength: 64 });
const tracking = new Buffer(rab);
shouldBe(tracking.length, 16, "tracking length");
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeUInt16LE(tracking, i & 0xffff, 14), 16, "tracking write at the end");
shouldBe(readUInt16LE(tracking, 14), i & 0xffff, "tracking read at the end");
shouldThrow(() => readUInt16LE(tracking, 15), RangeError, "tracking read straddling the end");
shouldThrow(() => readUInt16LE(tracking, 16), RangeError, "tracking read past the end");
}
rab.resize(64);
shouldBe(tracking.length, 64, "grown tracking length");
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeUInt16LE(tracking, i & 0xffff, 62), 64, "write near the grown end");
shouldBe(readUInt16LE(tracking, 62), i & 0xffff, "read near the grown end");
}
rab.resize(8);
shouldBe(tracking.length, 8, "shrunk tracking length");
for (let i = 0; i < 1e3; ++i) {
shouldBe(readUInt16LE(tracking, 6), 0, "read near the shrunk end (never written)");
shouldThrow(() => readUInt16LE(tracking, 7), RangeError, "read straddling the shrunk end");
shouldThrow(() => writeUInt16LE(tracking, 0, 62), RangeError, "write past the shrunk end");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// A fixed-length view over a resizable ArrayBuffer that shrinks out from under it.
{
const rab = new ArrayBuffer(32, { maxByteLength: 64 });
const fixed = new Buffer(rab, 8, 16);
shouldBe(fixed.length, 16, "fixed-length view length");
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeUInt16LE(fixed, i & 0xffff, 14), 16, "fixed-length view write");
shouldBe(readUInt16LE(fixed, 14), i & 0xffff, "fixed-length view read");
}
rab.resize(16); // The view [8, 24) no longer fits: it is out of bounds now.
for (let i = 0; i < 1e3; ++i) {
shouldThrow(() => readUInt16LE(fixed, 0), RangeError, "out-of-bounds view read");
shouldThrow(() => writeUInt16LE(fixed, 0, 0), RangeError, "out-of-bounds view write");
}
}

// A view over a growable SharedArrayBuffer.
{
const gsab = new SharedArrayBuffer(16, { maxByteLength: 64 });
const shared = new Buffer(gsab);
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeUInt16LE(shared, i & 0xffff, 14), 16, "shared write at the end");
shouldBe(readUInt16LE(shared, 14), i & 0xffff, "shared read at the end");
shouldThrow(() => readUInt16LE(shared, 15), RangeError, "shared read past the end");
}
gsab.grow(64);
for (let i = 0; i < 1e4; ++i) {
shouldBe(writeUInt16LE(shared, i & 0xffff, 62), 64, "write near the grown shared end");
shouldBe(readUInt16LE(shared, 62), i & 0xffff, "read near the grown shared end");
}
}
Loading
Loading