From ed546fb638fb9d3b667d69edeaf0df424688b46f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:00:48 +0000 Subject: [PATCH 1/3] =?UTF-8?q?BigInt("0x=E2=80=A6"/"0b=E2=80=A6"/"0o?= =?UTF-8?q?=E2=80=A6"):=20linear-time=20parse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps WebKit to pick up the JSBigInt::parseInt power-of-two fast path (oven-sh/WebKit#353). Before: O(n^2) because multiplyAdd runs over the full digit vector for every group of input characters. After: O(n) by packing bits directly into the digit vector, mirroring toStringBasePowerOfTwo. 200k-hex-char string: ~360ms -> ~1ms release. Adds parse correctness coverage for hex/binary/octal at all digit-word alignment boundaries plus a linearity check at 250k hex chars. --- scripts/build/deps/webkit.ts | 2 +- test/js/bun/jsc/bigint-parse.test.ts | 93 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 test/js/bun/jsc/bigint-parse.test.ts diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f84dd77e0e19..2862e7d38c86 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 = "549170099226f816a4b204ea1d8fa102fb79eefa"; +export const WEBKIT_VERSION = "autobuild-preview-pr-353-4b51ec68"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/js/bun/jsc/bigint-parse.test.ts b/test/js/bun/jsc/bigint-parse.test.ts new file mode 100644 index 000000000000..be592cd27656 --- /dev/null +++ b/test/js/bun/jsc/bigint-parse.test.ts @@ -0,0 +1,93 @@ +import { test, expect, describe } from "bun:test"; + +// JSBigInt::parseInt historically routed every radix through a loop that +// calls multiplyAdd over the full-length digit vector for each small group +// of characters, which is O(n^2) in the number of characters. For +// power-of-two radixes (0b/0o/0x prefixes) the parse is a straight bit-pack +// and should be O(n), matching toStringBasePowerOfTwo in the other direction. + +const rep = (c: string, n: number) => Buffer.alloc(n, c).toString(); + +describe("BigInt string parse, power-of-two radix", () => { + const roundtrip = (prefix: string, radix: number, body: string) => { + const v = BigInt(prefix + body); + expect(v.toString(radix)).toBe(body.toLowerCase().replace(/^0+(?=.)/, "")); + return v; + }; + + test("hex correctness", () => { + roundtrip("0x", 16, "1"); + roundtrip("0x", 16, "F"); + roundtrip("0x", 16, "DeadBeef"); + roundtrip("0x", 16, rep("ff", 8)); + roundtrip("0x", 16, rep("ff", 9)); + roundtrip("0x", 16, "1" + rep("0", 100)); + roundtrip("0x", 16, rep("f", 1000)); + roundtrip("0x", 16, rep("123456789abcdef0", 200)); + // Non-16-aligned lengths to hit partial high digits. + for (let n = 1; n <= 40; n++) roundtrip("0x", 16, rep("a", n)); + }); + + test("binary correctness", () => { + roundtrip("0b", 2, "1"); + roundtrip("0b", 2, rep("1", 63)); + roundtrip("0b", 2, rep("1", 64)); + roundtrip("0b", 2, rep("1", 65)); + roundtrip("0b", 2, rep("1", 1000)); + roundtrip("0b", 2, rep("10", 500)); + for (let n = 1; n <= 130; n++) roundtrip("0b", 2, rep("1", n)); + }); + + test("octal correctness (3 bits/char, spans digit boundaries)", () => { + roundtrip("0o", 8, "7"); + roundtrip("0o", 8, rep("7", 21)); + roundtrip("0o", 8, rep("7", 22)); + roundtrip("0o", 8, rep("7", 23)); + roundtrip("0o", 8, rep("1234567", 200)); + roundtrip("0o", 8, rep("7", 1000)); + // The msb of the leading char can land in the low bits of the next + // 64-bit word, leaving that word zero. Exercise every alignment. + for (let n = 1; n <= 70; n++) roundtrip("0o", 8, rep("1", n)); + for (let n = 1; n <= 70; n++) roundtrip("0o", 8, rep("7", n)); + }); + + test("cross-radix agreement on large values", () => { + const hex = rep("f", 4096); + const v16 = BigInt("0x" + hex); + const v2 = BigInt("0b" + rep("1", 4096 * 4)); + expect(v16 === v2).toBe(true); + expect(v16.toString(16)).toBe(hex); + expect(v16).toBe((1n << BigInt(4096 * 4)) - 1n); + }); + + test("leading zeros and whitespace still handled", () => { + expect(BigInt("0x" + rep("0", 1000) + "ff")).toBe(255n); + expect(BigInt(" 0x" + rep("f", 100) + " ")).toBe(BigInt("0x" + rep("f", 100))); + expect(BigInt("0x" + rep("0", 1000))).toBe(0n); + }); + + test("invalid characters still throw SyntaxError", () => { + expect(() => BigInt("0x" + rep("f", 1000) + "g")).toThrow(SyntaxError); + expect(() => BigInt("0b" + rep("1", 1000) + "2")).toThrow(SyntaxError); + expect(() => BigInt("0o" + rep("7", 1000) + "8")).toThrow(SyntaxError); + expect(() => BigInt("0xg" + rep("f", 1000))).toThrow(SyntaxError); + }); + + test("parse is linear, not quadratic", () => { + // JSC caps BigInt at 2^20 bits (262144 hex chars). Use 250000, large + // enough that the O(n^2) path is unmistakably slow on any build while the + // O(n) path stays well under the threshold even under debug+ASAN. + // Quadratic: ~570ms release, seconds under debug. Linear: a few ms + // release, tens of ms debug. + const n = 250_000; + const s = "0x" + rep("f", n); + const t0 = performance.now(); + const v = BigInt(s); + const parseMs = performance.now() - t0; + + // Correctness check on the same value. + expect(v.toString(16)).toBe(rep("f", n)); + + expect(parseMs).toBeLessThan(250); + }); +}); From e49ef8667170df87e904dc19098a149af98a64f8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:03:04 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- test/js/bun/jsc/bigint-parse.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/jsc/bigint-parse.test.ts b/test/js/bun/jsc/bigint-parse.test.ts index be592cd27656..dec594de1225 100644 --- a/test/js/bun/jsc/bigint-parse.test.ts +++ b/test/js/bun/jsc/bigint-parse.test.ts @@ -1,4 +1,4 @@ -import { test, expect, describe } from "bun:test"; +import { describe, expect, test } from "bun:test"; // JSBigInt::parseInt historically routed every radix through a loop that // calls multiplyAdd over the full-length digit vector for each small group From f7c2ba01dff7c1e581908da77026268aee6393fa Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:31:08 +0000 Subject: [PATCH 3/3] test: fix fill() byte-count semantics, add coverage for octal linearity / maxLength boundary / UChar path --- test/js/bun/jsc/bigint-parse.test.ts | 103 ++++++++++++++++----------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/test/js/bun/jsc/bigint-parse.test.ts b/test/js/bun/jsc/bigint-parse.test.ts index dec594de1225..9ba331e00d93 100644 --- a/test/js/bun/jsc/bigint-parse.test.ts +++ b/test/js/bun/jsc/bigint-parse.test.ts @@ -6,7 +6,9 @@ import { describe, expect, test } from "bun:test"; // power-of-two radixes (0b/0o/0x prefixes) the parse is a straight bit-pack // and should be O(n), matching toStringBasePowerOfTwo in the other direction. -const rep = (c: string, n: number) => Buffer.alloc(n, c).toString(); +// Buffer.alloc semantics: `len` is the OUTPUT length in bytes; `fill` is tiled +// into it. Not `fill.repeat(len)`. +const fill = (len: number, pattern: string) => Buffer.alloc(len, pattern).toString(); describe("BigInt string parse, power-of-two radix", () => { const roundtrip = (prefix: string, radix: number, body: string) => { @@ -19,75 +21,94 @@ describe("BigInt string parse, power-of-two radix", () => { roundtrip("0x", 16, "1"); roundtrip("0x", 16, "F"); roundtrip("0x", 16, "DeadBeef"); - roundtrip("0x", 16, rep("ff", 8)); - roundtrip("0x", 16, rep("ff", 9)); - roundtrip("0x", 16, "1" + rep("0", 100)); - roundtrip("0x", 16, rep("f", 1000)); - roundtrip("0x", 16, rep("123456789abcdef0", 200)); + roundtrip("0x", 16, fill(16, "f")); // exactly one 64-bit word + roundtrip("0x", 16, fill(17, "f")); // one word + 4 bits + roundtrip("0x", 16, "1" + fill(100, "0")); + roundtrip("0x", 16, fill(1000, "f")); + roundtrip("0x", 16, fill(3200, "123456789abcdef0")); // 200 full words, mixed digits // Non-16-aligned lengths to hit partial high digits. - for (let n = 1; n <= 40; n++) roundtrip("0x", 16, rep("a", n)); + for (let n = 1; n <= 40; n++) roundtrip("0x", 16, fill(n, "a")); }); test("binary correctness", () => { roundtrip("0b", 2, "1"); - roundtrip("0b", 2, rep("1", 63)); - roundtrip("0b", 2, rep("1", 64)); - roundtrip("0b", 2, rep("1", 65)); - roundtrip("0b", 2, rep("1", 1000)); - roundtrip("0b", 2, rep("10", 500)); - for (let n = 1; n <= 130; n++) roundtrip("0b", 2, rep("1", n)); + roundtrip("0b", 2, fill(63, "1")); + roundtrip("0b", 2, fill(64, "1")); + roundtrip("0b", 2, fill(65, "1")); + roundtrip("0b", 2, fill(1000, "1")); + roundtrip("0b", 2, fill(1000, "10")); + for (let n = 1; n <= 130; n++) roundtrip("0b", 2, fill(n, "1")); }); test("octal correctness (3 bits/char, spans digit boundaries)", () => { roundtrip("0o", 8, "7"); - roundtrip("0o", 8, rep("7", 21)); - roundtrip("0o", 8, rep("7", 22)); - roundtrip("0o", 8, rep("7", 23)); - roundtrip("0o", 8, rep("1234567", 200)); - roundtrip("0o", 8, rep("7", 1000)); + roundtrip("0o", 8, fill(21, "7")); + roundtrip("0o", 8, fill(22, "7")); + roundtrip("0o", 8, fill(23, "7")); + roundtrip("0o", 8, fill(1400, "1234567")); + roundtrip("0o", 8, fill(1000, "7")); // The msb of the leading char can land in the low bits of the next // 64-bit word, leaving that word zero. Exercise every alignment. - for (let n = 1; n <= 70; n++) roundtrip("0o", 8, rep("1", n)); - for (let n = 1; n <= 70; n++) roundtrip("0o", 8, rep("7", n)); + for (let n = 1; n <= 70; n++) roundtrip("0o", 8, fill(n, "1")); + for (let n = 1; n <= 70; n++) roundtrip("0o", 8, fill(n, "7")); }); test("cross-radix agreement on large values", () => { - const hex = rep("f", 4096); + const hex = fill(4096, "f"); const v16 = BigInt("0x" + hex); - const v2 = BigInt("0b" + rep("1", 4096 * 4)); + const v2 = BigInt("0b" + fill(4096 * 4, "1")); expect(v16 === v2).toBe(true); expect(v16.toString(16)).toBe(hex); expect(v16).toBe((1n << BigInt(4096 * 4)) - 1n); }); - test("leading zeros and whitespace still handled", () => { - expect(BigInt("0x" + rep("0", 1000) + "ff")).toBe(255n); - expect(BigInt(" 0x" + rep("f", 100) + " ")).toBe(BigInt("0x" + rep("f", 100))); - expect(BigInt("0x" + rep("0", 1000))).toBe(0n); + test("leading zeros, whitespace, and 16-bit string storage", () => { + expect(BigInt("0x" + fill(1000, "0") + "ff")).toBe(255n); + expect(BigInt(" 0x" + fill(100, "f") + " ")).toBe(BigInt("0x" + fill(100, "f"))); + expect(BigInt("0x" + fill(1000, "0"))).toBe(0n); + // Leading zeros pushing the raw character count past the 2^20-bit cap must + // still parse: the length check runs after zeros are stripped. + expect(BigInt("0x" + fill(300_000, "0") + "ff")).toBe(255n); + // U+2003 EM SPACE is a legal StrWhiteSpaceChar and forces 16-bit string + // storage, so this routes through the UChar instantiation of parseInt. + const body = fill(1000, "f"); + expect(BigInt("\u2003" + "0x" + body + "\u2003")).toBe(BigInt("0x" + body)); + }); + + test("maxLength boundary (2^20 bits)", () => { + const atLimit = BigInt("0x" + fill(262_144, "f")); + expect(atLimit.toString(16).length).toBe(262_144); + expect(() => BigInt("0x" + fill(262_145, "f"))).toThrow(RangeError); }); test("invalid characters still throw SyntaxError", () => { - expect(() => BigInt("0x" + rep("f", 1000) + "g")).toThrow(SyntaxError); - expect(() => BigInt("0b" + rep("1", 1000) + "2")).toThrow(SyntaxError); - expect(() => BigInt("0o" + rep("7", 1000) + "8")).toThrow(SyntaxError); - expect(() => BigInt("0xg" + rep("f", 1000))).toThrow(SyntaxError); + expect(() => BigInt("0x" + fill(1000, "f") + "g")).toThrow(SyntaxError); + expect(() => BigInt("0b" + fill(1000, "1") + "2")).toThrow(SyntaxError); + expect(() => BigInt("0o" + fill(1000, "7") + "8")).toThrow(SyntaxError); + expect(() => BigInt("0xg" + fill(1000, "f"))).toThrow(SyntaxError); }); test("parse is linear, not quadratic", () => { - // JSC caps BigInt at 2^20 bits (262144 hex chars). Use 250000, large - // enough that the O(n^2) path is unmistakably slow on any build while the - // O(n) path stays well under the threshold even under debug+ASAN. - // Quadratic: ~570ms release, seconds under debug. Linear: a few ms - // release, tens of ms debug. + // JSC caps BigInt at 2^20 bits (262144 hex chars / 349525 octal chars). + // Use 250000 chars: the O(n^2) path takes ~570ms hex and ~300ms octal on a + // release build (seconds under debug), while the O(n) path stays under + // ~10ms even under debug+ASAN. const n = 250_000; - const s = "0x" + rep("f", n); + const hex = "0x" + fill(n, "f"); + const oct = "0o" + fill(n, "7"); + const t0 = performance.now(); - const v = BigInt(s); - const parseMs = performance.now() - t0; + const vHex = BigInt(hex); + const hexMs = performance.now() - t0; + + const t1 = performance.now(); + const vOct = BigInt(oct); + const octMs = performance.now() - t1; - // Correctness check on the same value. - expect(v.toString(16)).toBe(rep("f", n)); + expect(vHex.toString(16)).toBe(fill(n, "f")); + expect(vOct.toString(8)).toBe(fill(n, "7")); - expect(parseMs).toBeLessThan(250); + expect(hexMs).toBeLessThan(250); + expect(octMs).toBeLessThan(250); }); });