From a77f8da3181b1b0d3940fb2b7acf30703b2c9d94 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 21 Aug 2026 11:21:24 +0000 Subject: [PATCH 01/13] Add BunTelemetrySpanType and @isTelemetrySpan; async-context capture in generators; leave-frame hook --- .../bytecode/BytecodeIntrinsicRegistry.h | 1 + .../bytecompiler/BytecodeGenerator.h | 1 + .../bytecompiler/NodesCodegen.cpp | 1 + .../JavaScriptCore/dfg/DFGSpeculativeJIT.cpp | 10 +++ Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp | 9 +++ .../runtime/AsyncContextSwapScope.h | 64 +++++++++++++++++-- .../runtime/JSAsyncFunctionGenerator.cpp | 23 +++++++ .../runtime/JSAsyncFunctionGenerator.h | 21 +++++- .../JavaScriptCore/runtime/JSAsyncGenerator.h | 19 +++++- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 56 +++++++++++----- Source/JavaScriptCore/runtime/JSPromise.cpp | 22 ++++--- .../runtime/JSPromiseReaction.cpp | 5 +- .../runtime/JSPromiseReaction.h | 6 +- Source/JavaScriptCore/runtime/JSType.h | 4 ++ Source/JavaScriptCore/runtime/Microtask.h | 3 + Source/JavaScriptCore/runtime/VM.h | 8 +++ 16 files changed, 213 insertions(+), 40 deletions(-) diff --git a/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h b/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h index 776b40d43bf1..d6d47816c6cd 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h +++ b/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h @@ -75,6 +75,7 @@ enum class LinkTimeConstant : int32_t; macro(isUndefinedOrNull) \ macro(isWrapForValidIterator) \ macro(isDisposableStack) \ + macro(isTelemetrySpan) \ macro(throwTypeError) \ macro(throwRangeError) \ macro(throwOutOfMemoryError) \ diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h index d665dc390661..6e1241ba1092 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h @@ -973,6 +973,7 @@ namespace JSC { RegisterID* emitIsEmpty(RegisterID* dst, RegisterID* src); RegisterID* emitIsDerivedArray(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, DerivedArrayType); } RegisterID* emitIsDisposableStack(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, DisposableStackType); } + RegisterID* emitIsTelemetrySpan(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, static_cast(BunTelemetrySpanType)); } RegisterID* emitIsAsyncDisposableStack(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, AsyncDisposableStackType); } void emitRequireObjectCoercible(RegisterID* value, ASCIILiteral error); void emitRequireObjectCoercibleForDestructuring(RegisterID* value, const Identifier* propertyName); diff --git a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp index 27bed9d5e093..bdb0530b8fb0 100644 --- a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp +++ b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp @@ -2049,6 +2049,7 @@ CREATE_INTRINSIC_FOR_BRAND_CHECK(isArrayIterator, IsArrayIterator) CREATE_INTRINSIC_FOR_BRAND_CHECK(isUndefinedOrNull, IsUndefinedOrNull) CREATE_INTRINSIC_FOR_BRAND_CHECK(isWrapForValidIterator, IsWrapForValidIterator) CREATE_INTRINSIC_FOR_BRAND_CHECK(isDisposableStack, IsDisposableStack) +CREATE_INTRINSIC_FOR_BRAND_CHECK(isTelemetrySpan, IsTelemetrySpan) CREATE_INTRINSIC_FOR_BRAND_CHECK(isAsyncDisposableStack, IsAsyncDisposableStack) #undef CREATE_INTRINSIC_FOR_BRAND_CHECK diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp index e27346d35a03..e320f384a8e2 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp @@ -15943,6 +15943,16 @@ void SpeculativeJIT::compileNewInternalFieldObjectImpl(Node* node, Operation ope static_assert(initialValues.size() == JSClass::numberOfInternalFields); for (unsigned index = 0; index < initialValues.size(); ++index) storeTrustedValue(initialValues[index], Address(resultGPR, JSInternalFieldObjectImpl<>::offsetOfInternalField(index))); +#if USE(BUN_JSC_ADDITIONS) + if constexpr (std::is_same_v) { + // See recordEntryAsyncContext() in JSAsyncFunctionGenerator.cpp. The + // tuple is allocated once per global object and never replaced. + if (auto* asyncContextData = m_graph.globalObjectFor(node->origin.semantic)->m_asyncContextData.get()) { + load64(std::bit_cast(asyncContextData) + JSInternalFieldObjectImpl<>::offsetOfInternalField(0), scratch1GPR); + store64(scratch1GPR, Address(resultGPR, JSInternalFieldObjectImpl<>::offsetOfInternalField(static_cast(JSAsyncFunctionGenerator::Field::AsyncContext)))); + } + } +#endif mutatorFence(vm()); addSlowPathGenerator(slowPathCall(slowCases, this, operation, resultGPR, TrustedImmPtr(&vm()), TrustedImmPtr(structure))); diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp index 312e0058ce51..c1411def513f 100644 --- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp +++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp @@ -10037,6 +10037,15 @@ IGNORE_CLANG_WARNINGS_END static_assert(initialValues.size() == JSClass::numberOfInternalFields); for (unsigned index = 0; index < initialValues.size(); ++index) m_out.store64(m_out.constInt64(JSValue::encode(initialValues[index])), object, m_heaps.JSInternalFieldObjectImpl_internalFields[index]); +#if USE(BUN_JSC_ADDITIONS) + if constexpr (std::is_same_v) { + // See recordEntryAsyncContext() in JSAsyncFunctionGenerator.cpp. + if (auto* asyncContextData = m_graph.globalObjectFor(m_origin.semantic)->m_asyncContextData.get()) { + LValue current = m_out.load64(m_out.absolute(std::bit_cast(asyncContextData) + JSInternalFieldObjectImpl<>::offsetOfInternalField(0))); + m_out.store64(current, object, m_heaps.JSInternalFieldObjectImpl_internalFields[static_cast(JSAsyncFunctionGenerator::Field::AsyncContext)]); + } + } +#endif mutatorFence(); ValueFromBlock fastResult = m_out.anchor(object); m_out.jump(continuation); diff --git a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h index 9d9a33c00687..ae1b21798323 100644 --- a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h +++ b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h @@ -28,6 +28,8 @@ #if USE(BUN_JSC_ADDITIONS) #include "InternalFieldTuple.h" +#include "JSAsyncFunctionGenerator.h" +#include "JSAsyncGenerator.h" #include "JSCast.h" #include "JSGlobalObject.h" #include @@ -38,8 +40,8 @@ namespace JSC { // RAII helper for Bun's AsyncLocalStorage: swaps an async context value into // JSGlobalObject::m_asyncContextData field 0 for the lifetime of the scope and // restores the previous value on destruction. A no-op when the supplied context -// is empty or undefined, so the common path (no async context active) costs a -// single branch. Also provides helpers for the snapshot side (capturing the +// is empty, or undefined while async context tracking has never been enabled on +// the global, so the common path (no async context in use) costs a branch. Also provides helpers for the snapshot side (capturing the // current context and wrapping it into an InternalFieldTuple alongside a user // context) and for unwrapping such a tuple on the restore side. class AsyncContextSwapScope { @@ -49,13 +51,21 @@ class AsyncContextSwapScope { ALWAYS_INLINE AsyncContextSwapScope(VM& vm, JSGlobalObject* globalObject, JSValue asyncContext) : m_vm(vm) { - if (asyncContext.isEmpty() || asyncContext.isUndefined()) + if (asyncContext.isEmpty()) + return; + // Once anything uses async context, every continuation installs the + // context it captured -- including "none" -- so a context entered inside + // one continuation (AsyncLocalStorage.enterWith, an activated span) is + // scoped to that continuation instead of leaking into whichever job + // happens to run next. Until then this stays a single branch. + if (asyncContext.isUndefined() && !globalObject->isAsyncContextTrackingEnabled()) return; m_asyncContextData = globalObject->m_asyncContextData.get(); if (!m_asyncContextData) return; m_restoreAsyncContext = m_asyncContextData->getInternalField(0); - m_asyncContextData->putInternalField(vm, 0, asyncContext); + if (m_restoreAsyncContext != asyncContext) + m_asyncContextData->putInternalField(vm, 0, asyncContext); } ALWAYS_INLINE ~AsyncContextSwapScope() @@ -111,6 +121,52 @@ class AsyncContextSwapScope { return InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), userContext, asyncContext); } + // Await-side capture. When the suspending object is an async (generator) + // function, record the current async context in its AsyncContext slot — a + // suspended async function has exactly one outstanding await, so this is a + // single barriered store instead of an InternalFieldTuple allocation per + // await — and return the object itself. Other drivers (AsyncFromSyncIterator, + // top-level-await module records) are rare and keep the tuple path. + static ALWAYS_INLINE JSValue captureForAwait(VM& vm, JSGlobalObject* globalObject, JSValue driver) + { + auto* asyncContextData = globalObject->m_asyncContextData.get(); + if (!asyncContextData) + return driver; + JSValue asyncContext = asyncContextData->getInternalField(0); + if (driver.isCell()) { + JSCell* cell = driver.asCell(); + if (cell->type() == JSAsyncFunctionGeneratorType) { + auto* generator = uncheckedDowncast(cell); + if (generator->asyncContext() != asyncContext) + generator->setAsyncContext(vm, asyncContext); + return driver; + } + if (cell->type() == JSAsyncGeneratorType) { + auto* generator = uncheckedDowncast(cell); + if (generator->asyncContext() != asyncContext) + generator->setAsyncContext(vm, asyncContext); + return driver; + } + } + if (asyncContext.isUndefined()) + return driver; + return InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), driver, asyncContext); + } + + // Resume-side counterpart of captureForAwait(): yields the async context to + // restore and leaves contextArg pointing at the unwrapped driver. + static ALWAYS_INLINE JSValue contextForResume(JSValue& contextArg) + { + if (contextArg.isCell()) { + JSCell* cell = contextArg.asCell(); + if (cell->type() == JSAsyncFunctionGeneratorType) + return uncheckedDowncast(cell)->asyncContext(); + if (cell->type() == JSAsyncGeneratorType) + return uncheckedDowncast(cell)->asyncContext(); + } + return unwrapContextTuple(contextArg); + } + private: VM& m_vm; InternalFieldTuple* m_asyncContextData { nullptr }; diff --git a/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.cpp b/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.cpp index 961b836fda30..cae35c533a3a 100644 --- a/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.cpp +++ b/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.cpp @@ -27,16 +27,36 @@ #include "JSAsyncFunctionGenerator.h" #include "JSCInlines.h" +#include "InternalFieldTuple.h" +#include "JSGlobalObject.h" #include "JSInternalFieldObjectImplInlines.h" namespace JSC { const ClassInfo JSAsyncFunctionGenerator::s_info = { "AsyncFunctionGenerator"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSAsyncFunctionGenerator) }; +#if USE(BUN_JSC_ADDITIONS) +// The AsyncContext slot starts out holding the async context that was current +// when the async function was called, so asyncFunctionDrive can tell whether +// the function's synchronous prefix changed it (see JSMicrotask.cpp). The +// DFG/FTL inline allocation paths do the same load. +static ALWAYS_INLINE void recordEntryAsyncContext(VM& vm, JSAsyncFunctionGenerator* generator, Structure* structure) +{ + if (auto* data = structure->globalObject()->m_asyncContextData.get()) { + JSValue current = data->getInternalField(0); + if (!current.isUndefined()) + generator->setAsyncContext(vm, current); + } +} +#endif + JSAsyncFunctionGenerator* JSAsyncFunctionGenerator::create(VM& vm, Structure* structure) { JSAsyncFunctionGenerator* generator = new (NotNull, allocateCell(vm)) JSAsyncFunctionGenerator(vm, structure); generator->finishCreation(vm); +#if USE(BUN_JSC_ADDITIONS) + recordEntryAsyncContext(vm, generator, structure); +#endif return generator; } @@ -44,6 +64,9 @@ JSAsyncFunctionGenerator* JSAsyncFunctionGenerator::createWithInitialValues(VM& { JSAsyncFunctionGenerator* generator = new (NotNull, allocateCell(vm)) JSAsyncFunctionGenerator(vm, structure); generator->finishCreation(vm); +#if USE(BUN_JSC_ADDITIONS) + recordEntryAsyncContext(vm, generator, structure); +#endif return generator; } diff --git a/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.h b/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.h index 386468725505..9fab485d4661 100644 --- a/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.h +++ b/Source/JavaScriptCore/runtime/JSAsyncFunctionGenerator.h @@ -30,9 +30,9 @@ namespace JSC { -class JSAsyncFunctionGenerator final : public JSInternalFieldObjectImpl<5> { +class JSAsyncFunctionGenerator final : public JSInternalFieldObjectImpl<6> { public: - using Base = JSInternalFieldObjectImpl<5>; + using Base = JSInternalFieldObjectImpl<6>; template static GCClient::IsoSubspace* subspaceFor(VM& vm) @@ -52,8 +52,12 @@ class JSAsyncFunctionGenerator final : public JSInternalFieldObjectImpl<5> { This, Frame, Context, + // Bun async context (AsyncLocalStorage) captured at the most recent await and + // restored when the function resumes. A suspended async function has exactly one + // outstanding await, so one slot replaces a per-await InternalFieldTuple. + AsyncContext, }; - static_assert(numberOfInternalFields == 5); + static_assert(numberOfInternalFields == 6); static_assert(static_cast(Field::State) == static_cast(JSGenerator::Field::State)); static_assert(static_cast(Field::Next) == static_cast(JSGenerator::Field::Next)); static_assert(static_cast(Field::This) == static_cast(JSGenerator::Field::This)); @@ -67,6 +71,7 @@ class JSAsyncFunctionGenerator final : public JSInternalFieldObjectImpl<5> { jsUndefined(), jsUndefined(), jsUndefined(), + jsUndefined(), } }; } @@ -108,6 +113,16 @@ class JSAsyncFunctionGenerator final : public JSInternalFieldObjectImpl<5> { return Base::internalField(static_cast(Field::Context)).get(); } + JSValue asyncContext() const + { + return Base::internalField(static_cast(Field::AsyncContext)).get(); + } + + void setAsyncContext(VM& vm, JSValue value) + { + Base::internalField(static_cast(Field::AsyncContext)).set(vm, this, value); + } + DECLARE_EXPORT_INFO; DECLARE_VISIT_CHILDREN; diff --git a/Source/JavaScriptCore/runtime/JSAsyncGenerator.h b/Source/JavaScriptCore/runtime/JSAsyncGenerator.h index 26de3dc50daf..25993f70a202 100644 --- a/Source/JavaScriptCore/runtime/JSAsyncGenerator.h +++ b/Source/JavaScriptCore/runtime/JSAsyncGenerator.h @@ -31,9 +31,9 @@ namespace JSC { -class JSAsyncGenerator final : public JSInternalFieldObjectImpl<10> { +class JSAsyncGenerator final : public JSInternalFieldObjectImpl<11> { public: - using Base = JSInternalFieldObjectImpl<10>; + using Base = JSInternalFieldObjectImpl<11>; template static GCClient::IsoSubspace* subspaceFor(VM& vm) @@ -86,8 +86,10 @@ class JSAsyncGenerator final : public JSInternalFieldObjectImpl<10> { ResumePromise, CachedDriverResult, CachedDriverResultTarget, + // Bun async context captured at the most recent await; see JSAsyncFunctionGenerator. + AsyncContext, }; - static_assert(numberOfInternalFields == 10); + static_assert(numberOfInternalFields == 11); static std::array initialValues() { return { { @@ -101,6 +103,7 @@ class JSAsyncGenerator final : public JSInternalFieldObjectImpl<10> { jsUndefined(), jsUndefined(), jsUndefined(), + jsUndefined(), } }; } @@ -197,6 +200,16 @@ class JSAsyncGenerator final : public JSInternalFieldObjectImpl<10> { Base::internalField(static_cast(Field::CachedDriverResultTarget)).set(vm, this, value); } + JSValue asyncContext() const + { + return Base::internalField(static_cast(Field::AsyncContext)).get(); + } + + void setAsyncContext(VM& vm, JSValue value) + { + Base::internalField(static_cast(Field::AsyncContext)).set(vm, this, value); + } + bool isQueueEmpty() const { return resumeMode() == static_cast(AsyncGeneratorResumeMode::Empty); diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 1b15d8377a73..f014f4e2e4c2 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -307,9 +307,9 @@ static ALWAYS_INLINE void settleDriverWithIteratorResult(JSGlobalObject* globalO { JSGlobalObject* realm = producer->realm(); #if USE(BUN_JSC_ADDITIONS) - // Capture Bun's async context alongside the driver so AsyncGeneratorDriverResume can restore it. + // Capture Bun's async context on the driver so AsyncGeneratorDriverResume can restore it. // The unwrapped target is still used for the cached-result identity check below. - JSValue wrappedTarget = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, target); + JSValue wrappedTarget = AsyncContextSwapScope::captureForAwait(vm, globalObject, target); #else UNUSED_PARAM(globalObject); JSValue wrappedTarget = target; @@ -551,7 +551,7 @@ static void asyncGeneratorCompleteStep(JSGlobalObject* globalObject, JSAsyncGene // resolveWithInternalMicrotask keeps resolvePromise's thenable check, matching a real Promise settlement. if (isThrow) { #if USE(BUN_JSC_ADDITIONS) - JSValue wrappedTarget = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, target); + JSValue wrappedTarget = AsyncContextSwapScope::captureForAwait(vm, globalObject, target); #else JSValue wrappedTarget = target; #endif @@ -1751,7 +1751,24 @@ JSC_DEFINE_HOST_FUNCTION(asyncFunctionDrive, (JSGlobalObject* globalObject, Call VM& vm = globalObject->vm(); JSValue resolution = callFrame->uncheckedArgument(0); auto* generator = uncheckedDowncast(callFrame->uncheckedArgument(1)); +#if USE(BUN_JSC_ADDITIONS) + // This is the first suspension of the async function: its synchronous + // prefix is about to return to the caller. If that prefix changed the async + // context (an `enterWith`-style activation), give the embedder a chance to + // decide what the caller should observe. The continuation itself keeps the + // prefix's context (captured below). + JSValue entryContext = generator->asyncContext(); +#endif asyncFunctionArrangeAwaitResume(globalObject, vm, generator, resolution); +#if USE(BUN_JSC_ADDITIONS) + if (auto hook = vm.asyncContextLeaveAsyncFrameHook) [[unlikely]] { + if (auto* data = globalObject->m_asyncContextData.get()) { + JSValue current = data->getInternalField(0); + if (current != entryContext) + data->putInternalField(vm, 0, hook(globalObject, entryContext, current)); + } + } +#endif return encodedJSUndefined(); } @@ -1845,7 +1862,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas // context may be an InternalFieldTuple [userContext, asyncContext]; the resolving // functions keep the tuple as-is, so only peek at field 1 for the swap. JSValue peek = context; - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(peek)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(peek)); #endif auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, task, context); @@ -1907,16 +1924,24 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas RELEASE_AND_RETURN(scope, promiseAnyResolveJob(resultPromise->realm(), vm, globalContext, arguments[1], static_cast(arguments[2].asAnyInt()), static_cast(payload))); } +#if USE(BUN_JSC_ADDITIONS) + case InternalMicrotask::PromiseReactionJobWithAsyncContext: +#endif case InternalMicrotask::PromiseReactionJob: { JSValue promiseOrCapability = arguments[0]; JSValue handler = arguments[1]; #if USE(BUN_JSC_ADDITIONS) - // arguments[3] is either an InternalFieldTuple [userContext, asyncContext] - // or userContext directly (legacy behavior). The scope stays active through - // resolvePromise/rejectPromise so thenables returned from the handler - // capture the correct async context, and restores on every return. + // arguments[3] is Bun's async context itself (PromiseReactionJobWithAsyncContext), an + // InternalFieldTuple [userContext, asyncContext] (performPromiseThenWithContext), or a + // bare userContext. The scope stays active through resolvePromise/rejectPromise so + // thenables returned from the handler capture the correct async context. JSValue userContext = arguments[3]; - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(userContext)); + JSValue asyncContext; + if (task == InternalMicrotask::PromiseReactionJobWithAsyncContext) + asyncContext = std::exchange(userContext, JSValue()); + else + asyncContext = AsyncContextSwapScope::unwrapContextTuple(userContext); + AsyncContextSwapScope asyncContextScope(vm, globalObject, asyncContext); #endif JSValue result; @@ -2001,8 +2026,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - // contextArg may be an InternalFieldTuple [generator, asyncContext]. - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(contextArg)); #endif auto* generator = uncheckedDowncast(contextArg); JSGlobalObject* generatorGlobalObject = generator->realm(); @@ -2026,7 +2050,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::AsyncGeneratorYieldAwaited: { JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(contextArg)); #endif auto* generator = uncheckedDowncast(contextArg); scope.release(); @@ -2037,7 +2061,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::AsyncGeneratorBodyCallNormal: { JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(contextArg)); #endif auto* generator = uncheckedDowncast(contextArg); scope.release(); @@ -2048,7 +2072,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::AsyncGeneratorBodyCallReturn: { JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(contextArg)); #endif auto* generator = uncheckedDowncast(contextArg); scope.release(); @@ -2059,7 +2083,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::AsyncGeneratorAwaitReturn: { JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(contextArg)); #endif auto* generator = uncheckedDowncast(contextArg); scope.release(); @@ -2070,7 +2094,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::AsyncGeneratorDriverResume: { JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::contextForResume(contextArg)); #endif scope.release(); asyncGeneratorDriverResume(vm, contextArg, arguments[1], static_cast(payload), microtaskCallCache); diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index ff34f32f3d6a..6761007d7d1c 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -345,8 +345,10 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue bool rejectedCallable = onRejected.isCallable(); #if USE(BUN_JSC_ADDITIONS) - // Capture async context for promise reaction as [userContext (undefined), asyncContext]. - JSValue context = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, jsUndefined()); + // Capture Bun's async context for the reaction. It travels as the reaction's context with the + // PromiseReactionJobWithAsyncContext tag so no [userContext, asyncContext] tuple is allocated. + JSValue context = AsyncContextSwapScope::current(globalObject); + InternalMicrotask reactionJob = context.isUndefined() ? InternalMicrotask::PromiseReactionJob : InternalMicrotask::PromiseReactionJobWithAsyncContext; #endif switch (status()) { @@ -375,7 +377,7 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue reaction = JSFullPromiseReaction::create(vm, promiseOrCapability, fulfilledCallable ? onFulfilled : jsUndefined(), rejectedCallable ? onRejected : jsUndefined(), - context, existing); + context, existing, InternalMicrotask::PromiseReactionJobWithAsyncContext); } else #endif if (onlyFulfill) @@ -396,7 +398,7 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, this, JSPromiseRejectionOperation::Handle); if (rejectedCallable) #if USE(BUN_JSC_ADDITIONS) - globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, settled, context); + globalObject->queueMicrotask(vm, reactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, settled, context); #else globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, settled); #endif @@ -409,7 +411,7 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue JSValue settled = settlementValue(); if (fulfilledCallable) #if USE(BUN_JSC_ADDITIONS) - globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, settled, context); + globalObject->queueMicrotask(vm, reactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, settled, context); #else globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, settled); #endif @@ -919,6 +921,8 @@ void JSPromise::triggerPromiseReactions(VM& vm, JSGlobalObject* globalObject, St } JSValue context = fullReaction->context(); if (!context.isUndefinedOrNull()) { + if (fullReaction->internalMicrotask() == InternalMicrotask::PromiseReactionJobWithAsyncContext) + task = InternalMicrotask::PromiseReactionJobWithAsyncContext; globalObject->queueMicrotask(vm, task, static_cast(status), promise, handler, arg, context); return; } @@ -965,10 +969,10 @@ void JSPromise::triggerPromiseReactions(VM& vm, JSGlobalObject* globalObject, St void JSPromise::resolveWithInternalMicrotaskForAsyncAwait(JSGlobalObject* globalObject, VM& vm, JSValue resolution, InternalMicrotask task, JSValue context) { #if USE(BUN_JSC_ADDITIONS) - // Capture Bun's async context at the point of await and wrap it with the generator context. - // This allows AsyncFunctionResume and related microtasks to restore the async context when - // resuming the async function. - JSValue wrappedContext = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, context); + // Capture Bun's async context at the point of await so AsyncFunctionResume and related + // microtasks restore it when resuming. For async (generator) functions this is a store into + // the generator; other drivers get an InternalFieldTuple [driver, asyncContext]. + JSValue wrappedContext = AsyncContextSwapScope::captureForAwait(vm, globalObject, context); #define BUN_CONTEXT wrappedContext #else #define BUN_CONTEXT context diff --git a/Source/JavaScriptCore/runtime/JSPromiseReaction.cpp b/Source/JavaScriptCore/runtime/JSPromiseReaction.cpp index 3e9117bd9dff..34c3929fa29c 100644 --- a/Source/JavaScriptCore/runtime/JSPromiseReaction.cpp +++ b/Source/JavaScriptCore/runtime/JSPromiseReaction.cpp @@ -107,9 +107,10 @@ DEFINE_VISIT_CHILDREN(JSSlimPromiseReaction); const ClassInfo JSFullPromiseReaction::s_info = { "FullPromiseReaction"_s, &JSPromiseReaction::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFullPromiseReaction) }; -JSFullPromiseReaction* JSFullPromiseReaction::create(VM& vm, JSValue promise, JSValue onFulfilled, JSValue onRejected, JSValue context, JSPromiseReaction* next) +JSFullPromiseReaction* JSFullPromiseReaction::create(VM& vm, JSValue promise, JSValue onFulfilled, JSValue onRejected, JSValue context, JSPromiseReaction* next, InternalMicrotask task) { - JSFullPromiseReaction* result = new (NotNull, allocateCell(vm)) JSFullPromiseReaction(vm, vm.fullPromiseReactionStructure.get(), promise, onFulfilled, onRejected, context, next); + ASSERT(task == InternalMicrotask::None || task == InternalMicrotask::PromiseReactionJobWithAsyncContext); + JSFullPromiseReaction* result = new (NotNull, allocateCell(vm)) JSFullPromiseReaction(vm, vm.fullPromiseReactionStructure.get(), promise, onFulfilled, onRejected, context, next, task); result->finishCreation(vm); return result; } diff --git a/Source/JavaScriptCore/runtime/JSPromiseReaction.h b/Source/JavaScriptCore/runtime/JSPromiseReaction.h index ebd95df45cc0..18eed862cd62 100644 --- a/Source/JavaScriptCore/runtime/JSPromiseReaction.h +++ b/Source/JavaScriptCore/runtime/JSPromiseReaction.h @@ -121,7 +121,7 @@ class JSFullPromiseReaction final : public JSPromiseReaction { static Structure* createStructure(VM&, JSGlobalObject*, JSValue); - static JSFullPromiseReaction* create(VM&, JSValue promise, JSValue onFulfilled, JSValue onRejected, JSValue context, JSPromiseReaction* next); + static JSFullPromiseReaction* create(VM&, JSValue promise, JSValue onFulfilled, JSValue onRejected, JSValue context, JSPromiseReaction* next, InternalMicrotask = InternalMicrotask::None); JSValue onFulfilled() const { return m_onFulfilled.get(); } JSValue onRejected() const { return m_onRejected.get(); } @@ -133,8 +133,8 @@ class JSFullPromiseReaction final : public JSPromiseReaction { private: - JSFullPromiseReaction(VM& vm, Structure* structure, JSValue promise, JSValue onFulfilled, JSValue onRejected, JSValue context, JSPromiseReaction* next) - : Base(vm, structure, promise, next, static_cast(InternalMicrotask::None)) + JSFullPromiseReaction(VM& vm, Structure* structure, JSValue promise, JSValue onFulfilled, JSValue onRejected, JSValue context, JSPromiseReaction* next, InternalMicrotask task) + : Base(vm, structure, promise, next, static_cast(task)) , m_onFulfilled(onFulfilled, WriteBarrierEarlyInit) , m_onRejected(onRejected, WriteBarrierEarlyInit) , m_context(context, WriteBarrierEarlyInit) diff --git a/Source/JavaScriptCore/runtime/JSType.h b/Source/JavaScriptCore/runtime/JSType.h index f83737c448aa..e6c54d5a7b3d 100644 --- a/Source/JavaScriptCore/runtime/JSType.h +++ b/Source/JavaScriptCore/runtime/JSType.h @@ -182,6 +182,10 @@ struct JSTypeRange { }; static constexpr uint8_t EmbedderArrayLikeType = 0b11101101; +// Bun's telemetry span cell (a JSInternalFieldObjectImpl subclass defined by +// the embedder). Named here so builtins get an @isTelemetrySpan brand check +// that lowers to IsCellWithType. +static constexpr uint8_t BunTelemetrySpanType = 0b11100001; static constexpr uint32_t LastValueCompareCellType = HeapBigIntType; diff --git a/Source/JavaScriptCore/runtime/Microtask.h b/Source/JavaScriptCore/runtime/Microtask.h index 73b5f8d3114c..a7eb4e2a256d 100644 --- a/Source/JavaScriptCore/runtime/Microtask.h +++ b/Source/JavaScriptCore/runtime/Microtask.h @@ -52,6 +52,9 @@ enum class InternalMicrotask : uint8_t { PromiseFinallyAwaitJob, PromiseReactionJob, +#if USE(BUN_JSC_ADDITIONS) + PromiseReactionJobWithAsyncContext, // PromiseReactionJob whose trailing argument is Bun's async context rather than a user context +#endif AsyncFunctionResume, AsyncFromSyncIteratorContinue, diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 2577c4b2c26e..4600f56f1c06 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -1366,6 +1366,14 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { SynchronousModuleQueue* prev { nullptr }; }; SynchronousModuleQueue* m_synchronousModuleQueue { nullptr }; + + // Called when an async function first suspends and its synchronous prefix + // changed the async context (m_asyncContextData field 0) relative to what + // was current at the call. Returns the value the caller should observe from + // here on. Null = Node's AsyncLocalStorage.enterWith semantics (leave the + // changed value in place). + using AsyncContextLeaveAsyncFrameHook = JSValue (*)(JSGlobalObject*, JSValue atEntry, JSValue current); + AsyncContextLeaveAsyncFrameHook asyncContextLeaveAsyncFrameHook { nullptr }; private: #endif From 12f5991857b796d4dc7124e73981a36109f7d69c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 21 Aug 2026 12:00:22 +0000 Subject: [PATCH 02/13] DFG: give SpeculateInt32Operand/SpeculateBooleanOperand move constructors (CallDOM with Int32/Boolean args double-unlocked) --- Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h index ea1a0c89fccc..7429f94036ac 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h @@ -2366,8 +2366,22 @@ class SpeculateInt32Operand { gpr(); } + explicit SpeculateInt32Operand(SpeculateInt32Operand&& other) + : m_jit(other.m_jit) + , m_edge(other.m_edge) + , m_gprOrInvalid(other.m_gprOrInvalid) +#ifndef NDEBUG + , m_format(other.m_format) +#endif + { + other.m_gprOrInvalid = InvalidGPRReg; + other.m_edge = Edge(); + } + ~SpeculateInt32Operand() { + if (!m_edge) + return; ASSERT(m_gprOrInvalid != InvalidGPRReg); m_jit->unlock(m_gprOrInvalid); } @@ -2759,8 +2773,19 @@ class SpeculateBooleanOperand { gpr(); } + explicit SpeculateBooleanOperand(SpeculateBooleanOperand&& other) + : m_jit(other.m_jit) + , m_edge(other.m_edge) + , m_gprOrInvalid(other.m_gprOrInvalid) + { + other.m_gprOrInvalid = InvalidGPRReg; + other.m_edge = Edge(); + } + ~SpeculateBooleanOperand() { + if (!m_edge) + return; ASSERT(m_gprOrInvalid != InvalidGPRReg); m_jit->unlock(m_gprOrInvalid); } From 67b6b5b8e9b4f24f6e08bdc7c9f14dc3d0f8abcb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 22 Aug 2026 23:30:52 +0000 Subject: [PATCH 03/13] Add JSTracedFunction: a JSFunction that runs embedder enter/leave hooks around a direct call For Bun's tracing API (Bun.otel.span / Bun.otel.wrap). A JSTracedFunction forwards this and its arguments to a target (or, in the CallLast shape, calls its last argument with the enter hook's value) from a JIT thunk modelled on boundFunctionCallGenerator / remoteFunctionCallGenerator, so the target is a direct JIT call and the only overhead is the two operations. An exception unwinding through the thunk frame is reported to the embedder from Interpreter::unwind (the hook's value is kept in a frame local), and JSPromise::addSettlementObserver lets the leave hook watch a returned promise without allocating a derived promise or marking it handled. The generic host-function path (tracedFunctionCallGeneric) is used without the JIT and on Windows. --- Source/JavaScriptCore/CMakeLists.txt | 2 + Source/JavaScriptCore/Sources.txt | 1 + .../JavaScriptCore/dfg/DFGSpeculativeJIT.cpp | 5 + Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp | 9 + Source/JavaScriptCore/heap/Heap.cpp | 1 + Source/JavaScriptCore/heap/Heap.h | 1 + .../interpreter/Interpreter.cpp | 21 ++ Source/JavaScriptCore/jit/JITOperations.cpp | 48 ++++ Source/JavaScriptCore/jit/JITOperations.h | 6 + Source/JavaScriptCore/jit/ThunkGenerators.cpp | 226 ++++++++++++++++++ Source/JavaScriptCore/jit/ThunkGenerators.h | 3 + .../runtime/FunctionPrototype.cpp | 7 +- Source/JavaScriptCore/runtime/Intrinsic.h | 1 + Source/JavaScriptCore/runtime/JSFunction.cpp | 24 +- .../runtime/JSFunctionInlines.h | 17 ++ .../JavaScriptCore/runtime/JSGlobalObject.cpp | 10 + .../JavaScriptCore/runtime/JSGlobalObject.h | 6 + Source/JavaScriptCore/runtime/JSMicrotask.cpp | 8 + Source/JavaScriptCore/runtime/JSPromise.cpp | 37 ++- Source/JavaScriptCore/runtime/JSPromise.h | 6 + .../runtime/JSTracedFunction.cpp | 148 ++++++++++++ .../JavaScriptCore/runtime/JSTracedFunction.h | 98 ++++++++ .../runtime/JSTracedFunctionInlines.h | 43 ++++ Source/JavaScriptCore/runtime/Microtask.h | 1 + Source/JavaScriptCore/runtime/VM.cpp | 28 +++ Source/JavaScriptCore/runtime/VM.h | 22 ++ 26 files changed, 776 insertions(+), 3 deletions(-) create mode 100644 Source/JavaScriptCore/runtime/JSTracedFunction.cpp create mode 100644 Source/JavaScriptCore/runtime/JSTracedFunction.h create mode 100644 Source/JavaScriptCore/runtime/JSTracedFunctionInlines.h diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index fa79d0d4aade..45ff35104858 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -1518,6 +1518,8 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS runtime/JSRegExpStringIterator.h runtime/JSRegExpStringIteratorInlines.h runtime/JSRemoteFunction.h + runtime/JSTracedFunction.h + runtime/JSTracedFunctionInlines.h runtime/JSRemoteFunctionInlines.h runtime/JSRunLoopTimer.h runtime/JSScope.h diff --git a/Source/JavaScriptCore/Sources.txt b/Source/JavaScriptCore/Sources.txt index 23315183aaba..1b09136d21d9 100644 --- a/Source/JavaScriptCore/Sources.txt +++ b/Source/JavaScriptCore/Sources.txt @@ -962,6 +962,7 @@ runtime/JSPropertyNameEnumerator.cpp runtime/JSRawJSONObject.cpp runtime/JSRegExpStringIterator.cpp runtime/JSRemoteFunction.cpp +runtime/JSTracedFunction.cpp runtime/JSRunLoopTimer.cpp runtime/JSScope.cpp runtime/JSSegmentedVariableObject.cpp diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp index e320f384a8e2..1d076f5de272 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp @@ -61,6 +61,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN #include "JSAsyncFunctionGenerator.h" #include "JSAsyncGeneratorFunction.h" #include "JSBoundFunction.h" +#include "JSTracedFunction.h" #include "JSCInlines.h" #include "JSCellButterfly.h" #include "JSGeneratorFunction.h" @@ -11248,6 +11249,10 @@ void SpeculativeJIT::compileFunctionToString(Node* node) static_assert(std::is_final_v, "We don't handle subclasses when comparing classInfo below"); slowCases.append(branchPtr(Equal, result.gpr(), TrustedImmPtr(JSRemoteFunction::info()))); +#if USE(BUN_JSC_ADDITIONS) + static_assert(std::is_final_v, "We don't handle subclasses when comparing classInfo below"); + slowCases.append(branchPtr(Equal, result.gpr(), TrustedImmPtr(JSTracedFunction::info()))); +#endif getExecutable(*this, function.gpr(), executable.gpr()); Jump isNativeExecutable = branch8(Equal, Address(executable.gpr(), JSCell::typeInfoTypeOffset()), TrustedImm32(NativeExecutableType)); diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp index c1411def513f..9ae86b8978ec 100644 --- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp +++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp @@ -88,6 +88,7 @@ #include "JSAsyncGenerator.h" #include "JSAsyncGeneratorFunction.h" #include "JSBoundFunction.h" +#include "JSTracedFunction.h" #include "JSCInlines.h" #include "JSCellButterfly.h" #include "JSGenerator.h" @@ -11651,7 +11652,15 @@ IGNORE_CLANG_WARNINGS_END static_assert(std::is_final_v, "We don't handle subclasses when comparing classInfo below"); m_out.appendTo(notBoundFunctionCase, notBoundOrRemoteFunctionCase); +#if USE(BUN_JSC_ADDITIONS) + LBasicBlock notTracedFunctionCase = m_out.newBlock(); + m_out.branch(m_out.equal(classInfo, m_out.constIntPtr(JSRemoteFunction::info())), rarely(slowCase), usually(notTracedFunctionCase)); + m_out.appendTo(notTracedFunctionCase, notBoundOrRemoteFunctionCase); + static_assert(std::is_final_v, "We don't handle subclasses when comparing classInfo below"); + m_out.branch(m_out.equal(classInfo, m_out.constIntPtr(JSTracedFunction::info())), rarely(slowCase), usually(notBoundOrRemoteFunctionCase)); +#else m_out.branch(m_out.equal(classInfo, m_out.constIntPtr(JSRemoteFunction::info())), rarely(slowCase), usually(notBoundOrRemoteFunctionCase)); +#endif LBasicBlock lastNext = m_out.appendTo(notBoundOrRemoteFunctionCase, nativeExecutableCase); LValue executable = getExecutable(function); diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index 2b2046fd4f3d..9db492167d22 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -65,6 +65,7 @@ #include "JSPromiseReaction.h" #include "JSRawJSONObject.h" #include "JSRemoteFunction.h" +#include "JSTracedFunction.h" #include "JSSentinel.h" #include "JSVirtualMachineInternal.h" #include "JSWeakMap.h" diff --git a/Source/JavaScriptCore/heap/Heap.h b/Source/JavaScriptCore/heap/Heap.h index 61ee1f3769ea..5176f07ddd27 100644 --- a/Source/JavaScriptCore/heap/Heap.h +++ b/Source/JavaScriptCore/heap/Heap.h @@ -285,6 +285,7 @@ class Heap; v(proxyRevokeSpace, cellHeapCellType, ProxyRevoke) \ v(rawJSONObjectSpace, cellHeapCellType, JSRawJSONObject) \ v(remoteFunctionSpace, cellHeapCellType, JSRemoteFunction) \ + v(tracedFunctionSpace, cellHeapCellType, JSTracedFunction) \ v(scopedArgumentsTableSpace, destructibleCellHeapCellType, ScopedArgumentsTable) \ v(setIteratorSpace, cellHeapCellType, JSSetIterator) \ v(setSpace, cellHeapCellType, JSSet) \ diff --git a/Source/JavaScriptCore/interpreter/Interpreter.cpp b/Source/JavaScriptCore/interpreter/Interpreter.cpp index 32e6f65ca0af..c391d7552718 100644 --- a/Source/JavaScriptCore/interpreter/Interpreter.cpp +++ b/Source/JavaScriptCore/interpreter/Interpreter.cpp @@ -68,6 +68,7 @@ #include "JSPromiseCombinatorsGlobalContext.h" #include "JSPromiseReaction.h" #include "JSRemoteFunction.h" +#include "JSTracedFunction.h" #include "JSString.h" #include "JSWebAssemblyException.h" #include "LLIntThunks.h" @@ -902,6 +903,16 @@ class UnwindFunctor : UnwindFunctorBase { // translate the exception before jumping to the handler. m_seenRemoteFunction = uncheckedDowncast(m_callFrame->jsCallee()); } +#if USE(BUN_JSC_ADDITIONS) && ENABLE(JIT) + if (!m_callFrame->isNativeCalleeFrame() && !m_callFrame->codeBlock()) { + if (auto* traced = dynamicDowncast(m_callFrame->jsCallee()); traced && m_callFrame->callSiteIndex().bits() == JSTracedFunction::spanLocalValidCallSiteIndex) { + // A tracedFunctionCallGenerator frame past its enter hook: the local holds the hook's value. + JSValue span = m_callFrame->registers()[virtualRegisterForLocal(JSTracedFunction::spanLocal).offset()].jsValue(); + if (span) + m_vm.m_unwoundTracedFrames.append({ traced, span }); + } + } +#endif JSGlobalObject* globalObject = m_callFrame->lexicalGlobalObject(m_vm); notifyDebuggerOfUnwinding(globalObject, m_callFrame); @@ -1008,6 +1019,16 @@ NEVER_INLINE CatchInfo Interpreter::unwind(VM& vm, CallFrame*& callFrame, Except sanitizeRemoteFunctionException(vm, seenRemoteFunction, exception); exception = scope.exception(); // clear m_needExceptionCheck } +#if USE(BUN_JSC_ADDITIONS) + if (!vm.m_unwoundTracedFrames.isEmpty()) { + auto frames = std::exchange(vm.m_unwoundTracedFrames, { }); + if (auto unwindHook = vm.tracedFunctionHooks().unwind) { + for (auto& [traced, span] : frames) + unwindHook(traced->globalObject(), traced, span, exception); + } + exception = scope.exception(); + } +#endif if (vm.hasCheckpointOSRSideState()) vm.popAllCheckpointOSRSideStateUntil(callFrame); diff --git a/Source/JavaScriptCore/jit/JITOperations.cpp b/Source/JavaScriptCore/jit/JITOperations.cpp index 2f2475fded76..4e732c7232b6 100644 --- a/Source/JavaScriptCore/jit/JITOperations.cpp +++ b/Source/JavaScriptCore/jit/JITOperations.cpp @@ -68,6 +68,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN #include "JSMicrotask.h" #include "JSPromise.h" #include "JSRemoteFunction.h" +#include "JSTracedFunction.h" #include "JSSentinel.h" #include "JSSetIterator.h" #include "JSStringIteratorInlines.h" @@ -242,6 +243,53 @@ JSC_DEFINE_JIT_OPERATION(operationMaterializeRemoteFunctionTargetCode, UGPRPair, OPERATION_RETURN(scope, materializeTargetCode(vm, targetFunction)); } +#if USE(BUN_JSC_ADDITIONS) +JSC_DEFINE_JIT_OPERATION(operationTracedFunctionEnter, EncodedJSValue, (JSTracedFunction* callee)) +{ + JSGlobalObject* globalObject = callee->globalObject(); + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + auto scope = DECLARE_THROW_SCOPE(vm); + auto enter = vm.tracedFunctionHooks().enter; + if (!enter) + OPERATION_RETURN(scope, encodedJSValue()); + OPERATION_RETURN(scope, enter(globalObject, callFrame, callee)); +} + +JSC_DEFINE_JIT_OPERATION(operationTracedFunctionLeave, EncodedJSValue, (JSTracedFunction* callee, EncodedJSValue span, EncodedJSValue result)) +{ + JSGlobalObject* globalObject = callee->globalObject(); + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + auto scope = DECLARE_THROW_SCOPE(vm); + auto leave = vm.tracedFunctionHooks().leave; + if (!leave) + OPERATION_RETURN(scope, result); + OPERATION_RETURN(scope, leave(globalObject, callee, span, result)); +} + +JSC_DEFINE_JIT_OPERATION(operationMaterializeTracedFunctionTargetCode, void*, (JSTracedFunction* callee, JSFunction* targetFunction)) +{ + JSGlobalObject* globalObject = callee->globalObject(); + VM& vm = globalObject->vm(); + CallFrame* callFrame = DECLARE_CALL_FRAME(vm); + JITOperationPrologueCallFrameTracer tracer(vm, callFrame); + auto scope = DECLARE_THROW_SCOPE(vm); + // materializeTargetCode, returning only the entry point: the thunk reloads + // the CodeBlock from the executable (keeps this off UGPRPair for Windows). + ExecutableBase* executable = targetFunction->executable(); + DeferTraps deferTraps(vm); + CodeBlock* codeBlockSlot = nullptr; + if (!executable->isHostFunction()) { + static_cast(executable)->prepareForExecution(vm, targetFunction, targetFunction->scopeUnchecked(), CodeSpecializationKind::CodeForCall, codeBlockSlot); + OPERATION_RETURN_IF_EXCEPTION(scope, nullptr); + } + OPERATION_RETURN(scope, executable->entrypointFor(CodeSpecializationKind::CodeForCall, ArityCheckMode::MustCheckArity).taggedPtr()); +} +#endif + JSC_DEFINE_JIT_OPERATION(operationThrowRemoteFunctionException, EncodedJSValue, (JSRemoteFunction* callee)) { JSGlobalObject* globalObject = callee->realm(); diff --git a/Source/JavaScriptCore/jit/JITOperations.h b/Source/JavaScriptCore/jit/JITOperations.h index 1dfb2154aacf..941dfa2cbbeb 100644 --- a/Source/JavaScriptCore/jit/JITOperations.h +++ b/Source/JavaScriptCore/jit/JITOperations.h @@ -62,6 +62,7 @@ class JSGlobalObject; class JSLexicalEnvironment; class JSObject; class JSRemoteFunction; +class JSTracedFunction; class JSScope; class JSString; class JSValue; @@ -168,6 +169,11 @@ JSC_DECLARE_JIT_OPERATION(operationThrowIteratorResultIsNotObject, void, (JSGlob JSC_DECLARE_JIT_OPERATION(operationGetWrappedValueForCaller, EncodedJSValue, (JSRemoteFunction*, EncodedJSValue)); JSC_DECLARE_JIT_OPERATION(operationGetWrappedValueForTarget, EncodedJSValue, (JSRemoteFunction*, EncodedJSValue)); JSC_DECLARE_JIT_OPERATION(operationMaterializeRemoteFunctionTargetCode, UGPRPair, (JSRemoteFunction*)); +#if USE(BUN_JSC_ADDITIONS) +JSC_DECLARE_JIT_OPERATION(operationTracedFunctionEnter, EncodedJSValue, (JSTracedFunction*)); +JSC_DECLARE_JIT_OPERATION(operationTracedFunctionLeave, EncodedJSValue, (JSTracedFunction*, EncodedJSValue span, EncodedJSValue result)); +JSC_DECLARE_JIT_OPERATION(operationMaterializeTracedFunctionTargetCode, void*, (JSTracedFunction*, JSFunction* target)); +#endif JSC_DECLARE_JIT_OPERATION(operationMaterializeBoundFunctionTargetCode, UGPRPair, (JSBoundFunction*)); JSC_DECLARE_JIT_OPERATION(operationThrowRemoteFunctionException, EncodedJSValue, (JSRemoteFunction*)); diff --git a/Source/JavaScriptCore/jit/ThunkGenerators.cpp b/Source/JavaScriptCore/jit/ThunkGenerators.cpp index 4cac108da754..743003ed2d5b 100644 --- a/Source/JavaScriptCore/jit/ThunkGenerators.cpp +++ b/Source/JavaScriptCore/jit/ThunkGenerators.cpp @@ -30,6 +30,7 @@ #include "JITThunks.h" #include "JSBoundFunction.h" #include "JSRemoteFunction.h" +#include "JSTracedFunction.h" #include "LLIntThunks.h" #include "MaxFrameExtentForSlowPathCall.h" #include "SpecializedThunkJIT.h" @@ -1446,6 +1447,231 @@ MacroAssemblerCodeRef boundFunctionCallGenerator(VM& vm) return FINALIZE_THUNK(linkBuffer, JITThunkPtrTag, "bound"_s, "Specialized thunk for bound function calls with no arguments"); } +#if USE(BUN_JSC_ADDITIONS) +// JSTracedFunction: operationTracedFunctionEnter, then the target called +// directly with the caller's `this` and arguments (Shape::Wrap) or with the +// enter hook's value as the only argument (Shape::CallLast), then +// operationTracedFunctionLeave on the result. An exception unwinds through this +// frame; Interpreter::unwind reads the span back from our frame local. +// Layout and slow paths follow boundFunctionCallGenerator / remoteFunctionCallGenerator. +MacroAssemblerCodeRef tracedFunctionCallGenerator(VM& vm) +{ + CCallHelpers jit; + jit.emitFunctionPrologue(); + + // Set up our call frame. + jit.storePtr(CCallHelpers::TrustedImmPtr(nullptr), CCallHelpers::addressFor(CallFrameSlot::codeBlock)); + jit.store32(CCallHelpers::TrustedImm32(0), CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); + + constexpr unsigned stackMisalignment = sizeof(CallerFrameAndPC) % stackAlignmentBytes(); + constexpr unsigned extraStackNeeded = stackMisalignment ? stackAlignmentBytes() - stackMisalignment : 0; + static constexpr int numFrameLocals = JSTracedFunction::numberOfFrameLocals; + VirtualRegister spanLocal = virtualRegisterForLocal(JSTracedFunction::spanLocal); + + jit.loadCell(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regT0); + jit.load32(CCallHelpers::lowWordFor(CallFrameSlot::argumentCountIncludingThis), GPRInfo::regT1); + + // Callee frame for max(argumentCountIncludingThis, 2) arguments plus our locals: + // stackAlign((numParams + numFrameLocals + (CallFrameHeaderSize - CallerFrameAndPCSize)) * sizeof(Register)) + jit.move(GPRInfo::regT1, GPRInfo::regT2); + auto atLeastTwo = jit.branch32(CCallHelpers::AboveOrEqual, GPRInfo::regT2, CCallHelpers::TrustedImm32(2)); + jit.move(CCallHelpers::TrustedImm32(2), GPRInfo::regT2); + atLeastTwo.link(&jit); + jit.add32(CCallHelpers::TrustedImm32(CallFrame::headerSizeInRegisters - CallerFrameAndPC::sizeInRegisters + numFrameLocals), GPRInfo::regT2, GPRInfo::regT2); + jit.lshift32(CCallHelpers::TrustedImm32(3), GPRInfo::regT2); + jit.add32(CCallHelpers::TrustedImm32(stackAlignmentBytes() - 1), GPRInfo::regT2); + jit.and32(CCallHelpers::TrustedImm32(-stackAlignmentBytes()), GPRInfo::regT2); + if (extraStackNeeded) + jit.add32(CCallHelpers::TrustedImm32(extraStackNeeded), GPRInfo::regT2); + + jit.negPtr(GPRInfo::regT2); + jit.addPtr(CCallHelpers::stackPointerRegister, GPRInfo::regT2); + CCallHelpers::Jump haveStackSpace = jit.branchPtr(CCallHelpers::LessThanOrEqual, CCallHelpers::AbsoluteAddress(vm.addressOfSoftStackLimit()), GPRInfo::regT2); + + // Throw Stack Overflow exception + jit.copyCalleeSavesToEntryFrameCalleeSavesBuffer(vm.topEntryFrame, GPRInfo::regT3); + jit.loadPtr(CCallHelpers::Address(GPRInfo::regT0, JSCallee::offsetOfScopeChain()), GPRInfo::regT3); + jit.setupArguments(GPRInfo::regT3); + jit.prepareCallOperation(vm); + jit.move(CCallHelpers::TrustedImmPtr(tagCFunction(operationThrowStackOverflowErrorFromThunk)), GPRInfo::nonArgGPR0); + emitPointerValidation(jit, GPRInfo::nonArgGPR0, OperationPtrTag); + jit.call(GPRInfo::nonArgGPR0, OperationPtrTag); + jit.jumpToExceptionHandler(vm); + + haveStackSpace.link(&jit); + jit.move(GPRInfo::regT2, CCallHelpers::stackPointerRegister); + + JSValueRegs valueRegs { GPRInfo::regT4 }; + + // Resolve the target into regT2: the wrapped function, or the last argument. + // Anything the fast path cannot call directly (not a JSFunction, or no + // JIT entry point yet) goes to tracedFunctionCallGeneric before any hook ran. + // Shape::CallLast without a function argument (`using span = span(name)`): + // just the enter hook, whose value is the result. + CCallHelpers::JumpList noCallee; + auto loadTarget = [&](GPRReg calleeGPR, GPRReg argumentCountGPR, GPRReg targetGPR, CCallHelpers::JumpList& slow) { + jit.loadPtr(CCallHelpers::Address(calleeGPR, JSTracedFunction::offsetOfTargetFunction()), targetGPR); + auto isWrap = jit.branchTestPtr(CCallHelpers::NonZero, targetGPR); + // Shape::CallLast: target = arguments[argumentCount - 1], if any and if a JSFunction. + noCallee.append(jit.branch32(CCallHelpers::Below, argumentCountGPR, CCallHelpers::TrustedImm32(2))); + jit.loadValue(CCallHelpers::addressFor(virtualRegisterForArgumentIncludingThis(0)).indexedBy(argumentCountGPR, CCallHelpers::TimesEight).withOffset(-static_cast(sizeof(Register))), JSValueRegs { targetGPR }); + noCallee.append(jit.branchIfNotCell(JSValueRegs { targetGPR }, DoNotHaveTagRegisters)); + // A non-function cell: objects that are callable (or an attributes + // object with no callback) take the generic path; other cells are "no callee". + auto isFunction = jit.branchIfType(targetGPR, JSFunctionType); + slow.append(jit.branchIfObject(targetGPR)); + noCallee.append(jit.jump()); + isFunction.link(&jit); + isWrap.link(&jit); + }; + auto loadCodePointer = [&](GPRReg targetGPR, GPRReg executableGPR, GPRReg codeGPR) { + jit.loadPtr(CCallHelpers::Address(targetGPR, JSFunction::offsetOfExecutableOrRareData()), executableGPR); + auto hasExecutable = jit.branchTestPtr(CCallHelpers::Zero, executableGPR, CCallHelpers::TrustedImm32(JSFunction::rareDataTag)); + jit.loadPtr(CCallHelpers::Address(executableGPR, FunctionRareData::offsetOfExecutable() - JSFunction::rareDataTag), executableGPR); + hasExecutable.link(&jit); + jit.loadPtr(CCallHelpers::Address(executableGPR, ExecutableBase::offsetOfJITCodeWithArityCheckFor(CodeSpecializationKind::CodeForCall)), codeGPR); + }; + + { + CCallHelpers::JumpList slow; + loadTarget(GPRInfo::regT0, GPRInfo::regT1, GPRInfo::regT2, slow); + loadCodePointer(GPRInfo::regT2, GPRInfo::regT3, GPRInfo::regT2); + slow.append(jit.branchTestPtr(CCallHelpers::Zero, GPRInfo::regT2)); + auto fast = jit.jump(); + slow.linkThunk(CodeLocationLabel { vm.jitStubs->ctiNativeTailCallWithoutSavedTags(vm) }, &jit); + fast.link(&jit); + } + + CCallHelpers::JumpList exceptionChecks; + + // span = operationTracedFunctionEnter(callee) + jit.setupArguments(GPRInfo::regT0); + jit.prepareCallOperation(vm); + jit.move(CCallHelpers::TrustedImmPtr(tagCFunction(operationTracedFunctionEnter)), GPRInfo::nonArgGPR0); + emitPointerValidation(jit, GPRInfo::nonArgGPR0, OperationPtrTag); + jit.call(GPRInfo::nonArgGPR0, OperationPtrTag); + exceptionChecks.append(jit.emitJumpIfException(vm)); + jit.setupResults(valueRegs); + jit.storeValue(valueRegs, jit.addressFor(spanLocal)); + // Tell Interpreter::unwind the span local is now meaningful (a native + // frame's CallSiteIndex is otherwise 0). + jit.store32(CCallHelpers::TrustedImm32(JSTracedFunction::spanLocalValidCallSiteIndex), CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); + + jit.loadCell(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regT0); + jit.load32(CCallHelpers::lowWordFor(CallFrameSlot::argumentCountIncludingThis), GPRInfo::regT1); + + // Build the callee frame. + jit.loadPtr(CCallHelpers::Address(GPRInfo::regT0, JSTracedFunction::offsetOfTargetFunction()), GPRInfo::regT2); + auto wrapShape = jit.branchTestPtr(CCallHelpers::NonZero, GPRInfo::regT2); + { + // Shape::CallLast: fn(span) with this = undefined. + jit.store32(CCallHelpers::TrustedImm32(2), CCallHelpers::calleeFrameLowWordSlot(CallFrameSlot::argumentCountIncludingThis)); + jit.storeTrustedValue(jsUndefined(), CCallHelpers::calleeArgumentSlot(0)); + auto haveSpan = jit.branchIfNotEmpty(valueRegs); + jit.moveTrustedValue(jsUndefined(), valueRegs); + haveSpan.link(&jit); + jit.storeValue(valueRegs, CCallHelpers::calleeArgumentSlot(1)); + jit.loadValue(CCallHelpers::addressFor(virtualRegisterForArgumentIncludingThis(0)).indexedBy(GPRInfo::regT1, CCallHelpers::TimesEight).withOffset(-static_cast(sizeof(Register))), JSValueRegs { GPRInfo::regT2 }); + } + auto frameBuilt = jit.jump(); + wrapShape.link(&jit); + { + // Shape::Wrap: forward this and every argument. + jit.store32(GPRInfo::regT1, CCallHelpers::calleeFrameLowWordSlot(CallFrameSlot::argumentCountIncludingThis)); + jit.move(GPRInfo::regT1, GPRInfo::regT3); + CCallHelpers::Label loop = jit.label(); + jit.sub32(CCallHelpers::TrustedImm32(1), GPRInfo::regT3); + jit.loadValue(CCallHelpers::addressFor(virtualRegisterForArgumentIncludingThis(0)).indexedBy(GPRInfo::regT3, CCallHelpers::TimesEight), valueRegs); + jit.storeValue(valueRegs, CCallHelpers::calleeArgumentSlot(0).indexedBy(GPRInfo::regT3, CCallHelpers::TimesEight)); + jit.branchTest32(CCallHelpers::NonZero, GPRInfo::regT3).linkTo(loop, &jit); + } + frameBuilt.link(&jit); + + // regT2 = target JSFunction (checked before the hook ran). + jit.storeCell(GPRInfo::regT2, CCallHelpers::calleeFrameSlot(CallFrameSlot::callee)); + loadCodePointer(GPRInfo::regT2, GPRInfo::regT1, GPRInfo::regT3); + auto codeExists = jit.branchTestPtr(CCallHelpers::NonZero, GPRInfo::regT3); + + // The enter hook may have allocated and jettisoned the target's code: re-materialize it. + jit.setupArguments(GPRInfo::regT0, GPRInfo::regT2); + jit.prepareCallOperation(vm); + jit.move(CCallHelpers::TrustedImmPtr(tagCFunction(operationMaterializeTracedFunctionTargetCode)), GPRInfo::nonArgGPR0); + emitPointerValidation(jit, GPRInfo::nonArgGPR0, OperationPtrTag); + jit.call(GPRInfo::nonArgGPR0, OperationPtrTag); + exceptionChecks.append(jit.emitJumpIfException(vm)); + jit.move(GPRInfo::returnValueGPR, GPRInfo::regT3); + jit.loadCell(CCallHelpers::calleeFrameSlot(CallFrameSlot::callee), GPRInfo::regT2); + jit.loadPtr(CCallHelpers::Address(GPRInfo::regT2, JSFunction::offsetOfExecutableOrRareData()), GPRInfo::regT1); + { + auto hasExecutable = jit.branchTestPtr(CCallHelpers::Zero, GPRInfo::regT1, CCallHelpers::TrustedImm32(JSFunction::rareDataTag)); + jit.loadPtr(CCallHelpers::Address(GPRInfo::regT1, FunctionRareData::offsetOfExecutable() - JSFunction::rareDataTag), GPRInfo::regT1); + hasExecutable.link(&jit); + } + + codeExists.link(&jit); + // regT1 = executable, regT3 = code pointer. + auto isNative = jit.branchIfNotType(GPRInfo::regT1, FunctionExecutableType); + jit.loadPtr(CCallHelpers::Address(GPRInfo::regT1, FunctionExecutable::offsetOfCodeBlockForCall()), GPRInfo::regT2); + jit.storePtr(GPRInfo::regT2, CCallHelpers::calleeFrameCodeBlockBeforeCall()); + isNative.link(&jit); + +#if ASSERT_ENABLED && !CPU(ARM64E) + { + CCallHelpers::Jump checkNotNull = jit.branchTestPtr(CCallHelpers::NonZero, GPRInfo::regT3); + jit.abortWithReason(TGInvalidPointer); + checkNotNull.link(&jit); + } +#endif + emitPointerValidation(jit, GPRInfo::regT3, JSEntryPtrTag); + jit.call(GPRInfo::regT3, JSEntryPtrTag); + + // result = operationTracedFunctionLeave(callee, span, result), unless untraced. + constexpr JSValueRegs resultRegs = JSRInfo::returnValueJSR; + jit.loadValue(jit.addressFor(spanLocal), valueRegs); + auto untraced = jit.branchIfEmpty(valueRegs); + jit.loadCell(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regT2); + jit.setupArguments(GPRInfo::regT2, valueRegs, resultRegs); + jit.prepareCallOperation(vm); + jit.move(CCallHelpers::TrustedImmPtr(tagCFunction(operationTracedFunctionLeave)), GPRInfo::nonArgGPR0); + emitPointerValidation(jit, GPRInfo::nonArgGPR0, OperationPtrTag); + jit.call(GPRInfo::nonArgGPR0, OperationPtrTag); + exceptionChecks.append(jit.emitJumpIfException(vm)); + + untraced.link(&jit); + jit.emitFunctionEpilogue(); + jit.ret(); + + // `span(name, attributes?)`: no call, the enter hook's value is the result. + noCallee.link(&jit); + jit.setupArguments(GPRInfo::regT0); + jit.prepareCallOperation(vm); + jit.move(CCallHelpers::TrustedImmPtr(tagCFunction(operationTracedFunctionEnter)), GPRInfo::nonArgGPR0); + emitPointerValidation(jit, GPRInfo::nonArgGPR0, OperationPtrTag); + jit.call(GPRInfo::nonArgGPR0, OperationPtrTag); + exceptionChecks.append(jit.emitJumpIfException(vm)); + { + constexpr JSValueRegs enterResultRegs = JSRInfo::returnValueJSR; + auto haveResult = jit.branchIfNotEmpty(enterResultRegs); + jit.moveTrustedValue(jsUndefined(), enterResultRegs); + haveResult.link(&jit); + } + jit.emitFunctionEpilogue(); + jit.ret(); + + exceptionChecks.link(&jit); + jit.copyCalleeSavesToEntryFrameCalleeSavesBuffer(vm.topEntryFrame, GPRInfo::argumentGPR0); + jit.setupArguments(CCallHelpers::TrustedImmPtr(&vm)); + jit.prepareCallOperation(vm); + jit.move(CCallHelpers::TrustedImmPtr(tagCFunction(operationLookupExceptionHandler)), GPRInfo::nonArgGPR0); + emitPointerValidation(jit, GPRInfo::nonArgGPR0, OperationPtrTag); + jit.call(GPRInfo::nonArgGPR0, OperationPtrTag); + jit.jumpToExceptionHandler(vm); + + LinkBuffer linkBuffer(jit, GLOBAL_THUNK_ID, LinkBuffer::Profile::Thunk); + return FINALIZE_THUNK(linkBuffer, JITThunkPtrTag, "traced"_s, "Specialized thunk for traced function calls"); +} +#endif + MacroAssemblerCodeRef remoteFunctionCallGenerator(VM& vm) { CCallHelpers jit; diff --git a/Source/JavaScriptCore/jit/ThunkGenerators.h b/Source/JavaScriptCore/jit/ThunkGenerators.h index 71cb885f0fe5..5f698257eb96 100644 --- a/Source/JavaScriptCore/jit/ThunkGenerators.h +++ b/Source/JavaScriptCore/jit/ThunkGenerators.h @@ -99,6 +99,9 @@ MacroAssemblerCodeRef numberConstructorCallThunkGenerator(VM&); MacroAssemblerCodeRef stringConstructorCallThunkGenerator(VM&); MacroAssemblerCodeRef boundFunctionCallGenerator(VM&); MacroAssemblerCodeRef remoteFunctionCallGenerator(VM&); +#if USE(BUN_JSC_ADDITIONS) +MacroAssemblerCodeRef tracedFunctionCallGenerator(VM&); +#endif MacroAssemblerCodeRef toIntegerOrInfinityThunkGenerator(VM&); MacroAssemblerCodeRef toLengthThunkGenerator(VM&); #if CPU(ARM64) diff --git a/Source/JavaScriptCore/runtime/FunctionPrototype.cpp b/Source/JavaScriptCore/runtime/FunctionPrototype.cpp index 95fe6fc4f88c..a53b02b9c04e 100644 --- a/Source/JavaScriptCore/runtime/FunctionPrototype.cpp +++ b/Source/JavaScriptCore/runtime/FunctionPrototype.cpp @@ -27,6 +27,7 @@ #include "FunctionExecutable.h" #include "IntegrityInlines.h" #include "JSBoundFunction.h" +#include "JSTracedFunction.h" #include "JSCInlines.h" namespace JSC { @@ -290,7 +291,11 @@ class RetrieveCallerFunctionFunctor { } if (callee) { - if (callee->inherits() || callee->inherits() || callee->type() == ProxyObjectType) + if (callee->inherits() || callee->inherits() +#if USE(BUN_JSC_ADDITIONS) + || callee->inherits() +#endif + || callee->type() == ProxyObjectType) return IterationStatus::Continue; if (callee->inherits()) { if (uncheckedDowncast(callee)->executable()->implementationVisibility() != ImplementationVisibility::Public) diff --git a/Source/JavaScriptCore/runtime/Intrinsic.h b/Source/JavaScriptCore/runtime/Intrinsic.h index 28e700b2f43d..451f5b09b3a3 100644 --- a/Source/JavaScriptCore/runtime/Intrinsic.h +++ b/Source/JavaScriptCore/runtime/Intrinsic.h @@ -178,6 +178,7 @@ namespace JSC { macro(ArrayBufferIsViewIntrinsic) \ macro(BoundFunctionCallIntrinsic) \ macro(RemoteFunctionCallIntrinsic) \ + macro(TracedFunctionCallIntrinsic) \ macro(IteratorIntrinsic) \ macro(JSMapGetIntrinsic) \ macro(JSMapHasIntrinsic) \ diff --git a/Source/JavaScriptCore/runtime/JSFunction.cpp b/Source/JavaScriptCore/runtime/JSFunction.cpp index 69b675d2d353..8127e3303212 100644 --- a/Source/JavaScriptCore/runtime/JSFunction.cpp +++ b/Source/JavaScriptCore/runtime/JSFunction.cpp @@ -36,6 +36,7 @@ #include "JSCInlines.h" #include "JSGlobalObject.h" #include "JSRemoteFunction.h" +#include "JSTracedFunction.h" #include "ObjectConstructor.h" #include "ObjectPrototype.h" #include "PropertyNameArray.h" @@ -258,6 +259,15 @@ JSString* JSFunction::toString(JSGlobalObject* globalObject) RETURN_IF_EXCEPTION(scope, nullptr); return asString(string); } +#if USE(BUN_JSC_ADDITIONS) + else if (inherits()) { + auto scope = DECLARE_THROW_SCOPE(vm); + JSString* name = uncheckedDowncast(this)->nameMayBeNull(); + JSValue string = jsMakeNontrivialString(globalObject, "function "_s, name ? name->tryGetValue() : emptyString(), "() {\n [native code]\n}"_s); + RETURN_IF_EXCEPTION(scope, nullptr); + return asString(string); + } +#endif if (isHostFunction()) return static_cast(executable())->toString(globalObject); @@ -673,7 +683,19 @@ JSFunction::PropertyStatus JSFunction::reifyLazyBoundNameIfNeeded(VM& vm, JSGlob unsigned initialAttributes = PropertyAttribute::DontEnum | PropertyAttribute::ReadOnly; rareData->setHasReifiedName(); putDirect(vm, nameIdent, name, initialAttributes); - } else { + } +#if USE(BUN_JSC_ADDITIONS) + else if (this->inherits()) { + FunctionRareData* rareData = this->ensureRareData(vm); + JSString* name = uncheckedDowncast(this)->nameMayBeNull(); + if (!name) + name = jsEmptyString(vm); + unsigned initialAttributes = PropertyAttribute::DontEnum | PropertyAttribute::ReadOnly; + rareData->setHasReifiedName(); + putDirect(vm, nameIdent, name, initialAttributes); + } +#endif + else { ASSERT(isNonBoundHostFunction()); FunctionRareData* rareData = this->ensureRareData(vm); JSString* name = uncheckedDowncast(executable())->nameJSString(vm); diff --git a/Source/JavaScriptCore/runtime/JSFunctionInlines.h b/Source/JavaScriptCore/runtime/JSFunctionInlines.h index 27e0d549e6b4..d21ea608e47f 100644 --- a/Source/JavaScriptCore/runtime/JSFunctionInlines.h +++ b/Source/JavaScriptCore/runtime/JSFunctionInlines.h @@ -30,6 +30,7 @@ #include "JSBoundFunction.h" #include "JSFunction.h" #include "JSRemoteFunction.h" +#include "JSTracedFunction.h" #include "NativeExecutable.h" #include "WebAssemblyFunction.h" #include @@ -163,6 +164,10 @@ inline double JSFunction::originalLength(VM& vm) return uncheckedDowncast(this)->length(vm); if (inherits()) return uncheckedDowncast(this)->length(vm); +#if USE(BUN_JSC_ADDITIONS) + if (inherits()) + return uncheckedDowncast(this)->length(vm); +#endif if (isHostFunction()) { // The original length is captured in NativeExecutable at creation time. return uncheckedDowncast(executable())->length(); @@ -199,6 +204,14 @@ inline JSString* JSFunction::originalName(JSGlobalObject* globalObject) return nameMayBeNull; return jsEmptyString(vm); } +#if USE(BUN_JSC_ADDITIONS) + if (this->inherits()) { + JSString* nameMayBeNull = uncheckedDowncast(this)->nameMayBeNull(); + if (nameMayBeNull) + return nameMayBeNull; + return jsEmptyString(vm); + } +#endif if (isHostFunction()) { // Mirror the JS path below: build a fresh JSString from the original name stored on @@ -289,6 +302,10 @@ inline JSString* JSFunction::asStringConcurrently() const { if (inherits() || inherits()) return nullptr; +#if USE(BUN_JSC_ADDITIONS) + if (inherits()) + return nullptr; +#endif if (isHostFunction()) return static_cast(executable())->asStringConcurrently(); return jsExecutable()->asStringConcurrently(); diff --git a/Source/JavaScriptCore/runtime/JSGlobalObject.cpp b/Source/JavaScriptCore/runtime/JSGlobalObject.cpp index a0097ce72112..9575e0efa480 100644 --- a/Source/JavaScriptCore/runtime/JSGlobalObject.cpp +++ b/Source/JavaScriptCore/runtime/JSGlobalObject.cpp @@ -192,6 +192,7 @@ #include "JSRawJSONObject.h" #include "JSRegExpStringIteratorInlines.h" #include "JSRemoteFunctionInlines.h" +#include "JSTracedFunctionInlines.h" #include "JSSetInlines.h" #include "JSSetIteratorInlines.h" #include "JSStringIteratorInlines.h" @@ -1150,6 +1151,12 @@ void JSGlobalObject::init(VM& vm) [] (const Initializer& init) { init.set(JSRemoteFunction::createStructure(init.vm, init.owner, init.owner->m_functionPrototype.get())); }); +#if USE(BUN_JSC_ADDITIONS) + m_tracedFunctionStructure.initLater( + [] (const Initializer& init) { + init.set(JSTracedFunction::createStructure(init.vm, init.owner, init.owner->m_functionPrototype.get())); + }); +#endif JSFunction* callFunction = nullptr; JSFunction* applyFunction = nullptr; JSFunction* hasInstanceSymbolFunction = nullptr; @@ -3167,6 +3174,9 @@ void JSGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_customSetterFunctionStructure.visit(visitor); thisObject->m_nativeStdFunctionStructure.visit(visitor); thisObject->m_remoteFunctionStructure.visit(visitor); +#if USE(BUN_JSC_ADDITIONS) + thisObject->m_tracedFunctionStructure.visit(visitor); +#endif visitor.append(thisObject->m_shadowRealmObjectStructure); visitor.append(thisObject->m_regExpStructure); visitor.append(thisObject->m_generatorFunctionStructure); diff --git a/Source/JavaScriptCore/runtime/JSGlobalObject.h b/Source/JavaScriptCore/runtime/JSGlobalObject.h index 2cfbfffa98f7..d7bc16a8d441 100644 --- a/Source/JavaScriptCore/runtime/JSGlobalObject.h +++ b/Source/JavaScriptCore/runtime/JSGlobalObject.h @@ -447,6 +447,9 @@ class JSGlobalObject : public JSSegmentedVariableObject { LazyProperty m_customSetterFunctionStructure; LazyProperty m_nativeStdFunctionStructure; LazyProperty m_remoteFunctionStructure; +#if USE(BUN_JSC_ADDITIONS) + LazyProperty m_tracedFunctionStructure; +#endif WriteBarrier m_asyncFunctionPrototype; WriteBarrier m_asyncGeneratorFunctionPrototype; LazyProperty m_iteratorResultObjectStructure; @@ -1058,6 +1061,9 @@ class JSGlobalObject : public JSSegmentedVariableObject { Structure* regExpMatchesIndicesArrayStructure() const { return m_regExpMatchesIndicesArrayStructure.get(); } Structure* regExpStringIteratorStructure() const { return m_regExpStringIteratorStructure.get(); } Structure* remoteFunctionStructure() const { return m_remoteFunctionStructure.get(this); } +#if USE(BUN_JSC_ADDITIONS) + Structure* tracedFunctionStructure() const { return m_tracedFunctionStructure.get(this); } +#endif Structure* moduleRecordStructure() const { return m_moduleRecordStructure.get(this); } Structure* syntheticModuleRecordStructure() const { return m_syntheticModuleRecordStructure.get(this); } Structure* moduleNamespaceObjectStructure() const { return m_moduleNamespaceObjectStructure.get(this); } diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index f014f4e2e4c2..db957b7a6500 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -50,6 +50,7 @@ #include "JSModuleNamespaceObject.h" #include "JSModuleRecord.h" #include "JSPromise.h" +#include "JSTracedFunction.h" #include "JSPromiseCombinatorsGlobalContext.h" #include "JSPromiseConstructor.h" #include "JSPromisePrototype.h" @@ -1925,6 +1926,13 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } #if USE(BUN_JSC_ADDITIONS) + case InternalMicrotask::TracedSettlementObserved: { + // (cell, resolution, context) from JSPromise::addSettlementObserver. + if (auto settled = vm.tracedFunctionHooks().settled) + settled(globalObject, arguments[2], static_cast(payload) == JSPromise::Status::Fulfilled, arguments[1]); + return; + } + case InternalMicrotask::PromiseReactionJobWithAsyncContext: #endif case InternalMicrotask::PromiseReactionJob: { diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index 6761007d7d1c..b288a772c4a8 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -471,6 +471,35 @@ void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObje } #endif +#if USE(BUN_JSC_ADDITIONS) +void JSPromise::addSettlementObserver(VM& vm, JSValue context) +{ + constexpr InternalMicrotask task = InternalMicrotask::TracedSettlementObserved; + switch (status()) { + case JSPromise::Status::Pending: { + bool wasHandled = isHandled(); + if (inlineReactionKind() == InlineReactionKind::None && !payloadCell()) [[likely]] + setInlineMicrotaskReaction(vm, task, nullptr, context); + else { + JSPromiseReaction* existing = reactionHead(vm); + auto* reaction = JSSlimPromiseReaction::create(vm, jsUndefined(), task, context, existing); + setPackedCell(vm, flags(), reaction); + } + // An observer is not a handler: keep unhandled-rejection tracking as it was. + if (!wasHandled) + setPackedCell(vm, flags() & ~isHandledFlag, payloadCell()); + break; + } + case JSPromise::Status::Rejected: + realm()->queueMicrotask(vm, task, static_cast(Status::Rejected), jsUndefined(), settlementValue(), context); + break; + case JSPromise::Status::Fulfilled: + realm()->queueMicrotask(vm, task, static_cast(Status::Fulfilled), jsUndefined(), settlementValue(), context); + break; + } +} +#endif + void JSPromise::performPromiseThenWithInternalMicrotask(VM& vm, InternalMicrotask task, JSCell* cell, JSValue context) { JSValue cellValue = cell ? JSValue(cell) : jsUndefined(); @@ -568,8 +597,14 @@ bool isDefinitelyNonThenable(JSObject* object, JSGlobalObject* globalObject) ALWAYS_INLINE void JSPromise::settleInlineInternalMicrotask(VM& vm, JSGlobalObject* globalObject, Status newStatus, JSValue argument, uint16_t flagsSnapshot) { ASSERT((flagsSnapshot & inlineReactionKindMask) == (static_cast(InlineReactionKind::InternalMicrotask) << inlineReactionKindShift)); - ASSERT(flagsSnapshot & isHandledFlag); InternalMicrotask task = static_cast((flagsSnapshot & inlineReactionMicrotaskMask) >> inlineReactionMicrotaskShift); +#if USE(BUN_JSC_ADDITIONS) + ASSERT((flagsSnapshot & isHandledFlag) || task == InternalMicrotask::TracedSettlementObserved); + if (!(flagsSnapshot & isHandledFlag) && newStatus == Status::Rejected) + globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, this, JSPromiseRejectionOperation::Reject); +#else + ASSERT(flagsSnapshot & isHandledFlag); +#endif JSValue context = m_slot.get(); JSCell* cell = payloadCell(); JSValue cellValue = cell ? JSValue(cell) : jsUndefined(); diff --git a/Source/JavaScriptCore/runtime/JSPromise.h b/Source/JavaScriptCore/runtime/JSPromise.h index 478ec3a09e5d..2a8c98efff4d 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.h +++ b/Source/JavaScriptCore/runtime/JSPromise.h @@ -165,6 +165,12 @@ class JSPromise : public JSNonFinalObject { static void fulfillWithInternalMicrotask(VM&, JSGlobalObject*, JSValue argument, InternalMicrotask, JSValue context); void performPromiseThenWithInternalMicrotask(VM&, InternalMicrotask, JSCell*, JSValue context); +#if USE(BUN_JSC_ADDITIONS) + // Have VM::tracedFunctionHooks().settled(context, …) run when this promise + // settles, without counting as a rejection handler and without a derived + // promise (no allocation when this is the first reaction). + JS_EXPORT_PRIVATE void addSettlementObserver(VM&, JSValue context); +#endif bool isThenFastAndNonObservable(); diff --git a/Source/JavaScriptCore/runtime/JSTracedFunction.cpp b/Source/JavaScriptCore/runtime/JSTracedFunction.cpp new file mode 100644 index 000000000000..b7bdf06e3e4f --- /dev/null +++ b/Source/JavaScriptCore/runtime/JSTracedFunction.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2026 Anthropic PBC. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "JSTracedFunction.h" + +#if USE(BUN_JSC_ADDITIONS) + +#include "ExecutableBaseInlines.h" +#include "JSCInlines.h" +#include "JSTracedFunctionInlines.h" + +namespace JSC { + +const ClassInfo JSTracedFunction::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTracedFunction) }; + +JSTracedFunction::JSTracedFunction(VM& vm, NativeExecutable* executable, JSGlobalObject* globalObject, Structure* structure, Shape shape, JSObject* target, JSValue data, double length) + : Base(vm, executable, globalObject, structure) + , m_targetFunction(target, WriteBarrierEarlyInit) + , m_data(data, WriteBarrierEarlyInit) + , m_length(length) + , m_shape(shape) +{ +} + +JSTracedFunction* JSTracedFunction::create(VM& vm, JSGlobalObject* globalObject, Shape shape, JSObject* target, JSValue data, const String& name, unsigned length) +{ + ASSERT(shape == Shape::CallLast || (target && target->isCallable())); + bool fast = shape == Shape::CallLast || (target && target->type() == JSFunctionType); + NativeExecutable* executable = vm.getTracedFunction(fast); + Structure* structure = globalObject->tracedFunctionStructure(); + JSTracedFunction* function = new (NotNull, allocateCell(vm)) JSTracedFunction(vm, executable, globalObject, structure, shape, target, data, length); + function->finishCreation(vm, name); + return function; +} + +void JSTracedFunction::finishCreation(VM& vm, const String& name) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + if (!name.isNull()) + m_name.set(vm, this, jsString(vm, name)); +} + +template +void JSTracedFunction::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSTracedFunction* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_targetFunction); + visitor.append(thisObject->m_data); + visitor.append(thisObject->m_name); +} + +DEFINE_VISIT_CHILDREN(JSTracedFunction); + +// The C++ path (no JIT, exotic callees). tracedFunctionCallGenerator is the +// fast equivalent; keep the two in step. +JSC_DEFINE_HOST_FUNCTION(tracedFunctionCallGeneric, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* traced = uncheckedDowncast(callFrame->jsCallee()); + auto& hooks = vm.tracedFunctionHooks(); + + JSValue target; + if (traced->shape() == JSTracedFunction::Shape::Wrap) + target = traced->targetFunction(); + else if (callFrame->argumentCount()) + target = callFrame->uncheckedArgument(callFrame->argumentCount() - 1); + + JSValue span; + if (hooks.enter) { + span = JSValue::decode(hooks.enter(globalObject, callFrame, traced)); + RETURN_IF_EXCEPTION(scope, { }); + } + if (traced->shape() == JSTracedFunction::Shape::CallLast && !target.isCallable()) { + // `span(name, attributes?)` with no callback: the hook's value is the result. + return JSValue::encode(span ? span : jsUndefined()); + } + + auto callData = JSC::getCallData(target); + if (callData.type == CallData::Type::None) [[unlikely]] + return throwVMTypeError(globalObject, scope, "Bun.otel.wrap target is not callable"_s); + auto cacheEntrypoint = [&] { + // Cache the arity-check entry point so later calls take the thunk + // (tracedFunctionCallGenerator bails here while it is null; installing + // new code clears it again). + if (callData.type == CallData::Type::JS && callData.js.functionExecutable->hasJITCodeForCall()) + callData.js.functionExecutable->entrypointFor(CodeSpecializationKind::CodeForCall, ArityCheckMode::MustCheckArity); + }; + cacheEntrypoint(); + JSValue result; + { + MarkedArgumentBuffer args; + JSValue thisValue; + if (traced->shape() == JSTracedFunction::Shape::Wrap) { + thisValue = callFrame->thisValue(); + for (unsigned i = 0; i < callFrame->argumentCount(); ++i) + args.append(callFrame->uncheckedArgument(i)); + if (args.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return { }; + } + } else { + thisValue = jsUndefined(); + args.append(span ? span : jsUndefined()); + } + result = call(globalObject, target, callData, thisValue, args); + } + cacheEntrypoint(); + if (Exception* exception = scope.exception()) [[unlikely]] { + // The thunk's equivalent is UnwindFunctor seeing the frame. + if (span && hooks.unwind) + hooks.unwind(globalObject, traced, span, exception); + return { }; + } + if (span && hooks.leave) + RELEASE_AND_RETURN(scope, hooks.leave(globalObject, traced, JSValue::encode(span), JSValue::encode(result))); + return JSValue::encode(result); +} + +} // namespace JSC + +#endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/JSTracedFunction.h b/Source/JavaScriptCore/runtime/JSTracedFunction.h new file mode 100644 index 000000000000..359a90c37f9b --- /dev/null +++ b/Source/JavaScriptCore/runtime/JSTracedFunction.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2026 Anthropic PBC. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if USE(BUN_JSC_ADDITIONS) + +#include "JSFunction.h" +#include "JSObject.h" +#include "JSString.h" + +namespace JSC { + +class JSPromise; + +JSC_DECLARE_HOST_FUNCTION(tracedFunctionCallGeneric); + +// A function that runs the embedder's enter/leave hooks (VM::TracedFunctionHooks) +// around a call, forwarding `this` and the arguments. Calls go through a JIT +// thunk (tracedFunctionCallGenerator) that calls the target directly, so the +// hooks are the only cost over a plain call. +// +// Two shapes share the class: +// - Wrap: the target is fixed (`m_targetFunction`); every call is traced. +// - CallLast: the target is the last argument of each call and receives the +// hook's span value as its only argument (`span(name, attributes?, fn)`). +class JSTracedFunction final : public JSFunction { +public: + using Base = JSFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + enum class Shape : uint8_t { Wrap, CallLast }; + + template + static GCClient::IsoSubspace* subspaceFor(VM& vm) + { + return vm.tracedFunctionSpace(); + } + + // `target` null for Shape::CallLast. `data` is opaque to JSC (the embedder's span name / scope). + JS_EXPORT_PRIVATE static JSTracedFunction* create(VM&, JSGlobalObject*, Shape, JSObject* target, JSValue data, const String& name, unsigned length); + + Shape shape() const { return m_shape; } + JSObject* targetFunction() LIFETIME_BOUND { return m_targetFunction.get(); } + JSValue data() const { return m_data.get(); } + JSString* nameMayBeNull() const LIFETIME_BOUND { return m_name.get(); } + double length(VM&) const { return m_length; } + + inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); + + static constexpr ptrdiff_t offsetOfTargetFunction() { return OBJECT_OFFSETOF(JSTracedFunction, m_targetFunction); } + static constexpr ptrdiff_t offsetOfShape() { return OBJECT_OFFSETOF(JSTracedFunction, m_shape); } + + // The frame local the thunk keeps the enter hook's value in (read back on + // unwind), valid once the frame's CallSiteIndex reads spanLocalValidCallSiteIndex. + static constexpr int spanLocal = 0; + static constexpr int numberOfFrameLocals = 1; + static constexpr uint32_t spanLocalValidCallSiteIndex = 1; + + DECLARE_EXPORT_INFO; + DECLARE_VISIT_CHILDREN; + +private: + JSTracedFunction(VM&, NativeExecutable*, JSGlobalObject*, Structure*, Shape, JSObject* target, JSValue data, double length); + void finishCreation(VM&, const String& name); + + WriteBarrier m_targetFunction; + WriteBarrier m_data; + WriteBarrier m_name; + double m_length; + Shape m_shape; +}; + +} // namespace JSC + +#endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/JSTracedFunctionInlines.h b/Source/JavaScriptCore/runtime/JSTracedFunctionInlines.h new file mode 100644 index 000000000000..c9c1cceed352 --- /dev/null +++ b/Source/JavaScriptCore/runtime/JSTracedFunctionInlines.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 Anthropic PBC. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if USE(BUN_JSC_ADDITIONS) + +#include "JSTracedFunction.h" +#include "StructureCreateInlines.h" + +namespace JSC { + +inline Structure* JSTracedFunction::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + ASSERT(globalObject); + return Structure::create(vm, globalObject, prototype, TypeInfo(JSFunctionType, StructureFlags), info()); +} + +} // namespace JSC + +#endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/Microtask.h b/Source/JavaScriptCore/runtime/Microtask.h index a7eb4e2a256d..41b69dbc11cf 100644 --- a/Source/JavaScriptCore/runtime/Microtask.h +++ b/Source/JavaScriptCore/runtime/Microtask.h @@ -54,6 +54,7 @@ enum class InternalMicrotask : uint8_t { PromiseReactionJob, #if USE(BUN_JSC_ADDITIONS) PromiseReactionJobWithAsyncContext, // PromiseReactionJob whose trailing argument is Bun's async context rather than a user context + TracedSettlementObserved, // JSPromise::addSettlementObserver: VM::tracedFunctionHooks().settled(context, fulfilled, result) #endif AsyncFunctionResume, diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index 9252010d8989..b5b570c40aaf 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -81,6 +81,7 @@ #include "JSMicrotaskDispatcher.h" #include "JSModuleLoaderInlines.h" #include "JSPromise.h" +#include "JSTracedFunction.h" #include "JSPromiseCombinatorsContextInlines.h" #include "JSPromiseCombinatorsGlobalContext.h" #include "JSPromiseConstructor.h" @@ -861,6 +862,10 @@ static ThunkGenerator NODELETE thunkGeneratorForIntrinsic(Intrinsic intrinsic) return boundFunctionCallGenerator; case RemoteFunctionCallIntrinsic: return remoteFunctionCallGenerator; +#if USE(BUN_JSC_ADDITIONS) + case TracedFunctionCallIntrinsic: + return tracedFunctionCallGenerator; +#endif case NumberConstructorIntrinsic: return numberConstructorCallThunkGenerator; case StringConstructorIntrinsic: @@ -993,6 +998,29 @@ NativeExecutable* VM::getRemoteFunction(bool isJSFunction) return getOrCreate(m_fastRemoteFunctionExecutable); } +#if USE(BUN_JSC_ADDITIONS) +NativeExecutable* VM::getTracedFunction(bool isJSFunction) +{ + bool slowCase = !isJSFunction; +#if !ENABLE(JIT) || OS(WINDOWS) + slowCase = true; +#endif + auto getOrCreate = [&] (Weak& slot) -> NativeExecutable* { + if (auto* cached = slot.get()) + return cached; + NativeExecutable* result = getHostFunction( + tracedFunctionCallGeneric, + ImplementationVisibility::Public, slowCase ? NoIntrinsic : TracedFunctionCallIntrinsic, + callHostFunctionAsConstructor, nullptr, 0, String()); + slot = Weak(result); + return result; + }; + if (slowCase) + return getOrCreate(m_slowTracedFunctionExecutable); + return getOrCreate(m_fastTracedFunctionExecutable); +} +#endif + CodePtr VM::getCTIInternalFunctionTrampolineFor(CodeSpecializationKind kind) { #if ENABLE(JIT) diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 4600f56f1c06..74d4d22e9872 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -130,6 +130,7 @@ class JSLock; class JSObject; struct JSPIContext; class JSPromise; +class JSTracedFunction; class JSPropertyNameEnumerator; class JITSizeStatistics; class JITThunks; @@ -204,6 +205,16 @@ struct DebugState; struct EntryFrame; +#if USE(BUN_JSC_ADDITIONS) +// The embedder's side of JSTracedFunction (see JSTracedFunction.h). All null = plain calls. +struct TracedFunctionHooks { + EncodedJSValue (*enter)(JSGlobalObject*, CallFrame*, JSTracedFunction*) { nullptr }; + EncodedJSValue (*leave)(JSGlobalObject*, JSTracedFunction*, EncodedJSValue span, EncodedJSValue result) { nullptr }; + void (*unwind)(JSGlobalObject*, JSTracedFunction*, JSValue span, Exception*) { nullptr }; + void (*settled)(JSGlobalObject*, JSValue context, bool fulfilled, JSValue result) { nullptr }; +}; +#endif + typedef uint8_t IndexingType; DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(VM); @@ -622,6 +633,13 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { Weak m_fastRemoteFunctionExecutable; Weak m_slowRemoteFunctionExecutable; +#if USE(BUN_JSC_ADDITIONS) + Weak m_fastTracedFunctionExecutable; + Weak m_slowTracedFunctionExecutable; + TracedFunctionHooks m_tracedFunctionHooks { }; + // Traced frames the current unwind passed through (innermost first); consumed by Interpreter::unwind. + Vector, 2> m_unwoundTracedFrames; +#endif const Ref deferredWorkTimer; @@ -775,6 +793,10 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { NativeExecutable* getBoundFunction(bool isJSFunction, SourceTaintedOrigin taintedness); NativeExecutable* getRemoteFunction(bool isJSFunction); +#if USE(BUN_JSC_ADDITIONS) + NativeExecutable* getTracedFunction(bool isJSFunction); + TracedFunctionHooks& tracedFunctionHooks() { return m_tracedFunctionHooks; } +#endif CodePtr getCTIInternalFunctionTrampolineFor(CodeSpecializationKind); MacroAssemblerCodeRef getCTIThrowExceptionFromCallSlowPath(); From a603e6023ecf42cd6492800f5e461d928aa416c7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 04:07:49 +0000 Subject: [PATCH 04/13] JSTracedFunction review pass: named frame state instead of a bare CallSiteIndex value, unwind list invariants asserted, Bun enum values appended, embedder cell type named generically --- .../bytecode/BytecodeIntrinsicRegistry.h | 2 +- .../bytecompiler/BytecodeGenerator.h | 2 +- .../JavaScriptCore/bytecompiler/NodesCodegen.cpp | 2 +- .../JavaScriptCore/interpreter/Interpreter.cpp | 16 +++++++++++++--- Source/JavaScriptCore/jit/ThunkGenerators.cpp | 16 ++++++++++------ Source/JavaScriptCore/runtime/JSTracedFunction.h | 9 ++++++--- Source/JavaScriptCore/runtime/JSType.h | 7 +++---- Source/JavaScriptCore/runtime/Microtask.h | 6 ++---- Source/JavaScriptCore/runtime/VM.h | 6 ++++-- 9 files changed, 41 insertions(+), 25 deletions(-) diff --git a/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h b/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h index d6d47816c6cd..a62a8fa68ab5 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h +++ b/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h @@ -75,7 +75,7 @@ enum class LinkTimeConstant : int32_t; macro(isUndefinedOrNull) \ macro(isWrapForValidIterator) \ macro(isDisposableStack) \ - macro(isTelemetrySpan) \ + macro(isEmbedderInternalFieldObject) \ macro(throwTypeError) \ macro(throwRangeError) \ macro(throwOutOfMemoryError) \ diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h index 6e1241ba1092..e9568495dee9 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h @@ -973,7 +973,7 @@ namespace JSC { RegisterID* emitIsEmpty(RegisterID* dst, RegisterID* src); RegisterID* emitIsDerivedArray(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, DerivedArrayType); } RegisterID* emitIsDisposableStack(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, DisposableStackType); } - RegisterID* emitIsTelemetrySpan(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, static_cast(BunTelemetrySpanType)); } + RegisterID* emitIsEmbedderInternalFieldObject(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, static_cast(EmbedderInternalFieldObjectType)); } RegisterID* emitIsAsyncDisposableStack(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, AsyncDisposableStackType); } void emitRequireObjectCoercible(RegisterID* value, ASCIILiteral error); void emitRequireObjectCoercibleForDestructuring(RegisterID* value, const Identifier* propertyName); diff --git a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp index bdb0530b8fb0..d5fd7d766a0d 100644 --- a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp +++ b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp @@ -2049,7 +2049,7 @@ CREATE_INTRINSIC_FOR_BRAND_CHECK(isArrayIterator, IsArrayIterator) CREATE_INTRINSIC_FOR_BRAND_CHECK(isUndefinedOrNull, IsUndefinedOrNull) CREATE_INTRINSIC_FOR_BRAND_CHECK(isWrapForValidIterator, IsWrapForValidIterator) CREATE_INTRINSIC_FOR_BRAND_CHECK(isDisposableStack, IsDisposableStack) -CREATE_INTRINSIC_FOR_BRAND_CHECK(isTelemetrySpan, IsTelemetrySpan) +CREATE_INTRINSIC_FOR_BRAND_CHECK(isEmbedderInternalFieldObject, IsEmbedderInternalFieldObject) CREATE_INTRINSIC_FOR_BRAND_CHECK(isAsyncDisposableStack, IsAsyncDisposableStack) #undef CREATE_INTRINSIC_FOR_BRAND_CHECK diff --git a/Source/JavaScriptCore/interpreter/Interpreter.cpp b/Source/JavaScriptCore/interpreter/Interpreter.cpp index c391d7552718..f5fa1f0d5fb6 100644 --- a/Source/JavaScriptCore/interpreter/Interpreter.cpp +++ b/Source/JavaScriptCore/interpreter/Interpreter.cpp @@ -905,8 +905,11 @@ class UnwindFunctor : UnwindFunctorBase { } #if USE(BUN_JSC_ADDITIONS) && ENABLE(JIT) if (!m_callFrame->isNativeCalleeFrame() && !m_callFrame->codeBlock()) { - if (auto* traced = dynamicDowncast(m_callFrame->jsCallee()); traced && m_callFrame->callSiteIndex().bits() == JSTracedFunction::spanLocalValidCallSiteIndex) { - // A tracedFunctionCallGenerator frame past its enter hook: the local holds the hook's value. + // tracedFunctionCallGenerator frames have no CodeBlock. Like every CodeBlock-less frame their + // CallSiteIndex word is otherwise unused; the thunk stores TracedFrameEntered there once the + // frame is allocated and the enter hook has run (before that, the local slot is uninitialized — + // e.g. while throwing StackOverflow from the thunk prologue). + if (auto* traced = dynamicDowncast(m_callFrame->jsCallee()); traced && m_callFrame->callSiteIndex().bits() == JSTracedFunction::TracedFrameEntered) { JSValue span = m_callFrame->registers()[virtualRegisterForLocal(JSTracedFunction::spanLocal).offset()].jsValue(); if (span) m_vm.m_unwoundTracedFrames.append({ traced, span }); @@ -971,6 +974,9 @@ static void sanitizeRemoteFunctionException(VM& vm, JSRemoteFunction* remoteFunc NEVER_INLINE CatchInfo Interpreter::unwind(VM& vm, CallFrame*& callFrame, Exception* exception) { +#if USE(BUN_JSC_ADDITIONS) + ASSERT(vm.m_unwoundTracedFrames.isEmpty()); +#endif // If we're unwinding the stack due to a regular exception (not a TerminationException), then // we want to use a DeferTerminationForAWhile scope. This is because we want to avoid a // TerminationException being raised (due to a concurrent termination request) in the middle @@ -1020,13 +1026,17 @@ NEVER_INLINE CatchInfo Interpreter::unwind(VM& vm, CallFrame*& callFrame, Except exception = scope.exception(); // clear m_needExceptionCheck } #if USE(BUN_JSC_ADDITIONS) - if (!vm.m_unwoundTracedFrames.isEmpty()) { + if (!vm.m_unwoundTracedFrames.isEmpty()) [[unlikely]] { + // Taken out before any hook runs: a hook that throws re-enters unwind() + // with an empty list and cannot observe or mutate this one. auto frames = std::exchange(vm.m_unwoundTracedFrames, { }); if (auto unwindHook = vm.tracedFunctionHooks().unwind) { for (auto& [traced, span] : frames) unwindHook(traced->globalObject(), traced, span, exception); } + // A hook may have replaced the pending exception; it may not clear it. exception = scope.exception(); + RELEASE_ASSERT(exception); } #endif diff --git a/Source/JavaScriptCore/jit/ThunkGenerators.cpp b/Source/JavaScriptCore/jit/ThunkGenerators.cpp index 743003ed2d5b..1330c901b712 100644 --- a/Source/JavaScriptCore/jit/ThunkGenerators.cpp +++ b/Source/JavaScriptCore/jit/ThunkGenerators.cpp @@ -1286,9 +1286,10 @@ MacroAssemblerCodeRef boundFunctionCallGenerator(VM& vm) jit.emitFunctionPrologue(); - // Set up our call frame. + // Set up our call frame: no CodeBlock, and CallSiteIndex = TracedFrameNotEntered + // until the span local below is initialized (see JSTracedFunction::FrameState). jit.storePtr(CCallHelpers::TrustedImmPtr(nullptr), CCallHelpers::addressFor(CallFrameSlot::codeBlock)); - jit.store32(CCallHelpers::TrustedImm32(0), CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); + jit.store32(CCallHelpers::TrustedImm32(JSTracedFunction::TracedFrameNotEntered), CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); constexpr unsigned stackMisalignment = sizeof(CallerFrameAndPC) % stackAlignmentBytes(); constexpr unsigned extraStackNeeded = stackMisalignment ? stackAlignmentBytes() - stackMisalignment : 0; @@ -1471,7 +1472,11 @@ MacroAssemblerCodeRef tracedFunctionCallGenerator(VM& vm) jit.loadCell(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regT0); jit.load32(CCallHelpers::lowWordFor(CallFrameSlot::argumentCountIncludingThis), GPRInfo::regT1); - // Callee frame for max(argumentCountIncludingThis, 2) arguments plus our locals: + // No register is live across any of the operation calls below; everything + // is reloaded from our frame (callee, argument count, the span local). + // + // Callee frame for max(argumentCountIncludingThis, 2) arguments plus our locals + // (Shape::CallLast always passes exactly [undefined, span], hence the 2): // stackAlign((numParams + numFrameLocals + (CallFrameHeaderSize - CallerFrameAndPCSize)) * sizeof(Register)) jit.move(GPRInfo::regT1, GPRInfo::regT2); auto atLeastTwo = jit.branch32(CCallHelpers::AboveOrEqual, GPRInfo::regT2, CCallHelpers::TrustedImm32(2)); @@ -1553,9 +1558,8 @@ MacroAssemblerCodeRef tracedFunctionCallGenerator(VM& vm) exceptionChecks.append(jit.emitJumpIfException(vm)); jit.setupResults(valueRegs); jit.storeValue(valueRegs, jit.addressFor(spanLocal)); - // Tell Interpreter::unwind the span local is now meaningful (a native - // frame's CallSiteIndex is otherwise 0). - jit.store32(CCallHelpers::TrustedImm32(JSTracedFunction::spanLocalValidCallSiteIndex), CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); + // From here on an exception unwinding through this frame reports the span. + jit.store32(CCallHelpers::TrustedImm32(JSTracedFunction::TracedFrameEntered), CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); jit.loadCell(CCallHelpers::addressFor(CallFrameSlot::callee), GPRInfo::regT0); jit.load32(CCallHelpers::lowWordFor(CallFrameSlot::argumentCountIncludingThis), GPRInfo::regT1); diff --git a/Source/JavaScriptCore/runtime/JSTracedFunction.h b/Source/JavaScriptCore/runtime/JSTracedFunction.h index 359a90c37f9b..e2bf2b863e6f 100644 --- a/Source/JavaScriptCore/runtime/JSTracedFunction.h +++ b/Source/JavaScriptCore/runtime/JSTracedFunction.h @@ -73,11 +73,14 @@ class JSTracedFunction final : public JSFunction { static constexpr ptrdiff_t offsetOfTargetFunction() { return OBJECT_OFFSETOF(JSTracedFunction, m_targetFunction); } static constexpr ptrdiff_t offsetOfShape() { return OBJECT_OFFSETOF(JSTracedFunction, m_shape); } - // The frame local the thunk keeps the enter hook's value in (read back on - // unwind), valid once the frame's CallSiteIndex reads spanLocalValidCallSiteIndex. + // tracedFunctionCallGenerator's frame: one local holding the enter hook's + // value, which Interpreter::unwind hands to the unwind hook. The frame has + // no CodeBlock, so its CallSiteIndex word carries the frame state instead: + // 0 from the prologue (frame not yet allocated / hook not yet run — the + // local slot is garbage), TracedFrameEntered once the local is meaningful. static constexpr int spanLocal = 0; static constexpr int numberOfFrameLocals = 1; - static constexpr uint32_t spanLocalValidCallSiteIndex = 1; + enum FrameState : uint32_t { TracedFrameNotEntered = 0, TracedFrameEntered = 1 }; DECLARE_EXPORT_INFO; DECLARE_VISIT_CHILDREN; diff --git a/Source/JavaScriptCore/runtime/JSType.h b/Source/JavaScriptCore/runtime/JSType.h index e6c54d5a7b3d..64b106e997fd 100644 --- a/Source/JavaScriptCore/runtime/JSType.h +++ b/Source/JavaScriptCore/runtime/JSType.h @@ -182,10 +182,9 @@ struct JSTypeRange { }; static constexpr uint8_t EmbedderArrayLikeType = 0b11101101; -// Bun's telemetry span cell (a JSInternalFieldObjectImpl subclass defined by -// the embedder). Named here so builtins get an @isTelemetrySpan brand check -// that lowers to IsCellWithType. -static constexpr uint8_t BunTelemetrySpanType = 0b11100001; +// A JSInternalFieldObjectImpl subclass defined by the embedder that its +// builtins brand-check via @isEmbedderInternalFieldObject (lowers to IsCellWithType). +static constexpr uint8_t EmbedderInternalFieldObjectType = 0b11100001; static constexpr uint32_t LastValueCompareCellType = HeapBigIntType; diff --git a/Source/JavaScriptCore/runtime/Microtask.h b/Source/JavaScriptCore/runtime/Microtask.h index 41b69dbc11cf..47aef87eaa81 100644 --- a/Source/JavaScriptCore/runtime/Microtask.h +++ b/Source/JavaScriptCore/runtime/Microtask.h @@ -52,10 +52,6 @@ enum class InternalMicrotask : uint8_t { PromiseFinallyAwaitJob, PromiseReactionJob, -#if USE(BUN_JSC_ADDITIONS) - PromiseReactionJobWithAsyncContext, // PromiseReactionJob whose trailing argument is Bun's async context rather than a user context - TracedSettlementObserved, // JSPromise::addSettlementObserver: VM::tracedFunctionHooks().settled(context, fulfilled, result) -#endif AsyncFunctionResume, AsyncFromSyncIteratorContinue, @@ -95,6 +91,8 @@ enum class InternalMicrotask : uint8_t { #if USE(BUN_JSC_ADDITIONS) BunPerformMicrotaskJob, // Bun's performMicrotask function with async context BunInvokeJobWithArguments, // Invoke job function with up to 4 arguments + PromiseReactionJobWithAsyncContext, // PromiseReactionJob whose trailing argument is Bun's async context rather than a user context + TracedSettlementObserved, // JSPromise::addSettlementObserver: VM::tracedFunctionHooks().settled(context, fulfilled, result) #endif }; diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 74d4d22e9872..9f3fee776ba9 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -637,8 +637,10 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { Weak m_fastTracedFunctionExecutable; Weak m_slowTracedFunctionExecutable; TracedFunctionHooks m_tracedFunctionHooks { }; - // Traced frames the current unwind passed through (innermost first); consumed by Interpreter::unwind. - Vector, 2> m_unwoundTracedFrames; + // Traced frames the current Interpreter::unwind walked through (innermost + // first). Filled during the walk, moved out and handed to + // tracedFunctionHooks().unwind right after it; empty at all other times. + Vector, 4> m_unwoundTracedFrames; #endif const Ref deferredWorkTimer; From c0ad48ab53dfbe4fe926c9a99b0e4eb2d8544582 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Aug 2026 04:40:20 +0000 Subject: [PATCH 05/13] Address review: entry async context is snapshotted by the wrapper bytecode (not stored on the generator, so for-await in the sync prefix and DFG allocation sinking cannot disturb it); AsyncFromSync reject paths capture for resume; unwind only inspects thunk frames and not after leave; settlement observer reports rejection after settling; null target on the CallLast slow path; non-Bun build guards --- .../bytecode/BytecodeIntrinsicRegistry.h | 8 ++++++- .../bytecompiler/BytecodeGenerator.h | 2 ++ .../bytecompiler/NodesCodegen.cpp | 18 +++++++++++++++ .../JavaScriptCore/dfg/DFGSpeculativeJIT.cpp | 10 -------- Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h | 2 -- Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp | 9 -------- Source/JavaScriptCore/heap/Heap.h | 4 ++-- .../interpreter/Interpreter.cpp | 6 ++++- Source/JavaScriptCore/jit/ThunkGenerators.cpp | 13 +++++++---- .../AsyncFromSyncIteratorPrototype.cpp | 5 ++++ .../runtime/JSAsyncFunctionGenerator.cpp | 23 ------------------- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 12 +++++++--- Source/JavaScriptCore/runtime/JSPromise.cpp | 9 ++++++-- .../runtime/JSPromiseReaction.cpp | 4 ++++ .../runtime/JSTracedFunction.cpp | 2 +- Source/JavaScriptCore/runtime/JSType.h | 2 ++ 16 files changed, 70 insertions(+), 59 deletions(-) diff --git a/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h b/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h index a62a8fa68ab5..cbebf96422d5 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h +++ b/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h @@ -40,6 +40,12 @@ class BytecodeIntrinsicNode; class RegisterID; enum class LinkTimeConstant : int32_t; +#if USE(BUN_JSC_ADDITIONS) +#define JSC_BUN_BYTECODE_INTRINSIC_FUNCTIONS_EACH_NAME(macro) macro(isEmbedderInternalFieldObject) +#else +#define JSC_BUN_BYTECODE_INTRINSIC_FUNCTIONS_EACH_NAME(macro) +#endif + #define JSC_COMMON_BYTECODE_INTRINSIC_FUNCTIONS_EACH_NAME(macro) \ macro(argument) \ macro(argumentCount) \ @@ -75,7 +81,7 @@ enum class LinkTimeConstant : int32_t; macro(isUndefinedOrNull) \ macro(isWrapForValidIterator) \ macro(isDisposableStack) \ - macro(isEmbedderInternalFieldObject) \ + JSC_BUN_BYTECODE_INTRINSIC_FUNCTIONS_EACH_NAME(macro) \ macro(throwTypeError) \ macro(throwRangeError) \ macro(throwOutOfMemoryError) \ diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h index e9568495dee9..393cc00d23e3 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h @@ -973,7 +973,9 @@ namespace JSC { RegisterID* emitIsEmpty(RegisterID* dst, RegisterID* src); RegisterID* emitIsDerivedArray(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, DerivedArrayType); } RegisterID* emitIsDisposableStack(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, DisposableStackType); } +#if USE(BUN_JSC_ADDITIONS) RegisterID* emitIsEmbedderInternalFieldObject(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, static_cast(EmbedderInternalFieldObjectType)); } +#endif RegisterID* emitIsAsyncDisposableStack(RegisterID* dst, RegisterID* src) { return emitIsCellWithType(dst, src, AsyncDisposableStackType); } void emitRequireObjectCoercible(RegisterID* value, ASCIILiteral error); void emitRequireObjectCoercibleForDestructuring(RegisterID* value, const Identifier* propertyName); diff --git a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp index d5fd7d766a0d..3bff40f1969c 100644 --- a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp +++ b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp @@ -2049,7 +2049,9 @@ CREATE_INTRINSIC_FOR_BRAND_CHECK(isArrayIterator, IsArrayIterator) CREATE_INTRINSIC_FOR_BRAND_CHECK(isUndefinedOrNull, IsUndefinedOrNull) CREATE_INTRINSIC_FOR_BRAND_CHECK(isWrapForValidIterator, IsWrapForValidIterator) CREATE_INTRINSIC_FOR_BRAND_CHECK(isDisposableStack, IsDisposableStack) +#if USE(BUN_JSC_ADDITIONS) CREATE_INTRINSIC_FOR_BRAND_CHECK(isEmbedderInternalFieldObject, IsEmbedderInternalFieldObject) +#endif CREATE_INTRINSIC_FOR_BRAND_CHECK(isAsyncDisposableStack, IsAsyncDisposableStack) #undef CREATE_INTRINSIC_FOR_BRAND_CHECK @@ -5389,6 +5391,17 @@ void FunctionNode::emitBytecode(BytecodeGenerator& generator, RegisterID*) generator.emitPutInternalField(generator.generatorRegister(), static_cast(JSGenerator::Field::Next), next.get()); generator.emitPutInternalField(generator.generatorRegister(), static_cast(JSGenerator::Field::State), generator.emitLoad(nullptr, jsNumber(static_cast(JSGenerator::State::Executing)))); +#if USE(BUN_JSC_ADDITIONS) + // The embedder's async context as it was before the body's synchronous + // prefix ran; asyncFunctionDrive compares it with the context at the + // first suspension (VM::asyncContextLeaveAsyncFrameHook). + RefPtr entryAsyncContext = generator.newTemporary(); + { + RefPtr asyncContextTuple = generator.emitGetGlobalPrivate(generator.newTemporary(), generator.vm().propertyNames->builtinNames().asyncContextPrivateName()); + generator.emitGetInternalField(entryAsyncContext.get(), asyncContextTuple.get(), 0); + } +#endif + Ref