From a3d73b8f92ec7809c7937f146a3a24d35edf3f74 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:01:42 +0000 Subject: [PATCH 1/2] Set methods: take the spec branch for a set-like whose size is 2^32 or more (WebKit bump for oven-sh/WebKit#596) GetSetRecord keeps a set-like's size as an unbounded integer. JSC truncated it to uint32 before comparing it with the receiver's size, so intersection, difference, isSubsetOf, isSupersetOf and isDisjointFrom consulted the wrong side (other.keys() instead of other.has(), or the reverse) once the reported size reached 2^32. Pin WebKit to the preview build of oven-sh/WebKit#596, which clamps the size at UINT32_MAX instead, and add coverage for all seven methods across the size boundary. --- scripts/build/deps/webkit.ts | 2 +- .../bun/jsc/set-methods-set-like-size.test.ts | 157 ++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 test/js/bun/jsc/set-methods-set-like-size.test.ts diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 359f8a466e32..a3b4d5bf2192 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "cf1b36ec8703d8e87436094d21d478d358c7d886"; +export const WEBKIT_VERSION = "autobuild-preview-pr-596-50c11bf6"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/js/bun/jsc/set-methods-set-like-size.test.ts b/test/js/bun/jsc/set-methods-set-like-size.test.ts new file mode 100644 index 000000000000..af1c682f00de --- /dev/null +++ b/test/js/bun/jsc/set-methods-set-like-size.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, test } from "bun:test"; + +// The Set methods take any "set-like" with a numeric `size`, a `has()` and a +// `keys()`. GetSetRecord (https://tc39.es/ecma262/#sec-getsetrecord) keeps that +// size as an unbounded integer or +Infinity, and each method compares it with +// the receiver's size to pick a strategy: ask `other.has()` about each of the +// receiver's elements, or iterate `other.keys()`. JSC used to truncate the size +// to uint32 first, so a set-like reporting 2^32 or more elements compared as +// size mod 2^32 and the methods consulted the wrong side. + +const sizes = [ + 0, + 1, + 2, + 3, + 2 ** 31, + 2 ** 32 - 1, + 2 ** 32, + 2 ** 32 + 1, + 2 ** 32 + 2, + 2 ** 33, + 2 ** 53 - 1, + 2 ** 53, + 2 ** 53 + 2, + 2 ** 64, + 1e300, + Number.MAX_VALUE, + Infinity, +]; + +// A set-like whose has() claims exactly the value 1 and whose keys() yields +// exactly the value 1, independent of the size it reports. It logs every +// observable interaction so the chosen strategy is visible. +function makeSetLike(size: number) { + const log: string[] = []; + const setLike = { + get size() { + log.push("size"); + return size; + }, + has(v: unknown) { + log.push("has:" + String(v)); + return v === 1; + }, + keys() { + log.push("keys"); + let done = false; + return { + next() { + log.push("next"); + if (done) return { value: undefined, done: true }; + done = true; + return { value: 1, done: false }; + }, + return() { + log.push("return"); + return {}; + }, + }; + }, + }; + return { setLike, log }; +} + +// What the spec algorithms do for a receiver of `thisSize` elements against a +// set-like reporting `otherSize`, given the has()/keys() behaviour above. +function expected(method: string, receiver: number[], otherSize: number) { + const thisSize = receiver.length; + const hasCalls = receiver.map(v => "has:" + v); + switch (method) { + case "union": + return { result: [...new Set([...receiver, 1])], log: ["size", "keys", "next", "next"] }; + case "symmetricDifference": + return { + result: receiver.includes(1) ? receiver.filter(v => v !== 1) : [...receiver, 1], + log: ["size", "keys", "next", "next"], + }; + case "intersection": + return thisSize <= otherSize + ? { result: receiver.filter(v => v === 1), log: ["size", ...hasCalls] } + : { result: receiver.includes(1) ? [1] : [], log: ["size", "keys", "next", "next"] }; + case "difference": + return thisSize <= otherSize + ? { result: receiver.filter(v => v !== 1), log: ["size", ...hasCalls] } + : { result: receiver.filter(v => v !== 1), log: ["size", "keys", "next", "next"] }; + case "isSubsetOf": + if (thisSize > otherSize) return { result: false, log: ["size"] }; + // Stops at the first element other.has() rejects. + return { + result: receiver.every(v => v === 1), + log: ["size", ...hasCalls.slice(0, receiver.findIndex(v => v !== 1) + 1 || receiver.length)], + }; + case "isSupersetOf": + if (thisSize < otherSize) return { result: false, log: ["size"] }; + // keys() yields 1; if the receiver lacks it the iterator is closed early. + return receiver.includes(1) + ? { result: true, log: ["size", "keys", "next", "next"] } + : { result: false, log: ["size", "keys", "next", "return"] }; + case "isDisjointFrom": + if (thisSize <= otherSize) { + // Stops at the first element other.has() accepts. + const i = receiver.indexOf(1); + return { result: i === -1, log: ["size", ...hasCalls.slice(0, i === -1 ? receiver.length : i + 1)] }; + } + return receiver.includes(1) + ? { result: false, log: ["size", "keys", "next", "return"] } + : { result: true, log: ["size", "keys", "next", "next"] }; + } + throw new Error(method); +} + +const methods = [ + "union", + "intersection", + "difference", + "symmetricDifference", + "isSubsetOf", + "isSupersetOf", + "isDisjointFrom", +] as const; + +const receivers = [[], [1], [2], [1, 2], [2, 3], [2, 1, 3]]; + +describe.each(methods)("Set.prototype.%s", method => { + test.each(sizes)("set-like with size %p", size => { + for (const receiver of receivers) { + const { setLike, log } = makeSetLike(size); + const raw = (new Set(receiver) as any)[method](setLike); + const result = raw instanceof Set ? [...raw] : raw; + expect({ receiver, size, result, log }).toEqual({ receiver, size, ...expected(method, receiver, size) }); + } + }); + + test("negative sizes throw RangeError, even beyond -2^32", () => { + for (const size of [-1, -(2 ** 31), -(2 ** 32), -(2 ** 32) - 1, -(2 ** 53), -1e300, -Infinity]) { + const { setLike, log } = makeSetLike(size); + expect(() => (new Set([1]) as any)[method](setLike)).toThrow(RangeError); + expect(log).toEqual(["size"]); + } + }); + + test("sizes that coerce to NaN throw TypeError", () => { + for (const size of [NaN, undefined, "x", {}]) { + const { setLike, log } = makeSetLike(size as number); + expect(() => (new Set([1]) as any)[method](setLike)).toThrow(TypeError); + expect(log).toEqual(["size"]); + } + }); + + test("fractional sizes truncate toward zero before the comparison", () => { + // 1.9 truncates to 1, so a two-element receiver is larger than the set-like. + const { setLike, log } = makeSetLike(1.9); + const raw = (new Set([1, 2]) as any)[method](setLike); + const result = raw instanceof Set ? [...raw] : raw; + expect({ result, log }).toEqual(expected(method, [1, 2], 1)); + }); +}); From 3fb49308cb1658629d59e5f33641d7a31c883e78 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:55:50 +0000 Subject: [PATCH 2/2] test: explicit slice bound in the isSubsetOf oracle, cover BigInt and Symbol sizes --- test/js/bun/jsc/set-methods-set-like-size.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/js/bun/jsc/set-methods-set-like-size.test.ts b/test/js/bun/jsc/set-methods-set-like-size.test.ts index af1c682f00de..f10bc8bb1929 100644 --- a/test/js/bun/jsc/set-methods-set-like-size.test.ts +++ b/test/js/bun/jsc/set-methods-set-like-size.test.ts @@ -83,13 +83,12 @@ function expected(method: string, receiver: number[], otherSize: number) { return thisSize <= otherSize ? { result: receiver.filter(v => v !== 1), log: ["size", ...hasCalls] } : { result: receiver.filter(v => v !== 1), log: ["size", "keys", "next", "next"] }; - case "isSubsetOf": + case "isSubsetOf": { if (thisSize > otherSize) return { result: false, log: ["size"] }; // Stops at the first element other.has() rejects. - return { - result: receiver.every(v => v === 1), - log: ["size", ...hasCalls.slice(0, receiver.findIndex(v => v !== 1) + 1 || receiver.length)], - }; + const i = receiver.findIndex(v => v !== 1); + return { result: i === -1, log: ["size", ...hasCalls.slice(0, i === -1 ? receiver.length : i + 1)] }; + } case "isSupersetOf": if (thisSize < otherSize) return { result: false, log: ["size"] }; // keys() yields 1; if the receiver lacks it the iterator is closed early. @@ -139,8 +138,9 @@ describe.each(methods)("Set.prototype.%s", method => { } }); - test("sizes that coerce to NaN throw TypeError", () => { - for (const size of [NaN, undefined, "x", {}]) { + test("sizes that coerce to NaN or cannot coerce throw TypeError", () => { + // NaN after ToNumber, or ToNumber itself throws (BigInt, Symbol). + for (const size of [NaN, undefined, "x", {}, 1n, Symbol("size")]) { const { setLike, log } = makeSetLike(size as number); expect(() => (new Set([1]) as any)[method](setLike)).toThrow(TypeError); expect(log).toEqual(["size"]);