DFG DOMJIT: fix attemptToMakeCallDOM predicates and restore typed-array/Int52 lowering - #318
DFG DOMJIT: fix attemptToMakeCallDOM predicates and restore typed-array/Int52 lowering#318robobun wants to merge 2 commits into
Conversation
…ay/Int52 lowering attemptToMakeCallDOM carried an inverted '!' on the String/Int32/Boolean shouldSpeculateNot* checks and the fork-added typed-array/Int52 cases bailed when the arg IS the expected type, so argument-bearing DOMJIT signatures never converted for correctly-typed calls (80063b3). Drop the '!' to match upstream and collapse the typed-array cases into one block that tests prediction & expected directly. SpeculativeJIT::compileCallDOM lost its typed-array and Int52 operand lambdas in fc230df while fixupCallDOM and FTL compileCallDOM kept the extended cases, so a correctly-typed typed-array/Int52 signature reaching the DFG backend would RELEASE_ASSERT. Restore appendTypedArray and appendStrictInt52, and add SpeculateStrictInt52Operand to the OperandVariant, so DFG / Fixup / FTL agree. See oven-sh/bun#35224 for the Bun-side companion.
|
Warning Review limit reached
Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
There was a problem hiding this comment.
Beyond the inline findings: appendTypedArray uses SpeculateCellOperand, which does have a move ctor (DFGSpeculativeJIT.h:2974), so the typed-array path is not affected by the same operand-lifetime issue. Also confirmed the inlined prediction && !(prediction & expected) test in FixupPhase matches the body of the existing isNotXSpeculation helpers in SpeculatedType.h exactly.
Extended reasoning...
This is DFG JIT compiler internals with a real correctness finding on the Int52 operand path, so deferring to human review. Recording two adjacent concerns that were checked and ruled out so they aren't re-investigated: the typed-array lambda's operand type is move-safe, and the collapsed predicate form in FixupPhase is byte-for-byte the same test the per-type isNot*Speculation helpers use.
…, guard Int52 for JSVALUE64, fix attemptToMakeCallDOM index compileCallDOM stores operands in a Variant and moves them in via WTF::move. SpeculateCellOperand has a move ctor that nulls the source; SpeculateInt32Operand, SpeculateBooleanOperand and SpeculateStrictInt52Operand do not, so WTF::move binds to the implicit copy and both the local and the vector element destruct and unlock the same GPR. Upstream has not hit this because every live WebCore CallDOM signature takes String arguments only (SpeculateCellOperand). Add move constructors and the matching !m_edge destructor guard to all three, mirroring SpeculateCellOperand. Guard the SpeculateStrictInt52Operand variant alternative, lambda and switch cases under USE(JSVALUE64); fillSpeculateInt52 is defined only in DFGSpeculativeJIT64.cpp and every other use in this file is so guarded. Restructure the attemptToMakeCallDOM doToChildren lambda so ++index is reached on every child: the previous 'if (!index) return;' left index at 0 forever, making the whole profitability switch (including every predicate this PR rewrites) dead code. This is also present in upstream WebKit; the switch has never run since its introduction.
| case SpecInt32AsInt52: | ||
| case SpecNonInt32AsInt52: | ||
| case SpecAnyIntAsDouble: { | ||
| if (edge->shouldSpeculateInt52()) | ||
| if (edge->prediction() && !(edge->prediction() & (SpecInt32Only | SpecInt52Any))) | ||
| shouldConvertToCallDOM = false; | ||
| break; | ||
| } |
There was a problem hiding this comment.
🟡 The Int52 profitability mask at line 5343 tests SpecInt32Only | SpecInt52Any, but at Fixup time a large integer argument (e.g. 2**40) is profiled as SpecAnyIntAsDouble (bit 41) — never SpecInt52Any (bits 39|40), which only appears on NodeResultInt52 nodes after Fixup inserts Int52Rep. So a pure-large-int argument sets shouldConvertToCallDOM = false and skips the DOMJIT fast path even though fixEdge<Int52RepUse> handles SpecAnyIntAsDouble fine. Consider using SpecIntAnyFormat (SpeculatedType.h:107) as the mask — the case-label list here already includes SpecAnyIntAsDouble, which suggests the body should accept it too.
Extended reasoning...
What the issue is
Now that 1f1383b restructures the doToChildren lambda so ++index runs on every child, the profitability switch at DFGFixupPhase.cpp:5312 actually executes for the first time. The Int52 group (lines 5339-5346) tests:
case SpecInt52Any:
case SpecInt32AsInt52:
case SpecNonInt32AsInt52:
case SpecAnyIntAsDouble: {
if (edge->prediction() && !(edge->prediction() & (SpecInt32Only | SpecInt52Any)))
shouldConvertToCallDOM = false;
break;
}But at Fixup time the argument is still a raw Call-node varargs child (a GetLocal / JSConstant / etc.) whose prediction() comes from bytecode value profiling. The SpecInt52Any bits (39|40, SpeculatedType.h:92-94) only ever appear on nodes with NodeResultInt52 — i.e. after Fixup has inserted an Int52Rep conversion. Value profiles never produce them: speculationFromValue() (SpeculatedType.cpp:631-635) sees a large integer as a double-boxed JSValue and returns SpecAnyIntAsDouble (bit 41), and SpecHeapTop excludes SpecInt52Any entirely.
Step-by-step example
Take a bun:ffi reader whose argument was consistently observed as 2**40 (a pointer-sized value):
2**40is boxed as a doubleJSValue;speculationFromValue()returnsSpecAnyIntAsDouble(1ull << 41).- At
attemptToMakeCallDOM, the argument edge points to aGetLocalwithprediction() == SpecAnyIntAsDouble. SpecInt32Only | SpecInt52Any= bits 34-40.SpecAnyIntAsDouble & (SpecInt32Only | SpecInt52Any)= 0.edge->prediction()is nonzero and the AND is zero →shouldConvertToCallDOM = false.- The function returns
false; the node stays a plainCalland never gets the DOMJIT fast path.
Note that fixEdge<Int52RepUse> in fixupCallDOM handles SpecAnyIntAsDouble just fine — it inserts Int52Rep(AnyIntUse), which converts an integral double to Int52 without a guaranteed BadType exit. So the profitability check is stricter than what the lowering actually requires. The case-label list itself already includes SpecAnyIntAsDouble (line 5342), which is a self-consistency signal that the mask should accept it.
Why nothing else prevents it
SpecIntAnyFormat (SpeculatedType.h:107) = SpecInt52Any | SpecInt32Only | SpecAnyIntAsDouble exists precisely for this "integer in any representation" test, and isIntAnyFormat() (line 417) is the corresponding predicate. The mask here just omits the third component. Mixed predictions that include any SpecInt32Only bit (e.g. an argument observed as both small and large integers) still pass because the test is any-overlap, so this only bites the pure-large-int case — which is exactly the shape a bun:ffi pointer/offset argument would have.
Impact
Not a correctness bug — the call falls back to a plain Call, so behavior is identical, just slower. Not a runtime regression vs. main either, since (a) this switch was dead code before 1f1383b, and (b) argument-bearing DOMJIT signatures are currently disabled in Bun. But the PR's stated purpose is to "unblock re-enabling argument-bearing DOMJIT signatures" and specifically calls out SpecInt52Any (bun:ffi readers); once those are re-enabled, this mask will keep the pure-large-int case on the slow path.
Suggested fix
Replace the mask with SpecIntAnyFormat:
if (edge->prediction() && !(edge->prediction() & SpecIntAnyFormat))
shouldConvertToCallDOM = false;or equivalently gate on !isIntAnyFormat(edge->prediction()) / !edge->shouldSpeculateInt52(), matching what Int52RepUse fixup can convert.
|
CI: 28 lanes pass (all musl/android/macos/windows/freebsd variants, debug/release/asan/lto). The linux-glibc matrix has one real failure ( The
Both changed translation units ( |
Fork-specific fixes to the DFG DOMJIT plumbing, confirmed by diffing against upstream
WebKit/WebKit@main. Companion to oven-sh/bun#35224, which fixes Bun's DOMJIT wrapper return protocol (the actual WebKit#14001 crash); this PR unblocks re-enabling argument-bearing DOMJIT signatures.1.
DFGFixupPhase.cppattemptToMakeCallDOM: profitability check is dead code, and its predicates are invertedThe
doToChildrenlambda doesif (!index) return;before the trailing++index, soindexstays 0 for every child and neither thethis-cell check nor the argument-type switch ever runs.shouldConvertToCallDOMis always lefttrue. This is present in upstream WebKit since 2f93d5d (2016); the switch has never executed.Separately, inside that dead switch the fork carries an extra
!on the three upstream cases (SpecString/SpecInt32Only/SpecBoolean) and the fork-added typed-array/Int52 cases bail when the prediction is the expected type. Introduced in 80063b3; a fix in bcfcd06 was reverted by e17d16e without a recorded reason.Fix: restructure to
if/else if/elseso++indexruns on every child (matchingcompileCallDOM). Drop the!on the three upstream cases to match upstream exactly. Collapse the nine typed-array cases into one block that reads the signature'sSpeculatedTypedirectly and testsprediction && !(prediction & expected)(the body of everyisNotXSpeculation). Apply the same inline test to the Int52 group againstSpecInt32Only | SpecInt52Any.2.
DFGSpeculativeJIT::compileCallDOM: typed-array / Int52 lowering droppedfixupCallDOMsets upCellUse/Int52RepUseedges for typed-array and Int52 signature arguments, andFTLLowerDFGToB3::compileCallDOMlowers them, but the DFG backend's switch handles onlySpecString/SpecInt32Only/SpecBooleanand falls through toRELEASE_ASSERT_NOT_REACHED(). The fork originally had per-typeappend*Array/appendAnyIntAsDoublelambdas here (present at bcfcd06); they were dropped in the fc230df upstream merge while Fixup and FTL kept the extended cases.Fix: restore the lowering so all three layers agree. Add
SpeculateStrictInt52Operandto theOperandVariant(guarded byUSE(JSVALUE64), matching every other use in this file), add anappendTypedArray(edge, JSType)lambda andappendStrictInt52, and wire the switch to matchfixupCallDOM/ FTL.3.
DFGSpeculativeJIT.h:Speculate{Int32,Boolean,StrictInt52}Operandneed move constructorscompileCallDOMstores operands in aVariantand moves them in viaWTF::move.SpeculateCellOperandhas a move constructor that nulls the moved-from object'sm_edge/m_gprOrInvalid;SpeculateInt32Operand,SpeculateBooleanOperandandSpeculateStrictInt52Operanddo not, soWTF::move(operand)binds to the implicit copy and both the local and the vector element destruct andunlock()the same GPR (debugASSERT(lockCount), release underflow). Upstream has not hit this because every live WebCoreCallDOMsignature takesSpecStringarguments only, routed throughSpeculateCellOperand. Bun's signatures useSpecInt32Only(Buffer.alloc) andSpecInt52Any(bun:ffireaders).Fix: add a move constructor and
if (!m_edge) return;destructor guard to all three, mirroringSpeculateCellOperand.Not changed
fixupCallDOMandFTLLowerDFGToB3::compileCallDOMalready handle the extended type set. NoSpeculatedType.h/DFGNode.hhelpers are added (bcfcd06 added ten of each; this PR inlines the one use site to keep the upstream-diff surface small).The dead-index bug (item 1) and the missing move constructors (item 3) are also present in upstream WebKit and worth reporting there separately.