Skip to content

Yarr: JIT lookbehinds, first-code-point alternation dispatch, Boyer-Moore and /u·/v spec alignment, RegExp correctness fixes - #299

Merged
Jarred-Sumner merged 61 commits into
mainfrom
claude/yarr-regex-perf-5197
Aug 11, 2026
Merged

Yarr: JIT lookbehinds, first-code-point alternation dispatch, Boyer-Moore and /u·/v spec alignment, RegExp correctness fixes#299
Jarred-Sumner merged 61 commits into
mainfrom
claude/yarr-regex-perf-5197

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Jul 16, 2026

Copy link
Copy Markdown
Member

Yarr (JavaScriptCore's RegExp engine): JIT-compile lookbehinds, factor and dispatch wide alternations on their first code point, extend the Boyer-Moore search, align /u·/v semantics with the spec, and fix a set of correctness bugs in both tiers.

Motivating case: oven-sh/bun#5197 — the isbot user-agent regex (~110 alternatives, four lookbehinds) ran ~200× slower in Bun than in Node because any lookbehind sent the whole pattern to the bytecode interpreter and wide alternations were tried one alternative at a time.

What changes

Lookbehinds are JIT-compiled. Each lookbehind body is compiled as a term-reversed mirrored copy driven through the existing forward machinery, with a per-op MatchDirection flipping only the primitives (input claim, character addressing, end-of-input, greedy signs, capture start/end slots, mirrored ^ $ \b). Unicode patterns are covered (leftward code-point reader, backward astral literals/classes in every quantifier form, backreferences with a frame slot for the span edge, quantified/nested groups, forward assertions nested in a mirrored body); backward greedy variable-width classes under /u backtrack in O(1) per step. If a mirrored body's layout disagrees with the pattern's offsets the compile falls back to the interpreter instead of asserting.

Wide alternations are cheaper. Engine-neutral YarrPattern rewrites factor shared literal prefixes (/aq|bx|ar|by//a(?:q|r)|b(?:x|y)/, recursively and inside existing groups, but never beneath a group that can repeat — see below) and fold a wide top-level alternation into one non-capturing group; the rewrite moves terms rather than copying and runs against a per-pattern work budget, so adversarial inputs stay linear in memory. In the JIT, a once-through group's alternatives are dispatched on their first code point: a decision tree over Latin-1, and a second one over code-point ranges above it (whole code points when the compile decodes surrogate pairs), each leaf jumping to the ordered chain of alternatives that can start there. Short literal alternatives are compared inline in the chain with 16/32/64-bit loads and emit no separate body; other alternatives are entered through a stub (new chainResume slot in the once-group frame). Sizing keeps small patterns in their existing code shape (bodies of < 16 alternatives are folded only if some alternative already needs a Yarr frame, groups dispatch from 4 alternatives / 12 literal characters, at most 2,048 chains / 4,096 stubs, and preparation abandons or falls back the moment a bound is passed). A top-level alternation whose alternatives' first-character sets are pairwise disjoint (keyword lists) is dispatched directly, with no group and no frame, so .test() loops keep RegExpTestInline.

Classes that contain strings (\q{…}, \p{RGI_Emoji} and the other properties of strings, results of /v set operations) expand to one alternative per string, longest first, then the single characters, then the empty member; a list of four or more strings gets its own once-through group so the dispatcher serves it even under a quantifier. /^\p{RGI_Emoji}$/v.test() 6–9 µs → 0.03–0.05 µs (V8 0.05–0.09); \p{RGI_Emoji}+ 21 µs → 0.02 µs per emoji; string-width@8 in Bun goes from ~15× slower than Node to slightly faster.

The Boyer-Moore search reaches more patterns and no longer depends on the first subject a RegExp saw. Every sub-range of candidate positions is scored by the stride it buys against how often its union hits; positions matching every character are excluded; sampled character frequencies are used only when the sample is long enough to resolve them; a single-position range is scored for the 16-byte vector scan when the compile can emit it. On x86_64 the vector scan is always used for Latin-1 subjects (as before); arm64 keeps a measured candidate-frequency gate.

Spec alignment for /u·/v match starts (both tiers). A lastIndex that splits a surrogate pair maps to the pair's lead, and a non-sticky match whose start would split a pair is never reported (RegExpBuiltinExec's inputIndex + AdvanceStringIndex). This is the one intentional remaining difference from V8, which attempts mid-pair positions.

Resource behaviour. maxRegExpStackSize 128 → 192 MB (interpreter frames grew with the new once-group slots; nested-quantifier tokenizers over ~600 KB inputs otherwise gave up ~20% sooner than before). The interpreter's BumpPointerAllocator keeps up to 64 KB of already-mapped overflow pools between matches instead of mapping/unmapping them around every match (js-tokens-style tokenizers: 29k mmap/munmap pairs → 49), and VM::deleteAllCode releases them.

Behaviour changes vs. current main (each was JSC ≠ V8/spec unless noted)

RegExp semantics — both tiers unless noted:

  • \p{…}/\P{…} under /iu and /iv take part in case folding (standalone, in classes, negated classes, /v set operations), per CharacterSetMatcher; test262's RegExp subset goes 2533 → 2535/2535.
  • Dot-star enclosure (/.*X.*/ fast path): honoured lastIndex under /s; a leading ^ is re-checked when lastIndex > 0 (also /m); an X that can consume a line terminator no longer widens to the wrong line; captures inside lookarounds in X are seen. Linear in all ^ cases; U+2028/2029 handled.
  • Sticky non-global String.prototype.replace matches at and updates lastIndex (C++ fast paths, DFG empty-replacement operation, DFG constant folding).
  • Lookbehind references: a backreference to a group inside the same lookbehind resolves when that group closes (so quantified enclosing groups copy a real backreference); quantified forward references; \k<name> to (duplicate-)named groups captured before the lookbehind.
  • Optional/quantified ^ groups (/(?:^)?a/, /(?:^b)?a|de/); ^-anchored alternatives are never filtered beneath an inverted assertion.
  • /v: set operations keep a class's astral width; an inverted property escape honours a pending &&/--; nested negated class with astral input; chained --; unions of string-bearing operands are merged; duplicate \q{} members are one member; the empty \q{} member matches last; [\q{zz}b] no longer matches U+0000; [\q{ab}\-]/[[a-z]\-] accepted and [\d&&-] rejected as the grammar says.
  • Character classes are normalized (no single entries inside a range), which negated-class complements relied on.
  • Interpreter: astral reads in /u lookbehinds; once-through alternative order after the first position.
  • JIT: <class><literal> after an astral char not at position 0; string-list group with an astral literal on 16-bit input; a unicode backreference walk past a lone-surrogate capture's end (unbounded read); the dotAll class rejects a dangling surrogate under /u; a {0,n} split copy that ran zero iterations no longer clears captures it shares with its mandatory sibling; a lazily quantified backreference whose extra iteration fails (zero-width, not enough input, or different characters) fails the term as the interpreter does — /z(bb)(?:.\1*?)+z/ on "zbbcz"+"c".repeat(30) returned null after 700 ms on main's JIT.
  • Runtime: "\u{1F600} x".replace(/\s*/gu, "") (and DFG's folding of it) advances past a surrogate pair after an empty /u match rather than looping.
  • RegExp::matchInline keeps the non-unicode/8-bit paths to a flag test plus a tail call.

Performance (x86_64, retired instructions, jsc shell, this branch ÷ main; wall-clock tables for arm64 in the PR thread)

workload ratio
isbot pattern, 200k .test() 0.004
/(?<=X[^"]*)!/u over 5 KB ×2000 0.0001
36 JS keywords \b(?:…)\b/g over 300 KB 0.79
rare-literal searches (/NEEDLE\d+/, /zqx\d/, /zalpha|qbravo|xcharlie/) over 130 KB 0.69 / 0.80 / 0.42
/^\p{RGI_Emoji}$/v.test, \p{RGI_Emoji}+ per emoji ~0.005, ~0.001
CSS scans /\d+px/g, /#[0-9a-fA-F]{6}\b/g, trailing-space /[ \t]+$/gm 1.00 / 1.00 / 0.7
camelCase→snake replace 1.3 MB; regex-escape; \s+ split; email validate; semver ×400k 1.00 ± 0.01
tiny .test() loops (bool / http-method / ext / alt8) +1…+9 instructions per call (≈ +0.6–2%)
42.5k npm regexes × 3.4M subjects no pattern ≥ 3× either way; sums 0.93 (JIT) / 0.80 (interpreter)
compile + first exec, 120 largest corpus patterns 0.83
compile of a 500–2,000-alternative keyword list (one-time) 1.5–1.7
Yarr JIT code size, alternation-heavy patterns 1.8–1.9× (was 2.5× before inline literals stopped emitting dead bodies); \p{RGI_Emoji} 0.5×

Known trade-offs kept: picomatch-globstar-class patterns run ~1.6× main's JIT instructions (optional ^ groups no longer take the once-through path that produced wrong results); (…)+ groups still fall back to the interpreter once the JIT's stack-carved paren-context pool is exhausted (~60k iterations of a dispatched string list; main behaves the same, follow-up planned to move that pool to the heap).

Verification

Against current main and Node 24–26, on Release, RelWithDebInfo and Debug+ASAN builds of this head:

  • JSTests regexp*/*lookbehind*/*yarr*/string-*{match,replace,split}* (311 files) on JIT, --useRegExpJIT=0, all new gates off, Debug+ASAN JIT and interpreter: identical to main in every configuration. test262 RegExp/String subset (2,535 files) × both tiers: 2535/2535.
  • Differential fuzzing (Tools/yarr-fuzz, in tree): grammar-generated (10 profiles: lookbehind nesting, wide/shared-prefix and wide-headed alternations, /u·/v with astral text and lone surrogates, \q{}/string-property classes, Boyer-Moore bait, deep nesting, DFG constant folding, small hot patterns), structure-mutated real-world regexes (60k patterns from a 685k-pattern npm/PyPI corpus, AST-level mutations), and a spec-literal reference matcher as an engine-independent oracle; every case as exec/test/match/matchAll/replace/split/search with sticky and lastIndex sweeps, 8-bit and forced 16-bit subjects, plus metamorphic wrappers. ≈ 20 M cases across JIT / interpreter / each gate off / eager DFG-FTL / main / V8, ≈ 1.5 M under Debug+ASAN. Result on this head: 0 crashes or sanitizer reports; JIT ≡ interpreter except step-budget exhaustion on catastrophic patterns (pre-existing, both directions); gates-off ≡ default; every difference from V8 is the mid-surrogate rule above, JSC's existing size limits, or a V8 bug; every difference from main is one of the fixes above.
  • Real-world corpora: ≈ 700k distinct patterns from ~500 npm packages exercised through their own APIs and fixtures plus the Lingua Franca corpus, ≈ 32 M (regex, subject) pairs, also under /v where they compile: same conclusion; 20 nested-quantifier tokenizers × 0.4–1 MB inputs identical across all engines.
  • Out-of-bounds subject reads by generated code (which sanitizers cannot see): a new --verifyRegExpJITReads option bounds-checks every load the RegExp JIT emits against the subject; four planted bugs of that shape (pair peek without its end check, backward reader without its start check, lookbehind offset off by one, vector-scan bound loose by 8 bytes) are each caught within seconds by the fuzzer, and the unmodified tree is clean over 830k verified cases (all profiles plus 58k mutated real-world patterns). A continuous-GC configuration (30k cases + the key stress tests) is also clean.
  • All 290 in-tree regression tests added by Yarr bug-fix commits since 2015 pass on JIT, interpreter and ASAN.
  • Downstream: Bump WebKit: JIT regex lookbehinds + wide-alternation dispatch (fixes isbot perf, #5197) bun#36789 (this branch's preview build) passes Bun's full CI matrix.

New tests: regexp-lookbehind-jit.js, regexp-review-regressions.js, regexp-interpreter-unicode-lookbehind.js, regexp-interpreter-once-through-order.js, regexp-unicode-no-mid-surrogate-pair-start.js, regexp-bm-search-any-character-position.js, regexp-unicode-fixed-class-then-single-class-backtrack.js, regexp-unicode-empty-match-replace-advances-past-surrogate-pair.js, regexp-dotstar-enclosure-soundness.js, regexp-unicode-property-escape-ignore-case.js, regexp-lookbehind-forward-reference-resolution.js, regexp-sticky-non-global-replace-uses-lastindex.js, regexp-class-strings-longest-first.js, regexp-shared-code-across-realms.js.

Pre-existing issues seen and deliberately left for follow-ups (present on main): /vi case folding of multi-character \q{} members inside set operations; a family of /v class-set syntax leniencies ([a\d&&c], [a[b]--c] accepted; a raw U+0000 inside a /v class rejected); {n} above 2³²; the interpreter reporting no match rather than an error when its step budget is exhausted.

Options

useRegExpLookbehindJIT, useRegExpAlternationFactoring, useRegExpAlternationDispatch (default true; each restores the previous behaviour of that feature — BUN_JSC_useRegExpAlternationDispatch=0 etc. in Bun, and …Dispatch=0 alone restores main's exact code shape for \p{RGI_Emoji}); regExpDispatchMaxInlineLiteralLength (default 32; 0 never compares alternatives inline) trades dispatch code size for speed; maxRegExpStackSize default 192 MB; verifyRegExpJITReads (default false) is the fuzzing aid described above.

@dylan-conway
dylan-conway marked this pull request as ready for review July 16, 2026 09:35
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change enables supported lookbehind patterns in the Yarr RegExp JIT through mirrored backward execution, adds direction-aware matching and dispatch infrastructure, updates alternation optimization, and introduces stress coverage comparing JIT and interpreter results.

Changes

Lookbehind JIT support

Layer / File(s) Summary
Backward frame primitives
Source/JavaScriptCore/yarr/Yarr.h, Source/JavaScriptCore/yarr/YarrPattern.h, Source/JavaScriptCore/yarr/YarrJIT.cpp
Adds direction-aware frame positions, input addressing, Unicode decoding, captures, cursor movement, and backtracking metadata.
Backward assertions and matching
Source/JavaScriptCore/yarr/YarrJIT.cpp
Updates assertions, character and character-class matching, backreferences, quantifiers, and backtracking for mirrored execution.
Lookbehind mirroring and compilation
Source/JavaScriptCore/yarr/YarrPattern.cpp, Source/JavaScriptCore/yarr/YarrJIT.cpp, Source/JavaScriptCore/yarr/YarrJIT.h, Source/JavaScriptCore/runtime/RegExp.cpp
Tracks lookbehind parser context, mirrors lookbehind bodies, enables supported JIT compilation, and preserves fallback handling for unsupported cases.
Alternation factoring and dispatch
Source/JavaScriptCore/yarr/YarrPattern.cpp, Source/JavaScriptCore/yarr/YarrJIT.cpp
Factors eligible alternation prefixes, wraps alternatives for dispatch, and adds dispatch-chain generation and backtracking.
Lookbehind stress coverage
JSTests/stress/regexp-lookbehind-jit.js
Compares JIT and interpreter results across lookbehind forms, captures, anchors, iteration, Unicode strings, backtracking, and large alternations.

Suggested reviewers: constellation, sosukesuzuki

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and relevant but omits the required Bugzilla link, reviewer line, and template-style changed-file entries. Add the Bugzilla bug link, the required reviewer line, and a template-formatted list of changed paths and functions.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main lookbehind JIT and alternation-dispatch changes, although it also mentions additional changes not shown in the summary.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@JSTests/stress/regexp-lookbehind-jit.js`:
- Around line 7-12: Update shouldBe to compare expected and actual values
recursively using Object.is, including each array element, so undefined captures
remain distinct from null. Preserve the existing mismatch error reporting and
message behavior while replacing the JSON.stringify-based equality check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d5f1f314-6f67-4953-aba9-cd18c8087599

📥 Commits

Reviewing files that changed from the base of the PR and between e5f7fc2 and 80c141b.

📒 Files selected for processing (3)
  • JSTests/stress/regexp-lookbehind-jit.js
  • Source/JavaScriptCore/runtime/RegExp.cpp
  • Source/JavaScriptCore/yarr/YarrJIT.cpp
💤 Files with no reviewable changes (1)
  • Source/JavaScriptCore/runtime/RegExp.cpp

Comment thread JSTests/stress/regexp-lookbehind-jit.js

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this is a substantial JIT codegen change — new backward-matching primitives, mirrored-disjunction construction, and direction-aware addressing across many term types — so it warrants a human reviewer familiar with Yarr.

What was reviewed:

  • Backward input-claim/read/end-of-input primitives and the negativePositionOffsetindex + (k-1) addressing transform.
  • Capture start/end slot swapping in backward frames vs. the interpreter's end-first recording.
  • mirrorDisjunctionForLookbehind fallback paths (backrefs, nested lookaheads, quantified groups >1, surrogate decoding) — each still sets a JITFailureReason so the interpreter path remains reachable.
  • The Boyer-Moore change treating parenthetical assertions as zero-width — checked that ignoring the assertion keeps the leading-char sets an over-approximation.
Extended reasoning...

Overview

This PR adds lookbehind-assertion support to the Yarr regex JIT. Previously, any pattern containing a lookbehind was rejected wholesale (both by a pre-check in RegExp::compile/compileMatchOnly and by JITFailureReason::Lookbehind) and fell back to the bytecode interpreter. The change compiles each lookbehind body as a term-reversed mirrored copy driven through the existing forward machinery, with a per-op MatchDirection that flips the input primitives (claim, read, end-of-input, consume/rewind, capture slot order, and BOL/EOL/word-boundary edge tests). It touches ~500 lines in YarrJIT.cpp, removes two guard lines in RegExp.cpp, and adds a new stress test.

Security risks

Regex JIT is security-sensitive: it emits machine code that indexes into string buffers. The new backward addressing (input[index + (k-1)], branchSub32 on the sign bit for input claims) must never read outside [input, input+length). The PR's argument that index and claim counts are both < 2^31 (so a borrowing sub sets the sign bit) looks sound given containsUnsignedLengthPattern() gates the JIT, and the k==0 peek case (input[index-1]) is only reached by anchors that first check index != 0. I did not find an OOB read path, but this is exactly the kind of invariant a Yarr expert should sanity-check across all the term-type call sites.

Level of scrutiny

High. This is new codegen logic in a JIT compiler with a novel coordinate transform (mirrored/virtual input positions) threaded through many existing generate/backtrack functions. The correctness surface is large — every direction-branched primitive (fixed/greedy/non-greedy pattern-char and char-class, anchors, word boundaries, nested-alternative check-adjust, parenthetical-assertion begin/end) has to agree on what index and inputPosition mean in a backward frame. The differential fuzzing (6000 patterns × 25 inputs × all flags, byte-identical vs. interpreter) and test262 lookBehind suite passing in both modes are strong evidence, but not a substitute for a design-level read by someone who owns this code.

Other factors

  • The bug-hunting system found no issues.
  • The mirrored-copy approach deliberately leaves the original YarrPattern untouched, so unsupported bodies (backrefs, nested lookaheads, maxCount > 1 groups, surrogate decoding) cleanly set m_failureReason and fall through to the pre-existing interpreter path — I verified each of those bailout branches is reachable and sets a reason before returning.
  • The Boyer-Moore change (return cursor for ParentheticalAssertion) is independently reasonable: assertions are zero-width, so skipping them keeps the leading-character sets a valid over-approximation.
  • The PR description explicitly flags follow-up refactors (moving mirroring into YarrPatternConstructor, reversed packed constants) as out of scope, which is a design decision worth a maintainer's nod.

Given the scope and the codegen/security surface, deferring to a human reviewer.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
b02abbc3 autobuild-preview-pr-299-b02abbc3 2026-08-11 10:55:35 UTC
c7bb0677 autobuild-preview-pr-299-c7bb0677 2026-08-11 10:18:12 UTC
567e7fa4 autobuild-preview-pr-299-567e7fa4 2026-08-11 07:19:56 UTC
fbaf9d4e autobuild-preview-pr-299-fbaf9d4e 2026-08-11 05:54:26 UTC
832878f6 autobuild-preview-pr-299-832878f6 2026-08-11 04:17:17 UTC
ffb2b037 autobuild-preview-pr-299-ffb2b037 2026-08-11 02:54:49 UTC
2aeaac3d autobuild-preview-pr-299-2aeaac3d 2026-08-11 00:45:05 UTC
a44e1f75 autobuild-preview-pr-299-a44e1f75 2026-08-10 16:26:33 UTC
b11b392e autobuild-preview-pr-299-b11b392e 2026-08-10 10:43:11 UTC
c884fc6a autobuild-preview-pr-299-c884fc6a 2026-08-10 03:58:43 UTC
c59dc9d5 autobuild-preview-pr-299-c59dc9d5 2026-08-10 02:05:21 UTC
697335d7 autobuild-preview-pr-299-697335d7 2026-08-10 00:56:27 UTC
127a0a36 autobuild-preview-pr-299-127a0a36 2026-08-04 03:23:42 UTC
20169bfd autobuild-preview-pr-299-20169bfd 2026-08-03 01:37:44 UTC
66bae199 autobuild-preview-pr-299-66bae199 2026-08-03 00:44:57 UTC
976dfb71 autobuild-preview-pr-299-976dfb71 2026-08-02 22:41:56 UTC
04f493f2 autobuild-preview-pr-299-04f493f2 2026-08-02 16:50:13 UTC
ff857b64 autobuild-preview-pr-299-ff857b64 2026-08-02 15:27:00 UTC
4d91f2c9 autobuild-preview-pr-299-4d91f2c9 2026-07-17 04:46:00 UTC
5cd184be autobuild-preview-pr-299-5cd184be 2026-07-17 03:30:25 UTC
1e122871 autobuild-preview-pr-299-1e122871 2026-07-16 19:00:11 UTC
b9615a17 autobuild-preview-pr-299-b9615a17 2026-07-16 14:28:30 UTC
4261b012 autobuild-preview-pr-299-4261b012 2026-07-16 11:49:41 UTC
80c141ba autobuild-preview-pr-299-80c141ba 2026-07-16 10:13:38 UTC

@dylan-conway dylan-conway changed the title Yarr JIT: compile lookbehind assertions Yarr: lookbehind JIT, alternation factoring, and first-character dispatch Jul 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Source/JavaScriptCore/yarr/YarrJIT.cpp`:
- Around line 5198-5229: Make the dispatch backtrack handling in the
op.m_dispatch branch explicitly enforce the Forward-direction invariant before
adjusting m_regs.index, or replace the unconditional sub32 with the
direction-aware adjustment used by the non-dispatch alternative path. Anchor the
change at the op.m_checkAdjust block and preserve the existing frame-resume jump
behavior.

In `@Source/JavaScriptCore/yarr/YarrPattern.cpp`:
- Around line 2608-2647: Update the repeated-alternative eligibility check
before constructing groupDisjunction so repeatedCount == 0 returns immediately,
while preserving the existing threshold behavior for non-empty ranges. Ensure
the loop that populates groupDisjunction and its final last() access only
execute when at least one repeated alternative exists.
- Around line 2637-2664: Mark the synthesized alternative returned by
body->addNewAlternative(...) as the last alternative by setting
wrapped->m_isLastAlternative to true before appending the group term, while
preserving the existing terminal marker handling for
groupDisjunction->m_alternatives.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: de1af30c-ea73-43b9-8a5c-31469f281caa

📥 Commits

Reviewing files that changed from the base of the PR and between 80c141b and 4261b01.

📒 Files selected for processing (5)
  • Source/JavaScriptCore/runtime/OptionsList.h
  • Source/JavaScriptCore/yarr/Yarr.h
  • Source/JavaScriptCore/yarr/YarrJIT.cpp
  • Source/JavaScriptCore/yarr/YarrPattern.cpp
  • Source/JavaScriptCore/yarr/YarrPattern.h

Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp
Comment thread Source/JavaScriptCore/yarr/YarrPattern.cpp
Comment thread Source/JavaScriptCore/yarr/YarrPattern.cpp
Comment thread Source/JavaScriptCore/yarr/YarrPattern.cpp Outdated
Comment thread Source/JavaScriptCore/yarr/YarrPattern.cpp Outdated
Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Source/JavaScriptCore/yarr/YarrPattern.cpp (1)

2613-2680: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

wrapped alternative never gets m_isLastAlternative = true.

The prior crash-on-empty-group finding (repeatedCount < 2 || guard) is fixed here. However, the new body-level alternative created at Line 2669 (wrapped) is the only remaining alternative after alternatives.shrink(firstRepeated) appends it, yet nothing sets wrapped->m_isLastAlternative = true. Every absorbed alternative had this flag explicitly cleared (Line 2652), and the codebase's consistent convention (checkForTerminalParentheses, atomParenthesesEnd, factorAlternatives, mergeSharedPrefix, and this same function's own groupDisjunction->m_alternatives.last()->m_isLastAlternative = true at Line 2660) confirms addNewAlternative does not default this to true. Net effect: after this transform, no alternative in body->m_alternatives is marked as last.

🐛 Proposed fix
     PatternAlternative* wrapped = body->addNewAlternative(hasCaptures ? firstCaptureId - 1 : m_pattern.m_numSubpatterns);
     wrapped->m_lastSubpatternId = groupLastSubpatternId;
     wrapped->m_containsBOL = containsBOL;
     wrapped->m_startsWithBOL = startsWithBOLCount == groupDisjunction->m_alternatives.size();
+    wrapped->m_isLastAlternative = true;
     groupDisjunction->m_parent = wrapped;
#!/bin/bash
# Confirm the default value of m_isLastAlternative and how it's consumed for body-level alternatives.
rg -n 'm_isLastAlternative' Source/JavaScriptCore/yarr/YarrPattern.h Source/JavaScriptCore/yarr/YarrJIT.cpp Source/JavaScriptCore/yarr/YarrInterpreter.cpp
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/yarr/YarrPattern.cpp` around lines 2613 - 2680, Mark
the newly created body-level alternative as terminal in
wrapAlternativesForDispatch by setting wrapped->m_isLastAlternative to true
after addNewAlternative. Preserve the existing terminal marking for the nested
group’s final alternative.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Source/JavaScriptCore/yarr/YarrPattern.cpp`:
- Around line 2613-2680: Mark the newly created body-level alternative as
terminal in wrapAlternativesForDispatch by setting wrapped->m_isLastAlternative
to true after addNewAlternative. Preserve the existing terminal marking for the
nested group’s final alternative.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 159f500d-9431-4417-ae9e-a88a6e336ff1

📥 Commits

Reviewing files that changed from the base of the PR and between 4261b01 and b9615a1.

📒 Files selected for processing (4)
  • JSTests/stress/regexp-lookbehind-jit.js
  • Source/JavaScriptCore/runtime/OptionsList.h
  • Source/JavaScriptCore/yarr/YarrJIT.cpp
  • Source/JavaScriptCore/yarr/YarrPattern.cpp

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — the fixup commit addresses the two functional items from the last pass (the repeatedCount < 2 guard in wrapAlternativesForDispatch and WTF_MAKE_TZONE_ALLOCATED_IMPL(DispatchInfo)). No new issues found on this revision, but this is a large change to Yarr JIT codegen and pattern rewriting that should get a human review.

Checked this round:

  • Mirrored-frame primitives (claim/release, negativePositionUntilInputBegin addressing, capture start/end swap, BOL/EOL/\b) for forward/backward symmetry.
  • wrapAlternativesForDispatch: the concern that the synthesized body alternative lacks m_isLastAlternative was examined and ruled out — the flag is not load-bearing on the body disjunction path.
  • tryPrepareDispatch gating (Forward-only, FixedCount 1:1, non-terminal/non-string-list, m_minimumSize > 0) vs. the assumptions the dispatch generate/backtrack paths make.
  • The remaining open inline comment (duplicate comment sentence) is cosmetic only.
Extended reasoning...

Overview

This PR adds three substantial features to the Yarr regex engine across ~1,500 lines of new logic in YarrJIT.cpp and YarrPattern.cpp: (1) JIT compilation of lookbehind assertions via term-reversed "mirrored" bodies with a per-op MatchDirection that flips the input-claim / character-addressing / anchor / capture primitives; (2) an engine-neutral pattern rewrite pass (factorAndWrapAlternatives) that stably sorts literal-leading alternatives, hoists shared prefixes into synthesized (?:…) groups, and folds large top-level alternations into a single group; and (3) a first-character dispatch scheme in the JIT that reads one character, routes through a binary decision tree to per-character chains, and threads chain continuation through a new BackTrackInfoParenthesesOnce::chainResume frame slot. It also removes the blanket m_containsLookbehinds JIT bailout in RegExp.cpp, adds four new Options, bumps YarrStackSpaceForBackTrackInfoParenthesesOnce 2→3, and fixes a pre-existing m_startsWithBOL bubbling bug in atomParenthesesEnd/quantifyAtom.

Prior review and current state

My previous run flagged three items. The fixup commit (b9615a1) addressed the two functional ones: wrapAlternativesForDispatch now guards repeatedCount < 2 so --regExpAlternationGroupThreshold=0 on an all-once-through body no longer reaches .last() on an empty vector, and WTF_MAKE_TZONE_ALLOCATED_IMPL(DispatchInfo) is now present. The third (a duplicated comment sentence) remains and is purely cosmetic. This run's finder/verifier pass raised and refuted the "body left with no m_isLastAlternative" concern for wrapAlternativesForDispatch — the body-disjunction path in the JIT does not rely on that flag the way nested alternatives do. No other findings.

Security risks

Regex JIT codegen is security-sensitive: it emits machine code that reads attacker-controlled input strings against attacker-controlled patterns. The new backward addressing (input[index + (k-1)] with k==0 mapping to index-1), the signed borrow-based bounds check, the frame-slot indirect jump through chainResume, and the pattern-tree restructuring (reparenting, capture-span recomputation, terminal-mark clearing) all have OOB-read / control-flow-hijack surface if any invariant is off by one. Nothing concrete was found, and the PR's differential harnesses (JIT vs. interpreter over hundreds of thousands of cases) plus the full stress/test262 pass are strong evidence — but this is exactly the kind of change where a second pair of expert eyes on the index arithmetic and frame layout is warranted.

Level of scrutiny

High. This is production-critical engine code (every regex in every Bun program), the diff is large, it introduces new JIT control-flow patterns (patched frame stores driving indirect jumps), and it changes observable regex semantics for a family of BOL-anchored patterns (a bug fix, but still a behavior change). It is well outside the "simple/mechanical" bar for auto-approval.

Other factors

The verification story is unusually thorough (randomized differential fuzzing against both the interpreter and node, enumerated small-alternation coverage, full stress + test262), and everything is behind runtime options with sensible defaults. The design is internally well-documented. Outstanding non-blocking bot comments remain (CodeRabbit's defensive-assert suggestion on the dispatch backtrack path, the test helper's undefined-vs-null comparison, and my comment-cosmetics nit). None of those block merge, but the scope alone means this needs a human reviewer familiar with Yarr.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Source/JavaScriptCore/yarr/YarrPattern.cpp (1)

2665-2673: ⚠️ Potential issue | 🟠 Major

Restore the terminal marker on the synthesized body alternative.

wrapped is created without m_isLastAlternative, while the previous terminal marker was cleared from the moved alternatives. The body can therefore have no terminal alternative. Set wrapped->m_isLastAlternative = true before appending the group.

Proposed fix
 PatternAlternative* wrapped = body->addNewAlternative(hasCaptures ? firstCaptureId - 1 : m_pattern.m_numSubpatterns);
 wrapped->m_lastSubpatternId = groupLastSubpatternId;
+wrapped->m_isLastAlternative = true;
 wrapped->m_containsBOL = containsBOL;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/yarr/YarrPattern.cpp` around lines 2665 - 2673, Set
wrapped->m_isLastAlternative to true immediately after creating the synthesized
alternative in the surrounding group-wrapping logic, before appending the
ParenthesesSubpattern group, so the synthesized body retains the
terminal-alternative marker.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@JSTests/stress/regexp-lookbehind-jit.js`:
- Around line 148-160: Update the pre-existing JIT defect comment above the
useRegExpJIT assertions to include a link or identifier for a tracking issue
covering the three divergent regular-expression cases. Keep the existing
per-tier expectations and test logic unchanged.

---

Duplicate comments:
In `@Source/JavaScriptCore/yarr/YarrPattern.cpp`:
- Around line 2665-2673: Set wrapped->m_isLastAlternative to true immediately
after creating the synthesized alternative in the surrounding group-wrapping
logic, before appending the ParenthesesSubpattern group, so the synthesized body
retains the terminal-alternative marker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: beb7c340-33af-444d-b0b3-9869b71be2aa

📥 Commits

Reviewing files that changed from the base of the PR and between b9615a1 and 1e12287.

📒 Files selected for processing (4)
  • JSTests/stress/regexp-lookbehind-jit.js
  • Source/JavaScriptCore/yarr/YarrJIT.cpp
  • Source/JavaScriptCore/yarr/YarrPattern.cpp
  • Source/JavaScriptCore/yarr/YarrPattern.h

Comment thread JSTests/stress/regexp-lookbehind-jit.js Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes in b9615a1 — the repeatedCount < 2 clamp and WTF_MAKE_TZONE_ALLOCATED_IMPL(DispatchInfo) are both in place now. I didn't find any new issues in this revision, but given the scope (backward-matching JIT primitives, engine-neutral pattern rewrites, first-character dispatch codegen and backtracking) this really wants eyes from someone who knows Yarr well.

Checked this run: the new m_containsBOLGroupBubble fallback keeps BOL-group lookbehind patterns on the interpreter without narrowing which non-lookbehind patterns reach the JIT; the synthesized wrapper alternative's unset m_isLastAlternative was examined and found harmless for body-level consumers; the test's JSON.stringify comparator masking undefined vs null was noted as a test-quality nit only. The remaining open inline notes (duplicate comment line, dispatch-backtrack direction assert) are cosmetic.

Extended reasoning...

Overview

This PR adds ~1,500 lines to YarrJIT.cpp and ~340 to YarrPattern.cpp, spanning three interacting features: (1) JIT compilation of lookbehind bodies via term-reversed mirroring with a per-op MatchDirection that flips input-claim/read/anchor/capture primitives; (2) an engine-neutral pattern rewrite that stably sorts and factors literal-leading alternatives and folds large top-level alternations into a synthetic (?:...) group; and (3) a first-character dispatch JIT path that reads one character, routes through a binary decision tree to per-character chains, and threads failure through a new chainResume frame slot. It also adds four runtime options, grows BackTrackInfoParenthesesOnce by one slot, adds a m_containsBOLGroupBubble pattern flag, and includes a stress test.

Security risks

This is JIT code generation for attacker-controlled input (regex patterns and subject strings) in a JavaScript engine. The backward-matching path introduces new signed-arithmetic index handling and a new BaseIndex addressing mode; the dispatch path stores and indirect-jumps through code addresses in the match frame; the pattern rewrite restructures the AST that downstream passes (setupOffsets, terminal/string-list optimizations, JIT op-compile) consume. Any of these could plausibly harbor OOB reads, index-register corruption, or backtracking-state confusion that only manifests on specific pattern shapes. The differential harnesses described in the PR are strong evidence of correctness, but this class of change is exactly where subtle exploitable bugs hide.

Level of scrutiny

High. This is neither mechanical nor small; it touches production-critical, security-sensitive codegen with many interacting invariants (frame offsets, checked-input accounting, direction state, capture-span bookkeeping, terminal/string-list marks). It is well outside the "approve without human review" bar.

Other factors

Since my previous inline comments the author pushed a fix commit addressing the threshold=0 crash and the missing TZone _IMPL, plus a follow-up (1e122871) that replaces the earlier BOL-bubbling narrowing with a per-pattern m_containsBOLGroupBubble flag so lookbehind patterns of that shape stay on the interpreter — a more conservative fix. The bug-hunting system found nothing this run; verifiers specifically examined and refuted the "wrapper alternative not marked m_isLastAlternative" concern and the test comparator's undefined/null conflation. The remaining open inline items (a duplicated comment sentence in wrapAlternativesForDispatch, and CodeRabbit's suggestion to assert Forward direction in the dispatch backtrack) are non-blocking cosmetics. Deferring to a human Yarr reviewer.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the two inline nits, I also checked the open coderabbit concern about wrapped->m_isLastAlternative in wrapAlternativesForDispatchm_isLastAlternative is only read for {Simple,}Nested/StringList alternative ops (YarrJIT.cpp:4792/4826), never for body alternatives, so leaving it unset on the synthesized body alternative is harmless. The earlier regExpAlternationGroupThreshold=0 and missing WTF_MAKE_TZONE_ALLOCATED_IMPL(DispatchInfo) findings are addressed in the current diff.

Extended reasoning...

The two new inline findings are cosmetic (a decimal-vs-hex mismatch in a verboseRegExpCompilation dump, and two empty #if/#endif pairs left after removing their bodies). The separately-raised concern that wrapAlternativesForDispatch leaves the body with no m_isLastAlternative marker was verified not to matter: that flag is consulted only in the nested/string-list alternative codegen paths, and the synthesized alternative is a body alternative compiled via BodyAlternativeBegin/Next/End, which never reads it.

Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp Outdated
Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp Outdated
@dylan-conway dylan-conway changed the title Yarr: lookbehind JIT, alternation factoring, and first-character dispatch Yarr: lookbehind JIT (incl. unicode), alternation factoring, and first-character dispatch Jul 17, 2026
Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp
Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp
@dylan-conway dylan-conway changed the title Yarr: lookbehind JIT (incl. unicode), alternation factoring, and first-character dispatch Yarr: lookbehind JIT (incl. unicode), alternation factoring, first-character dispatch, and Boyer-Moore search selection Jul 17, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found in this pass, but this is a large change to the Yarr JIT — new backward-matching codegen for lookbehinds (including unicode), pattern-level alternation rewrites, first-character dispatch, frame-layout growth, and a Boyer-Moore search rewrite — so it needs a human reviewer.

What was reviewed:

  • Direction-aware input primitives and negativeOffsetIndexedAddress backward mapping — offsets stay within claimed input and the negative-frontier borrow is undone on failure.
  • factorAlternatives / wrapAlternativesForDispatch — stable sort preserves leftmost-first ordering within same-first-character groups; terminal/string-list shapes are left untouched.
  • Frame-size bumps in Yarr.h line up with the new BackTrackInfoParenthesesOnce::chainResume and BackTrackInfoBackReference::backwardSpanEdge slots.
  • findBestCharacterSequence rewrite — empty-union ranges are skipped so an all-above-Latin1 class can't score as the best window.
Extended reasoning...

Overview

The PR spans seven files but the substance is in YarrJIT.cpp (~1.8k new lines) and YarrPattern.cpp (~400 new lines). It: (1) removes the compile-time lookbehind gate in RegExp.cpp and JIT-compiles lookbehind bodies via a term-reversed mirror driven through the existing forward machinery, with a per-op MatchDirection flipping the input-claim / addressing / end-of-input / capture-slot primitives; (2) adds a leftward unicode code-point reader and backward variants of every character-class and pattern-character quantifier form; (3) adds engine-neutral alternation prefix factoring and a body-folding wrapper in the pattern constructor; (4) adds first-character dispatch for once-groups in the JIT (binary decision tree over Latin-1 → per-character chain of applicable alternatives, with a new chainResume frame slot); (5) rewrites the Boyer-Moore sub-range scoring and adds a subject-frequency gate on the SIMD search loop; and (6) fixes several pre-existing correctness bugs (BOL/word-boundary position tests, firstCharacterAdditionalReadSize scoping, quantified-split capture clearing, dotAll dangling-surrogate rejection).

Security risks

Regex JIT is memory-safety–critical. The new backward addressing path computes input[index + (k-1)] from a leftward-moving frontier and relies on signed-branch underflow detection for input claims; an off-by-one in the offset mapping or a missed borrow-undo would be an OOB read against attacker-controlled subject strings. The dispatch machinery stores and later jumps through a code address in the runtime frame (chainResume), which is the same shape as the existing returnAddress slot but is a new indirect-jump surface. The alternation-factoring rewrite mutates the pattern tree (reparenting, capture-range recomputation) before offset assignment; a mis-bracketed capture span would corrupt the output vector. I did not find a concrete flaw in any of these paths, and the PR's differential and fuzz coverage is extensive, but the surface area is exactly the kind that has historically produced exploitable JIT bugs.

Level of scrutiny

High. This is production-critical engine code, not sandbox or tooling, and it removes a safety fallback (lookbehinds previously always ran in the interpreter). The mirroring design, the frame-layout changes shared with the interpreter, and the pattern-rewrite semantics are all architectural decisions a maintainer should sign off on.

Other factors

The verification story is strong — full test262 RegExp on both tiers, three JIT-vs-interpreter differential corpora, a large enumerated small-alternation sweep, and randomized fuzzing across quantifiers/lookarounds/unicode — and the added regexp-lookbehind-jit.js stress file pins the reproducers. All prior review threads are resolved; the two inline nits still open (unguarded readUnicodeCharBackward call sites on non-x64/arm64/riscv64 builds, and a cosmetic double blank line) don't affect Bun's shipped configs. Net: nothing blocking from automated review, but scope and risk class put this well outside auto-approval.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found this pass, but this is a ~2,000-line change to the Yarr JIT's core codegen and pattern layer (mirrored lookbehind compilation, direction-aware primitives, alternation prefix factoring/reordering, first-character dispatch chains, Boyer-Moore range scoring), so it needs a human reviewer.

What was reviewed:

  • Direction-aware input-claim/addressing/backtrack primitives and the mirrored-body offset assignment — checked that forward paths are byte-identical when m_direction == Forward.
  • Alternation factoring's stable sort + barrier rules and wrapAlternativesForDispatch — the compile-time alternationFactoringMinRun = 8 guard makes the empty-.last() path unreachable.
  • Boyer-Moore findBestCharacterSequence rewrite — the isAllSet() break and empty-map continue cover the any-character / above-Latin1-only cases the tests pin.
  • Frame-slot growth (YarrStackSpaceForBackTrackInfo*) matches the new BackTrackInfo* struct fields.

The two open inline nits (missing #if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) around five readUnicodeCharBackward call sites; a stray double blank line) are cosmetic / non-Bun-target only.

Extended reasoning...

Overview

This PR adds lookbehind JIT compilation to Yarr (previously an interpreter-only fallback), engine-neutral alternation prefix factoring and top-level folding in YarrPattern.cpp, first-character dispatch for wide alternations in YarrJIT.cpp, and reworks Boyer-Moore search-range scoring and SIMD-vs-scalar loop selection. It touches YarrJIT.cpp (~1,700 net lines), YarrPattern.cpp (~450 net lines), YarrPattern.h, Yarr.h, YarrJIT.h, RegExp.cpp, and adds two stress tests.

Security risks

Regex JIT is security-sensitive: it emits machine code from attacker-controllable patterns and subjects. The new backward-frame addressing (negativeOffsetIndexedAddress Backward branch, claimBackwardPairLead, checkNotEnoughInputBackward), the dispatch chain's patched frame stores (chainResume), and the pattern-level alternative reordering all create new surfaces for out-of-bounds reads or backtracking-state corruption if an invariant is violated. The PR's own verification (test262, differential JIT-vs-interpreter fuzzing, 52k BM cases) is substantial, but the design decisions — mirroring via deep-copied disjunctions, per-op direction stamping, forward-only dispatch — deserve a human reviewer familiar with Yarr's frame layout and backtracking model.

Level of scrutiny

High. This is core-engine JIT codegen with correctness and memory-safety implications, plus several large-scale design choices (mirrored-body compilation strategy, always-on pattern rewrites that reorder alternatives, frame-slot growth affecting the interpreter). It is well outside the "simple, mechanical, or obvious" bar for auto-approval.

Other factors

All prior review threads on this PR are resolved except two of my own cosmetic nits (unguarded readUnicodeCharBackward call sites for non-x64/arm64 targets, and a leftover double blank line); neither affects Bun's shipped builds. The bug-hunting pass on the current head found nothing new. Preview builds are green across all commits. No human has reviewed yet.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Pre-1.4 review, part 1/7 — cross-cutting engine integration & resource safety. Six more focused passes (lookbehind codegen, YarrPattern rewrites, first-char dispatch, Boyer-Moore, differential fuzzer, historical-bug regression scan) are running against Debug+ASAN and Release builds of ab39369 and will be posted as they finish.

Verified sound

  • Frame sizing: Yarr.h bumps (BackReference 3→4, ParenthesesOnce 2→3) match the YarrPattern.h structs; interpreter static_asserts cover both; the Once condition in setupDisjunctionOffsets is identical to the JIT's Once selection, so the chainResume slot always exists where dispatch uses it; dispatch is restricted to Once + NestedAlternative, never generic Parentheses.
  • Removing the m_containsLookbehinds JIT gate: nothing else in JSC keyed on "lookbehind ⇒ interpreter". All match entry points pass the full subject + startOffset; backward reads are bounded by 0, not by start. exec@lastIndex / sticky / replace / split / matchAll / search / 16-bit / lookbehind reaching 5000 chars left of lastIndex / DFG+FTL hot loops incl. constant folding all agree across Release, --jitPolicyScale=0 ± concurrent JIT, and Debug+ASAN. RegExpTestInline can never contain a lookbehind (assertion ⇒ non-zero frame).
  • Hang safety: 14 catastrophic / zero-width lookbehind patterns terminate on JIT (7–23 ms vs interpreter 140–220 ms at N=22); exponential in both tiers, no spin.
  • Executable-memory exhaustion: --jitMemoryReservationSize=65536 → 544 "Can't JIT" fallbacks, 2000/2000 correct. A 25,482-line differential corpus (52 patterns × 11 flag sets × 47 subjects; exec/test/split/replace) is byte-identical across branch-JIT, branch-interp, small-JIT-pool, Debug+ASAN(+DFG) and pre-PR baseline. JIT never mutates the shared YarrPattern (mirrors owned by m_mirroredDisjunctions); each of the 4 code variants builds a fresh YarrPattern, so no double-rewrite.
  • Thread-safety: no new static/global mutable state.
  • Compile stress: 100k alternatives, 5k nesting, 2000-deep lookbehind / lookahead-in-lookbehind nesting, quantifier-copy × mirror combos — all SyntaxError/RangeError or succeed; no SIGSEGV/ASAN.

Findings

  1. Quadratic JIT compile for wide alternations inside a lookbehind (newly reachable). YarrJIT.cpp ~4800/4834: each SimpleNestedAlternativeNext/NestedAlternativeNext walks m_nextOp to find the End op — O(n) per alternative, O(n²) per group. Pre-existing for lookaheads/groups, but lookbehinds now reach it and factorAlternatives can't rescue them (backward terms fail firstLiteralCharacter). (?<=k0|…|kN)z, first exec incl. compile: N=10k 356 ms, 20k 1.5 s, 40k 19.0 s, 50k 10.9 s / 305 MB RSS; baseline (interpreter) 2–22 ms / 147 MB. Compile-time DoS shape for attacker-supplied patterns. Suggested fix: record the End-op index at op-compile time (O(1)), or keep very wide backward alternations on the interpreter.
  2. DFG RegExpTestInline cliff at ≥8 short alternatives. wrapAlternativesForDispatch (alternationFactoringMinRun = 8) wraps the body in a Once group ⇒ m_callFrameSize 0→4 ⇒ canInline false. /ab|cd|ef|gh|ij|kl|mn|op/.test hot loop 69 ms → 193 ms (2.8× slower than baseline); 7 alternatives unchanged. The trade is clearly right for isbot-sized patterns (5.6 s → 162 ms) but this is a regression for common small keyword .test() loops. Consider raising the threshold, or not wrapping when the un-wrapped body would have been DFG-inlineable.
  3. Unguarded recursion in assignAlternativeOffsets/assignDisjunctionOffsets (YarrJIT.cpp) and accumulateCaptureRange (YarrPattern.cpp); sibling passes call isSafeToRecurse(). Not crashable today because the parser's nesting limit fires first (main thread N=30000, 512 KB agent threads N=3000), but cheap to guard.
  4. Informational: 100k-alternative construct+first-exec 1.5–3.7× slower than baseline (still linear, ~200 ms); RSS +7–15%.

Reproducers/benchmarks for all of the above are in ~/code/tmp/yarr299/infra/ on the review machine (lb_bigalt_scale.js, bench_inline.js, hang.js, diff_corpus.js, compile_stress.js, startoffset.js, deepnest_lb.js).

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Part 2/7 — lookbehind / mirrored-body codegen (MatchDirection::Backward arms of PatternCharacter/CharacterClass/BackReference generate+backtrack, negativeOffsetIndexedAddress, readUnicodeCharBackward, BOL/EOL/\b, ParentheticalAssertion, mirrorAlternativeInto/copyForwardDisjunctionForMirror/assignAlternativeOffsets).

Method: full read of the Backward paths, then a 3-way differential fuzzer (branch JIT vs --useRegExpJIT=false vs node 26) generating lookbehind-heavy patterns — nested lookaround, quantified/capturing groups, back/forward refs, named + duplicate-named groups, ^ $ \b \B, all of i/m/s/u/v/g/y, non-zero lastIndex, astral + lone surrogates, Latin1/UCS2 case-fold specials, via exec/test/search/replace/split — plus a "substring view" guard that re-runs each case on a buffer-sharing substring with attractive characters planted at input[-1]/input[length] to expose out-of-range reads. ~150k cases on Release, ~45k on Debug+ASAN. Zero JIT-vs-spec mismatches attributable to the mirror machinery, zero edge-guard hits, zero ASAN reports. Every JIT≠interpreter divergence found was the interpreter being wrong.

Findings

  1. Wrong result, pre-existing, but the PR's fix for it is incompletem_readsStartCharacter is stamped after optimizeAlternative() has reordered terms (YarrJIT.cpp ~6757 vs the loop at ~6762–6790). After the [class][char][char][class] swap, m_terms[0] reads inputPosition 1, so its non-BMP read still sets firstCharacterAdditionalReadSize and the next start position is skipped:
    /\u{1F600}|[ab]x/u.exec("c\u{1F600}")   // branch JIT: null   interp/node: index 1
    Baseline JIT also returns null here (the PR fixed the (?:…)-wrapped variant only), so not a regression — but the commit message's invariant ("only the term whose read is at the match start") doesn't hold. Fix: require term->inputPosition == 0, or compute before the swap. (I re-verified: branch-JIT null, branch-interp [1,"😀"], baseline null.)
  2. Tier divergence: the interpreter's tryReadBackward is wrong where the new JIT reader is right. The comment at YarrJIT.cpp ~1570 says the JIT reader matches the interpreter exactly; it doesn't, and the JIT/node answer is the correct one:
    /(?<=\u{1F600}{2})x/u.exec("😀😀x")        interp null, JIT/node 4
    /(?<=^[\s\S])$/u.exec("😀")                interp null, JIT/node 2
    /(?<=\p{L}\u{1F600}.)x/su.exec("é😀😁x")   interp null, JIT/node 5
    /(?<=[\u{1F600}a])/u.exec("😀abc")         interp 1 (mid-pair!), JIT/node 2
    
    Net effect for users is an improvement, but because the same pattern silently runs on the interpreter under maximumRegExpJITCodeSize, exec-memory failure, ParenthesisNestedTooDeep, or m_abortExecution, results for these patterns are now tier-dependent (demonstrated with --maximumRegExpJITCodeSize=200). The interpreter needs the matching fix so the fallback agrees; worth a follow-up issue at minimum.
  3. Rebase hazard (high). This branch is 381 commits behind main, and main's d886174 rewrote backtrackCharacterClassGreedy's unicode path (removed the rematch loop and the matchAmount store, added an O(1) negativeOffsetIndexedAddress(offset + 2) surrogate-pair peek) plus e13280f (regT3 reshuffle). The PR's Backward arm of that exact function (~4083–4130) depends on the removed loop/store; a mechanical conflict resolution that reuses main's offset + 2 peek inside a Backward frame reads the wrong side of the cursor. git merge-tree origin/main HEAD already conflicts in YarrJIT.cpp. The fuzzers from this review must be re-run after the rebase, not just before.
  4. Low: Backward branch of negativeOffsetIndexedAddress (~1501–1521) does Checked<int32_t>(int32_t(k)) - 1 before range-adjusting; only reachable with lookbehind body offsets ≥ 2³⁰ on ≥ 1 GiB subjects and containsUnsignedLengthPattern gates ≥ 2³¹, so worst case is a Checked crash, not OOB. Untested.

Believed sound (checked explicitly)

Every Backward read is input[index + k - 1] with k ≥ 1 except anchor peeks (k = 0 → index-1), and both BOL-backward and \b-backward test index == 0 first; readUnicodeCharBackward bounds its lead read by unitAddress > input; all leftward claims use branchSub32(Signed/PositiveOrZero) and claimBackwardPairLead undoes the borrow on failure; multi-char fused loads, BM, shared-lead fast paths and first-char dispatch are all disabled for Backward; capture start/end swap, backref span edge (new frame slot), greedy/non-greedy rewind signs, lazy-paren progress test, and assertion backtrack (bodyDirectionDiffers → reload beginIndex) all fuzz-clean; mirror/deep-copy lifetime is fine (unique_ptr-owned, terms fully appended before ops take PatternTerm*, originals never renumbered so the interpreter fallback sees an untouched pattern); lookbehinds can't reach the DFG inline-test path. ^ $ \b \B inside lookbehind, multiline, sticky/lastIndex left-look, 70k fixed counts, dup-named groups all agree with node.

Harness (reusable post-rebase): ~/code/tmp/yarr299/lookbehind/{fuzz.js,run.sh,tri.py}; repros 01-…swap.js, 02-tier-divergence-unicode-lookbehind.js, 03-mid-surrogate-start-positions.js.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Part 3/7 — Boyer-Moore first-character search (findBestCharacterSequence sub-range scoring, BoyerMooreBitmap::{add,addRanges,addCharacters,setAll}, collectBoyerMooreInfo*, createCandidateBitmap, scalar loop, generateBitInTableSIMDSearch, BodyAlternativeBegin emission/backtrack trampolines, /u-on-8-bit gate, sampler-driven loop choice) plus the firstCharacterAdditionalReadSize / astral-skip changes.

Method: end-to-end read, then ~360k randomized 4-way differential cases (branch JIT / branch interp / pre-PR JIT / node; 8-bit, 8-bit + one 16-bit char, full 16-bit; lengths 0–300; lastIndex ≠ 0; g/y/u/v/i/s/m; ⅓ of cases with 8–15 alternatives to trigger factoring), an exhaustive short-subject sweep (49 BM-eligible patterns × 11 flag sets × ~9.5k subjects of length 0–5, under 5 different sampler warm-ups so both vector and scalar loops were exercised), and 5k cases on Debug+ASAN.

Verdict on the BM changes themselves: sound. Zero JIT-vs-oracle divergences attributable to BM, zero ASAN reports, zero asserts. Specifically checked: [begin,end) never spans an isAllSet() position; createCandidateBitmap merges only that range; read offset checkedOffset-endIndex+1 and stride end-begin are the correct shift; map.isEmpty()→continue is sound (count==0 only when no alternative can place any 8-bit char there); chars > 0xFF dropped only in Char8; Char16 folds &127 (superset); ranges ≥128 wide / inverted / non-dotAll . / \S\D\WsetAll; /iu Latin1 case partners (K/k/U+212A, s/ſ, µ/Μ/μ, ÿ/Ÿ, ß/ẞ, à/À) arrive via parser-built classes so they're in the map; gate !m_decodeSurrogatePairs ≡ (Char8 ‖ !unicode); MaskedAlternativeInfo (also newly reachable for /u 8-bit) is ASCII-only and rejects classes with > 0xFF members; scalar loop reads < index ≤ length; SIMD bound index ≤ length-16-baseOffset keeps the 16-byte load in-buffer with the post-hit index>length→scalar guard; sticky excluded; sampler is per-compile, main-thread, m_size==0→1, uint8 can't overflow; minimumSize only ever underestimates.

Finding (same root cause as part 2 #1, independently found, with the realistic shape)

Wrong result / false negative in the m_readsStartCharacter fix, not in BM. opCompileAlternative runs optimizeAlternative() (swaps a leading one-width non-inverted CharacterClass with the following FixedCount PatternCharacter) before stamping m_terms[0] as the start-character reader (YarrJIT.cpp ~6757–6790). After the swap m_terms[0] is the literal at inputPosition 1, so its tryReadUnicodeChar is the one permitted to set firstCharacterAdditionalReadSize. When that alternative is the last body alternative, reading an astral pair at start+1 sets the register and the loop trampoline advances start by 2, skipping the match at start+1. Disassembly confirms the flag store is emitted in the swapped literal term. Trigger: /u or /v, 16-bit subject, last alternative shaped <class><literal> — under /iu that's any <letter><non-letter> (k_, e-, c\n), plus \d\., [a-z]-, \w::

"x😀 y😀 z-".match(/\p{Extended_Pictographic}|[a-z]-/gu)   // branch & baseline JIT: ["z-"]   interp/node: ["😀","😀","z-"]
/😀|k_/iu.exec("a😀")                                      // branch & baseline JIT: null     interp/node: ["😀"]

Pre-PR JIT is equally wrong (for the older any-read reason), so not a regression, but this one root cause accounts for essentially all ~150 residual JIT≠node cases across these sweeps, and the emoji-alternation shape is realistic. Fix: key m_readsStartCharacter on term->inputPosition == 0 rather than term order (or compute before the swap). Strongly suggest folding that one-liner into this PR since the PR already claims to fix this family.

Pre-existing, out of scope, noted for the record: interpreter matches /ſ/i against S on 8-bit (JIT/node don't); JSC rejects literal NUL inside a /v class; non-unicode /i canonicalization of ſ/K/ẞ differs among JIT, interpreter and V8.

Harness: ~/code/tmp/yarr299/bm/{fuzz.js,fuzz2.js,run-diff.sh,exhaust.js,minimize.js}; repros 01-readsStartCharacter-swap.js, 08-readsStart-swap-realistic.js; disassembly out/disasm-06.txt.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Part 4/7 — engine-neutral YarrPattern rewrites (prefix factoring mergeSharedPrefix/factorAlternatives, large-alternation folding, {0,n} split-copy capture fix, min>0 parens as single term, m_minimumSize/BOL propagation, copyTerm/deep-copy, atomParenthesesEnd). These affect the interpreter too, so --useRegExpJIT=0 is not an escape hatch for anything here.

🔴 Finding 1 — cubic time / cubic peak memory in prefix factoring (PR-introduced; DoS; both tiers). I consider this a merge blocker.

mergeSharedPrefix (YarrPattern.cpp ~2489–2528) copies every member's remaining terms into a new suffix disjunction (suffix->m_terms.append(member->m_terms[i])) and then recurses via factorAlternatives(*suffixDisjunction) while members — with their full, un-moved term vectors — are still alive on every ancestor frame. A staircase alternation az|aaz|aaaz|… peels one char per level ⇒ ~N³/6 live PatternTerms.

Re-verified on this machine, branch Release vs pre-PR Release, new RegExp(build(N)).exec(...):

N source len branch compile+exec branch peak RSS baseline
200 20 KB 28 ms 1.7 ms
400 81 KB 203 ms 5.2 ms
800 322 KB 1.7 s 6.3 GB 24.5 ms / 206 MB
(agent, larger) 983 KB 10.6 s 32.8 GB 0.13 s / 400 MB; node 50 ms

A < 1 MB regex source OOM-kills a Linux process, from new RegExp(userString). The PR body's "compile time grows 4–6%" only holds for non-adversarial shapes. There is also no runtime option to turn factoring off (useRegExpAlternationFactoring was removed in 5cd184b). Fix direction: move rather than copy member terms (or clear each member's tail immediately after copying), release members before recursing, and cap recursion depth / total copied terms with a bail-out that leaves the disjunction unfactored. Please also restore an Options kill switch for the rewrites.
Repro: ~/code/tmp/yarr299/pattern/05-deep-prefix-perf.js, 05b-deep-prefix-mem.js.

🔴 Finding 2 — wrong match (false positive) for a negative assertion whose body is BOL-anchored via a nested positive lookahead (PR-introduced in 5cd184b; both tiers; low real-world likelihood).

atomParenthesesEnd now refuses to bubble m_startsWithBOL through an inverted assertion (!lastTerm.invert()), so the enclosing alternative reaches optimizeBOL's loop copy; there copyTerm (~1752–1795) filters the (?=^…) alternative away, gets an empty body, and — because the assertion is inverted — declares it satisfiedByEmpty and deletes the term. Removing alternatives from a negative assertion widens what it accepts:

/(?!(?=^a))a/.exec("a")     // branch JIT+interp: ["a"]   baseline JIT+interp: null   node: null
/(?!(?=^))a|q/.exec("a")    // same

(Re-verified locally.) The partial-filter sibling /(?!(?=^)|y)x/ is already wrong on baseline — same unsound "filter under inversion" mechanism, pre-existing. Correct rule: never filter startsWithBOL alternatives beneath an inverted assertion. Repro: 07-neg-assert-bol-filter.js, 08-neg-lookahead-group-bol.js.

Pre-existing, adjacent, not from this PR

Interpreter tries once-through alternatives in the wrong order after the first position (YarrInterpreter.cpp:2099 skips only one once-through alternative on wrap): /a|^x|aa/.exec("caa") → interpreter "aa", JIT/node "a". In shipping Bun this hits every lookbehind-containing pattern (all interpreter today), e.g. /a(?<!q)|^x|aa/; this PR incidentally hides it by JITting lookbehinds and does not make it worse. Repro 02-interp-lazy-suffix.js, 03-narrow.js.

Checked and sound

  • Alternation order semantics: factoring only sorts maximal runs whose first term is a fixed-count-1, case-sensitive, Forward PatternCharacter; alternatives with different leading code points can't both match at one position; stable sort preserves order within a leading char; empty / once-through / class / group / quantified-first alternatives are barriers. ~12k generated alternations × 24 subjects (captures, backrefs, named/dup-named groups, lazy/greedy outer quantifiers, g/y/u/v/i/m/s, astral + lone surrogates): branch JIT == node in every non-lookbehind case; branch interpreter == baseline interpreter byte-for-byte.
  • Capture numbering / undefined-ness, backrefs into factored tails, accumulateCaptureRange, m_isLastAlternative, isTerminal clearing, isStringList / DotStarEnclosure exclusions, {0,n} split copies, nested min>0 lookbehind groups, isbot-style word lists up to 1200 alternatives with \b, $, {2,3}, lookbehind prefixes — all agree with node.
  • Memory safety of the rewrite code: no ASAN/asserts on Debug across the sweeps plus a stress file (deep merges, 3000-wide folds, nested 9-wide groups to depth 37). m_parent is unused downstream; offsets/minimumSize/callFrameSize are computed after the rewrite by the unchanged setupOffsets.
  • BOL-propagation changes fix several baseline JIT bugs ((?:^)?a, (^a|^b)?c, x(?:^a)?y); the dead-alternative logic is correct in non-inverted contexts.

Harness: ~/code/tmp/yarr299/pattern/{sweep.sh,gen.js,gen2.js,genu.js,run.js,01…12-*.js}.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Part 5/7 — historical-bug regression scan. Harvested 517 Source/JavaScriptCore/yarr commits (2015→now), classified ~70 bug-fix commits into 8 classes, ran every in-tree regression test they added (290 files) 3-way (branch Release JIT / branch Release interp / pre-PR baseline) plus branch Debug+ASAN JIT, test262 RegExp/lookBehind both tiers, 178 hand-written "same bug shape routed through the new code" variants 4-way, a 4,000-case lookbehind edge differential under ASAN, and 2³¹±1 offset/count overflow probes.

Class (representative rdar/bugzilla) PR touches it? In-tree tests on branch Fresh variants
A. Lookbehind interpreter series — b174931 rdar://33183185, b249330 rdar://103367993, b251435 rdar://104652578 (UAF), b253466 rdar://105669717 (ASAN), b259536, b258531, b266373, b273426, b273254, b276154 rdar://115244009, b286691 (ASAN_TRAP), b286315/b294681 (security branch), b312690 rdar://175122467, b314249 wholesale (every shape now has new JIT codegen) all PASS ×3; test262 lookBehind PASS both tiers 60 variants: JIT == node everywhere; 8 JIT≠interp, interpreter wrong in all 8
B. Boyer-Moore / first-char — b228301, b228810, SIMD 2026-01/02, b308246 rdar://170676343 (index>length), b311388, b312604 rdar://173555471 (corrupted JSString), b317626 yes all PASS ×3 KELVIN/ſ/µ/ÿ/Å on Latin1 /iu, LB-leading BM, astral /u on Latin1: all == node
C. Offset/count overflow — b159744, b159954, b220130 (CrashOnOverflow), b220357, b240552, b276306 yes (assignAlternativeOffsets, backward negativeOffsetIndexedAddress, dispatch firstCharacterOffset) all PASS ×3 2³¹±1 counts in LB / nested / backrefs / after huge prefix: no ASAN, JIT == interp
D. Paren/frame layout — b198065, b254600, b311716 rdar://174303892 (ParenContext UAF, IP control), b312976 (slot aliasing SIGBUS), b307532, b260928 yes (Once +1 slot, BackReference +1) all PASS ×3 incl. paren-context-UAF, slot-collision, large-paren-context == node
E. DotStar/BOL/sticky/alternation — b119191, b174044, b236332, b214181, b289028, b288102, b290789, b290567 yes (factor/wrap, BOL bubbling) all PASS ×3 sticky + ≥8 alts + LB, capture numbering after sort, empty-alt barrier, prefix-of-sibling, \b barrier: == node
F. Non-zero-min / empty-iteration / hangs — b167125, b177570, b178890, b224983, b307415/b307533 yes (direction-aware progress guard) all PASS ×3 == node; no hangs in 4k fuzz
G. Canonicalization / unicode index — b282582, b316056, b282200, b267011, b307774/b307964, b309870, b311241, b319370 (d886174, in main, not in this branch) yes (backward code-point reader, backward class take/backtrack) all PASS ×3 correctness == node; perf regression below
H. Parser/stack recursion — b201649, b203936, b314475, b285939 uses isSafeToRecurse PASS

No in-tree regression test passes on baseline and fails on the branch. No ASAN reports, crashes, or hangs across any of it.

🔴 Finding — resurfaced bug class G (b319370): unicode lookbehind ending in a variable-width greedy class is O(n³) in the new backward JIT path. New regression vs pre-PR.

backtrackCharacterClassGreedy Backward + m_decodeSurrogatePairs (YarrJIT.cpp ~4083–4118) uses a rematch-from-begin loop. Pre-PR these patterns ran in the interpreter, which is linear here. Re-verified locally, 16-bit subject len 5002 ("“" + "word ".repeat(1000) + "!"):

                         branch JIT    pre-PR    branch interp   node
/(?<=X[^"]*)!/u           14,368 ms    105 ms       ~18 ms       ~1 ms
/(?<!X[^"]*)!/u           12,151 ms    101 ms
/(?<=X\D*)!/u             11,743 ms    106 ms
/(?<=X[^\u{1F600}]*)!/u   12,224 ms    101 ms
/(?<=X\w*)!/u                  0 ms      0 ms   (fixed-width class, unaffected)

Latin1 subjects and non-/u unaffected. This is the same complexity bug upstream fixed for the forward direction in d886174 — which is in oven-sh main but not in this branch, and whose O(1) step (negativeOffsetIndexedAddress(checkedOffset - inputPosition + 2) + dropping the storeToFrame(matchAmount)) is wrong for a Backward frame, so a mechanical conflict resolution during rebase would be incorrect. Needs a backward-specific O(1) step (inspect the pair at/after the cursor). After rebasing, re-run regexp-unicode-greedy-charclass-backtrack-{exhaustive,linear}.js (main-only tests; the linear one currently takes 233 s on this branch's forward path, confirming it predates d886174) plus lookbehind variants of both. Also note main's e13280f / 4a3ab62 touch registers readUnicodeCharBackward uses (regT2 / regUnicodeInputAndTrail / unicodeAndSubpatternIdTemp) — historical "register clobber" class (e6c8fb7, 3c0ada1, bd4bbbb); x64 must be re-verified post-rebase. Repro: ~/code/tmp/yarr299/history/perf_lb_class3.js.

Medium — pre-existing interpreter bugs now visible as tier divergence (JIT correct = V8)

Same family as parts 2/3: non-BMP inside lookbehind in the bytecode interpreter, e.g. /(?<=[\u{1F600}a])b/u.exec("x😀b") interp null, JIT/node [3,"b"]; /(?<=\u{1F600}.)x/su.exec("😀😁x") interp null, JIT/node 4; plus 6 fuzz ids. Any JIT bail silently reverts to the wrong answer. l5.js.

Info — pre-existing, both tiers, deliberately preserved

Zero-width patterns match mid-surrogate-pair under /u (/(?<=\uD83D)/u.exec("😀") → index 1; V8 null per AdvanceStringIndex).

Not run: WebCore-only tests (b308707 etc.), //@ skip memory/stack hogs, microbenchmarks. Harness + data: ~/code/tmp/yarr299/history/{run.ts,run_dbg.ts,results*.json,variants.js,lb_edge_fuzz.js,ovf.js,perf_lb_class*.js,yarr_commit_tests_small.tsv}.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Part 6/7 — first-character dispatch, inline literal alternatives, chainResume slot (tryPrepareDispatch, decision-tree emission at NestedAlternativeBegin, inline stubs + unwind thunk, Next/Begin backtrack through chainResume, ParenContext save/restore of the new slot).

Verdict: no memory-safety or wrong-result bug found; this sub-feature looks sound. ~22.5k random cases on Debug+ASAN JIT (0 crashes/asserts), ~200k branch-vs-merge-base-build (0 regressions; every diff is a branch improvement), ~240k branch-vs-node, plus hand suites for icase / backtracking+quantifiers+captures+backrefs / big alternations / assertions / edges — identical across branch JIT, branch interp, main, node except pre-existing items below.

Checked explicitly:

  • Can-start set is a valid over-approximation: computed before optimizeAlternative reorders terms; ASCII-alpha /i adds both cases; non-ASCII /i PatternCharacter and any /i, table, or inverted class widen to any; unicode-mode case variants (K/KELVIN, s/ſ, ÿ/Ÿ, µ/Μ) arrive as CharacterClasses split at 0xFF; \q{} strings become their own alternatives; zero-width prefixes (^, \b, lookaround, forward refs, min-0 quantifiers/groups) fall through to the next term; backrefs → any; empty-capable alternative → any; dispatch refused when m_minimumSize == 0, when > 25% any, in Backward frames, and when m_decodeSurrogatePairs (so 16-bit reads are plain code units routed to wideChain).
  • End of input: read is input[index - firstCharacterOffset] with offset ≥ 1 inside already-checked input (checkedOffset >= term->inputPosition guard ~7358–7363); no OOB on 8/16-bit; sticky and lastIndex == length covered.
  • Inline literals: bounded by add extraClaim; branch Above length before compares; per-char jumpIfCharNotEquals (no truncating multi-char loads; chars > 0xFF never equal a load8); unwind thunk restores index and continues the chain; return-address store precedes the End join; captures around the group and MatchOnly correct; 32-char cap.
  • Backtracking order: chains are ascending alternative indices with any alts in every chain; Next/Begin backtrack releases m_checkAdjust then jumps through chainResume; group failure joins at Begin with index at group start; nested quantified parents restore the slot via ParenContext (iterates to m_body->m_callFrameSize). Dispatch inside lookahead-in-lookbehind enabled and correct.
  • Frame slot: +1 per Once group / +1 per backreference is consistent across YarrPattern.cpp:2001, YarrInterpreter.cpp static_asserts, and ParenContext; no off-by-one; "too large"/"too many captures" limits unchanged. Memory-only cost on the interpreter.
  • Scale: 5,000 alternatives, 95 chains × 8 × 32-char stubs, nesting depth 12 — no compile blowup (chains > 96 / stubs > 768 bail), tree depth ≤ 9, isSafeToRecurse in set analysis; cached temp registers invalidated at every join.

Findings

  1. No kill switch. useRegExpAlternationDispatch / regExpAlternationDispatchThreshold (and the factoring options) were added in 3037b8d and deleted in 5cd184b; tryPrepareDispatch now uses only constexpr alternationDispatchMinAlternatives = 4. Same request as part 4: please restore Options for dispatch, inline literals, and the YarrPattern rewrites before this ships in a Bun release, so a field regression has a same-day mitigation.
  2. Perf pessimization for many sequential tiny once-groups. (?:ab|cd|ef|gh) repeated 200–2000×: 1.9–3.2× slower steady-state than merge-base (0.205 vs 0.064 µs at 200 groups when alt 0 matches; 11.7 vs 4.0 µs at 2000). Dispatch + per-chain inline stubs are emitted in the hot path at every NestedAlternativeBegin, so a first-alternative hit now pays read + tree + jump + per-char compares + patch-store, and code size grows per group. Realistic single-group patterns (HTTP methods, extensions, months, log levels, isbot) are neutral to 9× faster. Low severity; consider skipping dispatch when the group has ≤ N total literal chars. 22-perf-seq-groups.js, 07-perf-small.js.

Not this PR, surfaced by the differential

  • Merge-base non-unicode /i canonicalization bugs (/K/i, /ſ+/i, /İ/i matching k/s/i) are fixed on main post-upgrade; the branch still has them and they leak into "JIT" results whenever the JIT punts to the interpreter (empty-iteration abort), giving lastIndex-dependent answers for e.g. /(S|(?<=a)|_)+$|(?:…|ſ|…)/ig. Disappears on rebase.
  • Pre-existing on main, interpreter only: /(k|^a)|(k)/.exec("Ak")["k",null,"k"] (should populate group 1). More visible now because DFG constant-folding uses the interpreter while runtime uses JIT.
  • jsc (JIT == interp == merge-base) vs V8 differences for captures inside quantified groups inside lookbehind — pre-existing semantics question.

Harness: ~/code/tmp/yarr299/dispatch/{fuzz.js,fuzz-lb.js,harness.js,run4.sh,run-diff.sh,run-mb.sh,classify.js,run-node.sh,01…07-*.js,20…23-*.js}; a merge-base jsc build is at ~/code/tmp/yarr299/mergebase/bun-webkit/bin/jsc.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Part 7/7 — black-box differential fuzzer + suites.

Volume: 564k branch-JIT-vs-branch-interp cases, 564k branch-vs-baseline-JIT, 500k three-way with node 26.5, 123k on Debug+ASAN (JIT vs interp), across five generators (lookbehind-heavy incl. nested lookaround/backrefs/anchors/all flags/lastIndex sweeps/astral + lone surrogates; trie-generated shared-prefix alternations up to 150 alts; Boyer-Moore bait on Latin1 vs Latin1+one-16-bit-char; the real isbot pattern × 100k UA strings × i/iu/iv/g/y/d; mutations of every regex literal in JSTests/stress + LayoutTests/fast/regex). Plus JSTests/stress regexp*|*lookbehind*|*yarr* (218 files × 2 tiers × {branch Release, branch Debug+ASAN, baseline}) and test262 RegExp/annexB/literals/String-regex-methods (2,535 files × 6 configs).

Crashes / asserts / ASAN: none. 0 non-zero exits, 0 ASSERT/ASAN across all of it. No engine-specific hangs. isbot: zero divergences across all five engines on 100k UAs — the motivating case is solid. Suites: no stress test passes on baseline and fails on branch; test262 only delta is explained below.

Triage of the "new vs baseline" divergences it reported

My baseline binary is current main (post upstream-upgrade), which is 381 commits ahead of this PR's merge-base, so I re-checked each "NEW" family against a true merge-base build:

  • Bogus surrogate pairing of U+F800–FBFF + U+FC00–FFFF under /su . (/^.$/su.test("fi!") → true, "豈ﰀ".match(/./gsu) → 1 match, /^\P{Co}+$/u test262 slow-path): pre-existing upstream bug at the merge-base (surrogateTagMask = 0xdc00dc00 ignores bit 0x2000), already fixed on main by the upstream upgrade (0xfc00fc00). Not introduced here; disappears on rebase. The PR's new backward reader already uses the correct 0xfc00 masks. ⚠️ One more reason the rebase must be done carefully in tryReadUnicodeCharImpl's neighbourhood.
  • Interpreter non-unicode /i cross-ASCII case partners (/ſ/i ~ "S", /K/i ~ "k", test262 u-case-mapping.js failing on --useRegExpJIT=0): also present on the merge-base interpreter, fixed on main. Not this PR; disappears on rebase.
  • Zero-width unicode lookbehind at a mid-surrogate index (/(?<=\p{Script=Zzzz})/u.exec("𐐨") → branch JIT index 1, all other JSC configs + node null; "😀".matchAll(/(?<=[\S])/gu) → branch JIT [1,2], other JSC [1], node [2]): genuinely PR-introduced tier divergence, but it's the new JIT applying JSC's pre-existing "zero-width may match mid-pair" rule (which all JSC tiers already exhibit for /(?<=\uD83D)/u → 1, node null) to lookbehind bodies that read a lone lead surrogate leftward. Low severity, exotic inputs; fold into the "interpreter unicode-lookbehind needs the same fixes" follow-up from parts 2/3/5, and consider aligning JSC with V8's AdvanceStringIndex behaviour for zero-width /u matches generally.
  • Everything in the "branch JIT now equals node, interpreter (both baseline and branch) differs" bucket (/(?<=.)a/su on "𐐀a", /(?<=[\u{10400}x])b/u, /(?<!\p{L})b/u …) is the same pre-existing interpreter tryReadBackward family already reported.
  • Pre-existing JIT-only, both merge-base and branch, worth separate issues: /v nested-class forms (/[f[^z]]/v, /[ca\S]/v, /[a[^\u{3}]]/v) match a lone lead surrogate of an astral char; /[^a-z]|f4/iu.exec("c😀") → null.
  • Pre-existing interpreter-only: /(?:^|[^g])x6|ar?/.exec("gax6")"a" (isbot-shaped (?:^|[^g]) prefix!), /(^|[^g])hz+|w/, /(.)\1*/u on astral. These affect shipping Bun today for any lookbehind-containing pattern and are hidden by this PR JIT-ing them; worth fixing in the interpreter regardless.
  • Branch fixes vs baseline confirmed: /(?:^)?a/, /(?:^b)?a/, /\B(?:^)?/, /.\n?X/u, /.Q/su on astral, etc.

Harness (parallel, seeded, re-runnable post-rebase): ~/code/tmp/yarr299/fuzz/{fuzz.js,driver.ts,recheck.ts,min.ts,triage.ts,suites.ts,test262.ts}; results under out/.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

(Claude wrote this comment.)

Review summary & recommendation for Bun 1.4

Seven independent passes (parts 1–7 above), ~1.9M differential regex executions across branch-JIT / branch-interp / merge-base / current-main / node, ~190k of them under Debug+ASAN, all 290 historical Yarr regression tests, test262 RegExp on both tiers, the real isbot pattern on 100k UAs, and targeted adversarial reads of every new code path. Every finding below was re-verified by hand on this machine.

Memory safety

Nothing found. Zero ASAN reports, zero asserts, zero crashes. Backward reads are bounded by 0 (checked with planted sentinels at input[-1]/input[length] on buffer-sharing substrings); frame-slot bumps are consistent across JIT/interpreter/ParenContext; 2³¹±1 offset probes clean; exec-memory exhaustion falls back correctly; no shared-pattern mutation; no new global state. For a 2,500-line Yarr JIT change that is a genuinely good result.

Must fix before merge (all PR-introduced, all small)

# What Where Impact
1 Cubic time/memory in prefix factoring — copies member tails and recurses with originals alive mergeSharedPrefix YarrPattern.cpp ~2489 az|aaz|aaaz… 322 KB source → 6.3 GB RSS / 1.7 s (baseline 206 MB / 25 ms); ~1 MB → 32 GB. Both tiers. new RegExp(userInput) DoS.
2 O(n³) backtrack for /u lookbehind ending in variable-width greedy class — rematch-from-begin loop backtrackCharacterClassGreedy Backward+decodeSurrogatePairs, YarrJIT.cpp ~4083 /(?<=X[^"]*)!/u on 5 KB 16-bit text: 105 ms → 14 s. ReDoS-shaped regression; same class as upstream b319370.
3 False positive: BOL filter applied under a negative assertion atomParenthesesEnd / copyTerm YarrPattern.cpp ~1588, ~1766 /(?!(?=^a))a/.exec("a")["a"] (should be null). Both tiers. Rare shape, trivial fix (never filter startsWithBOL alternatives beneath invert()).
4 Rebase onto main + re-run all harnesses 381 commits behind; conflicts in backtrackCharacterClassGreedy (d886174), register reshuffle (e13280f/4a3ab620), surrogateTagMask fix (0xdc00dc00→0xfc00fc00) Mechanical resolution of d886174 into the Backward arm is wrong; #2's fix and the rebase are the same piece of work.
5 Restore Options kill switches (useRegExpLookbehindJIT, useRegExpAlternationFactoring, useRegExpAlternationDispatch) — added in 3037b8d, removed in 5cd184b RegExp.cpp / YarrPattern.cpp / YarrJIT.cpp Without them a field regression in 1.4 means a WebKit rebuild + point release instead of BUN_JSC_useX=0.

Should fix (cheap, same PR)

  • m_readsStartCharacter stamped after optimizeAlternative() swap → "x😀 y😀 z-".match(/\p{Extended_Pictographic}|[a-z]-/gu) drops the emoji (pre-existing in baseline JIT too, but the PR claims to fix this family; one-line inputPosition == 0 fix).
  • O(n²) m_nextOp walk in NestedAlternativeNext now reachable via wide alternations inside lookbehind ((?<=k0|…|k40000)z 19 s compile). Cache the End-op index.
  • isSafeToRecurse() in assignDisjunctionOffsets / assignAlternativeOffsets / accumulateCaptureRange.

Accept / follow-up issues

  • DFG RegExpTestInline lost at exactly ≥ 8 short alternatives (2.8× slower .test() hot loop); many-sequential-tiny-groups 2–3× slower. Tuning, not blocking.
  • Interpreter tryReadBackward is wrong on several unicode lookbehinds where the new JIT is right → tier-dependent results on JIT bail. Pre-existing; file upstream + fix interpreter.
  • Pre-existing interpreter alternation-order bug (/(?:^|[^g])x6|ar?/) that affects every lookbehind pattern in shipping Bun and is hidden by this PR.

Recommendation

Don't merge ab39369 as-is. Do merge for 1.4 if items 1–5 land in the next couple of days, followed by a clean re-run of the seven harnesses in ~/code/tmp/yarr299/ on the rebased head (they're seeded and parallel; a full re-run is ~2–3 machine-hours). The core of the change — lookbehind JIT, dispatch, inline literals, BM scoring — held up under far more adversarial testing than upstream Yarr patches normally get, isbot is 200×+ faster with zero divergences, and none of the blockers are in the hard parts; they're two accidental complexity cliffs, one over-eager filter, a rebase, and putting the option flags back.

Patterns containing a lookbehind previously fell back entirely to the
bytecode interpreter (JITFailureReason::Lookbehind at compile time, and a
pre-check in RegExp::compile that never even attempted the JIT). A single
lookbehind therefore sent an otherwise cheap pattern to the interpreter;
the isbot user-agent regex (~110 alternatives, four of them lookbehinds)
ran ~40x slower than with the assertions JIT'd.

A lookbehind body matches right-to-left ending at the assertion point.
Matching backward over the input is equivalent to matching a term-reversed
copy of the body forward over the reversed prefix, so each body is compiled
as a mirrored copy (terms reversed, input positions reassigned with the same
width rules as setupAlternativeOffsets) driven through the existing forward
machinery. The original YarrPattern is untouched, keeping the bytecode
fallback valid. Each op carries a MatchDirection; in a backward body the
index register is the leftward-moving left frontier, and only the primitives
flip: input claims subtract (branchSub32 on the borrow's sign bit),
characters are read at input[index + (k - 1)], end-of-input is index == 0,
greedy consumption and give-back reverse sign, captures write the real
position with start/end slots swapped (backward captures record end-first,
matching the interpreter), and BOL/EOL/word-boundary anchors get mirrored
edge tests. Nested lookbehinds are mirrored on demand.

Still handled by the interpreter, exactly as before: surrogate-pair
decoding (unicode 16-bit), backreferences inside a body, lookaheads nested
inside a body, and quantified groups with maxCount > 1 inside a body.
Character fusion (multi-char wide loads) is disabled inside backward bodies
since ascending memory holds descending pattern positions.

collectBoyerMooreInfoFromTerm now treats a parenthetical assertion as
zero-width (like BOL/EOL/word-boundary) instead of aborting collection: an
assertion consumes no input, so the leading-character sets remain a valid
over-approximation.
…group

A large top-level alternation is matched by attempting each alternative
in turn at every candidate position, so cost grows linearly with the
alternative count regardless of how quickly each alternative fails.
Two pattern-level rewrites cut the number of alternatives entered.

Alternation prefix factoring: alternatives are tried leftmost-first, but
their order is only observable between alternatives that can match at
the same starting position. Alternatives that must begin with different
literal characters have disjoint starting points, so a maximal run of
consecutive alternatives that each begin with a fixed, case-sensitive
literal character is stably sorted by that character (stability keeps
same-first-character alternatives in source order) and each group
sharing its leading literal is merged into one alternative whose common
prefix is hoisted over a non-capturing group of the suffixes:
/aq|bx|ar|by/ becomes /a(?:q|r)|b(?:x|y)/. Any alternative that does
not begin with such a character (class, group, anchor, optional atom,
the empty alternative) is a barrier no reordering crosses, and the
rewrite recurses into the factored suffixes.

Group folding: the body's repeated (non-once-through) alternatives, when
numerous, are folded into a single alternative holding one non-capturing
group -- /X|Y|Z/ is exactly /(?:X|Y|Z)/ -- whose capture range brackets
the captures it absorbs. Nested alternatives claim and release their own
input, giving each alternative an index-neutral entry that a following
change dispatches on the first character.

Both rewrites are engine-neutral pattern equivalences (the interpreter and
the JIT see the same factored form) and run before setupOffsets(), which
lays the synthesized groups out like hand-written ones. Controlled by
useRegExpAlternationFactoring / regExpAlternationGroupThreshold.
Trying each alternative of a nested disjunction in sequence costs an
entry attempt (claim, character load, compare, release, jump) per
alternative at every candidate position, so a large alternation scales
linearly with its alternative count even when each alternative fails on
its first character.

For a once-quantified fixed group whose alternatives all consume at
least one character, compute per alternative a first-character set (an
over-approximation of the characters a match can start with; Latin-1
tracked precisely, wider characters and the empty prefix widen to
"any"). Every alternative's first character sits at the group's start
frame position, which is inside input the enclosing alternative already
claimed, so it is read once with no bounds check. A binary decision tree
over that character's value then jumps to the ordered chain of exactly
the alternatives whose set contains it; an "any" alternative appears in
every chain, so leftmost-alternative-wins ordering is preserved. When no
alternative can start with the character the group fails immediately.

Each chain is a sequence of stubs that store the address of the next
stub into a new frame slot (BackTrackInfoParenthesesOnce::chainResume,
patched at link time) before jumping into the alternative; a failing
alternative releases its claim and continues its own chain through
that slot rather than falling to a fixed successor, and an exhausted
chain joins the group's failure flow. Entry into an alternative is now
a labelled chain target rather than a fallthrough, and backtracking
back into a matched alternative reuses the still-valid resume address.

Combined with the alternation prefix factoring, the isbot user-agent
regex over the 361,829-string corpus goes from ~620 ms (factoring
alone) to ~120 ms; the sequential JIT was ~750 ms and the interpreter
~32,000 ms. Controlled by useRegExpAlternationDispatch /
regExpAlternationDispatchThreshold; interpreter behaviour is unchanged
and JIT results match it byte-for-byte across differential corpora.
Comment thread Source/JavaScriptCore/yarr/YarrJIT.cpp
… and dead checks, test probe, harness imports

- accumulateCaptureRange reads each parenthesis term's [subpatternId, lastSubpatternId] bracket
  instead of recursing through the nesting (no isSafeToRecurse needed before setupOffsets()).
- emitInlineLiteralAlternative's 64-bit fused loads sit behind the same 64-bit-CPU guard as the
  term path's; generateBitInTableSIMDSearch drops the checks canUseVectorScan() already made; the
  wide-routing fallback restores a stubCount snapshot rather than re-summing.
- regexp-unicode-property-escape-ignore-case.js's sixth probe is U+01C4 (uppercase DZ) rather than
  a repeat of U+01C5, with the expectation table regenerated.
- drive.mjs writes filename-safe output names for file: profiles; run262.mjs imports tidied;
  stray blank lines / a column-0 line.
…apture-range uses containsAnyCaptures(); Tools/yarr-fuzz gains the corpus mutator and the reference-matcher oracle

No behaviour change in the engine (the same instructions are emitted). Tools/yarr-fuzz/regex-fuzz.js is synced with the version the driver expects (file: replay profiles, strings/wide profiles), and mutate.mjs / refmatch.mjs / refgen.mjs / refrun.js / refcheck.mjs are added with a README section.
Comment thread Source/JavaScriptCore/yarr/YarrPattern.cpp
@Jarred-Sumner Jarred-Sumner changed the title Yarr: lookbehind JIT (incl. unicode), alternation factoring, first-character dispatch, and Boyer-Moore search selection Yarr: JIT lookbehinds, first-code-point alternation dispatch, Boyer-Moore and /u·/v spec alignment, RegExp correctness fixes Aug 11, 2026
… entry's leading slot

m_namedGroupToParenIndices maps a name to [id, id] or, once duplicated, to [duplicateNamedGroupId, id1, id2, ...]; comparing the closing group's subpatternId against slot [0] could take a duplicate-group id for a subpattern id and resolve \k<name> to an unnamed enclosing group. No observable difference (such a group encloses the reference, so both tiers read it as unset), but the lookup no longer depends on that.
…P latch's feature guard to its member's, whitespace; drive.mjs unused imports

No-Verification-Needed: dead-code/whitespace/import removal only; identical generated code.
Comment thread Tools/yarr-fuzz/bench.js
Comment on lines +5 to +6
const HERE = (typeof arguments !== "undefined" && arguments[0]) || (typeof process !== "undefined" && process.argv[2]) || ".";
const isbot = JSON.parse(RF(HERE + "/isbot-pattern.json"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Under Node the HERE detection at L5 picks up the CommonJS module wrapper's arguments object — arguments[0] is the (truthy) exports object, not a CLI arg — so HERE becomes {} and L6 calls readFileSync("[object Object]/isbot-pattern.json") → ENOENT. The file's own L1 comment documents node bench.js as a supported invocation. Fix: check process.argv[2] first, or guard with typeof arguments[0] === "string". Dev-tool only; zero product impact.

Extended reasoning...

What

Tools/yarr-fuzz/bench.js L5:

const HERE = (typeof arguments !== "undefined" && arguments[0]) || (typeof process !== "undefined" && process.argv[2]) || ".";

The intent is: under jsc use the shell's global CLI-argument array (arguments[0]), under Node use process.argv[2], else fall back to ".". But under Node.js a .js file loaded as CommonJS is wrapped in (function(exports, require, module, __filename, __dirname){ … }), so at module scope typeof arguments === "object" and arguments[0] is the module's exports object — an empty {}, which is truthy. The || chain therefore short-circuits to {}, the process.argv[2] branch is never reached, and HERE is an object rather than a path string.

L6 then evaluates RF(HERE + "/isbot-pattern.json"). String concatenation on an object gives "[object Object]/isbot-pattern.json", and readFileSync throws ENOENT immediately.

Step-by-step proof

  1. Write the exact expression to a .js file (not node -e, which has no module wrapper):
    $ printf 'const HERE=(typeof arguments!=="undefined"&&arguments[0])||(typeof process!=="undefined"&&process.argv[2])||".";console.log(typeof arguments, JSON.stringify(HERE), HERE+"/x")' > t.js
    $ node t.js somedir
    object {} [object Object]/x
    
    arguments exists at module scope, arguments[0] is {}, and the intended somedir argument in process.argv[2] is never consulted.
  2. Running node bench.js from Tools/yarr-fuzz/ therefore reaches L6 with HERE = {} and throws in fs.readFileSync on "[object Object]/isbot-pattern.json" before any benchmark runs.
  3. jsc bench.js -- <dir> works, because in the jsc shell the global arguments really is the CLI-arg array (a string at index 0, or undefined when none is passed).

Why nothing catches it

The file's own header comment (L1) says jsc bench.js (or node bench.js), and README.md lists bench.js as one of the reusable tools, so node bench.js is a documented invocation — but these are standalone scripts under Tools/, not compiled or exercised by any CI job in this repository. On the author's side the benchmarks were presumably run under jsc (the shell whose numbers the PR quotes), where the expression works.

Impact

Zero product impact. This is a dev-only benchmark script, not shipped in any Bun/JSC build and not run by CI. The Yarr changes themselves are unaffected. It is the same class as the earlier yarr-fuzz tool nits on this PR (run262.mjs/triage-base.mjs hardcoded paths, drive.mjs unused imports): a PR-introduced tool that does not work as its own comment says under one of its two documented hosts.

Fix

Test the Node path first (Node has both process and arguments; jsc has only arguments), or require the jsc-path value to be a string:

const HERE = (typeof process !== "undefined" && process.argv[2])
          || (typeof arguments !== "undefined" && typeof arguments[0] === "string" && arguments[0])
          || ".";

Either ordering fix or the typeof … === "string" guard alone is sufficient; combining them is belt-and-braces.

// astral / special characters spliced into literals
[2, () => { const n = pick(terms.filter(t => t.type === "Character")); if (!n) return null; return [splice(src, n, text(src, n) + pick(["\u{1F600}", "\\u{1F600}", "\uD83D", "é", "ſ", "K", "\\u212A", "ß", "\u{10400}"])), flags.includes("u") || flags.includes("v") || chance(0.5) ? flags : flags + "u"]; }],
// anchors / boundaries / modifiers
[2, () => { const n = pick(terms); if (!n) return null; return [splice(src, n, pick(["^", "$", "\\b", "\\B", "(?i:", "(?-i:", "(?s:"]).replace(/\($/, "(") + text(src, n) + (["(?i:", "(?-i:", "(?s:"].some(x => false) ? "" : "")), flags]; }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Two subexpressions on this line are dead editing debris: .replace(/\($/, "(") is a no-op (nothing in the pick list ends with a bare ( — the modifier openers end with :), and (["(?i:", "(?-i:", "(?s:"].some(x => false) ? "" : "") is always "" (.some(x => false) is always false, and both ternary branches are ""). So when pick() returns one of the three modifier openers the emitted pattern has no closing ), always fails compiles() (L115), and the mutation step is discarded — 3 of 7 options are dead weight, and redundant with L81 which already handles modifier groups correctly. Fix: drop "(?i:", "(?-i:", "(?s:" from the pick list and delete the two dead subexpressions. Zero product impact — dev-only fuzz tool.

Extended reasoning...

What

Tools/yarr-fuzz/mutate.mjs L80 is the "anchors / boundaries / modifiers" mutation op:

[2, () => { const n = pick(terms); if (!n) return null; return [splice(src, n,
    pick(["^", "$", "\\b", "\\B", "(?i:", "(?-i:", "(?s:"]).replace(/\($/, "(")
    + text(src, n)
    + (["(?i:", "(?-i:", "(?s:"].some(x => false) ? "" : "")
), flags]; }],

Two subexpressions in it are dead editing debris:

  1. .replace(/\($/, "(") — the regex matches a literal ( at end-of-string and replaces it with (, which is a no-op by construction. It also never matches: none of the seven pick-list strings end with ( — the four anchor/boundary strings contain no parenthesis at all, and the three modifier openers "(?i:", "(?-i:", "(?s:" all end with :.
  2. (["(?i:", "(?-i:", "(?s:"].some(x => false) ? "" : "") — the .some() callback ignores x and returns false unconditionally, so .some(...) is always false; and both branches of the ternary are "". The whole subexpression is therefore the constant "".

Step-by-step proof

Take a mutation step where pick() returns "(?i:":

  1. pick([...])"(?i:".
  2. .replace(/\($/, "(") on "(?i:"/\($/ requires the last character to be (, but the last character is :, so no match; result is still "(?i:".
  3. ["(?i:", "(?-i:", "(?s:"].some(x => false) — the callback returns false for every element, so .some() returns false.
  4. false ? "" : """".
  5. The spliced fragment is "(?i:" + text(src, n) + "" — an unclosed modifier group.
  6. splice(src, n, ...) inserts an unmatched ( into an otherwise-balanced source, so new RegExp(s2, f2) throws SyntaxError: Invalid regular expression: missing ).
  7. compiles(s2, f2) (L89) returns false, and the loop at L115 (if (s2.length > 600 || !compiles(s2, f2)) break;) discards the whole mutation step.

The same holds for "(?-i:" and "(?s:". So 3 of the 7 options in this weight-2 op are guaranteed to produce a syntax error and be filtered out — dead weight in the mutation-op distribution.

The very next op at L81 already handles modifier groups correctly:

[2, () => { ... const m = pick(["(?i:", "(?s:", "(?m:", "(?-i:", "(?i-s:"]); return [splice(src, n, m + text(src, n) + ")"), ...]; }],

— it appends the closing ) and even adjusts flags for -i. So L80's three modifier entries are also redundant with L81, not merely broken.

Why it looks like this

This is unmistakable unfinished-refactor debris. The .replace(/\($/, ...) and the ternary keyed on the modifier-opener list both suggest an earlier form where the pick list contained something ending in ( and the ternary appended ")" when a modifier opener was chosen — presumably before that responsibility was moved to L81. What remains is the scaffold with both effects neutered.

Why nothing catches it / impact

mutate.mjs is a standalone Node script under Tools/yarr-fuzz/, not compiled or exercised by any CI job. compiles() (L89) is expressly there to filter out invalid mutations, so the broken cases are silently absorbed — no wrong output, no crash, no test failure. The tool still works; L81 covers modifier groups. Zero product impact.

The only effect is on fuzzing coverage: this weight-2 op wastes ~3/7 of its picks (and one whole mutation step each time, since L115 breaks the step loop), slightly under-representing the "anchors / boundaries" mutations relative to their intended weight. Same class of PR-introduced editing debris as the other Tools/yarr-fuzz nits already flagged on this PR (unused imports, hardcoded scratchpad paths).

Fix

Drop the three modifier entries and the two dead subexpressions, leaving L80 as the anchors/boundaries op only (L81 already covers modifier groups):

// anchors / boundaries
[2, () => { const n = pick(terms); if (!n) return null; return [splice(src, n, pick(["^", "$", "\\b", "\\B"]) + text(src, n)), flags]; }],

…ect load the RegExp JIT emits

Sanitizers do not instrument JIT-generated code, so an out-of-bounds read of the subject string by
a compiled RegExp (past the end after a surrogate-pair peek, or before the start from a lookbehind)
is invisible to them unless it happens to fault. With --verifyRegExpJITReads=1 each of the JIT's
subject loads (single units, surrogate-pair peeks in the shared thunks and inline, the backward
reader, fused 16/32/64-bit literal compares, the Boyer-Moore scalar and vector scans) is preceded
by a probe that recomputes the effective address range from the live registers and compares it
with [input, input + length * unitSize), reporting the emitting source line and aborting on a
stray read. The input and length registers are never written by generated code, so the probe reads
them back directly. Off by default and free when off (the check is made at code-generation time).

Validated by planting four bugs of that shape (thunk pair read without its end check, backward
reader without its start check, lookbehind offset mapping off by one, vector-scan bound loose by
8 bytes): each is reported within seconds by Tools/yarr-fuzz's new `verifyreads` configuration,
which the un-modified tree passes. drive.mjs also gains a `gcstress` configuration
(--collectContinuously).
const ve = encMatch(vre.exec(subject), !!pat.flags.d);
vre.lastIndex = 0;
const vt = vre.test(subject);
const enc = JSON.stringify(ve && ve.map((v) => typeof v === "string" ? v : v)) + "|" + vt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: .map((v) => typeof v === "string" ? v : v) has identical ternary branches, so it is an identity map — dead editing debris (compare L512, where the string branch is safeStr(v)). No functional impact: baseE at L423 and the width variant at L451 both stringify the raw encMatch result without a map, so the comparison at L442 is self-consistent either way. Drop the .map(...)JSON.stringify(ve) + "|" + vt. Zero product impact (dev-only fuzz tool).

Extended reasoning...

What

Tools/yarr-fuzz/regex-fuzz.js L441, inside the metamorphic-variant loop:

const enc = JSON.stringify(ve && ve.map((v) => typeof v === "string" ? v : v)) + "|" + vt;

Both branches of the ternary return v, so .map((v) => typeof v === "string" ? v : v) is an identity map — the whole subexpression is equivalent to JSON.stringify(ve). This is unmistakable editing debris: the shape matches the file's own L512 (r.e.map((v) => typeof v === "string" ? safeStr(v) : ...)), so safeStr(v) (or some other transform) was presumably intended for the string branch and then dropped without deleting the scaffolding.

Step-by-step proof

  1. encMatch (L370-377) returns either null or a plain Array built as const out = [m.index]; ...; return out; — no extra own enumerable properties.
  2. At L439, ve = encMatch(vre.exec(subject), ...), so ve is null or such a plain array.
  3. ve && ve.map((v) => typeof v === "string" ? v : v): when ve is null the && short-circuits to null; when it is an array, .map returns a fresh array with f(v) = v at every index.
  4. JSON.stringify on a plain Array walks index 0..length-1 only, so a shallow copy produced by .map(x => x) stringifies identically to the original. Hence L441 ≡ JSON.stringify(ve) + "|" + vt.
  5. baseE at L423 is JSON.stringify(r.e) + "|" + r.t — the raw encMatch result of the original pattern, with no map — and the width-variant at L451 is JSON.stringify(oe) + "|" + ot, likewise unmapped. So the comparison at L442 (if (enc !== baseE)) is self-consistent with or without the .map: both sides encode raw encMatch output.

Therefore the .map(...) is dead code with no observable effect on the metamorphic oracle.

Why nothing catches it

Nothing in the toolchain flags a ternary with identical branches or a no-op .map; regex-fuzz.js is a standalone fuzz-driver script under Tools/, not compiled or exercised by any CI job in this repository, and the metamorphic check produces the same enc string with or without the map, so runtime behaviour is identical.

Impact

Zero product impact. Tools/yarr-fuzz/regex-fuzz.js is a dev-only differential-fuzzing driver, not shipped in any Bun/JSC build and not run by CI. The Yarr changes themselves are unaffected. This is the same class of PR-introduced dev-tool editing debris as the open mutate.mjs L80 comment (.some(x => false) ? "" : ""), at a distinct file/location no existing PR comment covers.

Fix

Drop the identity .map:

const enc = JSON.stringify(ve) + "|" + vt;

(If a safeStr transform was intended here, it would have to be applied to baseE at L423 and the width-variant enc at L451 as well to keep the comparison self-consistent — but those already compare raw encMatch output, and the metamorphic oracle only ever compares within one engine, so lone-surrogate encoding is not a concern; simply removing the map is correct.)

@Jarred-Sumner
Jarred-Sumner merged commit 09e4777 into main Aug 11, 2026
43 checks passed
@190n

190n commented Aug 13, 2026

Copy link
Copy Markdown

exciting, will this be upstreamed?

robobun added a commit to oven-sh/bun that referenced this pull request Aug 13, 2026
`[\P{Number}&&\P{Alphabetic}]` and friends unioned the inverted operand
instead of intersecting or subtracting it. The engine fix landed in
oven-sh/WebKit#299 and reached Bun with the WebKit bump in #37352; this
adds the coverage for it.

The four tests on the previously broken path fail on the build before
that bump (447082ab) and pass on the build main pins now.
robobun added a commit to oven-sh/bun that referenced this pull request Aug 13, 2026
`[\P{Number}&&\P{Alphabetic}]` and friends unioned the inverted operand
instead of intersecting or subtracting it. The engine fix landed in
oven-sh/WebKit#299 and reached Bun with the WebKit bump in #37352; this
adds the coverage for it.

The four tests on the previously broken path fail on the build before
that bump (447082ab) and pass on the build main pins now.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Aug 18, 2026
Upgrades the WebKit fork to upstream WebKit/WebKit@47f7250137c6
(2026-08-16) via oven-sh/WebKit#455: 846 upstream commits since the
previous merge base `3722912ff800` (2026-08-02), 235 of them in
JavaScriptCore, WTF or bmalloc.

`WEBKIT_VERSION` is pinned to oven-sh/WebKit@eeab04040fa6, the fork
`main` after oven-sh/WebKit#455 merged, plus oven-sh/WebKit#463
(URLParser host scanning, WTF only); its `autobuild-eeab04040fa6...`
release has all 42 variants. (The PR initially pinned the #455 preview
build while that PR was open.)

### Bun-side changes

- `root.h`: `<JavaScriptCore/HandleSet.h>` no longer exists (`Strong<>`
slots moved to `StrongSet`, upstream `ff64aee116d4`).
- `ScriptFetchParameters::Type` gained `Text` (import-text, upstream
`49246d2612`) ahead of the fork's `HostDefined`, so the ordinal Bun's
transpiler emits for host-defined import types
(`to_script_fetch_parameters_type`) is 5 instead of 4; the
static_asserts in `BunAnalyzeTranspiledModule.cpp` pin both values. With
the fork, `with { type: "text" }` still parses as a host-defined type,
so Bun's own text loader keeps handling it on every file type.
- `NodeVMSyntheticModule.cpp`: `SyntheticModuleRecord::create()` takes
the record's `SourceProviderSourceType` (it only feeds the module kind
attached to errors).
- `NodeVM.cpp`: the import attributes switch covers `Type::Text`.
- `wtf-bindings.cpp`: `StackBounds::currentThreadStackBounds()` is
private to `Thread` upstream (`f6bc402b83`);
`Bun__StackCheck__initialize` uses the once-per-thread accessor the fork
adds.

### Visible to JavaScript after this upgrade

- `Iterator.prototype.chunks` / `windows` / `join` and `Iterator.zip` /
`zipKeyed` are enabled by default (upstream flipped the flags;
`chunks`/`windows` also follow the latest spec text and throw on
non-integral sizes).
- intl-era-monthcode (Stage 4) is unconditional:
`Intl.supportedValuesOf("calendar")` returns the proposal's 16 calendars
(`islamic` and `islamic-rgsa` are gone, Temporal rejects them as
calendar ids), era / eraYear / monthCode handling reworked across the
non-ISO calendars.
- `Array.prototype.sort()` without a comparator is stable for small
buckets of equal keys (was not) and faster on string arrays.
- Temporal: a batch of spec fixes (constructor `newTarget` order,
Duration rounding in exact arithmetic, `.with()` field resolution, time
zone string parsing follows the spec's parse records, DST gap range
checks).
- `/^[\q{ab|c|1}&&\P{L}]$/v` no longer matches `"ab"` (ported upstream
fix, the one Yarr change of this range that #299 did not already
contain).
- Deliberately unchanged in the fork, each flagged in oven-sh/WebKit#455
so it can be revisited separately: `Buffer` `kMaxLength` /
`MAX_ARRAY_BUFFER_SIZE` stays 4 GB (upstream went to 16 GB),
`NUMBER_OF_PROCESSORS` does not influence
`navigator.hardwareConcurrency` / `os.availableParallelism()`
(upstream's WTF now reads it), import-text is not exposed (`type:
"text"` stays Bun's).

### WebKit-side notes (details in oven-sh/WebKit#455)

- Yarr is kept at the fork's version (#299); upstream's Yarr commits of
this range were checked one by one and upstream's new regexp JSTests run
against the fork's engine. Not yet ported: one JIT optimization and the
default-off `\A` `\z` buffer boundaries.
- Windows ARM64 now uses `__builtin_frame_address(1)` in JIT operations
like every other platform: upstream deleted the `topCallFrame` fallback
the fork had selected there since the January bring-up ("crashes in DFG
operations" back then). Checked on a Windows 11 ARM64 machine with a
debug build of this branch against the preview WebKit: a workload that
tiers up to DFG and FTL (6 compiles each, `reportCompileTimes`) and
calls operations for 300k iterations runs clean; debug builds assert
`topCallFrame == callFrame` in every operation, so a wrong frame address
would have fired immediately. The windows-aarch64 lanes of this PR cover
the rest.
- Other resolutions: `GCCompletionCallback`, `StrongSet` and
`reconcileWeakReferencesAtGCEnd` renames applied to fork code,
upstream's own CMake 4.4 fix replaces the fork's,
`SyntheticModuleRecord` lazy exports kept on top of upstream's source
type plumbing.
- `JSType.h` did not change, so `src/jsc/JSType.rs` stays valid. ICU is
unchanged (the fork's 78.3 bump is already in the current pin). Bytecode
caches are keyed on the WebKit version and invalidate on their own.

### Binary size

The stripped binaries grow 448 KB to 800 KB per target against main
(0.6% to 0.9%; the size check's 0.5 MB threshold trips on darwin,
android and freebsd), acknowledged with `[skip size check]` in 95d581dd.
Comparing the non-LTO linux-x64 WebKit prebuilts of the old and new pin:
`libJavaScriptCore.a` object code grows a net 90 KB spread over 117
object files (StrongSet replacing HandleSet, the typed array sort
rewrite, intl-era-monthcode, memory64/table64, the new Air analyses,
builtins metadata), `libWTF.a` 2 KB, `libbmalloc.a` unchanged; the
remainder of the per-binary delta is LTO inlining of the changed engine
headers into Bun's own objects. The zipped artifacts are slightly
smaller than main's, so the added bytes are highly compressible.

### How did you verify your code works?

- `bun run jsc:build:debug` and `bun run build:local -p '42'` on Linux
x64 against the merged tree.
- oven-sh/WebKit#455 built on every platform variant as a preview before
merging; the merged release pinned here built the same way.
- The JS-visible changes listed above and the fork-side decisions
(`type: "text"` staying host-defined on both module paths,
`NUMBER_OF_PROCESSORS` being ignored, the 4 GB limit) were checked
against this build and against the previous pin with a throwaway test;
`test/js/bun/jsc/webkit-upgrade-3722912f.test.ts` still passes. No test
file is added in this PR.
- With the locally linked build: `test/js/bun/jsc`, `jsc-stress`,
`node/vm`, `node/module`, `bun/resolve`, `node/buffer`,
`node/worker_threads`, `bun/wasm`, `node/util` (78 files, 2034 tests);
the only failures are 5 s timeout / RSS threshold tests that a debug
build of current main fails identically on the same machine, and the
scenarios behind them behave the same with both builds when run
directly.
- JSTests: upstream's 21 new regexp tests plus the 318 `regexp*` /
`string-*` / `yarr*` stress tests against the fork's Yarr, JIT and
interpreter modes (see oven-sh/WebKit#455 for the four explained
failures).

<details>
<summary>JavaScriptCore / WTF / bmalloc changes in
WebKit/WebKit@3722912ff800...47f7250137c6 (235 commits; the ones that
matter to an embedder)</summary>

### Highlights
- `2c2c1af35743` ArrayBuffer / Wasm memory sizing overhaul: upstream
raises 64-bit `MAX_ARRAY_BUFFER_SIZE` from 4 GiB to 16 GiB (the Bun fork
pins it back to 4 GiB under `BUN_JSC_ADDITIONS` because
`buffer.constants.MAX_LENGTH` derives from it), fixes
`ArrayBuffer.prototype.slice` truncating byte lengths to 32 bits, fixes
growing shared memory64 past 4 GiB, and makes typed-array string keys
past `MAX_ARRAY_INDEX` reach the element.
- `ff64aee116d4` `Strong<>` root slots move from
`HandleSet`/`HandleBlock` to new `StrongSet`/`StrongBlock` (faster and
smaller for embedders that create/destroy many `JSC::Strong` handles, as
Bun does); `HandleSet.h` is gone and `Heap::handleSet()` is now
`Heap::strongSet()` (Bun's `root.h` already switched).
- `f6bc402b8344` `StackBounds::currentThreadStackBounds()` is now
private (on Linux it can re-parse `/proc/self/maps` per call); Bun's
`Bun__StackCheck__initialize` called it directly and now goes through a
`USE(BUN_JSC_ADDITIONS)`-only `currentThreadStackBoundsForEmbedder()`
shim.
- `5fc5182bcf83` `WTF::numberOfProcessorCores()` upstream now honors
`NUMBER_OF_PROCESSORS`; kept out of Bun builds in the fork (it feeds
`navigator.hardwareConcurrency` / `os.availableParallelism()` and would
override the fork's cgroup aware count), so nothing changes for Bun.
- `49246d261276` Implements the import-text proposal behind new
`useImportText` (default true); `ScriptFetchParameters::Type` and
`SourceProviderSourceType` gain `Text` (Bun's `HostDefined` tag moves
from 4 to 5), `SyntheticModuleRecord::create` / `AbstractModuleRecord`
take a `SourceProviderSourceType`; the fork keeps `"text"` as
`HostDefined` so Bun's own text loader still wins.
- `547e1555ce4d` `Iterator.prototype.chunks/windows` (and via yaml-only
flips `Iterator.prototype.join`, `Iterator.zip`/`zipKeyed`) become
enabled by default in this range and Bun does not override the flags, so
they become visible to Bun users with this upgrade.
- `99473681ff5e` intl-era-monthcode (Stage 4) is now unconditional:
`Intl.supportedValuesOf("calendar")` returns the fixed 16-calendar list,
`islamic`/`islamic-rgsa` are dropped as Temporal calendar ids, and
era/eraYear/monthCode handling is reworked across all non-ISO calendars.
- `a011564b98ab` `Array.prototype.sort()` with no comparator was not
stable for buckets of <32 equal-key entries (spec violation); now stable
(and `6380373fc6a1` makes it 1.2x-3.9x faster on string arrays).
- `f641af0b8e47` DFG-inlined single-element `Array.prototype.unshift`
was missing a write barrier, so the shifted element could be hidden from
the concurrent collector; fixes a potential GC use-after-free/crash in
optimized code.
- `7ff1104e4d0b` DFG no longer re-speculates GlobalProperty scope
accesses (e.g. `console`, `process`) after a BadCache exit, fixing
repeated OSR exits when such globals are redefined.
- `f2b02eb84f25` `MicrotaskQueue::performMicrotaskCheckpoint` skips
`drain()` on an empty queue; an empty `VM::drainMicrotasks()` halves in
cost (Bun calls this after every task).

### Runtime / builtins
- `2c2c1af35743` Overhauled ArrayBuffer / Wasm memory sizing: upstream
raises the 64-bit `MAX_ARRAY_BUFFER_SIZE` from 4 GiB to 16 GiB, caps
memory64 at 262144 pages (over-declared modules now fail
`WebAssembly.Module`), caps a single resizable/growable buffer's
`maxByteLength` reservation at 1/4 of the primitive address-space
budget, stops GCing while holding the buffer-memory lock (fixes growing
a shared memory64 buffer past 4 GiB), fixes
`ArrayBuffer.prototype.slice` truncating byte lengths to 32 bits, and
makes typed-array string keys past `MAX_ARRAY_INDEX` (e.g.
`"4294967295"`) reach the element for get/set/define/delete. (The Bun
fork pins `MAX_ARRAY_BUFFER_SIZE` back to 4 GiB under
`BUN_JSC_ADDITIONS` in `Source/JavaScriptCore/runtime/PageCount.h`
because `buffer.constants.MAX_LENGTH` in `src/jsc/bindings/JSBuffer.h`
is derived from it.)
- `40d37f36527f` Follow-up: module parsing accepts arbitrarily large
memory64 limits (rejected at instantiate/grow instead);
`PageCount::maxPageCount` becomes a `uint64_t` and `PageCount::bytes()`
saturates instead of wrapping.
- `a011564b98ab` `Array.prototype.sort()` with no comparator was not
stable for buckets of <32 equal-key entries (spec violation); now uses a
stable sort.
- `6380373fc6a1` `Array.prototype.sort()` with no comparator rewritten
as an in-place counting sort over UTF-16 (still stable); 1.2x-3.9x
faster on string arrays such as `Object.keys(o).sort()`.
- `547e1555ce4d` `Iterator.prototype.chunks/windows` aligned to the
latest spec: non-number or non-integral size now throws TypeError (was
ToNumber coercion), invalid arguments close the underlying iterator,
`undersized` only defaults when `undefined`. These methods become
enabled by default in this range via `793e36fb835e` (yaml-only, outside
these paths); Bun does not override the flag, so they appear on
`Iterator.prototype` after this upgrade.
- `7417386b7da1` `Iterator.prototype.join` aligned to spec: a separator
is still emitted for `undefined`/`null` elements; builds the result with
a RopeBuilder; OOM closes the iterator. Enabled by default in this range
via `e9a62e6b4da5` (yaml-only), so `Iterator.prototype.join` now exists
in Bun. (`Iterator.zip`/`zipKeyed` are likewise enabled by
`934bb002485a`, yaml-only.)
- `c0625bcafb6c` `ErrorInstance` is now subclassable by embedders
(exported constructor/method-table entries plus a `finishCreation(VM&,
StackTraceCapturePolicy)` that captures no stack and adds no own props),
used by WebCore to make `Error.isError(new DOMException())` true;
`CloneSerializerBase` now consults the embedder's `dumpDerivedTerminal`
before its generic ErrorInstance path. Bun's `JSDOMException`
(`src/jsc/bindings/webcore/JSDOMException.h`) is still a plain wrapper,
so no behavior change in Bun unless adopted.
- `fedbb7bdc250`
`Int8Array/Uint8Array/Uint8ClampedArray.prototype.sort()` uses a SIMD
presorted check plus counting sort (2.5x-12x faster, ~38x on presorted
input).
- `4f3ecec97431` `JSON.stringify` fast path now accepts final objects
with non-`Object.prototype` prototypes (class instances) when the chain
has no `toJSON` (~3.5x on such payloads); also fixes
`noSideEffectMayHaveNonIndexProperty()` checking static properties on
the wrong chain entry.
- `8b5e6ebb64e6` FastStringifier caches buffer pointer/length across
property-name emission (reland of `ea3fbb33caa5`, which was reverted in
`ba1d526398de` for a perf regression; value half dropped).
- `da12fb32aeb9` FastStringifier adds a 4-7 byte two-window copy tier
and removes the 8-byte loop; faster `JSON.stringify` of short Latin-1
keys.
- `01ea2a8eb955` `String.prototype.split` no longer atomizes results
when the subject is not an atom string (~3.8x faster on runtime-built
strings; results are plain substrings now).
- `81a11702ef82` The `str.replace(/^\s+/, "")` / `/\s+$/` trim fast path
was unreachable once the caller tiered up to DFG/FTL; now applies in all
tiers (4.5x-5x).
- `a4df93500a72` `Array.prototype.join` / `toString` on Int32 arrays
writes numbers directly for any separator (~2x);
`JSOnlyStringsAndInt32sJoiner::tryJoin` is now templated on indexing
shape.
- `2af38faaec70` DFG `Function.prototype.bind` strength reduction now
also fires for method structures (`this.onClick.bind(this)` on class
methods, ~4.6x).
- `deb0d2fa4be6` BigInt add/sub/mul get fixed-size fast paths, squaring
optimization and carry handling that avoids flag spills on arm64.
- `0270fd0a8d77` BigInt Crandall modular reduction made branch-free for
the first corrective subtract (faster big modular arithmetic).
- `5d6747ef60d4` (parser) see below; memory-visible: closures no longer
retain all call arguments when an inner arrow uses object shorthand.
- `a73e86f9a37f` `Set.prototype`, `WeakRef` and `FinalizationRegistry`
are no longer materialized in `JSGlobalObject::init()`;
WeakRef/FinalizationRegistry become lazy static-table globals (~6.6 KB
saved per global object; `propertyNames->WeakRef` /
`->FinalizationRegistry` removed).
- `8d33a8ff591d` `UnlinkedFunctionExecutable` stores
`parentScopeTDZVariables` inline (RareData allocations drop ~100x in
let/const-heavy code at the same 96-byte cell size); bytecode cache
encoding in `CachedTypes.cpp` changed (Bun keys its cache on the WebKit
version, so old caches are simply invalidated).
- `edd953757f9f` `StructureRareData` shrunk back from 104 to 96 bytes
(cell 112 -> 96) with a static_assert so it does not regress.
- `f2b02eb84f25` `MicrotaskQueue::performMicrotaskCheckpoint` skips
`drain()` on an empty queue; an empty `VM::drainMicrotasks()` halves in
cost (Bun calls this after every task).
- `13dc8fa6e3d5` VM startup: AtomStringTable and BuiltinNames'
private-name set reserve capacity up front (fewer rehashes during VM
construction).
- `81d660ceeb2e` Builtin executable metadata (line counts, parameter
counts, etc.) is precomputed by the builtins generator instead of at VM
launch; `BuiltinCodeIndex::NumberOfBuiltinCodes` replaced by
`numberOfBuiltinCodes`. The free `JSC::createBuiltinExecutable()` used
by Bun's generated builtins is unchanged.
- `c1b19d012809` JIT thunks split into eagerly- and lazily-created sets
(less work at VM startup; `JITThunks::ctiStub` now takes `VM&`).
- `bff3814d76f7` Linux: checkpoint OSR side-state handling used uncached
stack bounds, which glibc implements by re-reading `/proc/self/maps` on
every call; now uses the thread's cached bounds (also on the release
path).
- `c00fd8a9713c` Baseline JIT gets an inline atom-identity fast path for
`switch` on strings; new option `maximumInlineStringSwitchCaseCount`
(default 64).
- `0c51f43daa3b` Wasm OMG recognizes naive byte-copy/fill loops and
prepends `memory.copy`/`memory.fill` fast paths; new option
`useWasmByteLoopReplacement` (default true).
- `c7ed9fcf7957` 32-bit only: typed-array put with an out-of-range
canonical numeric index keeps the index as `uint64_t` until
bounds-checked.
- `a53d011599e7` Tree-wide rename, no behavior change:
`finalizeUnconditionally` -> `reconcileWeakReferencesAtGCEnd` on
ErrorInstance, Structure, StructureRareData, SymbolTable, InferredValue,
JSWeakObjectRef, JSFinalizationRegistry, FunctionExecutable, etc.;
`Heap::finalizeUnconditionalFinalizers` ->
`reconcileWeakReferencesAtGCEnd`; `finalizerSet(For)` ->
`weakReconciliationSet(For)`; `ScriptExecutable::finalizeCodeBlockEdge`
-> `jettisonCodeBlockEdgeIfDead` (Bun only references the old names in
comments).

### Parser / bytecompiler
- `5d6747ef60d4` Object-literal shorthand inside an arrow function no
longer marks the enclosing function as using `eval`, so it stops
materializing `arguments` into its scope; closures returned from such
functions no longer keep all call arguments alive (memory + faster
function entry).
- `69b336c0ac05` `SourceProviderCacheItem` (one per function >16 chars
parsed, retained until full GC) is now a proper trailing array of
`PackedRefPtr`; ~12% less malloc memory for the source-provider cache on
large bundles.
- `9f770b1bd595` `Parser::useVariable` remembers the last variable added
and skips the set insertion on repeats (parse speed).
- `8aa3307b46af` Single-line-comment scanning and the arrow-function /
destructuring paths are moved out of the lexer and
`parseAssignmentExpression` hot loops (lower register pressure; parse
speed, no logic change).
- `71c68f4b3b35` `Lexer::lexExpectIdentifier()` removed; the vectorized
`parseIdentifier()` is now faster, so this shrinks hot code (header API
removal, internal to the parser).

### Intl / Temporal
- `99473681ff5e` intl-era-monthcode (Stage 4) is implemented
unconditionally and the previously default-off `useIntlEraMonthcode`
option is removed: `Intl.supportedValuesOf("calendar")` now returns the
proposal's fixed 16-calendar list, `islamic`/`islamic-rgsa` are dropped
as Temporal calendar ids (`islamic` maps to `islamic-tbla` in
DateTimeFormat, unknown calendars fall back to the locale default),
era/eraYear/monthCode handling reworked across all non-ISO calendars
with chinese/dangi falling back to ISO fields beyond +/-10000 instead of
throwing, and DateTimeFormat's era-text override only applies when an
era field was requested.
- `9ef04dabf52d` `Intl.Locale.prototype.getCollations()` etc. now return
sorted arrays per spec.
- `171864159318` DateTimeFormat with islamic-civil/tbla/umalqura
calendars rendered pre-Hijra years as e.g. `-332 Before Hijra`; now `333
Before Hijra` (computed from the calendar, works with `year:
"2-digit"`).
- `b2ec9a4586ee` `formatToParts()` now emits the separating space that
`format()` inserts before a synthesized coptic/islamic era, so joined
parts equal `format()` again.
- `33a5272cf9ac` `String.prototype.localeCompare(x, "locale")` (string
locale, no options) caches the collator per global object; the common
sort-comparator pattern is ~50x faster.
- `7d0200e4e6ed` That cache is invalidated when the user preferred
languages change (it returned stale orderings for unavailable locales
like `"xx"`).
- `b48f01b7f1b1` All eight Temporal constructors now validate fields
before reading `newTarget.prototype` (spec order; `Reflect.construct`
with a throwing prototype getter gets the RangeError);
`ZonedDateTime.prototype.with` now range-checks epoch nanoseconds;
`tryCreateIfValid`-style helpers renamed to
`createTemporalDate`/`createTemporalZonedDateTime`/... taking a
`TemporalNewTarget`.
- `11615f86705a` Duration rounding decisions now use exact Int128
instead of doubles, fixing wrong results such as `until(...,
{smallestUnit:"month", roundingMode:"ceil"})` returning `P1M` instead of
`P29DT1H`, and the half-even branch of ApplyUnsignedRoundingMode.
- `399973c04a04` `.with()` on all Temporal types now goes through spec
`ISODateToFields`/`CalendarMergeFields` (year-only changes on lunisolar
calendars pick the right month); fixes `PlainYearMonth.add/subtract`
shifting months by -2 for buddhist/roc/japanese in ISO years ~1-1582;
`ZonedDateTime.prototype.with` restored to spec step order.
- `22a13eb9ee2f` Time-zone string parsing follows the spec's parse
records: bracket annotations are now accepted on all six string
productions (`"2024-12[Europe/Berlin]"`, `"12:00[Europe/Berlin]"`, ...),
`"T12+01"` is rejected as an unavailable named zone instead of resolving
to `+01:00`, and IANA-name syntax drops the 14-char limit (accepting
e.g. `[..]`).
- `284afdacfb77` Non-ISO field resolution at range edges:
`PlainYearMonth.toPlainDate({day: 256})` no longer wraps the day to 0
(produced a live `...-01-00` date); chinese/dangi arithmetic at extreme
years no longer throws; `dateUntil` used the wrong year kind on ICU 76.
- `6fd438a4aef2` DST-gap disambiguation re-enters the epoch range check,
so `ZonedDateTime.from("+275760-10-05T02:30[Australia/Sydney]")` throws
instead of creating an out-of-range value; also fixes which candidate is
picked in gaps.
- `4f049dc9046e` `monthCode` given a non-string now throws TypeError
again in `PlainDateTime.from`/`PlainDate.with` (regression from
consolidation); ISO `.with()` no longer regulates day/month twice;
getter order test added.
- `89c1884e15a9` `PlainDate` construction clamps out-of-range years
itself (was a debug assertion crash); fixes `PlainYearMonth.toPlainDate`
clamp direction under `overflow: "constrain"` and a UB cast in the
PlainMonthDay constructor.
- `642d9211add2` ICU failures inside the calendar/time-zone bridges now
propagate as errors instead of being folded into plausible values (e.g.
hebrew `M05L` silently becoming `M06`, a sticky UErrorCode making
`getTimeZoneTransition` return bogus transitions).
- `b2233ac17643` Fixes uninitialized members in duration nudging, an
overflowable day bound, and a debug-only assertion crash when a
zero-length nudge window lands on a day a zone skips; removes dead
duration helpers.
- `8776c95a1b0a` Temporal time-zone cache widened from 8 to 16 entries
(parity with V8 on the duration-total benchmark).
- (`e07ecf4c4a07`, `ee16ce938a8f`, `1375d28c26b3`, `e165fd1fce9a`,
`f6c491404f2d`: internal Temporal refactors declared no-behavior-change;
omitted.)

### Modules
- `49246d261276` Implements the import-text proposal: `import x from
"./a.txt" with { type: "text" }` / dynamic import are handled by JSC as
synthetic default-export modules, gated by new option `useImportText`
(default true, generated from the preferences yaml). Adds
`ScriptFetchParameters::Type::Text` and
`SourceProviderSourceType::Text`, and
`AbstractModuleRecord`/`CyclicModuleRecord`/`SyntheticModuleRecord`
constructors and `SyntheticModuleRecord::create` now take a
`SourceProviderSourceType`. The Bun fork keeps `"text"` as a
`HostDefined` type in `ScriptFetchParameters::parseType` so Bun's own
text loader still wins; Bun's `HostDefined` tag moved from 4 to 5
(static_asserts in `src/jsc/bindings/BunAnalyzeTranspiledModule.cpp` and
`to_script_fetch_parameters_type` in `src/js_printer/lib.rs` are already
updated).
- `cc673d7b23bf` import-defer updated to proposal PRs #85/#87:
`ReadyForSyncExecution` and `GatherAsynchronousTransitiveDependencies`
now use `IsModuleSCCEvaluated` (new
`CyclicModuleRecord::isSCCEvaluated()`), so touching a deferred
namespace whose dependency sits in a still-awaiting TLA cycle correctly
throws "Unable to synchronously evaluate deferred module" instead of
evaluating early (and a debug assertion no longer fires). Bun already
forces `useImportDefer` on; upstream also flipped its default on in
`85e82ceefe1b` (yaml-only).

### API
- `62692012c98a` `HeapFinalizerCallback` renamed to
`GCCompletionCallback` (header `heap/HeapFinalizerCallback.h` ->
`heap/GCCompletionCallback.h`; `Heap::add/removeHeapFinalizerCallback`
-> `add/removeGCCompletionCallback`); the C entry points
`JSContextGroupAddHeapFinalizer` / `JSContextGroupRemoveHeapFinalizer`
keep their names and behavior.

### Embedder-relevant API changes
- Removed: `heap/HeapFinalizerCallback.h` / class
`HeapFinalizerCallback`, `Heap::addHeapFinalizerCallback`,
`Heap::removeHeapFinalizerCallback` -> `GCCompletionCallback.h`,
`Heap::addGCCompletionCallback`, `Heap::removeGCCompletionCallback`
(`62692012c98a`).
- Renamed: `T::finalizeUnconditionally(VM&, CollectionScope)` ->
`T::reconcileWeakReferencesAtGCEnd` on ErrorInstance, Structure,
StructureRareData, StructureTransitionTable, SymbolTable, InferredValue,
JSWeakObjectRef, JSFinalizationRegistry, FunctionExecutable,
GlobalExecutable, UnlinkedFunctionExecutable, CodeBlock;
`Heap::finalizeUnconditionalFinalizers` ->
`reconcileWeakReferencesAtGCEnd`;
`Heap::finalizeMarkedUnconditionalFinalizers` ->
`reconcileWeakReferencesInMarkedCells`; IsoCellSet
`finalizerSet`/`finalizerSetFor` ->
`weakReconciliationSet`/`weakReconciliationSetFor`;
`ScriptExecutable::finalizeCodeBlockEdge` ->
`jettisonCodeBlockEdgeIfDead`; `JITPlan::finalizeInGC` ->
`reconcileWeakReferencesAtGCEnd` (`a53d011599e7`). Any embedder class
registered for unconditional finalization must rename its method.
- Enums: `ScriptFetchParameters::Type` gains `Text` after `JSON` (shifts
any embedder-appended values); `SourceProviderSourceType` gains `Text`
between `JSON` and `ImportMap` (shifts `ImportMap` and any
embedder-appended values; exhaustive switches need a case);
`SourceProvider::isModuleType()` now also true for `Text`
(`49246d261276`).
- Signatures: `AbstractModuleRecord(VM&, Structure*, Identifier,
SourceProviderSourceType)`, `CyclicModuleRecord(...,
SourceProviderSourceType)`,
`SyntheticModuleRecord::create(JSGlobalObject*, VM&, Structure*, const
Identifier&, SourceProviderSourceType)`; new
`SyntheticModuleRecord::createTextModule` (`49246d261276`). Bun's
`NodeVMSyntheticModule.cpp` already passes the new argument.
- `ArrayBuffer::grow(const AbstractLocker&, VM&, size_t, bool)` removed;
replaced by `tryGrow(const AbstractLocker&, size_t, bool,
BufferMemoryResult::Kind&)` (the `grow(VM&, ...)` overload remains); new
`maxGrowableBufferReservationBytes` in `BufferMemoryHandle.h`; new
`Gigacage::primitiveAddressSpaceBudget`; `isCanonicalNumericIndexString`
gains an optional `std::optional<uint64_t>*` out-parameter (source
compatible); 64-bit `MAX_ARRAY_BUFFER_SIZE` is 16 GiB upstream (fork
keeps 4 GiB) (`2c2c1af35743`).
- `PageCount::maxPageCount` is now `uint64_t` with a much larger value;
`PageCount::bytes()` saturates (`40d37f36527f`).
- `ErrorInstance`: constructor and
`getOwnPropertySlot`/`put`/`defineOwnProperty`/`deleteProperty`/`getOwnSpecialPropertyNames`
are now `JS_EXPORT_PRIVATE`; new protected `finishCreation(VM&,
StackTraceCapturePolicy)`; `CloneSerializerBase::dumpIfTerminal` calls
`dumpDerivedTerminal` before the ErrorInstance path (`c0625bcafb6c`).
- `CommonIdentifiers`: `propertyNames->WeakRef` and
`propertyNames->FinalizationRegistry` removed;
WeakRef/FinalizationRegistry structures/prototypes become lazy accessors
(`a73e86f9a37f`).
- `BuiltinCodeIndex::NumberOfBuiltinCodes` removed ->
`JSC::numberOfBuiltinCodes`; new `BuiltinSourceMetadata` /
`s_JSCBuiltinSourceMetadata`; member
`BuiltinExecutables::createBuiltinExecutable` gains a metadata parameter
(free `JSC::createBuiltinExecutable()` and public static
`BuiltinExecutables::createExecutable()` unchanged) (`81d660ceeb2e`).
- `JSOnlyStringsAndInt32sJoiner::tryJoin` is now
`template<IndexingType>` (`a4df93500a72`);
`JITThunks::ctiStub(CommonJITThunkID)` now takes `VM&` first
(`c1b19d012809`); `Lexer::lexExpectIdentifier()` removed
(`71c68f4b3b35`); Temporal `try*` creation helpers replaced by
`createTemporal*(…, TemporalNewTarget)` free functions and
`TemporalPlainDate::mergeDateFields` removed (`b48f01b7f1b1`,
`4f049dc9046e`); `IntlObject.h` calendar-ID table drops `islamic` and
`islamic-rgsa`, `Options::useIntlEraMonthcode` removed (`99473681ff5e`).
- New options: `useImportText` (true),
`maximumInlineStringSwitchCaseCount` (64), `useWasmByteLoopReplacement`
(true). Defaults flipped to true in this range but via yaml-only commits
outside these paths: `useIteratorChunking` (`793e36fb835e`),
`useIteratorJoin` (`e9a62e6b4da5`), `useJointIteration`
(`934bb002485a`), `useImportDefer` (`85e82ceefe1b`); Bun overrides none
of the first three, so `Iterator.prototype.chunks/windows/join` and
`Iterator.zip/zipKeyed` become visible to Bun users with this upgrade.

### GC / heap
- `ff64aee116d45c` `Strong<>` root slots now live in new
`StrongBlock`/`StrongSet` (libpas-style bump+freelist pages, empty
blocks returned to the OS, no write barrier on set) replacing
`HandleSet`/`HandleBlock`; faster and smaller for embedders that
create/destroy many `JSC::Strong` handles (Bun does);
`Heap::handleSet()` is now `Heap::strongSet()` and `HandleSet.h` is gone
(Bun's `root.h` already switched to `StrongSet.h` in this PR). Follow-up
`55659d048725` drops a dead `USE(JSVALUE64_32)` branch from
`StrongBlock.h`.
- `f641af0b8e47` DFG-inlined single-element `Array.prototype.unshift` on
contiguous arrays was missing a write barrier, so the shifted element
could be hidden from the concurrent collector; fixes a potential GC
use-after-free/crash in optimized code.
- `6bdb4f69e23b` VM/Heap teardown (`lastChanceToFinalize`) uses a new
`StopAllocatingMode::ForGood` that skips recomputing allocation bitmaps;
faster VM destruction (e.g. Worker exit);
`MarkedSpace::stopAllocatingForGood()` removed.
- `a53d011599e7` Rename-only: `finalizeUnconditionally()` on all cell
types/VM becomes `reconcileWeakReferencesAtGCEnd()`,
`Heap::finalizeUnconditionalFinalizers` ->
`reconcileWeakReferencesAtGCEnd`, IsoCellSet `finalizerSet` ->
`weakReconciliationSet`, `ScriptExecutable::finalizeCodeBlockEdge` ->
`jettisonCodeBlockEdgeIfDead`; no behavior change (Bun only mentions the
old name in comments in `src/jsc/bindings/ErrorStackTrace.cpp`,
`JSCTaskScheduler.cpp`, `FormatStackTraceForJS.cpp`).
- `5602ec36107b` Rename-only follow-up: `visitWeak()` on
CallLinkInfo/PropertyInlineCache/InlineCacheHandler/JITStubRoutine/PolymorphicCallStubRoutine/MicrotaskCall
-> `reconcileWeakReferencesAtGCEnd()`;
`AccessCase`/`PolymorphicAccess::visitWeak` -> `isStillLive`.
- `3d37c6da40ba` Rename-only:
`GetByStatus`/`PutByStatus`/`InByStatus`/`DeleteByStatus`/`CallLinkStatus`/private-brand
statuses and their variants `finalize()` -> `isStillLive()`.
- `62692012c98a` Rename-only: `HeapFinalizerCallback` ->
`GCCompletionCallback` (header renamed too),
`Heap::add/removeHeapFinalizerCallback` ->
`add/removeGCCompletionCallback`; C API `JSContextGroupAddHeapFinalizer`
unchanged.
- `f4da7823ee1d` Rename-only: `Heap::finalize` ->
`runCollectionEpilogue` (and `needFinalize` bits); the only observable
change is the `--logGC=1` phase label "finalize" is now "epilogue".

### LLInt / Baseline / DFG / FTL / B3
- `a02f99629f76` FTL OSR-exit compiler hit `RELEASE_ASSERT_NOT_REACHED`
(crash) when exiting with a `PhantomNewArrayWithButterfly` whose
butterfly was still live (`DataFormatStorage`); now passed through like
`DataFormatJS`.
- `91d96b29d6b2` DFG
`AbstractInterpreter::forAllValues`/`dump`/`SafeToExecute` now handle
tuple nodes; the DFG-inlined `StringIterator.prototype.next` followed by
a structure transition in the same block asserted in debug builds and
silently skipped the tuple's values in release.
- `7ff1104e4d0b` DFG no longer re-speculates
`op_get_from_scope`/`op_put_to_scope` GlobalProperty accesses (e.g.
`console`, `process`, any global-object property) after a BadCache exit;
emits a generic IC instead, fixing repeated OSR exits when such globals
are redefined.
- `7600ab4bec97` DFG stops inlining varargs calls (`f(...args)`,
`f.apply`) once a `VarargsOverflow` exit has been seen at that site,
fixing perpetual OSR exit/recompile loops.
- `fbb79b137a90` Baseline JIT read the 1-byte
`maxArgumentCountIncludingThisForVarargs` profile with a 32-bit compare
(picking up adjacent bytes), so varargs argument-count feedback fed to
the DFG was wrong; now `load8` + compare.
- `465d5ab28c60` `String.prototype.substring` is now inlined in DFG/FTL
(shares `slice` lowering: empty/one-char/whole-string/rope fast paths);
1.6-2.1x faster in microbenchmarks.
- `fb299342a580` RegExp `test`/`exec` first-character filter now also
applies when the subject is an Untyped edge (runtime string check),
widening the fast path for real-world code.
- `c00fd8a9713c` Baseline JIT gets an inline pointer-identity dispatch
for `switch` on strings when the scrutinee is an atom (previously always
called the hashing slow path); new option
`maximumInlineStringSwitchCaseCount` (default 64).
- `a4df93500a72` `Array.prototype.join`/`toString` on Int32 arrays now
uses `JSOnlyStringsAndInt32sJoiner` for any separator (was only for
`""`), ~2x faster (one-line DFGOperations change; mostly runtime/).
- `0d25934d08a8` VM-independent JIT thunks (polymorphic call thunks,
most IC handler thunks) are generated once per process and shared across
VMs; less per-VM startup work and JIT memory when creating many VMs
(Workers); `JITThunks::ctiStub` now takes `VM&`, handler generators no
longer take `VM&`.
- `c1b19d012809` Remaining VM-dependent thunks split into eager
(exception/native-call/virtual-call) and lazily generated (IC
transition/custom-accessor handlers), so short-lived VMs do not generate
thunks they never use.
- `f40dcdd0730d` LLInt function prologue zeroes the new frame 16 bytes
per iteration with a hoisted zero register (4 instructions/16 bytes on
ARM64, 5 on x64, was 12); `76f57a9311b1` extends it to ARM64E (not built
by Bun).
- `bbab514b1010` DFG/FTL `LazyJSValue::emit` leaked a `StringImpl` ref
per emitted string constant when compilation was abandoned (JIT memory
exhausted or code block invalidated before finalize); now held in a
`RefPtr`.
- `74091f918bfc` New Air `Padding` pseudo-op that emits no bytes
replaces most `Nop` padding, and `reportUsedRegisters` is skipped for
Wasm OMG; faster OMG compiles with no extra `nop`s in generated code.
- `5821b05faa72` Air `TmpWidth` and `UseCounts` are now built in a
single graph walk via new `InstAnalyzer`; faster FTL/OMG register
allocation.
- `4a10860dc35c` Faster Air liveness (`WTF::Liveness` no longer re-walks
blocks or zeroes gen/kill sets; new
`forEachLiveAtHeadNotLiveAtTail`/`...TailNotLiveAtHead`), ~17% off
greedy allocator `buildLiveRanges`.
- `ee4f0240590d` Air DCE worklist seeded in reverse program order, ~20%
faster phase; `ae85b80e5fbe` same phase avoids Vector element removal.
- `4af3bbad9cda` Air, BBQ and Baseline JIT code-generation loops skip
disassembler-only label creation and hoist loop invariants; lower
compile latency in all JIT tiers.
- `6589b2e5c18c` WasmGC `struct.new`/`array.new` codegen tightened (new
`JITAllocator::variableNonNullWithConstantCellSize`, narrower B3
effects, constant-size array allocation folding); faster WasmGC
allocation and more B3 load motion around it.
- `2ec06de15a0d` B3 CSE stops walking every predecessor block for WasmGC
`struct.get`/`struct.set` when no other access to that field exists;
faster OMG compile of WasmGC modules.
- `53517eb3a2b8` / `31f35870966c` Wasm `memory.copy` and `memory.fill`
runtime operations inline small-size copies/fills before falling back to
`memcpy`/`memset`; faster small bulk-memory ops.
- `d02c68d04f96` IPInt mis-decoded `memory.size`/`memory.grow` when the
memory-index immediate took more than one LEB byte (multi-memory, on by
default), desynchronizing the following instructions; also removes the
`parseMemoryIndexForBulkOp` spec-test workaround.
- `9226ba78d93d` `DFG::enableInt52()` removed; Int52 speculation is
unconditional now that the only 64-bit JIT backends remain (no behavior
change on x64/arm64).
- `bf1dab73b14d` / `6010a9ea6ce6` / `84f83abd45c9` 32-bit/ARMv7 JIT
leftovers removed: `ARMv7Assembler.h` deleted, 32-bit DataFormats/GPR
pairs/OSR-entry paths dropped, `branchIfNumber`/`branchIfNotNumber` lose
their scratch-register parameter, `CCallHelpers` `extraGPRArgs` removed;
no codegen change on 64-bit.
- `2a8926009f45` `USE(BUILTIN_FRAME_ADDRESS)` removed (always on for JIT
platforms); `JSWebAssemblyInstance::temporaryCallFrame()` and its field
removed. The fork had it off on Windows ARM64; that configuration no
longer exists (see above).
- `ac2afd10b8ac` Yarr JIT sub-feature flags
(`YARR_JIT_ALL_PARENS_EXPRESSIONS`, `YARR_JIT_BACKREFERENCES`,
`YARR_JIT_REGEXP_TEST_INLINE`, `YARR_JIT_UNICODE_EXPRESSIONS`) removed
as always-on for x64/arm64, with matching DFG/FTL ifdef cleanup; no
behavior change.
- `ef6d9ba26b17` / `56baf6e01b3d` Linux RT-thread removal briefly set
JIT worklist threads to `ThreadQOS::Utility`, then was reverted for
JetStream/Speedometer regressions; net zero change to JSC.

### Bytecode / CodeBlock
- `8d33a8ff591d` `m_parentScopeTDZVariables` moves back into
`UnlinkedFunctionExecutable` (name stored as `m_ecmaName` + `m_hasName`
bit), so the 80-byte RareData is no longer malloc'ed for ~30-40% of
executables in let/const/class-heavy code; also changes the
`CachedTypes` bytecode-cache layout (Bun keys its cache version on
`BUN_WEBKIT_VERSION`, so old `--bytecode` artifacts are invalidated as
with any bump).
- `b00e0c35f823` Slow-path location and per-site register fields move
from `PropertyInlineCache` into `RepatchingPropertyInlineCache`; handler
ICs shrink 128->112 bytes, baseline unlinked ICs 40->32, DFG unlinked
ICs 64->40 (~465 KB saved on Octane typescript).

### Embedder-relevant API changes
- Removed headers: `heap/HandleSet.h`, `heap/HandleBlock.h`,
`heap/HandleBlockInlines.h` (use `heap/StrongSet.h` /
`heap/StrongBlock.h`); `assembler/ARMv7Assembler.h`. Renamed header:
`heap/HeapFinalizerCallback.h` -> `heap/GCCompletionCallback.h`.
- `Heap::handleSet()` -> `Heap::strongSet()`; `HandleSet::heapFor(slot)`
-> `StrongSet::setFor(slot)`; `HandleSet` -> `StrongSet`.
- `HeapFinalizerCallback` -> `GCCompletionCallback`;
`Heap::addHeapFinalizerCallback/removeHeapFinalizerCallback` ->
`addGCCompletionCallback/removeGCCompletionCallback` (C API
`JSContextGroupAdd/RemoveHeapFinalizer` unchanged).
- `finalizeUnconditionally()` -> `reconcileWeakReferencesAtGCEnd()` on
`VM`, `ErrorInstance`, `JSFinalizationRegistry`, `JSWeakObjectRef`,
`Structure`, `StructureRareData`, `SymbolTable`, `WeakMapImpl`,
`InferredValue`, `UnlinkedFunctionExecutable`, `FunctionExecutable`,
`GlobalExecutable`, `CodeBlock`, `JSWebAssemblyInstance`, `JITPlan` (was
`finalizeInGC`);
`Heap::ScriptExecutableSpaceAndSets::finalizerSet/finalizerSetFor` ->
`weakReconciliationSet/weakReconciliationSetFor`;
`ScriptExecutable::finalizeCodeBlockEdge` ->
`jettisonCodeBlockEdgeIfDead`;
`CodeBlock::finalizeLLIntInlineCaches/finalizeJITInlineCaches` ->
`reconcileLLIntInlineCachesAtGCEnd/reconcileJITInlineCachesAtGCEnd`;
`RecordedStatuses::finalize` -> `reconcileWeakReferences`.
- `visitWeak()` -> `reconcileWeakReferencesAtGCEnd()` on `CallLinkInfo`,
`DirectCallLinkInfo`, `PropertyInlineCache`, `InlineCacheHandler`,
`JITStubRoutine` (incl. the virtual `...Impl`),
`PolymorphicCallStubRoutine`, `MicrotaskCall`;
`AccessCase::visitWeak`/`PolymorphicAccess::visitWeak` -> `isStillLive`;
`*Status::finalize()`/`*Variant::finalize()` -> `isStillLive()`.
- `Heap::finalize` -> `Heap::runCollectionEpilogue`;
`MarkedSpace::stopAllocatingForGood()` removed;
`MarkedBlock::Handle::stopAllocating` and
`LocalAllocator::stopAllocating` gain a `StopAllocatingMode` parameter.
- `JITThunks::ctiStub(CommonJITThunkID)` -> `ctiStub(VM&,
CommonJITThunkID)`; `polymorphicThunk()`,
`polymorphicThunkForClosure()`, `polymorphicTopTierThunk[ForClosure]()`,
`returnFromBaselineGenerator()` and the VM-independent IC handler
generators in `InlineCacheCompiler.h` no longer take `VM&`;
`JSC_FOR_EACH_COMMON_THUNK` is now the union of
`JSC_FOR_EACH_VM_INDEPENDENT_COMMON_THUNK` and
`JSC_FOR_EACH_VM_DEPENDENT_{EAGER,LAZY}_COMMON_THUNK`.
- `AssemblyHelpers::branchIfNumber/branchIfNotNumber(JSValueRegs, GPRReg
scratch, ...)` overloads removed (now `(JSValueRegs,
TagRegistersMode)`); `storeValue(JSValue, Address, JSValueRegs)` ->
`storeValue(JSValue, Address)`; `DataFormat.h`
`isJSFormat/isJSInt32/isJSDouble/isJSCell/isJSBoolean` removed;
`DFG::enableInt52()` removed.
- `USE(BUILTIN_FRAME_ADDRESS)` macro removed (`DECLARE_CALL_FRAME` is
unconditionally builtin-frame-address based);
`JSWebAssemblyInstance::temporaryCallFrame()/setTemporaryCallFrame()/offsetOfTemporaryCallFrame()`
removed; `ENABLE(YARR_JIT_*)` sub-flags listed above removed;
`Yarr::JITFailureReason::{DecodeSurrogatePair,BackReference,ParenthesizedSubpattern}`
removed; `WTF_CPU_ARM_VFP_V3_D32/V2` removed.
- New JSC option: `maximumInlineStringSwitchCaseCount` (default 64).
`--logGC` phase label "finalize" -> "epilogue".
- Bun impact: only the `HandleSet.h` removal required a source change
(`src/jsc/bindings/root.h`, already in this PR's diff); the other
renamed symbols are not referenced by Bun's C++ apart from stale
comments naming `finalizeUnconditionally` in
`src/jsc/bindings/ErrorStackTrace.cpp`,
`src/jsc/bindings/JSCTaskScheduler.cpp`, and
`src/jsc/bindings/FormatStackTraceForJS.cpp`.

### WebAssembly
- `2c2c1af35743` Overhauls ArrayBuffer/Wasm::Memory sizing for memory64:
`MAX_ARRAY_BUFFER_SIZE` goes from 4 GiB to 16 GiB on 64-bit (the fork
keeps 4 GiB under `BUN_JSC_ADDITIONS`, so not in Bun; Bun's
`Buffer.kMaxLength`/`MAX_LENGTH` derive from this macro in
`src/jsc/bindings/JSBuffer.h`, and `src/jsc/array_buffer.rs` `MAX_SIZE`
is a hard-coded `u32::MAX`), memory32 capped at 4 GiB and memory64 at 16
GiB, growing a shared memory64 past 4 GiB no longer crashes, and a
memory's buffer now advertises the maximum it can actually grow to; no
GC is triggered while holding the buffer-memory lock.
- `40d37f36527f` Follow-up: memory64 modules may declare arbitrarily
large page limits (parsing accepts them, as for table64); the 16 GiB cap
is enforced when the Memory is created or grown at runtime instead of
failing `WebAssembly.Module()`.
- `d6d09268899b` BBQ and OMG now always emit explicit bounds checks for
memory64 (and non-zero multi-memory) accesses via
`ModuleInformation::memoryModeForAccess()`; signaling-mode fast paths
are reserved for 32-bit memory 0 (previously a release-assert
crash/unsafe path once memory64 code tiered up).
- `72928a517633` Instances whose module declares no memory now still
reserve and zero the memory-0 cached base/size slot that every wasm
entry reads (previously it overlapped the import call-link area).
- `bfe5073f4c99` Fixes a crash when an imported memory is grown while a
multi-memory instance is only partially linked (e.g. after a LinkError
on a later import).
- `f771c5060cd7` `ref.func`, `table.get` and `array.init_elem` slow
paths now set up a FrameTracer since they can allocate wrapper functions
and GC (fixes crashes/ShadowChicken corruption).
- `0a704bb74f1e` IPInt->BBQ loop OSR entry now rejects a stack pointer
exactly at the soft stack limit (and underflow) instead of crashing
inside BBQ.
- `0c51f43daa3b` OMG recognizes naive byte-at-a-time copy/fill loops and
prepends a guarded `memory.copy`/`memory.fill` fast path; new option
`useWasmByteLoopReplacement` (default on).
- `ca730ef8b0fe` Wasm-to-JS import stubs convert an already-BigInt i64
return value inline instead of calling out to `operationConvertToI64`
(faster imports returning i64).
- `6589b2e5c18c` Tighter WasmGC struct/array allocation codegen
(constant cell size with variable allocator, DFG-like effect model so
allocations no longer clobber loads, constant-size array.new folded).
- `3eee8becf0b5` WasmGC struct layouts fill alignment gaps with smaller
fields (V8 heuristic), shrinking structs that interleave narrow and wide
fields; adds `$vm.wasmStructFieldOffsets`/`wasmStructPayloadSize`.
- `4687d7ecfefa` BBQ skips null checks for `ref.as_non_null`, `call_ref`
and `throw_ref` on non-nullable reference types, matching OMG.
- `74091f918bfc` New Air `Padding` pseudo-op that emits no code; OMG
stops running `reportUsedRegisters`, cutting OMG compile time without
the nop-related regression.
- `4af3bbad9cda` Faster JIT code emission loops in Air, BBQ and baseline
(skip disassembler-only labels, hoist loop invariants).
- `099f93fe4993` memory64/table64 JS API fixes: i64 address values are
round-tripped as BigInt in descriptors, imports and type reflection, and
a memory64's maximum bytes is clamped to what ArrayBuffer supports; adds
`addressValueFromUint64` helper.
- `b8af849be6f0` table64: `WebAssembly.Table.prototype.length` returns a
BigInt for i64 tables and `grow()` throws RangeError on an out-of-range
delta, per JS API spec.
- `b91045c99b1b` table64 maximum sizes are no longer silently truncated
to 32 bits (`Table::maximum()` is now 64-bit).
- `e942b93cdaa0` Active element segment offsets into a table64 are read
as i64 and no longer truncated to uint32.
- `02cdfb795a84` BBQ/OMG zero-extend i32 table indices when calling into
the uint64 table operations (table64 correctness).
- `aa8167a2feb9` Oversized table declarations are accepted at parse time
and rejected when the table is created/grown, so type reflection reports
the declared sizes and the failure happens at instantiation.
- `47f20d8cfd63` `call_indirect` in unreachable code now validates the
table element type and that the type index is a function type;
previously-accepted invalid modules now fail with CompileError.
- `2e8a96a8c585` memarg offsets are decoded as u64 for both memory32 and
memory64 (range-checked for memory32), and call/table immediates in
unreachable code are scanned correctly.
- `64153f963497` memory64 memarg immediates in unreachable code were
decoded differently from reachable code, producing spurious parse errors
on valid modules.
- `d319ee7c278e` A module declaring a memory64 together with any other
memory is now rejected regardless of declaration order (JSC supports
memory64 only as a single memory).
- `d02c68d04f96` `memory.size`/`memory.grow` in IPInt now record the
memidx immediate length, fixing non-minimal LEB encodings of the memory
index under multi-memory; drops the `parseMemoryIndexForBulkOp` hack.
- `102fd6db184d` OMG now passes the memory index when building
loads/stores, so accesses to non-zero memories are marked trapping
correctly under multi-memory.
- `de45b9be42db` `memory.init` overflow check uses 64-bit arithmetic
(memory64); dead `Wasm::Memory::fill/copy` removed.
- `b1b0566f244e` `table.copy` detects source/destination aliasing by
table identity rather than index, so the same table imported under two
indices copies with overlap semantics.
- `2194da86b382` Spec-aligned limits: tag/exception section limit raised
100,000 -> 1,000,000, tables may have exactly 10,000,000 entries (was
exclusive), `maxTableInitializationEntries` removed, exception-section
error message fixed.
- `9b3637884b68` `WebAssembly.Global.prototype.value` setter called with
no argument now treats it as `undefined` instead of throwing a
not-enough-arguments TypeError (WPT behavior).
- `707048fdabb7` `WebAssembly.Memory.prototype.type()` (type reflection,
behind `useWasmJSTypes`) reports the current size as `minimum`, not the
initially declared size.
- `0cc69e2993f4` / `57a1c44be6eb` BBQ pointer materialization takes a
uint64 offset (no truncation for >4 GiB memory64 addresses) and queries
address-form validity with the actual access width (folds more offsets
into addressing).
- `01a43483d35f` `WasmCalleeGroup` stops using
`ThreadSafeWeakOrStrongPtr`, which is removed from WTF
(`wtf/ThreadSafeWeakPtr.h`) as prep for making `ThreadSafeWeakPtr`
thread-safe.
- `a53d011599e7` / `5602ec36107b` Heap-wide renames reaching wasm:
`finalizeUnconditionally` -> `reconcileWeakReferencesAtGCEnd` (and
`Heap`/`IsoCellSet` accessors), `visitWeak` family ->
`reconcileWeakReferencesAtGCEnd`/`isStillLive`; no behavior change.
- `bf1dab73b14d` / `84f83abd45c9` / `6010a9ea6ce6` / `2a8926009f45`
Post-32-bit-JIT-removal cleanups touching BBQ/JSToWasm/WasmToJS: 32-bit
register pairs and scratch registers dropped, `ARMv7Assembler.h`
deleted, `USE(BUILTIN_FRAME_ADDRESS)` made unconditional; no behavior
change on x64/arm64.

### RegExp (Yarr)
- `yarr/` and `RegExp.cpp` stay at the fork's version
(oven-sh/WebKit#299 already contains the lookbehind JIT and most of the
fixes upstream landed in this range); see oven-sh/WebKit#455 for the
commit-by-commit status. Net new for Bun from this range: the `&&` /
`--` with `\P{..}` fix (`7b5e7da783f5`, ported), nothing else. Not yet
in the fork: the BMP code-unit read optimization (`bbc000ae4f3d`), the
default-off `\A` `\z` buffer boundaries (`2f66f5ed23f9`, `37f4628ab5fe`)
and the `ENABLE(YARR_JIT_*)` ifdef removal (`ac2afd10b8ac`; the fork
keeps those macros defined, so the mentions of their removal below do
not apply to this build).

### Inspector / debugger
- `1a7f711d887e` `Debugger::sourceParsed` for WebAssembly modules now
reports the module's `sourceMappingURL` custom section, so
`Debugger.scriptParsed` for wasm scripts carries a source map URL that
inspector frontends can use to map byte offsets to source.
- `49246d261276` Implements the import-text proposal (`import x from
"./f.txt" with { type: "text" }` and the dynamic-import form) behind new
`useImportText` (default on); in this area it only teaches
`InspectorDebuggerAgent` about the new `SourceProviderSourceType::Text`,
but the module-loader API changes (listed below) affect embedders with
custom loaders.
- `4ccb3f3a1c85` / `af624adbb3bc` / `de82d6282625` / `86575c4e1516` /
`87399235b55a` / `fcd024f84dbb` / `1240a421fe56` Protocol schema churn
in the WebCore-only Canvas and Recording domains plus a new generic
`Size` type in `GenericTypes.json`; these flow into
`CombinedDomains.json` (and therefore into regenerated
bun-inspector-protocol types) but change no JSC agent behavior.

### Build / scripts
- `81d660ceeb2e` wkbuiltins generator now precomputes builtin executable
metadata (`BuiltinSourceMetadata`) at build time instead of scanning
sources at VM startup; `BuiltinExecutables::createBuiltinExecutable`
gains a metadata parameter (the free `JSC::createBuiltinExecutable(VM&,
...)` that Bun uses is unchanged).
- `ff64aee116d4` `HandleSet`/`HandleBlock` replaced by
`StrongSet`/`StrongBlock` (Sources.txt/CMakeLists updated): `Strong<>`
slots are allocated from a libpas-style segregated freelist, cheaper and
smaller; `<JavaScriptCore/HandleSet.h>` no longer exists and
`Heap::handleSet()` is now `Heap::strongSet()`.
- `62692012c98a` `heap/HeapFinalizerCallback.{h,cpp}` renamed to
`GCCompletionCallback.{h,cpp}` with
`Heap::add/removeHeapFinalizerCallback` ->
`add/removeGCCompletionCallback`; the C API
`JSContextGroupAdd/RemoveHeapFinalizer` keeps its names.
- `3c64729cefbc` Fixes a clang 18 `-Wthread-safety-precise`/constexpr
build break in `WasmCalleeGroup.cpp`.
- `b9d3ef9f6a0f` Removes the dead `JettisonDueToProfiledWatchpoint`
value from the profiler's `JettisonReason` enum.

### Embedder-relevant API changes
- `MAX_ARRAY_BUFFER_SIZE` (runtime/PageCount.h) is now `1 << 34` on
64-bit (was `1 << 32`); `PageCount::maxPageCount` is public and
redefined; `Wasm::maxMemoryPages` renamed `maxMemory32Pages`,
`maxMemory64Pages` redefined, `maxTableInitializationEntries` removed;
new
`Wasm::maxDeclarablePages/maxBufferByteLength/maxAllocatableBytes(AddressType)`;
`Gigacage::primitiveAddressSpaceBudget` added in bmalloc.
- `ArrayBuffer::grow(const AbstractLocker&, VM&, ...)` replaced by
`ArrayBuffer::tryGrow(const AbstractLocker&, size_t, bool,
BufferMemoryResult::Kind&)`; `Wasm::Memory::fill()`/`copy()` removed
(use `Wasm::memoryFill/memoryCopy`); `Wasm::Table::maximum()` is now
64-bit.
- WTF: `ThreadSafeWeakOrStrongPtr` removed from
`wtf/ThreadSafeWeakPtr.h`; `USE(BUILTIN_FRAME_ADDRESS)` removed
(`DECLARE_CALL_FRAME`/`DECLARE_WASM_CALL_FRAME` always use the
frame-address form);
`ENABLE(YARR_JIT_ALL_PARENS_EXPRESSIONS|YARR_JIT_BACKREFERENCES|YARR_JIT_BACKREFERENCES_FOR_16BIT_EXPRS|YARR_JIT_UNICODE_EXPRESSIONS|YARR_JIT_REGEXP_TEST_INLINE)`
removed from PlatformEnable.h.
- Yarr:
`JITFailureReason::{DecodeSurrogatePair,BackReference,ParenthesizedSubpattern}`
removed; `Yarr::parse()` gained a defaulted trailing
`allowRegExpBufferBoundaries` parameter; new
`Options::useRegExpBufferBoundaries` (off by default).
- Module loading (from import-text): `SourceProviderSourceType::Text`
inserted before `ImportMap` (renumbers `ImportMap`; Bun's fork also
appends `BunTranspiledModule`), `ScriptFetchParameters::Type::Text`
inserted before `HostDefined` (HostDefined becomes 5, matching the
updated static_asserts in
`src/jsc/bindings/BunAnalyzeTranspiledModule.cpp`),
`SyntheticModuleRecord::create()` and the `AbstractModuleRecord`
constructor now take a `SourceProviderSourceType` (already adapted in
`src/jsc/bindings/NodeVMSyntheticModule.cpp`), new
`SyntheticModuleRecord::createTextModule()`, new
`Options::useImportText` (on by default).
- Heap renames: `T::finalizeUnconditionally()` ->
`reconcileWeakReferencesAtGCEnd()`,
`Heap::finalizeUnconditionalFinalizers()` ->
`reconcileWeakReferencesAtGCEnd()`, `Heap::...::finalizerSetFor()` ->
`weakReconciliationSetFor()`, `CallLinkInfo::visitWeak` and friends ->
`reconcileWeakReferencesAtGCEnd`; `HeapFinalizerCallback` ->
`GCCompletionCallback` (header renamed);
`HandleSet.h`/`HandleBlock.h`/`HandleBlockInlines.h` removed in favor of
`StrongSet.h`/`StrongBlock.h`, `Heap::handleSet()` -> `strongSet()`
(Bun's `src/jsc/bindings/root.h` include already switched).
- `BuiltinExecutables::createBuiltinExecutable()`/`createExecutable()`
gained `const BuiltinSourceMetadata&` overloads (member function
signature changed; free function unchanged);
`JettisonReason::JettisonDueToProfiledWatchpoint` removed;
`assembler/ARMv7Assembler.h` deleted;
`Wasm::ModuleInformation::memoryModeForAccess()` added.

### WTF

- `f6bc402b8344` `StackBounds::currentThreadStackBounds()` is now
private (only `Thread`/`StackStats` may call it; other code is meant to
read the cached `Thread::currentSingleton().stack()`) because on Linux
it can re-parse `/proc/self/maps` on every call. Bun's
`Bun__StackCheck__initialize` called it once per thread, including on
non-WTF threads, and now uses the
`currentThreadStackBoundsForEmbedder()` accessor the fork adds under
`USE(BUN_JSC_ADDITIONS)`. `36403ca62849` re-adds `WTF_EXPORT_PRIVATE` on
`currentThreadStackBoundsInternal()`.
- `5fc5182bcf83` `WTF::numberOfProcessorCores()` now honors a
`NUMBER_OF_PROCESSORS` env var (after the existing
`WTF_numberOfProcessorCores`) before asking the OS. Bun reports this
value as `navigator.hardwareConcurrency` / `os.availableParallelism()`
and it would take precedence over the fork's affinity/cgroup aware
count, so the fork keeps this lookup out of Bun builds
(oven-sh/WebKit#455, 8fc20b18b9); no change for Bun.
- `957b52180bee` `MemoryPressureHandler` no longer inherits
`CanMakeWeakPtr` (timers bind to the singleton via lambdas); fixes a
debug-build WeakPtr thread assertion when the singleton is first touched
off the main thread (JSC's `FullGCActivityCallback` does this, e.g. from
a Worker), and `s_hasCreatedMemoryPressureHandler` is now only set once
the singleton really exists.
- `4a10860dc35c` `WTF::Liveness` iterates less (no separate boundary
pass, no zeroing of the gen store) and gains
`forEachLiveAtHeadNotLiveAtTail` / `forEachLiveAtTailNotLiveAtHead`;
used by the Air greedy register allocator (~17% faster
`buildLiveRanges`), i.e. lower DFG/FTL/OMG compile latency.
- `01a43483d35f` `ThreadSafeWeakOrStrongPtr` removed from
`wtf/ThreadSafeWeakPtr.h` (its only user, `Wasm::CalleeGroup`, was
rewritten); groundwork for shrinking `ThreadSafeWeakPtr` to one pointer
and making it atomic.
- `ac2afd10b8ac` Removes the `ENABLE_YARR_JIT_*` sub-feature macros
upstream (unconditional on x64/arm64). The fork keeps them defined
because its YarrJIT still tests them; no behavior change either way.
- `6010a9ea6ce6` ARMv7 JIT removal follow-ups: drops
`CPU(ARM_VFP_V2)`/`CPU(ARM_VFP_V3_D32)`, simplifies
`ASSERT_VALID_CODE_POINTER`, `ENABLE(JUMP_ISLANDS)` is now arm64-only
and `LLINT_EMBEDDED_OPCODE_ID` drops Thumb2; no effect on x64/arm64
builds.
- `2a8926009f45` `USE(BUILTIN_FRAME_ADDRESS)` macro removed; JSC now
unconditionally uses `__builtin_frame_address` on JIT platforms. The
fork had it disabled on Windows ARM64 only; that fallback is gone with
this merge (see the Windows ARM64 note above).
- `ef6d9ba26b17` removed Linux real-time threads in favor of nice/RTKit
priorities, `44bab332e0f1` fixed its JSCOnly build, and `56baf6e01b3d`
reverted the whole thing for ~2% JetStream3/Speedometer3 regressions:
net zero change to `Threading.h`/`AutomaticThread`/`RealTimeThreads.cpp`
in this range.
- `1240a421fe56` Additive
`JSON::Array::set{Boolean,Integer,Double,String,Value,Object,Array}(index,
…)` and `JSON::ArrayOf<T>::setItem(index, …)` (in-place replacement;
`RELEASE_ASSERT`s index in range) in `wtf/JSONValues.h`, which Bun's
inspector/profiler bindings include.
- `5720766c8056` Reverts the IPC URL-size limit, removing the
`WTF::maxURLLength` constant from `wtf/URL.h`; no URL parsing behavior
change.
- `3089b5074c3d` Deletes the empty `wtf/text/WYHash.h`; any `#include`
of it now fails (Bun has none).
- `e9a62e6b4da5` `85e82ceefe1b` `793e36fb835e` `934bb002485a`
`2f66f5ed23f9` `49246d261276` `99473681ff5e` only touch
`Scripts/Preferences/UnifiedWebPreferences.yaml` on the WTF side,
mirroring JSC option changes (iterator join / import defer / iterator
chunking / joint iteration flipped to default-on, new RegExp
buffer-boundaries and import-text prefs, `IntlEraMonthcodeEnabled` pref
removed since the feature is now unconditional); the actual behavior
lives in the JavaScriptCore commits. All other yaml-only commits in this
range are WebCore/WebKit feature flags and irrelevant to Bun.

**Embedder-relevant API changes**
- `StackBounds::currentThreadStackBounds()` is private (`friend class
Thread`); replacement is `Thread::currentSingleton().stack()`
(`f6bc402b8344`).
- `WTF::ThreadSafeWeakOrStrongPtr` removed (`01a43483d35f`).
- Header `wtf/text/WYHash.h` removed (`3089b5074c3d`); header
`wtf/Nonallocatable.h` added and `RefCountedWithInlineWeakPtrBase`
removed / `RefCountedWithInlineWeakPtr<T>` made non-`new`-able
(`f880bc57ad50`).
- `using WTF::Task` removed from `wtf/CoroutineUtilities.h`
(`9f82586af24c`); `WTF::maxURLLength` removed from `wtf/URL.h`
(`5720766c8056`).
- `MemoryPressureHandler` no longer derives from `CanMakeWeakPtr` and
lost its no-op `ref()`/`deref()` (`957b52180bee`).
- Config macros removed: `USE(BUILTIN_FRAME_ADDRESS)`,
`ENABLE(YARR_JIT_ALL_PARENS_EXPRESSIONS)`,
`ENABLE(YARR_JIT_REGEXP_TEST_INLINE)`,
`ENABLE(YARR_JIT_BACKREFERENCES)`,
`ENABLE(YARR_JIT_BACKREFERENCES_FOR_16BIT_EXPRS)`,
`ENABLE(YARR_JIT_UNICODE_EXPRESSIONS)`, `CPU(ARM_VFP_V2)`,
`CPU(ARM_VFP_V3_D32)`; `ENABLE(JUMP_ISLANDS)` now arm64-only; new
`ENABLE(JIT_CAGE_RELAXATION)`.
- Additive only: `JSON::Array::set*`/`ArrayOf<T>::setItem`,
`Liveness::forEachLiveAt{Head,Tail}NotLiveAt{Tail,Head}`,
`WTF::isInBaseSystem()` (Cocoa port only, not compiled in JSCOnly/Bun),
`numberOfProcessorCores()` reading `NUMBER_OF_PROCESSORS`.

### bmalloc

- `2c2c1af35743` Adds `Gigacage::primitiveAddressSpaceBudget` (a
`constexpr uint64_t`, 64 GB on 64-bit desktop/server targets, 16 GB on
iOS/32-bit) to `Gigacage.h`, defined *outside* `#if GIGACAGE_ENABLED` so
it exists even when the Gigacage is compiled out;
`primitiveGigacageSize` is now derived from it (same value as before, so
no cage-size change). This is the bmalloc half of the ArrayBuffer/Wasm
memory64 sizing overhaul: JSC's `BufferMemoryHandle.h` uses it to cap
the virtual reservation of any one resizable `ArrayBuffer` / growable
`SharedArrayBuffer` / `WebAssembly.Memory` at budget/4 (16 GB on Bun's
platforms), which is what lets those buffers reach the new 16 GB
`MAX_ARRAY_BUFFER_SIZE` (previously 4 GB) and lets memory64 grow past 4
GB without crashing. (The fork pins `MAX_ARRAY_BUFFER_SIZE` at 4 GB, so
in Bun only the crash fix applies.)

Embedder-relevant API changes (bmalloc):
- Added: `Gigacage::primitiveAddressSpaceBudget` (`constexpr uint64_t`)
in `Source/bmalloc/bmalloc/Gigacage.h`; `primitiveGigacageSize`
unchanged in type and value. No removals or renames. Bun's own C++
(`src/jsc/bindings`) references no Gigacage symbols, so nothing on the
Bun side needs updating for this area.

</details>

<!-- robobun:evidence:begin -->

---

**[decide:webkit]** gate passed · iteration 0 · 8 files touched

<!-- robobun:evidence:end -->

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants