forked from WebKit/WebKit
-
Notifications
You must be signed in to change notification settings - Fork 52
Add DFG/FTL nodes for byte-offset scalar accessors on ArrayBufferView receivers #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jarred-Sumner
wants to merge
12
commits into
main
Choose a base branch
from
claude/buffer-jit
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,412
−55
Open
Changes from 6 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
fb957cf
Add DFG/FTL nodes for byte-offset scalar accessors on ArrayBufferView…
Jarred-Sumner abf1dc3
Fixup: keep BufferReadInt/BufferWrite edges well-formed on the ForceE…
Jarred-Sumner 4fa7b25
Address review: parser resizable fallback, FTL guards, test assertion
Jarred-Sumner f2c0e3b
Buffer accessors: variable-width descriptors, inlined when the byteLe…
Jarred-Sumner be77ad7
JSDollarVM: mark the buffer accessor test host functions JSC_HOST_CAL…
Jarred-Sumner 2a16ffb
Merge remote-tracking branch 'origin/main' into claude/buffer-jit
Jarred-Sumner 6d8df12
Buffer accessors: graph-level value range checks, dead return value
Jarred-Sumner a4ecdd6
JSDollarVM: purify NaN on buffer accessor float reads, reject NaN writes
Jarred-Sumner cee81fb
Remove the buffer accessor comments
Jarred-Sumner b2e9f59
Buffer accessors: use the Int52 typed array length in the FTL
Jarred-Sumner 93cf9dc
Buffer accessors: stress test views with 2GB / ~4GB byteOffsets
Jarred-Sumner 8debd97
Buffer accessors: differential fuzzer stress test, restore Overflow gate
Jarred-Sumner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| //@ requireOptions("--useDollarVM=1") | ||
|
|
||
| // writeBigInt64* / writeBigUInt64*: a BufferWrite node with a heap-BigInt value edge and an inline | ||
| // "fits in 64 bits" check; anything wider (or negative, for the unsigned writers) OSR-exits to the | ||
| // host function, which throws. | ||
|
|
||
| function shouldBe(actual, expected, message) { | ||
| if (actual !== expected) throw new Error(message + ": expected " + expected + " but got " + actual); | ||
| } | ||
| function shouldThrow(f, expected, message) { | ||
| let error = null; | ||
| try { | ||
| f(); | ||
| } catch (e) { | ||
| error = e; | ||
| } | ||
| if (!(error instanceof expected)) throw new Error(message + ": expected a " + expected.name + " but got " + error); | ||
| } | ||
|
|
||
| const accessors = $vm.createBufferAccessors(); | ||
| class Buffer extends Uint8Array {} | ||
| Object.assign(Buffer.prototype, accessors); | ||
|
|
||
| const buf = new Buffer(64); | ||
| const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); | ||
|
|
||
| function writeBigInt64LE(b, v, o) { | ||
| return b.writeBigInt64LE(v, o); | ||
| } | ||
| noInline(writeBigInt64LE); | ||
| function writeBigInt64BE(b, v, o) { | ||
| return b.writeBigInt64BE(v, o); | ||
| } | ||
| noInline(writeBigInt64BE); | ||
| function writeBigUInt64LE(b, v, o) { | ||
| return b.writeBigUInt64LE(v, o); | ||
| } | ||
| noInline(writeBigUInt64LE); | ||
| function writeBigUInt64BE(b, v, o) { | ||
| return b.writeBigUInt64BE(v, o); | ||
| } | ||
| noInline(writeBigUInt64BE); | ||
|
|
||
| const values = [0n, 1n, -1n, 42n, -42n, 2n ** 31n, -(2n ** 31n), 2n ** 32n + 7n, 2n ** 63n - 1n, -(2n ** 63n)]; | ||
| for (let i = 0; i < 2e4; ++i) { | ||
| const o = (i & 7) * 8; | ||
| const v = values[i % values.length]; | ||
| shouldBe(writeBigInt64LE(buf, v, o), o + 8, "writeBigInt64LE result"); | ||
| shouldBe(dv.getBigInt64(o, true), v, "writeBigInt64LE store"); | ||
| shouldBe(writeBigInt64BE(buf, v, o), o + 8, "writeBigInt64BE result"); | ||
| shouldBe(dv.getBigInt64(o, false), v, "writeBigInt64BE store"); | ||
| if (v >= 0n) { | ||
| shouldBe(writeBigUInt64LE(buf, v, o), o + 8, "writeBigUInt64LE result"); | ||
| shouldBe(dv.getBigUint64(o, true), v, "writeBigUInt64LE store"); | ||
| shouldBe(writeBigUInt64BE(buf, v, o), o + 8, "writeBigUInt64BE result"); | ||
| shouldBe(dv.getBigUint64(o, false), v, "writeBigUInt64BE store"); | ||
| } | ||
| } | ||
|
|
||
| // unsigned max, and the out-of-range / wrong-type exits. | ||
| for (let i = 0; i < 2e4; ++i) { | ||
| shouldBe(writeBigUInt64LE(buf, 2n ** 64n - 1n, 0), 8, "unsigned max"); | ||
| shouldBe(dv.getBigUint64(0, true), 2n ** 64n - 1n, "unsigned max store"); | ||
| shouldThrow(() => writeBigUInt64LE(buf, -1n, 0), RangeError, "unsigned negative"); | ||
| shouldThrow(() => writeBigUInt64LE(buf, 2n ** 64n, 0), RangeError, "unsigned too big"); | ||
| shouldThrow(() => writeBigInt64LE(buf, 2n ** 63n, 0), RangeError, "signed too big"); | ||
| shouldThrow(() => writeBigInt64LE(buf, -(2n ** 63n) - 1n, 0), RangeError, "signed too small"); | ||
| shouldThrow(() => writeBigInt64LE(buf, 2n ** 100n, 0), RangeError, "way too big"); | ||
| shouldThrow(() => writeBigInt64LE(buf, 5, 0), TypeError, "a number is not a BigInt"); | ||
| shouldThrow(() => writeBigInt64LE(buf, 0n, 57), RangeError, "out of bounds"); | ||
| shouldBe(dv.getBigInt64(0, true), -1n, "the failed writes stored nothing"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| //@ requireOptions("--useDollarVM=1") | ||
|
|
||
| // The Buffer accessor nodes speculate: a Uint8Array receiver, an int32 in-bounds offset, an in-range | ||
| // value. Everything else must OSR-exit to the host function and behave exactly as it does in the | ||
| // interpreter -- in particular an out-of-bounds access must always throw, never return undefined, | ||
| // and no store may happen (or happen twice) around an exit. | ||
|
|
||
| function shouldThrow(f, expected, message) { | ||
| let error = null; | ||
| try { | ||
| f(); | ||
| } catch (e) { | ||
| error = e; | ||
| } | ||
| if (!error) throw new Error(message + ": expected a " + expected.name + " but got no throw"); | ||
| if (!(error instanceof expected)) throw new Error(message + ": expected a " + expected.name + " but got " + error); | ||
| } | ||
| function shouldBe(actual, expected, message) { | ||
| if (actual !== expected) throw new Error(message + ": expected " + expected + " but got " + actual); | ||
| } | ||
|
|
||
| const accessors = $vm.createBufferAccessors(); | ||
| class Buffer extends Uint8Array {} | ||
| Object.assign(Buffer.prototype, accessors); | ||
|
|
||
| const buf = new Buffer(16); | ||
| const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); | ||
|
|
||
| // Offsets that leave the fast path. | ||
| function readInt32LE(b, o) { | ||
| return b.readInt32LE(o); | ||
| } | ||
| noInline(readInt32LE); | ||
| function readUInt8(b, o) { | ||
| return b.readUInt8(o); | ||
| } | ||
| noInline(readUInt8); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| dv.setInt32(12, i, true); | ||
| shouldBe(readInt32LE(buf, 12), i, "last valid offset"); | ||
| shouldThrow(() => readInt32LE(buf, 13), RangeError, "one past the last valid offset"); | ||
| shouldThrow(() => readInt32LE(buf, 16), RangeError, "offset === length"); | ||
| shouldThrow(() => readInt32LE(buf, -1), RangeError, "negative offset"); | ||
| shouldThrow(() => readInt32LE(buf, 1.5), RangeError, "fractional offset"); | ||
| shouldThrow(() => readInt32LE(buf, NaN), RangeError, "NaN offset"); | ||
| shouldThrow(() => readInt32LE(buf, Infinity), RangeError, "Infinity offset"); | ||
| shouldThrow(() => readInt32LE(buf, "0"), RangeError, "string offset"); | ||
| shouldBe(readInt32LE(buf, 4.0), dv.getInt32(4, true), "integral double offset"); | ||
| shouldBe(readUInt8(buf, 15), buf[15], "last byte"); | ||
| shouldThrow(() => readUInt8(buf, 16), RangeError, "one-byte read one past the end"); | ||
| } | ||
|
|
||
| // Values that leave the fast path: the narrow writers range-check. | ||
| function writeInt8(b, v, o) { | ||
| return b.writeInt8(v, o); | ||
| } | ||
| noInline(writeInt8); | ||
| function writeUInt16BE(b, v, o) { | ||
| return b.writeUInt16BE(v, o); | ||
| } | ||
| noInline(writeUInt16BE); | ||
| function writeUInt32LE(b, v, o) { | ||
| return b.writeUInt32LE(v, o); | ||
| } | ||
| noInline(writeUInt32LE); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeInt8(buf, 127, 3), 4, "writeInt8 max"); | ||
| shouldBe(dv.getInt8(3), 127, "writeInt8 max store"); | ||
| shouldBe(writeInt8(buf, -128, 3), 4, "writeInt8 min"); | ||
| shouldBe(dv.getInt8(3), -128, "writeInt8 min store"); | ||
| shouldThrow(() => writeInt8(buf, 128, 3), RangeError, "writeInt8 too big"); | ||
| shouldThrow(() => writeInt8(buf, -129, 3), RangeError, "writeInt8 too small"); | ||
| shouldBe(dv.getInt8(3), -128, "a throwing writeInt8 stores nothing"); | ||
| shouldThrow(() => writeUInt16BE(buf, -1, 2), RangeError, "writeUInt16BE negative"); | ||
| shouldThrow(() => writeUInt16BE(buf, 65536, 2), RangeError, "writeUInt16BE too big"); | ||
| shouldBe(writeUInt16BE(buf, 65535, 2), 4, "writeUInt16BE max"); | ||
| shouldBe(dv.getUint16(2, false), 65535, "writeUInt16BE max store"); | ||
| shouldThrow(() => writeUInt32LE(buf, -1, 4), RangeError, "writeUInt32LE negative"); | ||
| shouldThrow(() => writeUInt32LE(buf, 4294967296, 4), RangeError, "writeUInt32LE too big"); | ||
| shouldThrow(() => writeUInt32LE(buf, 4294967295, 14), RangeError, "writeUInt32LE out of bounds"); | ||
| shouldBe(writeUInt32LE(buf, 4294967295, 4), 8, "writeUInt32LE max"); | ||
| shouldBe(dv.getUint32(4, true), 4294967295, "writeUInt32LE max store"); | ||
| } | ||
|
|
||
| // Receivers other than a Uint8Array (Buffer) exit the CheckArray and take the host path, which | ||
| // (unlike the JIT'd form) accepts any ArrayBufferView with byte-length semantics and rejects the rest. | ||
| { | ||
| const floats = new Float64Array(4); | ||
| const dataView = new DataView(new ArrayBuffer(8)); | ||
| const otherView = new Uint32Array(4); | ||
| function readOnAnything(b, o) { | ||
| return accessors.readInt32LE.call(b, o); | ||
| } | ||
| noInline(readOnAnything); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| floats[0] = i; | ||
| shouldBe(readOnAnything(buf, 0), dv.getInt32(0, true), "Buffer receiver"); | ||
| shouldBe(readOnAnything(floats, 0), new DataView(floats.buffer).getInt32(0, true), "Float64Array receiver"); | ||
| shouldBe(readOnAnything(dataView, 4), 0, "DataView receiver"); | ||
| shouldBe(readOnAnything(otherView, 12), 0, "Uint32Array receiver (byte semantics)"); | ||
| shouldThrow(() => readOnAnything({}, 0), TypeError, "plain object receiver"); | ||
| shouldThrow(() => readOnAnything(null, 0), TypeError, "null receiver"); | ||
| } | ||
| } | ||
|
|
||
| // A detached receiver has length 0: always the host path, always a RangeError. | ||
| { | ||
| const detached = new Buffer(16); | ||
| function readDetached(b) { | ||
| return b.readUInt16LE(0); | ||
| } | ||
| noInline(readDetached); | ||
| for (let i = 0; i < 1e3; ++i) shouldBe(readDetached(detached), 0, "before detach"); | ||
| transferArrayBuffer(detached.buffer); | ||
| for (let i = 0; i < 1e3; ++i) shouldThrow(() => readDetached(detached), RangeError, "after detach"); | ||
| } | ||
|
|
||
| // A write's value coercion happens exactly once even when the offset then fails (the JIT exits | ||
| // before any effect; the host coerces first and validates the offset second). | ||
| { | ||
| let calls = 0; | ||
| const value = { | ||
| valueOf() { | ||
| calls++; | ||
| return 5; | ||
| }, | ||
| }; | ||
| function writeWithBadOffset(b, o) { | ||
| return b.writeInt32LE(value, o); | ||
| } | ||
| noInline(writeWithBadOffset); | ||
| for (let i = 0; i < 1e3; ++i) { | ||
| shouldBe(writeWithBadOffset(buf, 0), 4, "good offset"); | ||
| shouldThrow(() => writeWithBadOffset(buf, 100), RangeError, "bad offset"); | ||
| } | ||
| shouldBe(calls, 2000, "valueOf calls"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| //@ requireOptions("--useDollarVM=1") | ||
|
|
||
| // Buffer accessors on views over a resizable ArrayBuffer / a growable SharedArrayBuffer: the length | ||
| // load must track the current (byte) length, the first resizable receiver at a site OSR-exits | ||
| // (UnexpectedResizableArrayBufferView) and the recompile keeps working, and out-of-bounds after a | ||
| // shrink must throw. | ||
|
|
||
| function shouldBe(actual, expected, message) { | ||
| if (actual !== expected) throw new Error(message + ": expected " + expected + " but got " + actual); | ||
| } | ||
| function shouldThrow(f, expected, message) { | ||
| let error = null; | ||
| try { | ||
| f(); | ||
| } catch (e) { | ||
| error = e; | ||
| } | ||
| if (!(error instanceof expected)) throw new Error(message + ": expected a " + expected.name + " but got " + error); | ||
| } | ||
|
|
||
| const accessors = $vm.createBufferAccessors(); | ||
| class Buffer extends Uint8Array {} | ||
| Object.assign(Buffer.prototype, accessors); | ||
|
|
||
| function readUInt16LE(b, o) { | ||
| return b.readUInt16LE(o); | ||
| } | ||
| noInline(readUInt16LE); | ||
| function writeUInt16LE(b, v, o) { | ||
| return b.writeUInt16LE(v, o); | ||
| } | ||
| noInline(writeUInt16LE); | ||
|
|
||
| // Warm up on fixed-length views first, so the resizable ones arrive after the code is optimized. | ||
| { | ||
| const fixed = new Buffer(64); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeUInt16LE(fixed, i & 0xffff, i & 62), (i & 62) + 2, "fixed write"); | ||
| shouldBe(readUInt16LE(fixed, i & 62), i & 0xffff, "fixed read"); | ||
| } | ||
| } | ||
|
|
||
| // A length-tracking view over a resizable ArrayBuffer. | ||
| { | ||
| const rab = new ArrayBuffer(16, { maxByteLength: 64 }); | ||
| const tracking = new Buffer(rab); | ||
| shouldBe(tracking.length, 16, "tracking length"); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeUInt16LE(tracking, i & 0xffff, 14), 16, "tracking write at the end"); | ||
| shouldBe(readUInt16LE(tracking, 14), i & 0xffff, "tracking read at the end"); | ||
| shouldThrow(() => readUInt16LE(tracking, 15), RangeError, "tracking read straddling the end"); | ||
| shouldThrow(() => readUInt16LE(tracking, 16), RangeError, "tracking read past the end"); | ||
| } | ||
| rab.resize(64); | ||
| shouldBe(tracking.length, 64, "grown tracking length"); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeUInt16LE(tracking, i & 0xffff, 62), 64, "write near the grown end"); | ||
| shouldBe(readUInt16LE(tracking, 62), i & 0xffff, "read near the grown end"); | ||
| } | ||
| rab.resize(8); | ||
| shouldBe(tracking.length, 8, "shrunk tracking length"); | ||
| for (let i = 0; i < 1e3; ++i) { | ||
| shouldBe(readUInt16LE(tracking, 6), 0, "read near the shrunk end (never written)"); | ||
| shouldThrow(() => readUInt16LE(tracking, 7), RangeError, "read straddling the shrunk end"); | ||
| shouldThrow(() => writeUInt16LE(tracking, 0, 62), RangeError, "write past the shrunk end"); | ||
| } | ||
| } | ||
|
|
||
| // A fixed-length view over a resizable ArrayBuffer that shrinks out from under it. | ||
| { | ||
| const rab = new ArrayBuffer(32, { maxByteLength: 64 }); | ||
| const fixed = new Buffer(rab, 8, 16); | ||
| shouldBe(fixed.length, 16, "fixed-length view length"); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeUInt16LE(fixed, i & 0xffff, 14), 16, "fixed-length view write"); | ||
| shouldBe(readUInt16LE(fixed, 14), i & 0xffff, "fixed-length view read"); | ||
| } | ||
| rab.resize(16); // The view [8, 24) no longer fits: it is out of bounds now. | ||
| for (let i = 0; i < 1e3; ++i) { | ||
| shouldThrow(() => readUInt16LE(fixed, 0), RangeError, "out-of-bounds view read"); | ||
| shouldThrow(() => writeUInt16LE(fixed, 0, 0), RangeError, "out-of-bounds view write"); | ||
| } | ||
| } | ||
|
|
||
| // A view over a growable SharedArrayBuffer. | ||
| { | ||
| const gsab = new SharedArrayBuffer(16, { maxByteLength: 64 }); | ||
| const shared = new Buffer(gsab); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeUInt16LE(shared, i & 0xffff, 14), 16, "shared write at the end"); | ||
| shouldBe(readUInt16LE(shared, 14), i & 0xffff, "shared read at the end"); | ||
| shouldThrow(() => readUInt16LE(shared, 15), RangeError, "shared read past the end"); | ||
| } | ||
| gsab.grow(64); | ||
| for (let i = 0; i < 1e4; ++i) { | ||
| shouldBe(writeUInt16LE(shared, i & 0xffff, 62), 64, "write near the grown shared end"); | ||
| shouldBe(readUInt16LE(shared, 62), i & 0xffff, "read near the grown shared end"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.