diff --git a/JSTests/stress/buffer-accessor-jit-bigint-write.js b/JSTests/stress/buffer-accessor-jit-bigint-write.js new file mode 100644 index 0000000000000..c162aa7363471 --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-bigint-write.js @@ -0,0 +1,67 @@ +//@ requireOptions("--useDollarVM=1") + +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"); + } +} + +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"); +} diff --git a/JSTests/stress/buffer-accessor-jit-byteoffset.js b/JSTests/stress/buffer-accessor-jit-byteoffset.js new file mode 100644 index 0000000000000..aa454b76cbf3a --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-byteoffset.js @@ -0,0 +1,46 @@ +//@ requireOptions("--useDollarVM=1") + +let ab; +try { + ab = new ArrayBuffer(4 * 2 ** 30); +} catch (e) { + quit(); +} +Object.assign(Uint8Array.prototype, $vm.createBufferAccessors()); + +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 tailOffset = 4 * 2 ** 30 - 64; +const tail = new Uint8Array(ab, tailOffset, 64); +const wide = new Uint8Array(ab, 2 ** 31); +const raw = new DataView(ab); + +function readAt(v, o) { return v.readInt32LE(o); } +function writeAt(v, x, o) { return v.writeInt32LE(x, o); } +noInline(readAt); +noInline(writeAt); + +for (let i = 0; i < 3e5; ++i) { + shouldBe(writeAt(tail, i, 8), 12, "write into the ~4GB byteOffset view"); + shouldBe(readAt(tail, 8), i, "read the ~4GB byteOffset view back"); + shouldBe(writeAt(wide, ~i, wide.length - 4), wide.length, "write at the top of the 2GB byteOffset view"); + shouldBe(readAt(wide, wide.length - 4), ~i, "read at the top of the 2GB byteOffset view"); +} +shouldBe(raw.getInt32(tailOffset + 8, true), 3e5 - 1, "the store landed at byteOffset + offset in the raw buffer"); +shouldBe(raw.getInt32(2 ** 31 + wide.length - 4, true), ~(3e5 - 1), "the store landed at the 2GB byteOffset"); +shouldThrow(() => readAt(tail, 61), RangeError, "straddling the end of the small view"); +shouldThrow(() => writeAt(wide, 0, wide.length - 3), RangeError, "straddling the end of the wide view"); +shouldBe(numberOfDFGCompiles(readAt) <= 3, true, "the huge byteOffset does not cause recompiles"); diff --git a/JSTests/stress/buffer-accessor-jit-differential.js b/JSTests/stress/buffer-accessor-jit-differential.js new file mode 100644 index 0000000000000..e52b406c42c33 --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-differential.js @@ -0,0 +1,317 @@ +//@ requireOptions("--useDollarVM=1") + +class Buffer extends Uint8Array {} +Object.assign(Buffer.prototype, $vm.createBufferAccessors()); + +let seed = 0x9e3779b1; +function rand() { + seed ^= seed << 13; + seed |= 0; + seed ^= seed >>> 17; + seed ^= seed << 5; + seed |= 0; + return (seed >>> 0) / 4294967296; +} +function pick(list) { + return list[(rand() * list.length) | 0]; +} +function randInt(lowInclusive, highInclusive) { + return lowInclusive + ((rand() * (highInclusive - lowInclusive + 1)) | 0); +} + +const accessorNames = Object.keys($vm.createBufferAccessors()); +const readers = accessorNames.filter(n => n.startsWith("read")); +const writers = accessorNames.filter(n => n.startsWith("write")); + +function describe(name) { + const isWrite = name.startsWith("write"); + const isFloat = /Float/.test(name); + const isDouble = /Double/.test(name); + const isBigInt = /Big/.test(name); + const isVarWidth = /Int(LE|BE)$/.test(name) && !/(8|16|32|64)/.test(name); + const isSigned = !/UInt/.test(name); + let byteSize = isDouble ? 8 : isFloat ? 4 : isBigInt ? 8 : isVarWidth ? 0 : Number(name.match(/(8|16|32|64)/)[0]) / 8; + return { isWrite, isFloat: isFloat || isDouble, isBigInt, isVarWidth, isSigned, byteSize }; +} + +function makeInvoker(name, arm) { + if (!/^[A-Za-z0-9_]+$/.test(name)) throw new Error("unexpected accessor name: " + name); + const source = + "return function invoke_" + arm + "_" + name + "(receiver, args, box) {" + + " try {" + + " let result;" + + " switch (args.length) {" + + " case 0: result = receiver." + name + "(); break;" + + " case 1: result = receiver." + name + "(args[0]); break;" + + " case 2: result = receiver." + name + "(args[0], args[1]); break;" + + " default: result = receiver." + name + "(args[0], args[1], args[2]); break;" + + " }" + + " box.value = result; box.error = null;" + + " } catch (e) {" + + " box.value = undefined;" + + " box.error = e === null || typeof e !== 'object'" + + " ? 'throw:' + String(e)" + + " : 'throw:' + e.constructor.name + ':' + (e.code === undefined ? '' : e.code) + ':' + e.message;" + + " }" + + "};"; + return new Function(source)(); +} + +const invokerPairs = new Map(); +function pairFor(name, key) { + let pair = invokerPairs.get(key); + if (!pair) { + const jitInvoke = makeInvoker(name, "jit"); + const refInvoke = makeInvoker(name, "ref"); + noDFG(refInvoke); + noFTL(refInvoke); + pair = { jitInvoke, refInvoke }; + invokerPairs.set(key, pair); + } + return pair; +} + +function cleanValue(shape, byteSize) { + if (shape.isBigInt) { + const bits = shape.isSigned + ? [-(2n ** 63n), 2n ** 63n - 1n, 0n, -1n, BigInt(randInt(-1e6, 1e6))] + : [0n, 2n ** 64n - 1n, 12345678901234567890n, BigInt(randInt(0, 1e6))]; + return pick(bits); + } + if (shape.isFloat) + return pick([ + () => rand() * 1e6 - 5e5, + () => Math.fround(rand() * 100), + () => -0, + () => Infinity, + () => 2 ** -1074, + () => 1e300, + ])(); + const size = shape.isVarWidth ? byteSize : shape.byteSize; + const min = shape.isSigned ? -(2 ** (8 * size - 1)) : 0; + const max = shape.isSigned ? 2 ** (8 * size - 1) - 1 : 2 ** (8 * size) - 1; + return pick([ + () => min, + () => max, + () => randInt(min, max), + () => randInt(min, max), + () => randInt(min, max), + () => 0, + ])(); +} + +function dirtyValue() { + return pick([ + () => (rand() * 2 ** 32) | 0, + () => -((rand() * 2 ** 31) | 0), + () => 2 ** 31, + () => 2 ** 32, + () => -(2 ** 31) - 1, + () => rand() * 1e6 - 5e5, + () => 0.5, + () => -0.5, + () => -0, + () => NaN, + () => Infinity, + () => -Infinity, + () => 2 ** 53 + 1, + () => "42", + () => "abc", + () => "", + () => true, + () => false, + () => null, + () => undefined, + () => 5n, + () => 2n ** 63n, + () => 2n ** 64n, + () => -1n, + () => -(2n ** 63n) - 1n, + () => Symbol("v"), + ])(); +} + +function dirtyOffset(length) { + return pick([ + () => randInt(0, length + 3), + () => -randInt(1, 8), + () => length - randInt(0, 8), + () => rand() * length, + () => -0, + () => 2 ** 31 + randInt(0, 8), + () => 2 ** 32, + () => 2 ** 53 + 2, + () => NaN, + () => Infinity, + () => -Infinity, + () => undefined, + () => null, + () => String(randInt(0, length)), + () => "not a number", + () => true, + () => Symbol("s"), + () => 3n, + ])(); +} + +function dirtyByteLength() { + return pick([ + () => randInt(1, 6), + () => 0, + () => 7, + () => -1, + () => 2.5, + () => NaN, + () => "4", + () => undefined, + () => 9n, + ])(); +} + +function makeReceiverFactory() { + return pick([ + () => new Buffer(32), + () => new Buffer(new ArrayBuffer(64), 8, 24), + () => new Buffer(7), + () => new Buffer(new ArrayBuffer(16, { maxByteLength: 64 })), + () => new Buffer(new ArrayBuffer(48, { maxByteLength: 64 }), 8, 16), + () => new Buffer(new SharedArrayBuffer(32, { maxByteLength: 64 })), + ]); +} + +function sameOutcome(jitBox, refBox) { + if (jitBox.error !== refBox.error) return false; + if (jitBox.error !== null) return true; + return ( + Object.is(jitBox.value, refBox.value) || + (typeof jitBox.value === "bigint" && jitBox.value === refBox.value) || + (Number.isNaN(jitBox.value) && Number.isNaN(refBox.value)) + ); +} +function sameBytes(a, b) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false; + return true; +} + +const rounds = 200; +const opsPerRound = 1500; +let mismatch = null; +let cleanOps = 0, + dirtyOps = 0; + +for (let round = 0; round < rounds && !mismatch; ++round) { + const factory = makeReceiverFactory(); + const jitReceiver = factory(); + const refReceiver = factory(); + if (jitReceiver.length !== refReceiver.length || jitReceiver.buffer.constructor !== refReceiver.buffer.constructor) + continue; + const name = pick(rand() < 0.5 ? readers : writers); + const shape = describe(name); + const clean = rand() < 0.6; + const { jitInvoke, refInvoke } = pairFor(name, name + (clean ? ":clean" : ":dirty")); + const jitBox = { value: undefined, error: null }; + const refBox = { value: undefined, error: null }; + let resizeCountdown = clean ? Infinity : 100 + randInt(0, 400); + const width = shape.isVarWidth ? randInt(1, 6) : shape.byteSize; + + for (let step = 0; step < opsPerRound; ++step) { + const args = []; + const maxOffset = jitReceiver.length - width; + if (clean) { + if (maxOffset < 0) break; + if (shape.isWrite) args.push(cleanValue(shape, width)); + args.push(randInt(0, maxOffset)); + if (shape.isVarWidth) args.push(width); + cleanOps++; + } else { + if (shape.isWrite) args.push(dirtyValue()); + if (rand() < 0.9 || shape.isVarWidth) args.push(dirtyOffset(jitReceiver.length)); + if (shape.isVarWidth) args.push(dirtyByteLength()); + while (args.length && args[args.length - 1] === undefined && rand() < 0.3) args.pop(); + if (args.length && typeof args[args.length - 1] === "symbol" && rand() < 0.5) args[args.length - 1] = 0; + dirtyOps++; + } + + jitInvoke(jitReceiver, args, jitBox); + refInvoke(refReceiver, args, refBox); + + if (!sameOutcome(jitBox, refBox)) { + mismatch = { + round, + step, + name, + clean, + args, + jit: jitBox.error === null ? String(jitBox.value) : jitBox.error, + ref: refBox.error === null ? String(refBox.value) : refBox.error, + }; + break; + } + if (!sameBytes(jitReceiver, refReceiver)) { + mismatch = { + round, + step, + name, + clean, + args, + jit: "bytes:" + Array.from(jitReceiver).join(","), + ref: "bytes:" + Array.from(refReceiver).join(","), + }; + break; + } + + if (--resizeCountdown === 0) { + resizeCountdown = 100 + randInt(0, 400); + const jb = jitReceiver.buffer, + rb = refReceiver.buffer; + if (typeof jb.resize === "function" && jb.resizable) { + const size = randInt(0, jb.maxByteLength); + try { + jb.resize(size); + rb.resize(size); + } catch {} + } else if (typeof jb.grow === "function" && jb.growable) { + const size = randInt(jb.byteLength, jb.maxByteLength); + try { + jb.grow(size); + rb.grow(size); + } catch {} + } else if (rand() < 0.15) { + try { + structuredClone(jb, { transfer: [jb] }); + structuredClone(rb, { transfer: [rb] }); + } catch {} + } + } + } +} + +if (mismatch) { + const shown = mismatch.args.map(a => + typeof a === "bigint" + ? a.toString() + "n" + : typeof a === "symbol" + ? "Symbol" + : typeof a === "object" && a !== null + ? "{obj}" + : String(a), + ); + throw new Error( + "differential mismatch (seed 0x9e3779b1): round " + + mismatch.round + + " step " + + mismatch.step + + " " + + mismatch.name + + (mismatch.clean ? " [clean]" : " [dirty]") + + "(" + + shown.join(", ") + + ") jit=" + + mismatch.jit + + " ref=" + + mismatch.ref, + ); +} +if (cleanOps < 50000 || dirtyOps < 20000) + throw new Error("fuzzer under-covered: cleanOps=" + cleanOps + " dirtyOps=" + dirtyOps); diff --git a/JSTests/stress/buffer-accessor-jit-exits.js b/JSTests/stress/buffer-accessor-jit-exits.js new file mode 100644 index 0000000000000..17b8bb0a89686 --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-exits.js @@ -0,0 +1,125 @@ +//@ requireOptions("--useDollarVM=1") + +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); + +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"); +} + +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"); +} + +{ + 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"); + } +} + +{ + 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"); +} + +{ + 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"); +} diff --git a/JSTests/stress/buffer-accessor-jit-large.js b/JSTests/stress/buffer-accessor-jit-large.js new file mode 100644 index 0000000000000..e0562955dcf63 --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-large.js @@ -0,0 +1,46 @@ +//@ requireOptions("--useDollarVM=1") + +let big; +try { + big = new Uint8Array(3 * 2 ** 30); +} catch (e) { + quit(); +} +Object.assign(Uint8Array.prototype, $vm.createBufferAccessors()); + +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); +} + +function readAt(b, o) { + return b.readInt32LE(o); +} +function writeAt(b, v, o) { + return b.writeInt32LE(v, o); +} +noInline(readAt); +noInline(writeAt); + +const small = new Uint8Array(64); +const top = 2 ** 31 - 4; +for (let i = 0; i < 5e5; ++i) { + shouldBe(writeAt(big, i, 100), 104, "write low"); + shouldBe(readAt(big, 100), i, "read low"); + shouldBe(writeAt(big, ~i, top), top + 4, "write at the int32 offset ceiling"); + shouldBe(readAt(big, top), ~i, "read at the int32 offset ceiling"); + shouldBe(writeAt(small, i, 60), 64, "the same site with a small receiver"); + shouldBe(readAt(small, 60), i, "read the small receiver back"); +} +shouldBe(numberOfDFGCompiles(readAt) <= 3, true, "the large receiver does not cause recompiles"); +shouldBe(numberOfDFGCompiles(writeAt) <= 3, true, "the large receiver does not cause recompiles"); +shouldThrow(() => readAt(big, big.length - 3), RangeError, "straddling the end of a >2GB view"); +shouldThrow(() => writeAt(big, 0, big.length), RangeError, "past the end of a >2GB view"); diff --git a/JSTests/stress/buffer-accessor-jit-resizable.js b/JSTests/stress/buffer-accessor-jit-resizable.js new file mode 100644 index 0000000000000..a08cc0bf4cb9d --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-resizable.js @@ -0,0 +1,90 @@ +//@ requireOptions("--useDollarVM=1") + +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); + +{ + 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"); + } +} + +{ + 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"); + } +} + +{ + 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); + 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"); + } +} + +{ + 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"); + } +} diff --git a/JSTests/stress/buffer-accessor-jit-varwidth.js b/JSTests/stress/buffer-accessor-jit-varwidth.js new file mode 100644 index 0000000000000..921dcefa07e18 --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit-varwidth.js @@ -0,0 +1,113 @@ +//@ requireOptions("--useDollarVM=1") + +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); +} + +class Buffer extends Uint8Array {} +Object.assign(Buffer.prototype, $vm.createBufferAccessors()); + +const buf = new Buffer(64); +const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); +for (let i = 0; i < buf.length; ++i) buf[i] = (i * 71 + 5) & 0xff; + +function readIntLE(b, o, l) { + return b.readIntLE(o, l); +} +function readIntBE(b, o, l) { + return b.readIntBE(o, l); +} +function readUIntLE(b, o, l) { + return b.readUIntLE(o, l); +} +function readUIntBE(b, o, l) { + return b.readUIntBE(o, l); +} +function writeIntLE(b, v, o, l) { + return b.writeIntLE(v, o, l); +} +function writeUIntBE(b, v, o, l) { + return b.writeUIntBE(v, o, l); +} +function readInt32ConstLE(b, o) { + return b.readIntLE(o, 4); +} +function readUInt16ConstBE(b, o) { + return b.readUIntBE(o, 2); +} +function readUInt24ConstLE(b, o) { + return b.readUIntLE(o, 3); +} +function writeInt8Const(b, v, o) { + return b.writeIntLE(v, o, 1); +} +for (const f of [ + readIntLE, + readIntBE, + readUIntLE, + readUIntBE, + writeIntLE, + writeUIntBE, + readInt32ConstLE, + readUInt16ConstBE, + readUInt24ConstLE, + writeInt8Const, +]) + noInline(f); + +const uint = (o, l, le) => { + let value = 0; + for (let i = 0; i < l; ++i) value = le ? value + buf[o + i] * 2 ** (8 * i) : value * 256 + buf[o + i]; + return value; +}; +const sint = (o, l, le) => { + const value = uint(o, l, le); + return value >= 2 ** (8 * l - 1) ? value - 2 ** (8 * l) : value; +}; + +for (let i = 0; i < 1e4; ++i) { + const o = i & 15; + for (const l of [1, 2, 3, 4, 5, 6]) { + shouldBe(readIntLE(buf, o, l), sint(o, l, true), "readIntLE " + l); + shouldBe(readIntBE(buf, o, l), sint(o, l, false), "readIntBE " + l); + shouldBe(readUIntLE(buf, o, l), uint(o, l, true), "readUIntLE " + l); + shouldBe(readUIntBE(buf, o, l), uint(o, l, false), "readUIntBE " + l); + } + shouldBe(readInt32ConstLE(buf, o), dv.getInt32(o, true), "readIntLE const 4"); + shouldBe(readUInt16ConstBE(buf, o), dv.getUint16(o, false), "readUIntBE const 2"); + shouldBe(readUInt24ConstLE(buf, o), uint(o, 3, true), "readUIntLE const 3"); +} + +const scratch = new Buffer(64); +const scratchDV = new DataView(scratch.buffer); +for (let i = 0; i < 1e4; ++i) { + const o = i & 15; + shouldBe(writeIntLE(scratch, -(i & 0x7fff), o, 4), o + 4, "writeIntLE 4 result"); + shouldBe(scratchDV.getInt32(o, true), -(i & 0x7fff), "writeIntLE 4 store"); + shouldBe(writeUIntBE(scratch, i & 0xffffff, o, 3), o + 3, "writeUIntBE 3 result"); + shouldBe(scratch[o] * 65536 + scratch[o + 1] * 256 + scratch[o + 2], i & 0xffffff, "writeUIntBE 3 store"); + shouldBe(writeInt8Const(scratch, (i & 0xff) - 128, o), o + 1, "writeIntLE const 1 result"); + shouldBe(scratchDV.getInt8(o), (i & 0xff) - 128, "writeIntLE const 1 store"); +} + +for (let i = 0; i < 3e3; ++i) { + shouldThrow(() => readIntLE(buf, 0, 7), RangeError, "byteLength 7"); + shouldThrow(() => readIntLE(buf, 0, 0), RangeError, "byteLength 0"); + shouldThrow(() => readInt32ConstLE(buf, undefined), TypeError, "undefined offset"); + shouldThrow(() => readInt32ConstLE(buf, 61), RangeError, "out of bounds"); + shouldThrow(() => writeInt8Const(scratch, 128, 0), RangeError, "value out of range"); + shouldThrow(() => writeUIntBE(scratch, 2 ** 24, 0, 3), RangeError, "3-byte value out of range"); + shouldThrow(() => writeIntLE(scratch, NaN, 0, 4), RangeError, "NaN value"); + shouldThrow(() => writeIntLE(scratch, Infinity, 0, 4), RangeError, "Infinity value"); + shouldThrow(() => writeIntLE(scratch, -Infinity, 0, 4), RangeError, "-Infinity value"); + shouldThrow(() => writeInt8Const(scratch, NaN, 0), RangeError, "NaN value, constant width"); +} diff --git a/JSTests/stress/buffer-accessor-jit.js b/JSTests/stress/buffer-accessor-jit.js new file mode 100644 index 0000000000000..b796a11dca896 --- /dev/null +++ b/JSTests/stress/buffer-accessor-jit.js @@ -0,0 +1,139 @@ +//@ requireOptions("--useDollarVM=1") + +function shouldBe(actual, expected, message) { + if (Number.isNaN(expected)) { + if (!Number.isNaN(actual)) throw new Error(message + ": expected NaN but got " + actual); + return; + } + 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(64); +const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); +for (let i = 0; i < buf.length; ++i) buf[i] = (i * 37 + 11) & 0xff; + +let readers = [ + ["readInt8", o => dv.getInt8(o)], + ["readUInt8", o => dv.getUint8(o)], + ["readInt16LE", o => dv.getInt16(o, true)], + ["readInt16BE", o => dv.getInt16(o, false)], + ["readUInt16LE", o => dv.getUint16(o, true)], + ["readUInt16BE", o => dv.getUint16(o, false)], + ["readInt32LE", o => dv.getInt32(o, true)], + ["readInt32BE", o => dv.getInt32(o, false)], + ["readUInt32LE", o => dv.getUint32(o, true)], + ["readUInt32BE", o => dv.getUint32(o, false)], + ["readFloatLE", o => dv.getFloat32(o, true)], + ["readFloatBE", o => dv.getFloat32(o, false)], + ["readDoubleLE", o => dv.getFloat64(o, true)], + ["readDoubleBE", o => dv.getFloat64(o, false)], + ["readBigInt64LE", o => dv.getBigInt64(o, true)], + ["readBigInt64BE", o => dv.getBigInt64(o, false)], + ["readBigUInt64LE", o => dv.getBigUint64(o, true)], + ["readBigUInt64BE", o => dv.getBigUint64(o, false)], +]; +for (let [name, reference] of readers) { + let read = new Function("b", "o", `return b.${name}(o);`); + noInline(read); + for (let i = 0; i < 1e4; ++i) { + let o = i & 31; + shouldBe(read(buf, o), reference(o), name + " @" + o); + } + let readDefault = new Function("b", `return b.${name}();`); + noInline(readDefault); + let accessor = accessors[name]; + let readPlain = function (b) { + return accessor.call(b, 0); + }; + noInline(readPlain); + let plain = new Uint8Array(buf); + for (let i = 0; i < 1e4; ++i) { + shouldBe(readDefault(buf), reference(0), name + " default offset"); + shouldBe(readPlain(plain), reference(0), name + " on a plain Uint8Array"); + } +} + +let writers = [ + ["writeInt8", o => dv.getInt8(o), i => (i & 0xff) - 128], + ["writeUInt8", o => dv.getUint8(o), i => i & 0xff], + ["writeInt16LE", o => dv.getInt16(o, true), i => (i & 0xffff) - 0x8000], + ["writeInt16BE", o => dv.getInt16(o, false), i => (i & 0xffff) - 0x8000], + ["writeUInt16LE", o => dv.getUint16(o, true), i => i & 0xffff], + ["writeUInt16BE", o => dv.getUint16(o, false), i => i & 0xffff], + ["writeInt32LE", o => dv.getInt32(o, true), i => (-i * 1000) | 0], + ["writeInt32BE", o => dv.getInt32(o, false), i => (i * 1000) | 0], + ["writeUInt32LE", o => dv.getUint32(o, true), i => 4294967295 - i], + ["writeUInt32BE", o => dv.getUint32(o, false), i => 2147483648 + i], + ["writeFloatLE", o => dv.getFloat32(o, true), i => Math.fround(i / 3)], + ["writeFloatBE", o => dv.getFloat32(o, false), i => Math.fround(-i / 7)], + ["writeDoubleLE", o => dv.getFloat64(o, true), i => i + 0.25], + ["writeDoubleBE", o => dv.getFloat64(o, false), i => -i - 0.5], +]; +for (let [name, reference, value] of writers) { + let byteSize = name.match(/8/) ? 1 : name.match(/16/) ? 2 : name.match(/Float/) ? 4 : name.match(/32/) ? 4 : 8; + let write = new Function("b", "v", "o", `return b.${name}(v, o);`); + noInline(write); + for (let i = 0; i < 1e4; ++i) { + let o = i & 31; + let v = value(i); + shouldBe(write(buf, v, o), o + byteSize, name + " result @" + o); + shouldBe(reference(o), v, name + " store @" + o); + } + let writeDefault = new Function("b", "v", `return b.${name}(v);`); + noInline(writeDefault); + for (let i = 0; i < 1e4; ++i) { + shouldBe(writeDefault(buf, value(i)), byteSize, name + " default offset"); + shouldBe(reference(0), value(i), name + " default offset store"); + } +} + +function writeInt32LEValue(b, v) { + return b.writeInt32LE(v, 12); +} +noInline(writeInt32LEValue); +function writeUInt32LEValue(b, v) { + return b.writeUInt32LE(v, 16); +} +noInline(writeUInt32LEValue); +for (let i = 0; i < 1e4; ++i) { + shouldBe(writeInt32LEValue(buf, i + 0.75), 16, "writeInt32LE fractional result"); + shouldBe(dv.getInt32(12, true), i, "writeInt32LE fractional store"); + shouldBe(writeInt32LEValue(buf, NaN), 16, "writeInt32LE NaN result"); + shouldBe(dv.getInt32(12, true), 0, "writeInt32LE NaN store"); + shouldBe(writeUInt32LEValue(buf, 4294967295), 20, "writeUInt32LE max result"); + shouldBe(dv.getUint32(16, true), 4294967295, "writeUInt32LE max store"); + shouldBe(writeUInt32LEValue(buf, 1.5), 20, "writeUInt32LE fractional result"); + shouldBe(dv.getUint32(16, true), 1, "writeUInt32LE fractional store"); + shouldBe( + writeInt32LEValue(buf, { + valueOf() { + return 7; + }, + }), + 16, + "writeInt32LE valueOf result", + ); + shouldBe(dv.getInt32(12, true), 7, "writeInt32LE valueOf store"); +} + +{ + const impure = new Buffer(16); + impure.fill(0xff); + const dvImpure = new DataView(impure.buffer); + const readFloat = new Function("b", "o", "return b.readFloatLE(o);"); + const readDouble = new Function("b", "o", "return b.readDoubleLE(o);"); + noInline(readFloat); + noInline(readDouble); + for (let i = 0; i < 1e4; ++i) { + const f = readFloat(impure, 0); + const d = readDouble(impure, 8); + shouldBe(f, dvImpure.getFloat32(0, true), "impure NaN float matches the DataView reference"); + shouldBe(d, dvImpure.getFloat64(8, true), "impure NaN double matches the DataView reference"); + shouldBe(f + 1, NaN, "the boxed NaN stays usable in arithmetic"); + shouldBe(d * 2, NaN, "the boxed NaN stays usable in arithmetic"); + } +} diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index 4e044a61d9e52..d69107b0a4dbf 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -2049,6 +2049,9 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS heap/BunV8HeapSnapshotBuilder.h heap/HeapProfiler.h + runtime/BufferAccessorRegistry.h + dfg/DFGDataViewData.h + bytecode/GlobalCodeBlock.h bytecode/ModuleProgramCodeBlock.h bytecode/ProgramCodeBlock.h diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt index 50505d40a3586..1bc2955165399 100644 --- a/Source/JavaScriptCore/Sources.txt +++ b/Source/JavaScriptCore/Sources.txt @@ -1317,4 +1317,6 @@ yarr/YarrCanonicalizeUnicode.cpp runtime/InternalFieldTuple.cpp -heap/BunV8HeapSnapshotBuilder.cpp \ No newline at end of file +heap/BunV8HeapSnapshotBuilder.cpp + +runtime/BufferAccessorRegistry.cpp diff --git a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h index fdceccaacdf04..b828ced444e1b 100644 --- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h +++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h @@ -5887,6 +5887,41 @@ bool AbstractInterpreter::executeEffects(unsigned clobberLimi break; } + case BufferReadInt: { + if (node->arrayMode().type() == Array::ForceExit) { + m_state.setIsValid(false); + break; + } + DataViewData data = node->bufferAccessData(); + if (data.byteSize < 4) + setNonCellTypeForNode(node, SpecInt32Only); + else if (data.byteSize == 4) { + if (data.isSigned) + setNonCellTypeForNode(node, SpecInt32Only); + else + setNonCellTypeForNode(node, SpecInt52Any); + } else { + ASSERT(data.byteSize == 8); + setTypeForNode(node, SpecHeapBigInt); + } + break; + } + + case BufferReadFloat: { + if (node->arrayMode().type() == Array::ForceExit) { + m_state.setIsValid(false); + break; + } + setNonCellTypeForNode(node, SpecFullDouble); + break; + } + + case BufferWrite: { + if (node->arrayMode().type() == Array::ForceExit) + m_state.setIsValid(false); + break; + } + case DataViewGetInt: { DataViewData data = node->dataViewData(); if (data.byteSize < 4) diff --git a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp index f7c96e814cdfb..9afe757d39285 100644 --- a/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp +++ b/Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp @@ -69,6 +69,9 @@ #include "JSBoundFunctionInlines.h" #include "JSCInlines.h" #include "JSCellButterfly.h" +#if USE(BUN_JSC_ADDITIONS) +#include "BufferAccessorRegistry.h" +#endif #include "JSIteratorHelper.h" #include "JSMapIterator.h" #include "JSModuleEnvironment.h" @@ -967,7 +970,6 @@ class ByteCodeParser { op = TailCallInlinedCaller; } - Node* call = addCallWithoutSettingResult(op, opInfo, callee, argCount, registerOffset, OpInfo(prediction), thisValueForEval, scopeForEval); if (result.isValid()) set(result, call); @@ -1692,7 +1694,6 @@ bool ByteCodeParser::handleRecursiveTailCall(Node* callTargetNode, CallVariant c else if (stackEntry->m_inlineCallFrame->isClosureCall) setDirect(remapOperand(stackEntry->m_inlineCallFrame, CallFrameSlot::callee), callTargetNode, NormalSet); - // We must set the arguments to the right values if (!stackEntry->m_inlineCallFrame) addToGraph(SetArgumentCountIncludingThis, OpInfo(argumentCountIncludingThis)); @@ -2296,7 +2297,6 @@ bool ByteCodeParser::handleVarargsInlining(Node* callTargetNode, Operand result, // calling LoadVarargs twice. inlineCall(callTargetNode, result, callVariant, registerOffset, maxArgumentCountIncludingThis, kind, nullptr, insertChecks); - VERBOSE_LOG("Successful inlining (varargs, monomorphic).\nStack: ", currentCodeOrigin(), "\n"); return true; } @@ -5089,6 +5089,82 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca return CallOptimizationResult::Inlined; } +#if USE(BUN_JSC_ADDITIONS) + case BufferAccessorIntrinsic: { + if (!is64Bit()) + return CallOptimizationResult::DidNothing; + + if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadType) + || m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, BadIndexingType) + || m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, OutOfBounds) + || m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, Overflow)) + return CallOptimizationResult::DidNothing; + + NativeExecutable* nativeExecutable = variant.nativeExecutable(); + if (!nativeExecutable) + return CallOptimizationResult::DidNothing; + std::optional descriptor = bufferAccessorDescriptor(nativeExecutable->function()); + if (!descriptor) + return CallOptimizationResult::DidNothing; + + DataViewData data = descriptor->data; + if (m_inlineStackTop->m_exitProfile.hasExitSite(m_currentIndex, UnexpectedResizableArrayBufferView)) + data.isResizable = true; + else + data.isResizable = getArrayMode(descriptor->isWrite ? Array::Write : Array::Read).mayBeResizableOrGrowableSharedTypedArray(); + ArrayMode arrayMode = ArrayMode(Array::SelectUsingPredictions, descriptor->isWrite ? Array::Write : Array::Read); + + if (descriptor->byteLengthFromArgument) { + int byteLengthArgument = descriptor->isWrite ? 3 : 2; + if (argumentCountIncludingThis <= byteLengthArgument) + return CallOptimizationResult::DidNothing; + Node* byteLength = get(virtualRegisterForArgumentIncludingThis(byteLengthArgument, registerOffset)); + if (!byteLength->isNumberConstant()) + return CallOptimizationResult::DidNothing; + double width = byteLength->asNumber(); + if (width != 1 && width != 2 && width != 4) + return CallOptimizationResult::DidNothing; + data.byteSize = static_cast(width); + } + + auto offsetArgument = [&](int argumentIndex) -> Node* { + if (argumentCountIncludingThis <= argumentIndex) + return jsConstant(jsNumber(0)); + Node* offset = get(virtualRegisterForArgumentIncludingThis(argumentIndex, registerOffset)); + if (!descriptor->byteLengthFromArgument && offset->isUndefinedOrNullConstant() && !offset->asJSValue().isNull()) + return jsConstant(jsNumber(0)); + return offset; + }; + + if (descriptor->isWrite) { + if (argumentCountIncludingThis < 2) + return CallOptimizationResult::DidNothing; + + insertChecks(); + + Node* offset = offsetArgument(2); + Node* returnValue = makeSafe(addToGraph(ArithAdd, offset, jsConstant(jsNumber(data.byteSize)))); + addVarArgChild(get(virtualRegisterForArgumentIncludingThis(0, registerOffset))); + addVarArgChild(offset); + addVarArgChild(get(virtualRegisterForArgumentIncludingThis(1, registerOffset))); + addVarArgChild(nullptr); + addToGraph(Node::VarArg, BufferWrite, OpInfo(arrayMode.asWord()), OpInfo(data.asQuadWord)); + setResult(returnValue); + return CallOptimizationResult::Inlined; + } + + insertChecks(); + + Node* offset = offsetArgument(1); + + addVarArgChild(get(virtualRegisterForArgumentIncludingThis(0, registerOffset))); + addVarArgChild(offset); + addVarArgChild(nullptr); + setResult(addToGraph(Node::VarArg, data.isFloatingPoint ? BufferReadFloat : BufferReadInt, OpInfo(arrayMode.asWord()), OpInfo(data.asQuadWord))); + return CallOptimizationResult::Inlined; + } +#endif // USE(BUN_JSC_ADDITIONS) + case ObjectHasOwnIntrinsic: case HasOwnPropertyIntrinsic: { bool isObjectHasOwn = intrinsic == ObjectHasOwnIntrinsic; @@ -5706,7 +5782,6 @@ bool ByteCodeParser::handleDOMJITCall(Node* callTarget, Operand result, const DO return true; } - template bool ByteCodeParser::handleIntrinsicGetter(Operand result, SpeculatedType prediction, const GetByVariant& variant, Node* thisNode, Node* unwrapped, const ChecksFunctor& insertChecks) { @@ -5837,7 +5912,6 @@ bool ByteCodeParser::handleIntrinsicGetter(Operand result, SpeculatedType predic ASSERT(arrayType != Array::Generic); }); - #if USE(JSVALUE32_64) if (mayBeResizableOrGrowableSharedTypedArray) return false; @@ -9132,7 +9206,6 @@ void ByteCodeParser::parseBlock(unsigned limit) FrozenValue* frozen = m_graph.freezeStrong(identifier.cell()); addToGraph(CheckIsConstant, OpInfo(frozen), brand); - // FIXME: We should include a MultiSetPrivateBrand to handle polymorphic cases // https://bugs.webkit.org/show_bug.cgi?id=221570 if (setStatus.isSimple() && setStatus.variants().size() == 1 && Options::useAccessInlining()) { diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h index 9f5d120b6edc9..187d8b95710cb 100644 --- a/Source/JavaScriptCore/dfg/DFGClobberize.h +++ b/Source/JavaScriptCore/dfg/DFGClobberize.h @@ -190,6 +190,9 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu case ArraySortCompact: case ArraySortCommit: case GetCellButterflySlot: + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: return clobberTop(); default: DFG_CRASH(graph, node, "Unhandled ArrayMode opcode."); @@ -2705,6 +2708,37 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu return; } + case BufferReadInt: + case BufferReadFloat: { + if (node->arrayMode().type() == Array::ForceExit) { + write(SideState); + return; + } + DataViewData data = node->bufferAccessData(); + read(MiscFields); + read(TypedArrayProperties); + if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) { + write(MiscFields); + write(TypedArrayProperties); + } else + def(HeapLocation(indexedPropertyLocForResultType(node->result()), AbstractHeap(TypedArrayProperties, data.asQuadWord), graph.varArgChild(node, 0), graph.varArgChild(node, 1)), LazyNode(node)); + return; + } + + case BufferWrite: { + if (node->arrayMode().type() == Array::ForceExit) { + write(SideState); + return; + } + read(MiscFields); + if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) { + read(TypedArrayProperties); + write(MiscFields); + } + write(TypedArrayProperties); + return; + } + case ResolvePromiseFirstResolving: case RejectPromiseFirstResolving: case FulfillPromiseFirstResolving: diff --git a/Source/JavaScriptCore/dfg/DFGCloneHelper.h b/Source/JavaScriptCore/dfg/DFGCloneHelper.h index b22b25e4de906..3ac1c735be2f9 100644 --- a/Source/JavaScriptCore/dfg/DFGCloneHelper.h +++ b/Source/JavaScriptCore/dfg/DFGCloneHelper.h @@ -175,6 +175,9 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc CLONE_STATUS(BooleanToNumber, Common) \ CLONE_STATUS(BottomValue, Common) \ CLONE_STATUS(Branch, Special) \ + CLONE_STATUS(BufferReadFloat, Common) \ + CLONE_STATUS(BufferReadInt, Common) \ + CLONE_STATUS(BufferWrite, Common) \ CLONE_STATUS(Call, Common) \ CLONE_STATUS(CallCustomAccessorGetter, Common) \ CLONE_STATUS(CallDirectEval, Common) \ diff --git a/Source/JavaScriptCore/dfg/DFGDataViewData.h b/Source/JavaScriptCore/dfg/DFGDataViewData.h new file mode 100644 index 0000000000000..aeca7bd5c7647 --- /dev/null +++ b/Source/JavaScriptCore/dfg/DFGDataViewData.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2026 Oven-sh Inc. All rights reserved. + * Copyright (C) 2018-2023 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace JSC { namespace DFG { + +struct DataViewData { + union { + struct { + uint8_t byteSize; + bool isSigned; + bool isResizable; + bool isFloatingPoint; + TriState isLittleEndian; + }; + uint64_t asQuadWord; + }; +}; +static_assert(sizeof(DataViewData) == sizeof(uint64_t)); + +} } // namespace JSC::DFG diff --git a/Source/JavaScriptCore/dfg/DFGDoesGC.cpp b/Source/JavaScriptCore/dfg/DFGDoesGC.cpp index 02e2e0a8862c1..edc13e460393e 100644 --- a/Source/JavaScriptCore/dfg/DFGDoesGC.cpp +++ b/Source/JavaScriptCore/dfg/DFGDoesGC.cpp @@ -538,6 +538,13 @@ bool doesGC(Graph& graph, Node* node) case DataViewGetInt: return node->dataViewData().byteSize == 8; + case BufferReadInt: + return node->bufferAccessData().byteSize == 8; + + case BufferReadFloat: + case BufferWrite: + return false; + case CallNumberConstructor: switch (node->child1().useKind()) { case BigInt32Use: diff --git a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp index 0563563cd6664..ab54a89c10317 100644 --- a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp +++ b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp @@ -1284,7 +1284,6 @@ class FixupPhase : public Phase { } } - node->setArrayMode( node->arrayMode().refine( m_graph, node, @@ -1306,7 +1305,6 @@ class FixupPhase : public Phase { node->setArrayMode(ArrayMode(Array::Generic, node->arrayMode().action())); break; - case Array::ForceExit: { // Don't force OSR because we have only seen OwnStructureMode. // FIXME: We should have a better way to do this... @@ -3737,6 +3735,90 @@ class FixupPhase : public Phase { break; } + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: { +#if USE(BUN_JSC_ADDITIONS) + Edge& base = m_graph.varArgChild(node, 0); + Edge& offset = m_graph.varArgChild(node, 1); + DataViewData data = node->bufferAccessData(); + + bool forceExit = !base->prediction() || !offset->prediction(); + if (forceExit) { + node->setArrayMode(ArrayMode(Array::ForceExit, node->arrayMode().action())); + blessArrayOperation(base, offset, m_graph.varArgChild(node, node->storageChildIndex())); + } else { + if (!isInt32Speculation(offset->prediction()) && isFullNumberSpeculation(offset->prediction())) { + Node* newOffset = m_insertionSet.insertNode( + m_indexInBlock, SpecInt32Only, DoubleAsInt32, node->origin, + Edge(offset.node(), DoubleRepUse)); + newOffset->setArithMode(Arith::CheckOverflow); + offset.setNode(newOffset); + } + + bool mayBeResizable = data.isResizable || m_graph.hasExitSite(node->origin.semantic, UnexpectedResizableArrayBufferView); + node->setArrayMode(ArrayMode(Array::Uint8Array, Array::NonArray, Array::InBounds, Array::AsIs, node->arrayMode().action(), false, mayBeResizable)); + blessArrayOperation(base, offset, m_graph.varArgChild(node, node->storageChildIndex())); + fixEdge(base); + fixEdge(offset); + } + + switch (node->op()) { + case BufferReadInt: + switch (data.byteSize) { + case 1: + case 2: + node->setResult(NodeResultInt32); + break; + case 4: + if (data.isSigned) + node->setResult(NodeResultInt32); + else + node->setResult(NodeResultInt52); + break; + case 8: + node->setResult(NodeResultJS); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + break; + case BufferReadFloat: + break; + case BufferWrite: { + Edge& value = m_graph.varArgChild(node, 2); + if (data.isFloatingPoint) + fixEdge(value); + else { + switch (data.byteSize) { + case 1: + case 2: + fixEdge(value); + break; + case 4: + if (data.isSigned) + fixEdge(value); + else + fixEdge(value); + break; + case 8: + fixEdge(value); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + } + break; + } + default: + RELEASE_ASSERT_NOT_REACHED(); + } +#else + DFG_CRASH(m_graph, node, "Unexpected node type"); +#endif + break; + } + case ForwardVarargs: fixEdge(node->child1()); break; @@ -5655,7 +5737,6 @@ class FixupPhase : public Phase { return; } - if (Node::shouldSpeculateBoolean(node->child1().node(), node->child2().node())) { fixEdge(node->child1()); fixEdge(node->child2()); @@ -5773,7 +5854,6 @@ class FixupPhase : public Phase { return; } - if (node->child1()->shouldSpeculateMisc() && node->child2()->shouldSpeculateMisc()) { fixEdge(node->child1()); fixEdge(node->child2()); diff --git a/Source/JavaScriptCore/dfg/DFGNode.h b/Source/JavaScriptCore/dfg/DFGNode.h index 699aaf4605818..3fa40f95649dd 100644 --- a/Source/JavaScriptCore/dfg/DFGNode.h +++ b/Source/JavaScriptCore/dfg/DFGNode.h @@ -35,6 +35,7 @@ #include "DFGArithMode.h" #include "DFGArrayMode.h" #include "DFGCommon.h" +#include "DFGDataViewData.h" #include "DFGEpoch.h" #include "DFGLazyJSValue.h" #include "DFGMultiGetByOffsetData.h" @@ -150,20 +151,6 @@ struct NewArrayWithSpeciesData { static_assert(sizeof(IndexingType) <= sizeof(unsigned)); static_assert(sizeof(ArrayMode) <= sizeof(unsigned)); -struct DataViewData { - union { - struct { - uint8_t byteSize; - bool isSigned; - bool isResizable; - bool isFloatingPoint; // Used for the DataViewSet node. - TriState isLittleEndian; - }; - uint64_t asQuadWord; - }; -}; -static_assert(sizeof(DataViewData) == sizeof(uint64_t)); - struct BranchTarget { BranchTarget() = default; explicit BranchTarget(BasicBlock* block) @@ -2353,6 +2340,9 @@ struct Node { case ArrayIncludes: case ArrayIndexOf: case ArrayJoin: + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: return true; default: break; @@ -2367,12 +2357,15 @@ struct Node { case EnumeratorGetByVal: case GetByVal: case GetByValMegamorphic: + case BufferReadInt: + case BufferReadFloat: return 2; case EnumeratorPutByVal: case PutByValDirect: case PutByVal: case PutByValDirectResolved: case PutByValMegamorphic: + case BufferWrite: return 3; case AtomicsAdd: case AtomicsAnd: @@ -2784,6 +2777,9 @@ struct Node { case ArraySortCompact: case ArraySortCommit: case GetCellButterflySlot: + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: return true; default: return false; @@ -3011,6 +3007,12 @@ struct Node { return std::bit_cast(m_opInfo.as()); } + DataViewData bufferAccessData() + { + ASSERT(op() == BufferReadInt || op() == BufferReadFloat || op() == BufferWrite); + return std::bit_cast(m_opInfo2.as()); + } + bool shouldGenerate() { return m_refCount; diff --git a/Source/JavaScriptCore/dfg/DFGNodeType.h b/Source/JavaScriptCore/dfg/DFGNodeType.h index 41df5d9ebdf80..1908e5e10bb1b 100644 --- a/Source/JavaScriptCore/dfg/DFGNodeType.h +++ b/Source/JavaScriptCore/dfg/DFGNodeType.h @@ -665,6 +665,9 @@ namespace JSC { namespace DFG { macro(DataViewSet, NodeMustGenerate | NodeMustGenerate | NodeHasVarArgs) \ macro(DataViewGetByteLength, NodeResultInt32) \ macro(DataViewGetByteLengthAsInt52, NodeResultInt52) \ + macro(BufferReadInt, NodeResultJS | NodeMustGenerate | NodeHasVarArgs) \ + macro(BufferReadFloat, NodeResultDouble | NodeMustGenerate | NodeHasVarArgs) \ + macro(BufferWrite, NodeMustGenerate | NodeHasVarArgs) \ /* Date access */ \ macro(DateNow, NodeMustGenerate | NodeResultDouble) \ macro(DateGetInt32OrNaN, NodeResultJS) \ @@ -682,7 +685,6 @@ namespace JSC { namespace DFG { macro(PerformPromiseThen, NodeMustGenerate | NodeHasVarArgs) \ macro(PerformPromiseThenOneHandler, NodeMustGenerate) \ - // This enum generates a monotonically increasing id for all Node types, // and is used by the subsequent enum to fill out the id (as accessed via the NodeIdMask). enum NodeType : uint16_t { diff --git a/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp b/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp index cf42a81910923..6dbae93e1f366 100644 --- a/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp +++ b/Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp @@ -400,7 +400,6 @@ class PredictionPropagationPhase : public Phase { break; } - case ToNumber: case ToNumeric: { SpeculatedType prediction = node->child1()->prediction(); @@ -945,6 +944,13 @@ class PredictionPropagationPhase : public Phase { break; } + case BufferWrite: { + DataViewData data = node->bufferAccessData(); + if (data.isFloatingPoint) + m_graph.voteNode(m_graph.varArgChild(node, 2), VoteValue, weight); + break; + } + case MovHint: // Ignore these since they have no effect on in-DFG execution. break; @@ -1106,6 +1112,30 @@ class PredictionPropagationPhase : public Phase { break; } + case BufferReadInt: { + DataViewData data = m_currentNode->bufferAccessData(); + switch (data.byteSize) { + case 1: + case 2: + setPrediction(SpecInt32Only); + break; + case 4: + setPrediction(data.isSigned ? SpecInt32Only : SpecInt52Any); + break; + case 8: + setPrediction(SpecHeapBigInt); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + break; + } + + case BufferReadFloat: { + setPrediction(SpecFullDouble); + break; + } + case GetWebAssemblyInstanceExports: { setPrediction(SpecFinalObject); break; @@ -1854,6 +1884,7 @@ class PredictionPropagationPhase : public Phase { case FilterSetPrivateBrandStatus: case ClearCatchLocals: case DataViewSet: + case BufferWrite: case InvalidationPoint: case ObjectAssign: case ResolvePromiseFirstResolving: @@ -1947,4 +1978,3 @@ bool performPredictionPropagation(Graph& graph) } } // namespace JSC::DFG #endif // ENABLE(DFG_JIT) - diff --git a/Source/JavaScriptCore/dfg/DFGSSALoweringPhase.cpp b/Source/JavaScriptCore/dfg/DFGSSALoweringPhase.cpp index 1ef8666afef1d..a528f23f8fb60 100644 --- a/Source/JavaScriptCore/dfg/DFGSSALoweringPhase.cpp +++ b/Source/JavaScriptCore/dfg/DFGSSALoweringPhase.cpp @@ -91,6 +91,17 @@ class SSALoweringPhase : public Phase { break; } + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: { +#if USE(BUN_JSC_ADDITIONS) + lowerBufferAccessBoundsChecks(); +#else + DFG_CRASH(m_graph, m_node, "Unexpected node type"); +#endif + break; + } + case StringCharCodeAt: { lowerStringBoundsCheck(m_graph.child(m_node, 0), m_graph.child(m_node, 1), m_graph.child(m_node, 2)); break; @@ -194,6 +205,72 @@ class SSALoweringPhase : public Phase { return true; } +#if USE(BUN_JSC_ADDITIONS) + void lowerBufferAccessBoundsChecks() + { + ArrayMode arrayMode = m_node->arrayMode(); + if (arrayMode.type() == Array::ForceExit) + return; + RELEASE_ASSERT(arrayMode.type() == Array::Uint8Array && arrayMode.isInBounds() && !arrayMode.lengthNeedsStorage()); + DataViewData data = m_node->bufferAccessData(); + Edge base = m_graph.varArgChild(m_node, 0); + Edge offset = m_graph.varArgChild(m_node, 1); + RELEASE_ASSERT(offset.useKind() == Int32Use); + +#if USE(LARGE_TYPED_ARRAYS) + constexpr bool lengthAsInt52 = true; +#else + constexpr bool lengthAsInt52 = false; +#endif + Node* length = m_insertionSet.insertNode( + m_nodeIndex, lengthAsInt52 ? SpecInt52Any : SpecInt32Only, lengthAsInt52 ? GetTypedArrayLengthAsInt52 : GetArrayLength, m_node->origin, + OpInfo(arrayMode.asWord()), Edge(base.node(), KnownCellUse), Edge()); + if (arrayMode.mayBeResizableOrGrowableSharedTypedArray()) + m_insertionSet.insertNode(m_nodeIndex, SpecNone, ExitOK, m_node->origin.withExitOK(true)); + Edge lengthEdge = lengthAsInt52 ? Edge(length, Int52RepUse) : Edge(length, KnownInt32Use); + NodeType checkInBounds = lengthAsInt52 ? CheckInBoundsInt52 : CheckInBounds; + + unsigned appended = 1; + Node* checkFirstByte = m_insertionSet.insertNode(m_nodeIndex, SpecInt32Only, checkInBounds, m_node->origin, offset, lengthEdge); + Node* checkLastByte = nullptr; + if (data.byteSize > 1) { + Node* lastByteOffset = m_insertionSet.insertNode( + m_nodeIndex, SpecInt32Only, NodeResultInt32, ArithAdd, m_node->origin, OpInfo(Arith::CheckOverflow), + Edge(offset.node(), Int32Use), + m_insertionSet.insertConstantForUse(m_nodeIndex, m_node->origin, jsNumber(data.byteSize - 1), Int32Use)); + checkLastByte = m_insertionSet.insertNode(m_nodeIndex, SpecInt32Only, checkInBounds, m_node->origin, Edge(lastByteOffset, Int32Use), lengthEdge); + appended = 2; + } + + Node* checkValueRange = nullptr; + if (m_node->op() == BufferWrite && !data.isFloatingPoint && data.byteSize <= 2) { + Edge value = m_graph.varArgChild(m_node, 2); + RELEASE_ASSERT(value.useKind() == Int32Use); + int32_t range = 1 << (8 * data.byteSize); + Node* checked = value.node(); + if (data.isSigned) { + checked = m_insertionSet.insertNode( + m_nodeIndex, SpecInt32Only, NodeResultInt32, ArithAdd, m_node->origin, OpInfo(Arith::CheckOverflow), + Edge(value.node(), Int32Use), + m_insertionSet.insertConstantForUse(m_nodeIndex, m_node->origin, jsNumber(range / 2), Int32Use)); + } + checkValueRange = m_insertionSet.insertNode( + m_nodeIndex, SpecInt32Only, CheckInBounds, m_node->origin, Edge(checked, Int32Use), + m_insertionSet.insertConstantForUse(m_nodeIndex, m_node->origin, jsNumber(range), Int32Use)); + appended++; + } + + AdjacencyList adjacencyList = m_graph.copyVarargChildren(m_node); + m_graph.m_varArgChildren.append(Edge(checkFirstByte, UntypedUse)); + if (checkLastByte) + m_graph.m_varArgChildren.append(Edge(checkLastByte, UntypedUse)); + if (checkValueRange) + m_graph.m_varArgChildren.append(Edge(checkValueRange, UntypedUse)); + adjacencyList.setNumChildren(adjacencyList.numChildren() + appended); + m_node->children = adjacencyList; + } +#endif // USE(BUN_JSC_ADDITIONS) + bool lowerStringBoundsCheck(Edge base, Edge index, Edge& checkInBoundsEdge) { if (!m_node->arrayMode().isInBounds()) @@ -219,4 +296,3 @@ bool performSSALowering(Graph& graph) } } // namespace JSC::DFG #endif // ENABLE(DFG_JIT) - diff --git a/Source/JavaScriptCore/dfg/DFGSafeToExecute.h b/Source/JavaScriptCore/dfg/DFGSafeToExecute.h index 0ab636444a3b7..130611e65832f 100644 --- a/Source/JavaScriptCore/dfg/DFGSafeToExecute.h +++ b/Source/JavaScriptCore/dfg/DFGSafeToExecute.h @@ -430,6 +430,8 @@ bool safeToExecute(AbstractStateType& state, Graph& graph, Node* node, bool igno case StringCharAt: case StringCharCodeAt: case StringCodePointAt: + case BufferReadInt: + case BufferReadFloat: return node->arrayMode().alreadyChecked(graph, node, state.forNode(graph.child(node, 0))); // We can make them non conservative by checking the condition safely. @@ -812,6 +814,7 @@ bool safeToExecute(AbstractStateType& state, Graph& graph, Node* node, bool igno case GetInternalField: case PutInternalField: case DataViewSet: + case BufferWrite: case ResolvePromiseFirstResolving: case RejectPromiseFirstResolving: case FulfillPromiseFirstResolving: diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h index 4d3b1750527ac..381947e0f5217 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h @@ -1594,6 +1594,10 @@ class SpeculativeJIT : public JITCompiler { #if USE(LARGE_TYPED_ARRAYS) void compileDataViewGetByteLengthAsInt52(Node*); #endif +#if USE(BUN_JSC_ADDITIONS) + void compileBufferRead(Node*); + void compileBufferWrite(Node*); +#endif void compileCheckTypeInfoFlags(Node*); void compileCheckIdent(Node*); @@ -2160,7 +2164,6 @@ class SpeculativeJIT : public JITCompiler { std::optional m_outOfLineStreamIndex; }; - // === Operand types === // // These classes are used to lock the operands to a node into machine @@ -2386,7 +2389,6 @@ class StorageOperand { GPRReg m_gprOrInvalid { InvalidGPRReg }; }; - // === Temporaries === // // These classes are used to allocate temporary registers. @@ -2548,7 +2550,6 @@ class FPRTemporary { FPRReg m_fpr; }; - // === Results === // // These classes lock the result of a call to a C++ helper function. @@ -2615,7 +2616,6 @@ class JSValueRegsFlushedCallResult { #endif }; - // === Speculative Operand types === // // SpeculateInt32Operand, SpeculateStrictInt32Operand and SpeculateCellOperand. diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp index 28c1a609fab88..102852c0f0c14 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp @@ -1358,7 +1358,6 @@ void SpeculativeJIT::compileObjectToObjectOrOtherEquality(Edge leftChild, Edge r TrustedImm32(MasqueradesAsUndefined))); } - // It seems that most of the time when programs do a == b where b may be either null/undefined // or an object, b is usually an object. Balance the branches to make that case fast. Jump rightNotCell = branchIfNotCell(op2.jsValueRegs()); @@ -3899,7 +3898,6 @@ void SpeculativeJIT::compile(Node* node) break; } - case IsBoolean: { JSValueOperand value(this, node->child1()); GPRTemporary result(this, Reuse, value, TagWord); @@ -4617,6 +4615,9 @@ void SpeculativeJIT::compile(Node* node) case DataViewGetInt: case DataViewGetFloat: case DataViewSet: + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: case DateNow: case DateGetInt32OrNaN: case DateGetTime: diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp index 2b85ae7e464a0..2cd4cdc6d71d2 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp @@ -2871,7 +2871,6 @@ void SpeculativeJIT::compileGetByVal(Node* node, const ScopedLambdachild1()); @@ -7108,7 +7123,6 @@ void SpeculativeJIT::compileArithRandom(Node* node) doubleResult(result.fpr(), node); } - void SpeculativeJIT::compileDateGet(Node* node) { SpeculateCellOperand base(this, node->child1()); @@ -9947,6 +9961,308 @@ void SpeculativeJIT::compileMultiPutByVal(Node* node) noResult(node); } +#if USE(BUN_JSC_ADDITIONS) + +void SpeculativeJIT::compileBufferRead(Node* node) +{ + Edge& baseEdge = m_graph.varArgChild(node, 0); + Edge& offsetEdge = m_graph.varArgChild(node, 1); + Edge& storageEdge = m_graph.varArgChild(node, 2); + DataViewData data = node->bufferAccessData(); + ASSERT(data.byteSize == 1 || data.isLittleEndian != TriState::Indeterminate); + + SpeculateCellOperand base(this, baseEdge); + SpeculateStrictInt32Operand offset(this, offsetEdge); + StorageOperand storage(this, storageEdge); + GPRReg baseGPR = base.gpr(); + GPRReg offsetGPR = offset.gpr(); + GPRReg storageGPR = storage.gpr(); + + GPRTemporary temp1(this); + GPRTemporary temp2(this); + GPRReg t1 = temp1.gpr(); + GPRReg t2 = temp2.gpr(); + + if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) + loadTypedArrayLength(baseGPR, t1, t2, t1, TypeUint8); + else { + if (!m_graph.isNeverResizableOrGrowableSharedTypedArrayIncludingDataView(m_state.forNode(baseEdge))) + speculationCheck(UnexpectedResizableArrayBufferView, JSValueSource::unboxedCell(baseGPR), node, branchTest8(NonZero, Address(baseGPR, JSArrayBufferView::offsetOfMode()), TrustedImm32(isResizableOrGrowableSharedMode))); +#if USE(LARGE_TYPED_ARRAYS) + load64(Address(baseGPR, JSArrayBufferView::offsetOfLength()), t1); +#else + load32(Address(baseGPR, JSArrayBufferView::offsetOfLength()), t1); +#endif + } + + speculationCheck(OutOfBounds, JSValueRegs(), node, branch32(LessThan, offsetGPR, TrustedImm32(0))); + zeroExtend32ToWord(offsetGPR, t2); + if (data.byteSize > 1) + add64(TrustedImm32(data.byteSize - 1), t2); + speculationCheck(OutOfBounds, JSValueRegs(), node, branch64(AboveOrEqual, t2, t1)); + + zeroExtend32ToWord(offsetGPR, t1); + auto address = BaseIndex(storageGPR, t1, TimesOne); + bool isBigEndian = data.isLittleEndian == TriState::False; + + if (node->op() == BufferReadInt) { + switch (data.byteSize) { + case 1: + if (data.isSigned) + load8SignedExtendTo32(address, t2); + else + load8(address, t2); + strictInt32Result(t2, node); + break; + case 2: + if (isBigEndian) { + load16(address, t2); + byteSwap16(t2); + if (data.isSigned) + signExtend16To32(t2, t2); + } else if (data.isSigned) + load16SignedExtendTo32(address, t2); + else + load16(address, t2); + strictInt32Result(t2, node); + break; + case 4: + load32(address, t2); + if (isBigEndian) + byteSwap32(t2); + if (data.isSigned) + strictInt32Result(t2, node); + else + strictInt52Result(t2, node); + break; + case 8: { + load64(address, t2); + if (isBigEndian) + byteSwap64(t2); + flushRegisters(); + GPRFlushedCallResult result(this); + GPRReg resultGPR = result.gpr(); + if (data.isSigned) + callOperation(operationInt64ToBigInt, resultGPR, LinkableConstant::globalObject(*this, node), t2); + else + callOperation(operationUInt64ToBigInt, resultGPR, LinkableConstant::globalObject(*this, node), t2); + exceptionCheck(); + jsValueResult(resultGPR, node); + break; + } + default: + RELEASE_ASSERT_NOT_REACHED(); + } + return; + } + + ASSERT(node->op() == BufferReadFloat); + FPRTemporary result(this); + FPRReg resultFPR = result.fpr(); + switch (data.byteSize) { + case 4: + if (isBigEndian) { + load32(address, t2); + byteSwap32(t2); + move32ToFloat(t2, resultFPR); + } else + loadFloat(address, resultFPR); + convertFloatToDouble(resultFPR, resultFPR); + break; + case 8: + if (isBigEndian) { + load64(address, t2); + byteSwap64(t2); + move64ToDouble(t2, resultFPR); + } else + loadDouble(address, resultFPR); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + doubleResult(resultFPR, node); +} + +void SpeculativeJIT::compileBufferWrite(Node* node) +{ + Edge& baseEdge = m_graph.varArgChild(node, 0); + Edge& offsetEdge = m_graph.varArgChild(node, 1); + Edge& valueEdge = m_graph.varArgChild(node, 2); + Edge& storageEdge = m_graph.varArgChild(node, 3); + DataViewData data = node->bufferAccessData(); + ASSERT(data.byteSize == 1 || data.isLittleEndian != TriState::Indeterminate); + + SpeculateCellOperand base(this, baseEdge); + SpeculateStrictInt32Operand offset(this, offsetEdge); + StorageOperand storage(this, storageEdge); + GPRReg baseGPR = base.gpr(); + GPRReg offsetGPR = offset.gpr(); + GPRReg storageGPR = storage.gpr(); + + std::optional int32Value; + std::optional int52Value; + std::optional doubleValue; + std::optional bigIntValue; + std::optional fprTemporary; + GPRReg valueGPR = InvalidGPRReg; + FPRReg valueFPR = InvalidFPRReg; + FPRReg tempFPR = InvalidFPRReg; + switch (valueEdge.useKind()) { + case Int32Use: + int32Value.emplace(this, valueEdge); + valueGPR = int32Value->gpr(); + break; + case Int52RepUse: + int52Value.emplace(this, valueEdge); + valueGPR = int52Value->gpr(); + break; + case DoubleRepUse: + doubleValue.emplace(this, valueEdge); + valueFPR = doubleValue->fpr(); + if (data.byteSize == 4) { + fprTemporary.emplace(this); + tempFPR = fprTemporary->fpr(); + } + break; + case HeapBigIntUse: + bigIntValue.emplace(this, valueEdge); + valueGPR = bigIntValue->gpr(); + speculateHeapBigInt(valueEdge, valueGPR); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + + GPRTemporary temp1(this); + GPRTemporary temp2(this); + GPRTemporary temp3(this); + GPRReg t1 = temp1.gpr(); + GPRReg t2 = temp2.gpr(); + GPRReg t3 = temp3.gpr(); + + if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) + loadTypedArrayLength(baseGPR, t1, t2, t1, TypeUint8); + else { + if (!m_graph.isNeverResizableOrGrowableSharedTypedArrayIncludingDataView(m_state.forNode(baseEdge))) + speculationCheck(UnexpectedResizableArrayBufferView, JSValueSource::unboxedCell(baseGPR), node, branchTest8(NonZero, Address(baseGPR, JSArrayBufferView::offsetOfMode()), TrustedImm32(isResizableOrGrowableSharedMode))); +#if USE(LARGE_TYPED_ARRAYS) + load64(Address(baseGPR, JSArrayBufferView::offsetOfLength()), t1); +#else + load32(Address(baseGPR, JSArrayBufferView::offsetOfLength()), t1); +#endif + } + + speculationCheck(OutOfBounds, JSValueRegs(), node, branch32(LessThan, offsetGPR, TrustedImm32(0))); + zeroExtend32ToWord(offsetGPR, t2); + if (data.byteSize > 1) + add64(TrustedImm32(data.byteSize - 1), t2); + speculationCheck(OutOfBounds, JSValueRegs(), node, branch64(AboveOrEqual, t2, t1)); + + if (!data.isFloatingPoint) { + switch (data.byteSize) { + case 1: + RELEASE_ASSERT(valueEdge.useKind() == Int32Use); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch32(LessThan, valueGPR, TrustedImm32(data.isSigned ? -0x80 : 0))); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch32(GreaterThan, valueGPR, TrustedImm32(data.isSigned ? 0x7f : 0xff))); + break; + case 2: + RELEASE_ASSERT(valueEdge.useKind() == Int32Use); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch32(LessThan, valueGPR, TrustedImm32(data.isSigned ? -0x8000 : 0))); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch32(GreaterThan, valueGPR, TrustedImm32(data.isSigned ? 0x7fff : 0xffff))); + break; + case 4: + if (data.isSigned) + RELEASE_ASSERT(valueEdge.useKind() == Int32Use); + else { + RELEASE_ASSERT(valueEdge.useKind() == Int52RepUse); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch64(Above, valueGPR, TrustedImm64(0xffffffffLL))); + } + break; + case 8: { + RELEASE_ASSERT(valueEdge.useKind() == HeapBigIntUse); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch32(Above, Address(valueGPR, JSBigInt::offsetOfLength()), TrustedImm32(1))); + load8(Address(valueGPR, JSCell::typeInfoFlagsOffset()), t1); + and32(TrustedImm32(TypeInfoPerCellBit), t1); + if (data.isSigned) { + toBigInt64(valueGPR, t2); + auto isZero = branchTest64(Zero, t2); + compare32(NotEqual, t1, TrustedImm32(0), t1); + compare64(LessThan, t2, TrustedImm32(0), t3); + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branch32(NotEqual, t1, t3)); + isZero.link(this); + } else { + speculationCheck(ExitKind::Overflow, JSValueRegs(), node, branchTest32(NonZero, t1)); + toBigInt64(valueGPR, t2); + } + break; + } + default: + RELEASE_ASSERT_NOT_REACHED(); + } + } + + zeroExtend32ToWord(offsetGPR, t1); + auto address = BaseIndex(storageGPR, t1, TimesOne); + bool isBigEndian = data.isLittleEndian == TriState::False; + + if (data.isFloatingPoint) { + RELEASE_ASSERT(valueEdge.useKind() == DoubleRepUse); + RELEASE_ASSERT(valueFPR != InvalidFPRReg); + if (data.byteSize == 4) { + RELEASE_ASSERT(tempFPR != InvalidFPRReg); + convertDoubleToFloat(valueFPR, tempFPR); + if (isBigEndian) { + moveFloatTo32(tempFPR, t2); + byteSwap32(t2); + store32(t2, address); + } else + storeFloat(tempFPR, address); + } else { + RELEASE_ASSERT(data.byteSize == 8); + if (isBigEndian) { + moveDoubleTo64(valueFPR, t2); + byteSwap64(t2); + store64(t2, address); + } else + storeDouble(valueFPR, address); + } + } else { + RELEASE_ASSERT(valueGPR != InvalidGPRReg); + switch (data.byteSize) { + case 1: + store8(valueGPR, address); + break; + case 2: + if (isBigEndian) { + move(valueGPR, t2); + byteSwap16(t2); + store16(t2, address); + } else + store16(valueGPR, address); + break; + case 4: + if (isBigEndian) { + zeroExtend32ToWord(valueGPR, t2); + byteSwap32(t2); + store32(t2, address); + } else + store32(valueGPR, address); + break; + case 8: + if (isBigEndian) + byteSwap64(t2); + store64(t2, address); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + } + + noResult(node); +} + +#endif // USE(BUN_JSC_ADDITIONS) + #endif } } // namespace JSC::DFG diff --git a/Source/JavaScriptCore/ftl/FTLCapabilities.cpp b/Source/JavaScriptCore/ftl/FTLCapabilities.cpp index 7c390c902f944..cbd3958084c3e 100644 --- a/Source/JavaScriptCore/ftl/FTLCapabilities.cpp +++ b/Source/JavaScriptCore/ftl/FTLCapabilities.cpp @@ -522,6 +522,9 @@ inline CapabilityLevel canCompile(DFG::Node* node) case DataViewGetInt: case DataViewGetFloat: case DataViewSet: + case BufferReadInt: + case BufferReadFloat: + case BufferWrite: case DateNow: case DateGetInt32OrNaN: case DateGetTime: @@ -695,4 +698,3 @@ CapabilityLevel canCompile(Graph& graph) } } // namespace JSC::FTL #endif // ENABLE(FTL_JIT) - diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp index 3866991e70c0c..28b9d297f46e2 100644 --- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp +++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp @@ -396,7 +396,6 @@ class LowerDFGToB3 { m_out.jump(firstDFGBasicBlock); } - m_out.appendTo(m_handleExceptions, firstDFGBasicBlock); Box exceptionHandler = state->exceptionHandler; m_out.patchpoint(Void)->setGenerator( @@ -1986,6 +1985,21 @@ class LowerDFGToB3 { case DataViewSet: compileDataViewSet(); break; + case BufferReadInt: + case BufferReadFloat: +#if USE(BUN_JSC_ADDITIONS) + compileBufferRead(); +#else + DFG_CRASH(m_graph, m_node, "Unexpected node"); +#endif + break; + case BufferWrite: +#if USE(BUN_JSC_ADDITIONS) + compileBufferWrite(); +#else + DFG_CRASH(m_graph, m_node, "Unexpected node"); +#endif + break; case ResolvePromiseFirstResolving: compileResolvePromiseFirstResolving(); @@ -3006,7 +3020,6 @@ class LowerDFGToB3 { [=] (CCallHelpers& jit, const StackmapGenerationParams& params) { AllowMacroScratchRegisterUsage allowScratch(jit); - Box exceptions = exceptionHandle->scheduleExitCreation(params)->jumps(jit); @@ -6202,7 +6215,6 @@ IGNORE_CLANG_WARNINGS_END #endif } - void compileGetArrayLength() { switch (m_node->arrayMode().type()) { @@ -7563,7 +7575,6 @@ IGNORE_CLANG_WARNINGS_END weakPointer(globalObject), base, index, value); m_out.jump(continuation); - if (arrayMode.isSlowPut()) { m_out.appendTo(inBoundCase, doStoreCase); m_out.branch(m_out.isZero64(m_out.load64(elementPointer)), rarely(slowCase), usually(doStoreCase)); @@ -8983,7 +8994,6 @@ IGNORE_CLANG_WARNINGS_END } } - void compileArrayPop() { JSGlobalObject* globalObject = m_graph.globalObjectFor(m_origin.semantic); @@ -9426,7 +9436,6 @@ IGNORE_CLANG_WARNINGS_END isAsyncGeneratorFunction ? allocateObject(structure, m_out.intPtrZero, slowPath) : allocateObject(structure, m_out.intPtrZero, slowPath); - // We don't need memory barriers since we just fast-created the function, so it // must be young. m_out.storePtr(scope, fastObject, m_heaps.JSCallee_scope); @@ -11424,7 +11433,6 @@ IGNORE_CLANG_WARNINGS_END setJSValue(m_out.phi(Int64, fastResult, slowResult)); } - void compileToStringOrCallStringConstructorOrStringValueOf() { JSGlobalObject* globalObject = m_graph.globalObjectFor(m_origin.semantic); @@ -14633,7 +14641,6 @@ IGNORE_CLANG_WARNINGS_END } } - PatchpointValue* patchpoint = m_out.patchpoint(Int64); // Append the forms of the arguments that we will use before any clobbering happens. @@ -15342,7 +15349,6 @@ IGNORE_CLANG_WARNINGS_END return m_out.constInt32(knownLength); } - // We need to perform the same logical operation as the code above, but through dynamic operations. if (!numberOfArgumentsToSkip) return argumentsLength.value; @@ -18477,7 +18483,6 @@ IGNORE_CLANG_WARNINGS_END // If it's an Int32 and we use it as such this boxing will be DCE'd by b3 later anyway. lowJSValue(propertyNameEdge, ManualOperandSpeculation); - LValue index = lowInt32(indexEdge); LValue mode = lowInt32(m_graph.varArgChild(m_node, 4)); LValue enumerator = lowCell(m_graph.varArgChild(m_node, 5)); @@ -19185,7 +19190,6 @@ IGNORE_CLANG_WARNINGS_END m_out.storePtr(scope, fastObject, m_heaps.JSScope_next); m_out.storePtr(weakPointer(table), fastObject, m_heaps.JSSymbolTableObject_symbolTable); - ValueFromBlock fastResult = m_out.anchor(fastObject); m_out.jump(continuation); @@ -21898,6 +21902,227 @@ IGNORE_CLANG_WARNINGS_END } } +#if USE(BUN_JSC_ADDITIONS) + void compileBufferRead() + { + DataViewData data = m_node->bufferAccessData(); + ASSERT(data.byteSize == 1 || data.isLittleEndian != TriState::Indeterminate); + Edge baseEdge = m_graph.varArgChild(m_node, 0); + LValue base = lowCell(baseEdge); + LValue offset = lowInt32(m_graph.varArgChild(m_node, 1)); + LValue storage = lowStorage(m_graph.varArgChild(m_node, 2)); + + TypedPointer pointer(m_heaps.TypedArrayProperties, m_out.add(storage, m_out.zeroExtPtr(offset))); + bool isBigEndian = data.isLittleEndian == TriState::False; + + auto keepBaseAlive = [&] { + ensureStillAliveHere(base); + }; + + if (m_node->op() == BufferReadInt) { + switch (data.byteSize) { + case 1: + setInt32(data.isSigned ? m_out.load8SignExt32(pointer) : m_out.load8ZeroExt32(pointer)); + break; + case 2: { + if (!isBigEndian) + setInt32(data.isSigned ? m_out.load16SignExt32(pointer) : m_out.load16ZeroExt32(pointer)); + else { + LValue loadedValue = m_out.load16ZeroExt32(pointer); + PatchpointValue* patchpoint = m_out.patchpoint(Int32); + patchpoint->appendSomeRegister(loadedValue); + patchpoint->setGenerator([=] (CCallHelpers& jit, const StackmapGenerationParams& params) { + jit.move(params[1].gpr(), params[0].gpr()); + jit.byteSwap16(params[0].gpr()); + if (data.isSigned) + jit.signExtend16To32(params[0].gpr(), params[0].gpr()); + }); + patchpoint->effects = Effects::none(); + setInt32(patchpoint); + } + break; + } + case 4: { + LValue loadedValue = m_out.load32(pointer); + if (isBigEndian) + loadedValue = byteSwap32(loadedValue); + if (data.isSigned) + setInt32(loadedValue); + else + setStrictInt52(m_out.zeroExt(loadedValue, Int64)); + break; + } + case 8: { + LValue loadedValue = m_out.load64(pointer); + if (isBigEndian) + loadedValue = byteSwap64(loadedValue); + JSGlobalObject* globalObject = m_graph.globalObjectFor(m_origin.semantic); + if (data.isSigned) + setJSValue(vmCall(Int64, operationInt64ToBigInt, weakPointer(globalObject), loadedValue)); + else + setJSValue(vmCall(Int64, operationUInt64ToBigInt, weakPointer(globalObject), loadedValue)); + break; + } + default: + RELEASE_ASSERT_NOT_REACHED(); + } + keepBaseAlive(); + return; + } + + ASSERT(m_node->op() == BufferReadFloat); + switch (data.byteSize) { + case 4: + if (!isBigEndian) + setDouble(m_out.floatToDouble(m_out.loadFloat(pointer))); + else { + LValue loadedValue = m_out.load32(pointer); + PatchpointValue* patchpoint = m_out.patchpoint(Double); + patchpoint->appendSomeRegister(loadedValue); + patchpoint->numGPScratchRegisters = 1; + patchpoint->setGenerator([=] (CCallHelpers& jit, const StackmapGenerationParams& params) { + jit.move(params[1].gpr(), params.gpScratch(0)); + jit.byteSwap32(params.gpScratch(0)); + jit.move32ToFloat(params.gpScratch(0), params[0].fpr()); + jit.convertFloatToDouble(params[0].fpr(), params[0].fpr()); + }); + patchpoint->effects = Effects::none(); + setDouble(patchpoint); + } + break; + case 8: + if (!isBigEndian) + setDouble(m_out.loadDouble(pointer)); + else + setDouble(m_out.bitCast(byteSwap64(m_out.load64(pointer)), Double)); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + keepBaseAlive(); + } + + void compileBufferWrite() + { + DataViewData data = m_node->bufferAccessData(); + ASSERT(data.byteSize == 1 || data.isLittleEndian != TriState::Indeterminate); + LValue base = lowCell(m_graph.varArgChild(m_node, 0)); + LValue offset = lowInt32(m_graph.varArgChild(m_node, 1)); + Edge valueEdge = m_graph.varArgChild(m_node, 2); + LValue storage = lowStorage(m_graph.varArgChild(m_node, 3)); + + LValue valueToStore; + LValue bigInt = nullptr; + switch (valueEdge.useKind()) { + case Int32Use: + valueToStore = lowInt32(valueEdge); + break; + case Int52RepUse: + valueToStore = lowStrictInt52(valueEdge); + break; + case DoubleRepUse: + valueToStore = lowDouble(valueEdge); + break; + case HeapBigIntUse: + bigInt = lowHeapBigInt(valueEdge); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + + if (bigInt) { + RELEASE_ASSERT(data.byteSize == 8); + speculate(Overflow, noValue(), nullptr, m_out.above(m_out.load32NonNegative(bigInt, m_heaps.JSBigInt_length), m_out.constInt32(1))); + LValue isNegative = m_out.testNonZero32(m_out.load8ZeroExt32(bigInt, m_heaps.JSCell_typeInfoFlags), m_out.constInt32(TypeInfoPerCellBit)); + if (!data.isSigned) + speculate(Overflow, noValue(), nullptr, isNegative); + valueToStore = toBigInt64(bigInt); + if (data.isSigned) { + LValue signMismatch = m_out.notEqual(isNegative, m_out.lessThan(valueToStore, m_out.int64Zero)); + speculate(Overflow, noValue(), nullptr, m_out.bitAnd(m_out.notZero64(valueToStore), signMismatch)); + } + } else if (!data.isFloatingPoint) { + switch (data.byteSize) { + case 1: + case 2: + RELEASE_ASSERT(valueEdge.useKind() == Int32Use); + break; + case 4: + if (data.isSigned) + RELEASE_ASSERT(valueEdge.useKind() == Int32Use); + else { + RELEASE_ASSERT(valueEdge.useKind() == Int52RepUse); + speculate(Overflow, noValue(), nullptr, m_out.above(valueToStore, m_out.constInt64(0xffffffffLL))); + valueToStore = m_out.castToInt32(valueToStore); + } + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + } + + TypedPointer pointer(m_heaps.TypedArrayProperties, m_out.add(storage, m_out.zeroExtPtr(offset))); + bool isBigEndian = data.isLittleEndian == TriState::False; + + if (data.isFloatingPoint) { + RELEASE_ASSERT(valueEdge.useKind() == DoubleRepUse); + if (data.byteSize == 4) { + LValue floatValue = m_out.doubleToFloat(valueToStore); + if (!isBigEndian) + m_out.storeFloat(floatValue, pointer); + else { + PatchpointValue* patchpoint = m_out.patchpoint(Int32); + patchpoint->appendSomeRegister(floatValue); + patchpoint->setGenerator([=] (CCallHelpers& jit, const StackmapGenerationParams& params) { + jit.moveFloatTo32(params[1].fpr(), params[0].gpr()); + jit.byteSwap32(params[0].gpr()); + }); + patchpoint->effects = Effects::none(); + m_out.store32(patchpoint, pointer); + } + } else { + RELEASE_ASSERT(data.byteSize == 8); + if (!isBigEndian) + m_out.storeDouble(valueToStore, pointer); + else + m_out.store64(byteSwap64(m_out.bitCast(valueToStore, Int64)), pointer); + } + } else { + switch (data.byteSize) { + case 1: + m_out.store32As8(valueToStore, pointer); + break; + case 2: { + if (!isBigEndian) + m_out.store32As16(valueToStore, pointer); + else { + PatchpointValue* patchpoint = m_out.patchpoint(Int32); + patchpoint->appendSomeRegister(valueToStore); + patchpoint->setGenerator([=] (CCallHelpers& jit, const StackmapGenerationParams& params) { + jit.move(params[1].gpr(), params[0].gpr()); + jit.byteSwap16(params[0].gpr()); + }); + patchpoint->effects = Effects::none(); + m_out.store32As16(patchpoint, pointer); + } + break; + } + case 4: + m_out.store32(isBigEndian ? byteSwap32(valueToStore) : valueToStore, pointer); + break; + case 8: + RELEASE_ASSERT(bigInt); + m_out.store64(isBigEndian ? byteSwap64(valueToStore) : valueToStore, pointer); + break; + default: + RELEASE_ASSERT_NOT_REACHED(); + } + } + + ensureStillAliveHere(base); + } +#endif // USE(BUN_JSC_ADDITIONS) + void compileDateNow() { JSGlobalObject* globalObject = m_graph.globalObjectFor(m_node->origin.semantic); @@ -23058,7 +23283,6 @@ IGNORE_CLANG_WARNINGS_END m_out.shl(m_out.zeroExt(preCapacity, pointerType()), m_out.constIntPtr(3)), m_out.constIntPtr(sizeof(IndexingHeader)))); - m_out.store32(publicLength, butterfly, m_heaps.Butterfly_publicLength); m_out.store32(vectorLength, butterfly, m_heaps.Butterfly_vectorLength); diff --git a/Source/JavaScriptCore/runtime/BufferAccessorRegistry.cpp b/Source/JavaScriptCore/runtime/BufferAccessorRegistry.cpp new file mode 100644 index 0000000000000..9cff7c689ae30 --- /dev/null +++ b/Source/JavaScriptCore/runtime/BufferAccessorRegistry.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2026 Oven-sh Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "BufferAccessorRegistry.h" + +#if USE(BUN_JSC_ADDITIONS) + +#include +#include +#include + +namespace JSC { + +struct BufferAccessorRegistry { + Lock lock; + struct Entry { + uint64_t data { 0 }; + bool isWrite { false }; + bool byteLengthFromArgument { false }; + }; + UncheckedKeyHashMap entries WTF_GUARDED_BY_LOCK(lock); +}; + +static BufferAccessorRegistry& bufferAccessorRegistry() +{ + static LazyNeverDestroyed registry; + static std::once_flag onceFlag; + std::call_once(onceFlag, [] { + registry.construct(); + }); + return registry.get(); +} + +void registerBufferAccessor(TaggedNativeFunction function, BufferAccessorDescriptor descriptor) +{ + ASSERT(descriptor.byteLengthFromArgument ? !descriptor.data.byteSize : (descriptor.data.byteSize == 1 || descriptor.data.byteSize == 2 || descriptor.data.byteSize == 4 || descriptor.data.byteSize == 8)); + ASSERT(descriptor.data.byteSize == 1 || descriptor.data.isLittleEndian != TriState::Indeterminate); + auto& registry = bufferAccessorRegistry(); + Locker locker { registry.lock }; + registry.entries.set(function.untaggedPtr(), BufferAccessorRegistry::Entry { descriptor.data.asQuadWord, descriptor.isWrite, descriptor.byteLengthFromArgument }); +} + +std::optional bufferAccessorDescriptor(TaggedNativeFunction function) +{ + auto& registry = bufferAccessorRegistry(); + Locker locker { registry.lock }; + auto iterator = registry.entries.find(function.untaggedPtr()); + if (iterator == registry.entries.end()) + return std::nullopt; + BufferAccessorDescriptor descriptor; + descriptor.data.asQuadWord = iterator->value.data; + descriptor.isWrite = iterator->value.isWrite; + descriptor.byteLengthFromArgument = iterator->value.byteLengthFromArgument; + return descriptor; +} + +} // namespace JSC + +#endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/BufferAccessorRegistry.h b/Source/JavaScriptCore/runtime/BufferAccessorRegistry.h new file mode 100644 index 0000000000000..290dd14873b7c --- /dev/null +++ b/Source/JavaScriptCore/runtime/BufferAccessorRegistry.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2026 Oven-sh Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if USE(BUN_JSC_ADDITIONS) + +#include "DFGDataViewData.h" +#include "NativeFunction.h" +#include + +namespace JSC { + +struct BufferAccessorDescriptor { + DFG::DataViewData data; + bool isWrite; + bool byteLengthFromArgument { false }; +}; + +JS_EXPORT_PRIVATE void registerBufferAccessor(TaggedNativeFunction function, BufferAccessorDescriptor); + +JS_EXPORT_PRIVATE std::optional bufferAccessorDescriptor(TaggedNativeFunction function); + +} // namespace JSC + +#endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/Intrinsic.h b/Source/JavaScriptCore/runtime/Intrinsic.h index f8e7495706bdd..64ccea42c16ff 100644 --- a/Source/JavaScriptCore/runtime/Intrinsic.h +++ b/Source/JavaScriptCore/runtime/Intrinsic.h @@ -31,6 +31,14 @@ namespace JSC { +#if USE(BUN_JSC_ADDITIONS) +#define JSC_FOR_EACH_BUN_JSC_INTRINSIC(macro) \ + macro(BufferAccessorIntrinsic) \ + +#else +#define JSC_FOR_EACH_BUN_JSC_INTRINSIC(macro) +#endif + #define JSC_FOR_EACH_INTRINSIC(macro) \ /* Call intrinsics. */ \ macro(NoIntrinsic) \ @@ -293,6 +301,8 @@ namespace JSC { macro(DataViewSetBigInt64) \ macro(DataViewSetBigUint64) \ \ + JSC_FOR_EACH_BUN_JSC_INTRINSIC(macro) \ + \ macro(WasmFunctionIntrinsic) \ enum Intrinsic : uint8_t { diff --git a/Source/JavaScriptCore/tools/JSDollarVM.cpp b/Source/JavaScriptCore/tools/JSDollarVM.cpp index 61aa178807127..5dff79e264b47 100644 --- a/Source/JavaScriptCore/tools/JSDollarVM.cpp +++ b/Source/JavaScriptCore/tools/JSDollarVM.cpp @@ -76,6 +76,14 @@ #include "VMInspector.h" #include "VMTrapsInlines.h" #include "WasmCapabilities.h" +#if USE(BUN_JSC_ADDITIONS) +#include "BufferAccessorRegistry.h" +#include "JSArrayBufferView.h" +#include "JSBigInt.h" +#include "MathCommon.h" +#include "ObjectConstructor.h" +#include +#endif #include #include #include @@ -1080,7 +1088,6 @@ class DOMJITNode : public JSNonFinalObject { int32_t m_value { 42 }; }; - static JSC_DECLARE_CUSTOM_GETTER(domJITGetterCustomGetter); JSC_DECLARE_JIT_OPERATION(domJITGetterSlowCall, EncodedJSValue, (JSGlobalObject*, void*)); @@ -1186,7 +1193,6 @@ JSC_DEFINE_JIT_OPERATION(domJITGetterSlowCall, EncodedJSValue, (JSGlobalObject* OPERATION_RETURN(scope, JSValue::encode(jsNumber(static_cast(pointer)->value()))); } - static JSC_DECLARE_CUSTOM_GETTER(domJITGetterNoEffectCustomGetter); JSC_DECLARE_JIT_OPERATION(domJITGetterNoEffectSlowCall, EncodedJSValue, (JSGlobalObject*, void*)); @@ -1706,7 +1712,6 @@ class JSTestCustomGetterSetter : public JSNonFinalObject { DECLARE_INFO; }; - static JSC_DECLARE_CUSTOM_GETTER(customGetAccessor); static JSC_DECLARE_CUSTOM_GETTER(customGetValue); static JSC_DECLARE_CUSTOM_GETTER(customGetValue2); @@ -4373,7 +4378,6 @@ JSC_DEFINE_HOST_FUNCTION(functionEnsureArrayStorage, (JSGlobalObject* globalObje return JSValue::encode(jsUndefined()); } - #if PLATFORM(COCOA) JSC_DEFINE_HOST_FUNCTION(functionSetCrashLogMessage, (JSGlobalObject* globalObject, CallFrame* callFrame)) { @@ -4498,6 +4502,332 @@ JSC_DEFINE_HOST_FUNCTION(functionWeakCreate, (JSGlobalObject* globalObject, Call return JSValue::encode(jsUndefined()); } +#if USE(BUN_JSC_ADDITIONS) +namespace BufferAccessorTest { + +enum class Kind : uint8_t { Int8, Uint8, Int16, Uint16, Int32, Uint32, Float32, Float64, BigInt64, BigUint64 }; + +static constexpr uint8_t byteSizeFor(Kind kind) +{ + switch (kind) { + case Kind::Int8: + case Kind::Uint8: + return 1; + case Kind::Int16: + case Kind::Uint16: + return 2; + case Kind::Int32: + case Kind::Uint32: + case Kind::Float32: + return 4; + case Kind::Float64: + case Kind::BigInt64: + case Kind::BigUint64: + return 8; + } + return 0; +} + +template +static EncodedJSValue JSC_HOST_CALL_ATTRIBUTES accessor(JSGlobalObject* globalObject, CallFrame* callFrame) +{ + DollarVMAssertScope assertScope; + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + constexpr size_t byteSize = byteSizeFor(kind); + constexpr bool isBigInt = kind == Kind::BigInt64 || kind == Kind::BigUint64; + constexpr bool isFloat = kind == Kind::Float32 || kind == Kind::Float64; + + auto* view = dynamicDowncast(callFrame->thisValue()); + if (!view) + return throwVMTypeError(globalObject, scope, "Buffer accessor receiver must be an ArrayBufferView"_s); + + double numberValue = 0; + uint64_t bigIntValue = 0; + if constexpr (isWrite) { + JSValue value = callFrame->argument(0); + if constexpr (isBigInt) { + if (!value.isBigInt()) + return throwVMTypeError(globalObject, scope, "Buffer accessor value must be a BigInt"_s); + if (auto* heapBigInt = value.isCell() ? dynamicDowncast(value.asCell()) : nullptr) { + if (heapBigInt->length() > 1) + return throwVMRangeError(globalObject, scope, "Buffer accessor value is out of range"_s); + uint64_t digit = heapBigInt->length() ? heapBigInt->digit(0) : 0; + bool outOfRange = kind == Kind::BigUint64 + ? heapBigInt->sign() && heapBigInt->length() + : (heapBigInt->sign() ? digit > (1ULL << 63) : digit > static_cast(std::numeric_limits::max())); + if (outOfRange) + return throwVMRangeError(globalObject, scope, "Buffer accessor value is out of range"_s); + } + bigIntValue = kind == Kind::BigInt64 ? static_cast(value.toBigInt64(globalObject)) : value.toBigUInt64(globalObject); + RETURN_IF_EXCEPTION(scope, { }); + } else { + numberValue = value.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, { }); + } + } + + JSValue offsetValue = callFrame->argument(isWrite ? 1 : 0); + double offsetNumber = 0; + if (!offsetValue.isUndefined()) { + if (!offsetValue.isNumber()) + return throwVMRangeError(globalObject, scope, "Buffer accessor offset must be a number"_s); + offsetNumber = offsetValue.asNumber(); + } + size_t byteLength = view->byteLength(); + if (std::floor(offsetNumber) != offsetNumber || byteLength < byteSize || !(offsetNumber >= 0 && offsetNumber <= static_cast(byteLength - byteSize))) + return throwVMRangeError(globalObject, scope, "Buffer accessor offset is out of range"_s); + size_t offset = static_cast(offsetNumber); + uint8_t* address = static_cast(view->vector()) + offset; + + auto swapIfBigEndian = [](auto integer) { + if constexpr (isLittleEndian || sizeof(integer) == 1) + return integer; + else if constexpr (sizeof(integer) == 2) + return static_cast(__builtin_bswap16(integer)); + else if constexpr (sizeof(integer) == 4) + return static_cast(__builtin_bswap32(integer)); + else + return static_cast(__builtin_bswap64(integer)); + }; + + if constexpr (!isWrite) { + if constexpr (kind == Kind::Int8) + return JSValue::encode(jsNumber(WTF::unalignedLoad(address))); + else if constexpr (kind == Kind::Uint8) + return JSValue::encode(jsNumber(WTF::unalignedLoad(address))); + else if constexpr (kind == Kind::Int16) + return JSValue::encode(jsNumber(static_cast(swapIfBigEndian(WTF::unalignedLoad(address))))); + else if constexpr (kind == Kind::Uint16) + return JSValue::encode(jsNumber(swapIfBigEndian(WTF::unalignedLoad(address)))); + else if constexpr (kind == Kind::Int32) + return JSValue::encode(jsNumber(static_cast(swapIfBigEndian(WTF::unalignedLoad(address))))); + else if constexpr (kind == Kind::Uint32) + return JSValue::encode(jsNumber(swapIfBigEndian(WTF::unalignedLoad(address)))); + else if constexpr (kind == Kind::Float32) + return JSValue::encode(jsNumber(purifyNaN(std::bit_cast(swapIfBigEndian(WTF::unalignedLoad(address)))))); + else if constexpr (kind == Kind::Float64) + return JSValue::encode(jsNumber(purifyNaN(std::bit_cast(swapIfBigEndian(WTF::unalignedLoad(address)))))); + else if constexpr (kind == Kind::BigInt64) { + int64_t loaded = static_cast(swapIfBigEndian(WTF::unalignedLoad(address))); + RELEASE_AND_RETURN(scope, JSValue::encode(JSBigInt::makeHeapBigIntOrBigInt32(globalObject, loaded))); + } else { + uint64_t loaded = swapIfBigEndian(WTF::unalignedLoad(address)); + RELEASE_AND_RETURN(scope, JSValue::encode(JSBigInt::makeHeapBigIntOrBigInt32(globalObject, loaded))); + } + } else { + if constexpr (isFloat) { + if constexpr (kind == Kind::Float32) + WTF::unalignedStore(address, swapIfBigEndian(std::bit_cast(static_cast(numberValue)))); + else + WTF::unalignedStore(address, swapIfBigEndian(std::bit_cast(numberValue))); + } else if constexpr (isBigInt) + WTF::unalignedStore(address, swapIfBigEndian(bigIntValue)); + else { + double minimum, maximum; + switch (kind) { + case Kind::Int8: + minimum = -0x80; + maximum = 0x7f; + break; + case Kind::Uint8: + minimum = 0; + maximum = 0xff; + break; + case Kind::Int16: + minimum = -0x8000; + maximum = 0x7fff; + break; + case Kind::Uint16: + minimum = 0; + maximum = 0xffff; + break; + case Kind::Int32: + minimum = INT32_MIN; + maximum = INT32_MAX; + break; + case Kind::Uint32: + default: + minimum = 0; + maximum = 4294967295.0; + break; + } + if (numberValue < minimum || numberValue > maximum) + return throwVMRangeError(globalObject, scope, "Buffer accessor value is out of range"_s); + if constexpr (byteSize == 1) + WTF::unalignedStore(address, static_cast(toInt32(numberValue))); + else if constexpr (byteSize == 2) + WTF::unalignedStore(address, swapIfBigEndian(static_cast(toInt32(numberValue)))); + else if constexpr (kind == Kind::Int32) + WTF::unalignedStore(address, swapIfBigEndian(static_cast(toInt32(numberValue)))); + else + WTF::unalignedStore(address, swapIfBigEndian(toUInt32(numberValue))); + } + return JSValue::encode(jsNumber(offset + byteSize)); + } +} + +struct Entry { + ASCIILiteral name; + NativeFunction function; + Kind kind; + bool isLittleEndian; + bool isWrite; + unsigned arity; +}; + +#define BUFFER_ACCESSOR_TEST_ENTRY(name, kindName, isLittleEndian, isWrite, arity) \ + { name ""_s, accessor, Kind::kindName, isLittleEndian, isWrite, arity } + +static const Entry entries[] = { + BUFFER_ACCESSOR_TEST_ENTRY("readInt8", Int8, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readUInt8", Uint8, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readInt16LE", Int16, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readInt16BE", Int16, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readUInt16LE", Uint16, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readUInt16BE", Uint16, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readInt32LE", Int32, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readInt32BE", Int32, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readUInt32LE", Uint32, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readUInt32BE", Uint32, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readFloatLE", Float32, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readFloatBE", Float32, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readDoubleLE", Float64, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readDoubleBE", Float64, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readBigInt64LE", BigInt64, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readBigInt64BE", BigInt64, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readBigUInt64LE", BigUint64, true, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("readBigUInt64BE", BigUint64, false, false, 1), + BUFFER_ACCESSOR_TEST_ENTRY("writeInt8", Int8, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeUInt8", Uint8, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeInt16LE", Int16, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeInt16BE", Int16, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeUInt16LE", Uint16, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeUInt16BE", Uint16, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeInt32LE", Int32, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeInt32BE", Int32, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeUInt32LE", Uint32, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeUInt32BE", Uint32, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeFloatLE", Float32, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeFloatBE", Float32, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeDoubleLE", Float64, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeDoubleBE", Float64, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeBigInt64LE", BigInt64, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeBigInt64BE", BigInt64, false, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeBigUInt64LE", BigUint64, true, true, 2), + BUFFER_ACCESSOR_TEST_ENTRY("writeBigUInt64BE", BigUint64, false, true, 2), +}; + +#undef BUFFER_ACCESSOR_TEST_ENTRY + +template +static EncodedJSValue JSC_HOST_CALL_ATTRIBUTES varWidthAccessor(JSGlobalObject* globalObject, CallFrame* callFrame) +{ + DollarVMAssertScope assertScope; + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* view = dynamicDowncast(callFrame->thisValue()); + if (!view) + return throwVMTypeError(globalObject, scope, "Buffer accessor receiver must be an ArrayBufferView"_s); + + double numberValue = 0; + if constexpr (isWrite) { + numberValue = callFrame->argument(0).toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, { }); + } + JSValue offsetValue = callFrame->argument(isWrite ? 1 : 0); + JSValue byteLengthValue = callFrame->argument(isWrite ? 2 : 1); + if (!byteLengthValue.isNumber() || byteLengthValue.asNumber() < 1 || byteLengthValue.asNumber() > 6 || std::floor(byteLengthValue.asNumber()) != byteLengthValue.asNumber()) + return throwVMRangeError(globalObject, scope, "Buffer accessor byteLength must be 1..6"_s); + size_t byteLength = static_cast(byteLengthValue.asNumber()); + if (!offsetValue.isNumber()) + return throwVMTypeError(globalObject, scope, "Buffer accessor offset must be a number"_s); + double offsetNumber = offsetValue.asNumber(); + size_t viewByteLength = view->byteLength(); + if (std::floor(offsetNumber) != offsetNumber || viewByteLength < byteLength || !(offsetNumber >= 0 && offsetNumber <= static_cast(viewByteLength - byteLength))) + return throwVMRangeError(globalObject, scope, "Buffer accessor offset is out of range"_s); + size_t offset = static_cast(offsetNumber); + uint8_t* address = static_cast(view->vector()) + offset; + + if constexpr (isWrite) { + double min = isSigned ? -std::pow(2.0, 8.0 * byteLength - 1) : 0; + double max = (isSigned ? std::pow(2.0, 8.0 * byteLength - 1) : std::pow(2.0, 8.0 * byteLength)) - 1; + if (!(numberValue >= min && numberValue <= max)) + return throwVMRangeError(globalObject, scope, "Buffer accessor value is out of range"_s); + int64_t bits = static_cast(std::trunc(numberValue)); + for (size_t i = 0; i < byteLength; ++i) + address[isLittleEndian ? i : byteLength - 1 - i] = static_cast(static_cast(bits) >> (8 * i)); + return JSValue::encode(jsNumber(offset + byteLength)); + } + + uint64_t bits = 0; + for (size_t i = 0; i < byteLength; ++i) + bits |= static_cast(address[isLittleEndian ? i : byteLength - 1 - i]) << (8 * i); + if constexpr (isSigned) { + unsigned shift = 64 - 8 * byteLength; + return JSValue::encode(jsNumber(static_cast(static_cast(bits << shift) >> shift))); + } else + return JSValue::encode(jsNumber(static_cast(bits))); +} + +struct VarWidthEntry { + ASCIILiteral name; + NativeFunction function; + bool isSigned; + bool isLittleEndian; + bool isWrite; + unsigned arity; +}; + +static const VarWidthEntry varWidthEntries[] = { + { "readIntLE"_s, varWidthAccessor, true, true, false, 2 }, + { "readIntBE"_s, varWidthAccessor, true, false, false, 2 }, + { "readUIntLE"_s, varWidthAccessor, false, true, false, 2 }, + { "readUIntBE"_s, varWidthAccessor, false, false, false, 2 }, + { "writeIntLE"_s, varWidthAccessor, true, true, true, 3 }, + { "writeIntBE"_s, varWidthAccessor, true, false, true, 3 }, + { "writeUIntLE"_s, varWidthAccessor, false, true, true, 3 }, + { "writeUIntBE"_s, varWidthAccessor, false, false, true, 3 }, +}; + +} // namespace BufferAccessorTest + +static JSC_DECLARE_HOST_FUNCTION(functionCreateBufferAccessors); +JSC_DEFINE_HOST_FUNCTION(functionCreateBufferAccessors, (JSGlobalObject* globalObject, CallFrame*)) +{ + DollarVMAssertScope assertScope; + VM& vm = globalObject->vm(); + + using namespace BufferAccessorTest; + JSObject* result = constructEmptyObject(globalObject); + for (auto& entry : entries) { + DFG::DataViewData data { }; + data.byteSize = byteSizeFor(entry.kind); + data.isSigned = entry.kind == Kind::Int8 || entry.kind == Kind::Int16 || entry.kind == Kind::Int32 || entry.kind == Kind::BigInt64; + data.isFloatingPoint = entry.kind == Kind::Float32 || entry.kind == Kind::Float64; + data.isResizable = false; + data.isLittleEndian = triState(entry.isLittleEndian); + registerBufferAccessor(toTagged(entry.function), BufferAccessorDescriptor { data, entry.isWrite }); + JSFunction* function = JSFunction::create(vm, globalObject, entry.arity, entry.name, entry.function, ImplementationVisibility::Public, BufferAccessorIntrinsic); + result->putDirect(vm, Identifier::fromString(vm, entry.name), function); + } + for (auto& entry : varWidthEntries) { + DFG::DataViewData data { }; + data.byteSize = 0; + data.isSigned = entry.isSigned; + data.isFloatingPoint = false; + data.isResizable = false; + data.isLittleEndian = triState(entry.isLittleEndian); + registerBufferAccessor(toTagged(entry.function), BufferAccessorDescriptor { data, entry.isWrite, true }); + JSFunction* function = JSFunction::create(vm, globalObject, entry.arity, entry.name, entry.function, ImplementationVisibility::Public, BufferAccessorIntrinsic); + result->putDirect(vm, Identifier::fromString(vm, entry.name), function); + } + return JSValue::encode(result); +} +#endif // USE(BUN_JSC_ADDITIONS) + constexpr unsigned jsDollarVMPropertyAttributes = PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum | PropertyAttribute::DontDelete; void JSDollarVM::finishCreation(VM& vm) @@ -4715,6 +5045,9 @@ void JSDollarVM::finishCreation(VM& vm) addFunction(vm, alwaysAllow, "cachedCallFromCPP"_s, functionCachedCallFromCPP, 2); addFunction(vm, alwaysAllow, "dumpLineBreakData"_s, functionDumpLineBreakData, 0); addFunction(vm, alwaysAllow, "weakCreate"_s, functionWeakCreate, 0); +#if USE(BUN_JSC_ADDITIONS) + addFunction(vm, alwaysAllow, "createBufferAccessors"_s, functionCreateBufferAccessors, 0); +#endif if (allowIfNotFuzz) { m_objectDoingSideEffectPutWithoutCorrectSlotStatusStructureID.set(vm, this, ObjectDoingSideEffectPutWithoutCorrectSlotStatus::createStructure(vm, globalObject, jsNull()));