[JSC] Cache `isDefinitelyNonThenable` result on Structure
Component: JSC | cdf77b6
The ECMAScript spec requires Promise.resolve and the await desugaring to detect thenables — objects with a callable .then — because a thenable must be assimilated rather than wrapped. JSC implements this via isDefinitelyNonThenable(), walking the object's prototype chain each time. A Structure in JSC is the hidden class (V8's Map): it encodes shape, property layout, and prototype pointer. The promiseThenWatchpointSet is an existing invalidation mechanism firing when Function.prototype.then is mutated.
Source/JavaScriptCore/runtime/JSPromise.cpp
Source/JavaScriptCore/runtime/Structure.h
The result is cached on each Structure as a 2-bit lazily-computed state. The NonThenable state is trusted only while the realm's promiseThenWatchpointSet is intact — now extended to also cover then-absence on Object.prototype, which is what makes caching a positive result safe. Chains deeper than [self, Object.prototype] or [self] (null proto) are marked Uncacheable because deeper custom chains cannot be globally watched; dictionary structures are excluded entirely.
2-bit NonThenableStatus state machine on Structure:
Unknown
│
├─[walk: short chain, watchpoint intact]──► NonThenable ──[watchpoint fires]──► Unknown
│
├─[walk: `then` property found]──────────► MaybeThenable (always safe to cache)
│
└─[walk: deep chain]─────────────────────► Uncacheable (skip cache entirely)
Significance
Every await expr and Promise.resolve(obj) on a plain object literal now skips the prototype-chain walk entirely, worth 3-8% on promise/await microbenchmarks. The correctness cost is that a positive "not a thenable" answer is now cached rather than recomputed, so its validity depends on watchpoint coverage rather than on a fresh walk.
Audit directions
Narrow: the cached NonThenable result is guarded by two separate loads — nonThenableStatus() and promiseThenWatchpointSet().isStillValid() — with nothing between them.
case NonThenableStatus::NonThenable:
if (structure->globalObject() == globalObject
&& globalObject->promiseThenWatchpointSet().isStillValid())
return true;
break; // stale or wrong realm — fall through to walk
The watchpoint fires synchronously on Object.prototype.then assignment, but investigate whether a concurrent invalidation from another agent or SAB context can race the cache read. The tell in review is any cached-truth read paired with a separately-loaded validity check.
Wider: state transitions that can leave the cached bit stale on a mutated structure. A structure caches NonThenable, then Object.assign() or bulk property addition triggers dictionary inflation — dictionary structures are mutated in place under the same Structure*, so the window between adding then and the watchpoint firing is where a stale NonThenable bit could still be readable. The commit claims hasSpecialProperties is updated in place for dictionaries; the timing of that update relative to the cache state bit needs verification. Same question for null-proto objects: null-proto chains qualify for caching, and adding then should transition to a new Structure, but confirm Object.defineProperty(obj, 'then', {get: ...}) always produces a new Structure rather than mutating in place. Generalize this to every lazily-computed cached bit on Structure — enumerate them and check each against the dictionary-inflation and in-place-mutation paths.
Widest: cross-realm trust. The realm check compares structure->globalObject() == globalObject, so a structure created in realm A resolving in realm B's promise pipeline must fail it. Confirm that structures of objects created by createGlobalObject() or cross-realm eval always carry the originating realm's global rather than the calling realm's. A misclassification here turns a thenable into a plain value, which is a capability bypass if promise-guarded objects can be smuggled through as non-thenables — audit every other realm-identity check guarding a cached optimization for the same question.