← All reports

[JSC] Optimize initial `then` call

Component: JSC | c3276f4

JSC's DFG IR encodes both operation and proven type constraints per node. PerformPromiseThen is the generic four-child node (promise, fulfill handler, reject handler, result capability), conservative because either handler can be callable or null/undefined at runtime. The Abstract Interpreter propagates type predictions using SpeculatedType bit sets — SpecFunction for callables, SpecOther for null/undefined.

JSTests/stress/perform-promise-then-one-handler.js

+// Conversion happens in DFGConstantFoldingPhase only when the abstract interpreter
+// has *proven* that one handler is SpecFunction and the other is SpecOther
+// (undefined or null). Otherwise we keep the generic 4-child PerformPromiseThen
+// node, whose runtime semantics this test also verifies.
+
+// --- Converted FulfillHandler kind: .then(fn) — onRejected is implicit undefined ---
+function fulfillOnly(p) {
+ return p.then(v => v + 1);
+}
+noInline(fulfillOnly);
+
+// --- Converted RejectHandler kind: .then(undefined, fn) ---
+function rejectOnlyUndefined(p) {
+ return p.then(undefined, e => 'caught:' + e);
+}
+noInline(rejectOnlyUndefined);
+
+// --- Non-converted: two callable handlers — neither side is SpecOther ---
+function bothHandlers(p) {
+ return p.then(v => 'ok:' + v, e => 'err:' + e);
+}
+
+// --- Non-converted: AI proves SpecInt32, neither SpecFunction nor SpecOther ---
+function intHandlerWithReject(p) {
+ return p.then(1, e => 'int-rej:' + e);
+}

A new PerformPromiseThenOneHandler node inlines the fast path when the AI has proven exactly one handler is SpecOther. DFGConstantFoldingPhase performs the conversion, encoding the handler kind (fulfill vs. reject) in a node flag and emitting direct flag writes and value stores instead of allocating a reaction cell — building on 313220's elimination of reaction cells for the initial .then().

Promise.then(fn, undefined)  — hot loop
        │
        ▼
  DFGFixupPhase
  PerformPromiseThen [promise, fn, undefined, resultCap]  (4 children)
        │
        ▼
  AbstractInterpreter
  handler[1] proven SpecOther? ──yes──► DFGConstantFoldingPhase
        │                                converts to:
        │                                PerformPromiseThenOneHandler
        │                                  kind=FulfillHandler
       no                                  (2 effective children)
        │                                      │
        ▼                                      ▼
  keep PerformPromiseThen            DFG/FTL SpeculativeJIT
  (generic slow-path operation)      inline: write flags + store handler slot
                                     slow branch → operationPerformPromiseThenOneHandler
                                     (when promise already settled at JIT time)

.then(fn) and .then(undefined, fn) — ubiquitous in production JS — now compile to tight inline memory operations instead of bailing to reaction-cell allocation. The tradeoff is that promise handler dispatch, a security-sensitive operation, now has JIT-emitted code writing internal object layout directly.

Narrow: the classification gate in classifyPerformPromiseThen is an AI type-proof that one handler is SpecOther. If the AI can be made to over-widen or misclassify — a polymorphic call site that initially sees only null and later a callable — the specialized node fires with a handler slot that is actually callable, silently dropping or misrouting the handler. The tell is a node conversion gated on a proven-absent type where the absence is inferred from call-site history rather than checked at runtime.

Wider: store barriers. The new node stores a JSObject (the handler) into the promise's internal slot. If DFGStoreBarrierInsertionPhase.cpp does not insert write barriers for the new node type, the GC misses roots and collects live handler functions — a use-after-free in managed-heap terms. Every new DFG node that stores a cell into an existing object carries this obligation; sweep the barrier phase's node switch for cases added later than the nodes they should cover. Alongside it, verify the inline flag encoding and slot offsets match what the slow path and the GC expect, since divergence corrupts promise state in ways that surface as type confusion across await chains.

Widest: the fast/slow boundary contract. The JIT gates the inline path on the promise being pending and drops already-settled promises to operationPerformPromiseThenOneHandler. Audit whether that C++ operation assumes preconditions the JIT does not actually guarantee — this asymmetry between what a JIT fast path checks and what its runtime fallback assumes is a recurring shape, worth sweeping across every operation* entry point paired with an inline fast path. Same question for the FTL lowering in FTLLowerDFGToB3.cpp: confirm the B3-emitted code respects the DFG path's invariants and that no exit mishandles the InlineReactionKind flag.