diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bun.js/bindings/CallSite.cpp | 91 | ||||
-rw-r--r-- | src/bun.js/bindings/CallSite.h | 92 | ||||
-rw-r--r-- | src/bun.js/bindings/CallSitePrototype.cpp | 228 | ||||
-rw-r--r-- | src/bun.js/bindings/CallSitePrototype.h | 46 | ||||
-rw-r--r-- | src/bun.js/bindings/ErrorStackTrace.cpp | 375 | ||||
-rw-r--r-- | src/bun.js/bindings/ErrorStackTrace.h | 180 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 226 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 12 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 24 |
11 files changed, 1276 insertions, 0 deletions
diff --git a/src/bun.js/bindings/CallSite.cpp b/src/bun.js/bindings/CallSite.cpp new file mode 100644 index 000000000..522c6db1c --- /dev/null +++ b/src/bun.js/bindings/CallSite.cpp @@ -0,0 +1,91 @@ +/** + * This source code is licensed under the terms found in the LICENSE file in + * node-jsc's root directory. + */ + +#include "config.h" +#include "CallSite.h" + +#include "helpers.h" + +#include <JavaScriptCore/JSCInlines.h> + +using namespace JSC; +using namespace WebCore; + +namespace Zig { + +const JSC::ClassInfo CallSite::s_info = { "CallSite"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(CallSite) }; + +void CallSite::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSCStackFrame& stackFrame, bool encounteredStrictFrame) +{ + Base::finishCreation(vm); + + /* From v8's "Stack Trace API" (https://github.com/v8/v8/wiki/Stack-Trace-API): + * "To maintain restrictions imposed on strict mode functions, frames that have a + * strict mode function and all frames below (its caller etc.) are not allow to access + * their receiver and function objects. For those frames, getFunction() and getThis() + * will return undefined.". + * Thus, if we've already encountered a strict frame, we'll treat our frame as strict too. */ + + bool isStrictFrame = encounteredStrictFrame; + if (!isStrictFrame) { + JSC::CodeBlock* codeBlock = stackFrame.codeBlock(); + if (codeBlock) { + isStrictFrame = codeBlock->ownerExecutable()->isInStrictContext(); + } + } + + JSC::JSObject* calleeObject = JSC::jsCast<JSC::JSObject*>(stackFrame.callee()); + + // Initialize "this" and "function" (and set the "IsStrict" flag if needed) + JSC::CallFrame* callFrame = stackFrame.callFrame(); + if (isStrictFrame) { + m_thisValue.set(vm, this, JSC::jsUndefined()); + m_function.set(vm, this, JSC::jsUndefined()); + m_flags |= static_cast<unsigned int>(Flags::IsStrict); + } else { + if (callFrame) { + // We know that we're not in strict mode + m_thisValue.set(vm, this, callFrame->thisValue().toThis(globalObject, JSC::ECMAMode::sloppy())); + } else { + m_thisValue.set(vm, this, JSC::jsUndefined()); + } + + m_function.set(vm, this, stackFrame.callee()); + } + + m_functionName.set(vm, this, stackFrame.functionName()); + m_sourceURL.set(vm, this, stackFrame.sourceURL()); + + const auto* sourcePositions = stackFrame.getSourcePositions(); + if (sourcePositions) { + m_lineNumber = JSC::jsNumber(sourcePositions->line.oneBasedInt()); + m_columnNumber = JSC::jsNumber(sourcePositions->startColumn.oneBasedInt()); + } + + if (stackFrame.isEval()) { + m_flags |= static_cast<unsigned int>(Flags::IsEval); + } + if (stackFrame.isConstructor()) { + m_flags |= static_cast<unsigned int>(Flags::IsConstructor); + } + if (!stackFrame.codeBlock()) { + m_flags |= static_cast<unsigned int>(Flags::IsNative); + } +} + +template<typename Visitor> +void CallSite::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + CallSite* thisCallSite = jsCast<CallSite*>(cell); + Base::visitChildren(thisCallSite, visitor); + visitor.append(thisCallSite->m_thisValue); + visitor.append(thisCallSite->m_function); + visitor.append(thisCallSite->m_functionName); + visitor.append(thisCallSite->m_sourceURL); +} + +DEFINE_VISIT_CHILDREN(CallSite); + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/CallSite.h b/src/bun.js/bindings/CallSite.h new file mode 100644 index 000000000..57b222880 --- /dev/null +++ b/src/bun.js/bindings/CallSite.h @@ -0,0 +1,92 @@ +/** + * This source code is licensed under the terms found in the LICENSE file in + * node-jsc's root directory. + */ + +#pragma once + +#include "ErrorStackTrace.h" + +#include <JavaScriptCore/JSObject.h> + +using namespace JSC; +using namespace WebCore; + +namespace Zig { + +class JSCStackFrame; + +class CallSite final : public JSC::JSNonFinalObject { +public: + enum class Flags { + IsStrict = 1, + IsEval = 2, + IsConstructor = 4, + IsNative = 8, + }; + +private: + JSC::WriteBarrier<JSC::Unknown> m_thisValue; + JSC::WriteBarrier<JSC::Unknown> m_function; + JSC::WriteBarrier<JSC::Unknown> m_functionName; + JSC::WriteBarrier<JSC::Unknown> m_sourceURL; + JSC::JSValue m_lineNumber; + JSC::JSValue m_columnNumber; + unsigned int m_flags; + +public: + using Base = JSC::JSNonFinalObject; + + static CallSite* create(JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSCStackFrame& stackFrame, bool encounteredStrictFrame) + { + JSC::VM& vm = globalObject->vm(); + CallSite* callSite = new (NotNull, JSC::allocateCell<CallSite>(vm)) CallSite(vm, structure); + callSite->finishCreation(vm, globalObject, stackFrame, encounteredStrictFrame); + return callSite; + } + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + + return WebCore::subspaceForImpl<CallSite, UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCallSite.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCallSite = WTFMove(space); }, + [](auto& spaces) { return spaces.m_subspaceForCallSite.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCallSite = WTFMove(space); }); + } + + JSC::JSValue thisValue() const { return m_thisValue.get(); } + JSC::JSValue function() const { return m_function.get(); } + JSC::JSValue functionName() const { return m_functionName.get(); } + JSC::JSValue sourceURL() const { return m_sourceURL.get(); } + JSC::JSValue lineNumber() const { return m_lineNumber; } + JSC::JSValue columnNumber() const { return m_columnNumber; } + bool isEval() const { return m_flags & static_cast<unsigned int>(Flags::IsEval); } + bool isConstructor() const { return m_flags & static_cast<unsigned int>(Flags::IsConstructor); } + bool isStrict() const { return m_flags & static_cast<unsigned int>(Flags::IsStrict); } + bool isNative() const { return m_flags & static_cast<unsigned int>(Flags::IsNative); } + +private: + CallSite(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + , m_lineNumber(-1) + , m_columnNumber(-1) + { + } + + void finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSCStackFrame& stackFrame, bool encounteredStrictFrame); + + DECLARE_VISIT_CHILDREN; +}; + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/CallSitePrototype.cpp b/src/bun.js/bindings/CallSitePrototype.cpp new file mode 100644 index 000000000..faaf571e2 --- /dev/null +++ b/src/bun.js/bindings/CallSitePrototype.cpp @@ -0,0 +1,228 @@ +/** + * This source code is licensed under the terms found in the LICENSE file in + * node-jsc's root directory. + */ + +#include "config.h" +#include "CallSitePrototype.h" +#include "CallSite.h" +#include "helpers.h" + +#include <JavaScriptCore/JSGlobalObject.h> +#include <JavaScriptCore/Error.h> +#include <JavaScriptCore/CodeBlock.h> +#include <JavaScriptCore/Operations.h> +#include <JavaScriptCore/JSCInlines.h> + +using namespace JSC; + +namespace Zig { + +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetThis); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetTypeName); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetFunction); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetFunctionName); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetMethodName); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetFileName); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetLineNumber); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetColumnNumber); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetEvalOrigin); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetScriptNameOrSourceURL); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncIsToplevel); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncIsEval); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncIsNative); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncIsConstructor); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncIsAsync); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncIsPromiseAll); +static JSC_DECLARE_HOST_FUNCTION(callSiteProtoFuncGetPromiseIndex); + +ALWAYS_INLINE static CallSite* getCallSite(JSGlobalObject* globalObject, JSC::JSValue thisValue) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (UNLIKELY(!thisValue.isCell())) { + JSC::throwVMError(globalObject, scope, createNotAnObjectError(globalObject, thisValue)); + return nullptr; + } + + if (LIKELY(thisValue.asCell()->inherits(CallSite::info()))) { + return JSC::jsCast<CallSite*>(thisValue); + } + + throwTypeError(globalObject, scope, "CallSite operation called on non-CallSite object"_s); + return nullptr; +} + +#define ENTER_PROTO_FUNC() \ + JSC::VM& vm = globalObject->vm(); \ + auto scope = DECLARE_THROW_SCOPE(vm); \ + \ + CallSite* callSite = getCallSite(globalObject, callFrame->thisValue()); \ + if (!callSite) { \ + return JSC::JSValue::encode(JSC::jsUndefined()); \ + } + +static const HashTableValue CallSitePrototypeTableValues[] + = { + { "getThis"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetThis, 0 } }, + { "getTypeName"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetTypeName, 0 } }, + { "getFunction"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetFunction, 0 } }, + { "getFunctionName"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetFunctionName, 0 } }, + { "getMethodName"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetMethodName, 0 } }, + { "getFileName"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetFileName, 0 } }, + { "getLineNumber"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetLineNumber, 0 } }, + { "getColumnNumber"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetColumnNumber, 0 } }, + { "getEvalOrigin"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetEvalOrigin, 0 } }, + { "getScriptNameOrSourceURL"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetScriptNameOrSourceURL, 0 } }, + { "isToplevel"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncIsToplevel, 0 } }, + { "isEval"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncIsEval, 0 } }, + { "isNative"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncIsNative, 0 } }, + { "isConstructor"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncIsConstructor, 0 } }, + { "isAsync"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncIsAsync, 0 } }, + { "isPromiseAll"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncIsPromiseAll, 0 } }, + { "getPromiseIndex"_s, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Function, NoIntrinsic, { HashTableValue::NativeFunctionType, callSiteProtoFuncGetPromiseIndex, 0 } }, + }; + +const JSC::ClassInfo CallSitePrototype::s_info = { "CallSite"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(CallSitePrototype) }; + +void CallSitePrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(vm, info())); + + reifyStaticProperties(vm, CallSite::info(), CallSitePrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// TODO: doesn't recognize thisValue as global object +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetThis, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->thisValue()); +} + +// TODO: doesn't get class name +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetTypeName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(JSC::jsTypeStringForValue(globalObject, callSite->thisValue())); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFunction, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->function()); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFunctionName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->functionName()); +} + +// TODO +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetMethodName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + return callSiteProtoFuncGetFunctionName(globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetFileName, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->sourceURL()); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetLineNumber, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->lineNumber()); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetColumnNumber, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->columnNumber()); +} + +// TODO: +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetEvalOrigin, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetScriptNameOrSourceURL, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + return JSC::JSValue::encode(callSite->sourceURL()); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsToplevel, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + JSC::JSValue thisValue = callSite->thisValue(); + + // This is what v8 does (JSStackFrame::IsToplevel in messages.cc): + + if (thisValue.isUndefinedOrNull()) { + return JSC::JSValue::encode(JSC::jsBoolean(true)); + } + + JSC::JSObject* thisObject = thisValue.getObject(); + if (thisObject && thisObject->isGlobalObject()) { + return JSC::JSValue::encode(JSC::jsBoolean(true)); + } + + return JSC::JSValue::encode(JSC::jsBoolean(false)); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsEval, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + bool isEval = callSite->isEval(); + return JSC::JSValue::encode(JSC::jsBoolean(isEval)); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsNative, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + bool isNative = callSite->isNative(); + return JSC::JSValue::encode(JSC::jsBoolean(isNative)); +} + +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsConstructor, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + bool isConstructor = callSite->isConstructor(); + return JSC::JSValue::encode(JSC::jsBoolean(isConstructor)); +} + +// TODO: +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsAsync, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + return JSC::JSValue::encode(JSC::jsBoolean(false)); +} + +// TODO: +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncIsPromiseAll, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + return JSC::JSValue::encode(JSC::jsBoolean(false)); +} + +// TODO: +JSC_DEFINE_HOST_FUNCTION(callSiteProtoFuncGetPromiseIndex, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + ENTER_PROTO_FUNC(); + + return JSC::JSValue::encode(JSC::jsNumber(0)); +} + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/CallSitePrototype.h b/src/bun.js/bindings/CallSitePrototype.h new file mode 100644 index 000000000..000bce2de --- /dev/null +++ b/src/bun.js/bindings/CallSitePrototype.h @@ -0,0 +1,46 @@ +/** + * This source code is licensed under the terms found in the LICENSE file in + * node-jsc's root directory. + */ + +#pragma once + +#include <JavaScriptCore/JSObject.h> + +using namespace JSC; + +namespace Zig { + +class CallSitePrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static CallSitePrototype* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject) + { + CallSitePrototype* callSitePrototype = new (NotNull, JSC::allocateCell<CallSitePrototype>(vm)) CallSitePrototype(vm, structure); + callSitePrototype->finishCreation(vm, globalObject); + return callSitePrototype; + } + + DECLARE_INFO; + template<typename CellType, JSC::SubspaceAccess> + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + CallSitePrototype(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject); +}; + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/ErrorStackTrace.cpp b/src/bun.js/bindings/ErrorStackTrace.cpp new file mode 100644 index 000000000..dddaa31ea --- /dev/null +++ b/src/bun.js/bindings/ErrorStackTrace.cpp @@ -0,0 +1,375 @@ +/** + * This source code is licensed under the terms found in the LICENSE file in + * node-jsc's root directory. + */ + +#include "config.h" +#include "ErrorStackTrace.h" + +#include <JavaScriptCore/CatchScope.h> +#include <JavaScriptCore/DebuggerPrimitives.h> +#include <JavaScriptCore/Exception.h> +#include <JavaScriptCore/JSCInlines.h> +#include <JavaScriptCore/ErrorInstance.h> +#include <JavaScriptCore/StackVisitor.h> +#include <wtf/IterationStatus.h> + +using namespace JSC; +using namespace WebCore; + +namespace Zig { + +JSCStackTrace JSCStackTrace::fromExisting(JSC::VM& vm, const WTF::Vector<JSC::StackFrame>& existingFrames) +{ + WTF::Vector<JSCStackFrame> newFrames; + + size_t frameCount = existingFrames.size(); + if (0 == frameCount) { + return JSCStackTrace(); + } + + newFrames.reserveInitialCapacity(frameCount); + for (size_t i = 0; i < frameCount; i++) { + newFrames.constructAndAppend(vm, existingFrames.at(i)); + } + + return JSCStackTrace(newFrames); +} + +JSCStackTrace JSCStackTrace::captureCurrentJSStackTrace(Zig::GlobalObject* globalObject, JSC::CallFrame* callFrame, size_t frameLimit, JSC::JSValue caller) +{ + JSC::VM& vm = globalObject->vm(); + if (!callFrame) { + return JSCStackTrace(); + } + + WTF::Vector<JSCStackFrame> stackFrames; + size_t framesCount = 0; + + bool belowCaller = false; + int32_t skipFrames = 0; + + WTF::String callerName {}; + if (JSC::JSFunction* callerFunction = JSC::jsDynamicCast<JSC::JSFunction*>(caller)) { + callerName = callerFunction->name(vm); + if (!callerFunction->name(vm).isEmpty() || callerFunction->isHostOrBuiltinFunction()) { + callerName = callerFunction->name(vm); + } else { + callerName = callerFunction->jsExecutable()->name().string(); + } + } + if (JSC::InternalFunction* callerFunctionInternal = JSC::jsDynamicCast<JSC::InternalFunction*>(caller)) { + callerName = callerFunctionInternal->name(); + } + + JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { + // skip caller frame and all frames above it + if (!callerName.isEmpty()) { + if (!belowCaller) { + if (visitor->functionName() == callerName) { + belowCaller = true; + return WTF::IterationStatus::Continue; + } + skipFrames += 1; + } + } + if (!visitor->isNativeFrame()) { + framesCount++; + } + + return WTF::IterationStatus::Continue; + }); + framesCount = std::min(frameLimit, framesCount); + + // Create the actual stack frames + size_t i = 0; + stackFrames.reserveInitialCapacity(framesCount); + JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { + // Skip native frames + if (visitor->isNativeFrame()) { + return WTF::IterationStatus::Continue; + } + + // Skip frames if needed + if (skipFrames > 0) { + skipFrames--; + return WTF::IterationStatus::Continue; + } + + stackFrames.constructAndAppend(vm, visitor); + i++; + + return (i == framesCount) ? WTF::IterationStatus::Done : WTF::IterationStatus::Continue; + }); + + return JSCStackTrace(stackFrames); +} + +JSCStackTrace JSCStackTrace::getStackTraceForThrownValue(JSC::VM& vm, JSC::JSValue thrownValue) +{ + const WTF::Vector<JSC::StackFrame>* jscStackTrace = nullptr; + + JSC::Exception* currentException = DECLARE_CATCH_SCOPE(vm).exception(); + if (currentException && currentException->value() == thrownValue) { + jscStackTrace = ¤tException->stack(); + } else { + JSC::ErrorInstance* error = JSC::jsDynamicCast<JSC::ErrorInstance*>(thrownValue); + if (error) { + jscStackTrace = error->stackTrace(); + } + } + + if (!jscStackTrace) { + return JSCStackTrace(); + } + + return fromExisting(vm, *jscStackTrace); +} + +JSCStackFrame::JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor) + : m_vm(vm) + , m_codeBlock(nullptr) + , m_bytecodeIndex(JSC::BytecodeIndex()) + , m_sourceURL(nullptr) + , m_functionName(nullptr) + , m_isWasmFrame(false) + , m_sourcePositionsState(SourcePositionsState::NotCalculated) +{ + m_callee = visitor->callee().asCell(); + m_callFrame = visitor->callFrame(); + + // Based on JSC's GetStackTraceFunctor (Interpreter.cpp) + if (visitor->isWasmFrame()) { + m_wasmFunctionIndexOrName = visitor->wasmFunctionIndexOrName(); + m_isWasmFrame = true; + } else if (!!visitor->codeBlock() && !visitor->codeBlock()->unlinkedCodeBlock()->isBuiltinFunction()) { + m_codeBlock = visitor->codeBlock(); + m_bytecodeIndex = visitor->bytecodeIndex(); + } +} + +JSCStackFrame::JSCStackFrame(JSC::VM& vm, const JSC::StackFrame& frame) + : m_vm(vm) + , m_callFrame(nullptr) + , m_codeBlock(nullptr) + , m_bytecodeIndex(JSC::BytecodeIndex()) + , m_sourceURL(nullptr) + , m_functionName(nullptr) + , m_isWasmFrame(false) + , m_sourcePositionsState(SourcePositionsState::NotCalculated) +{ + m_callee = frame.callee(); + + // Based on JSC's GetStackTraceFunctor (Interpreter.cpp) + if (frame.isWasmFrame()) { + m_wasmFunctionIndexOrName = frame.wasmFunctionIndexOrName(); + m_isWasmFrame = true; + } else { + m_codeBlock = frame.codeBlock(); + if (frame.hasBytecodeIndex()) { + m_bytecodeIndex = frame.bytecodeIndex(); + } + } +} + +intptr_t JSCStackFrame::sourceID() const +{ + return m_codeBlock ? m_codeBlock->ownerExecutable()->sourceID() : JSC::noSourceID; +} + +JSC::JSString* JSCStackFrame::sourceURL() +{ + if (!m_sourceURL) { + m_sourceURL = retrieveSourceURL(); + } + + return m_sourceURL; +} + +JSC::JSString* JSCStackFrame::functionName() +{ + if (!m_functionName) { + m_functionName = retrieveFunctionName(); + } + + return m_functionName; +} + +JSC::JSString* JSCStackFrame::typeName() +{ + if (!m_typeName) { + m_typeName = retrieveTypeName(); + } + + return m_typeName; +} + +JSCStackFrame::SourcePositions* JSCStackFrame::getSourcePositions() +{ + if (SourcePositionsState::NotCalculated == m_sourcePositionsState) { + m_sourcePositionsState = calculateSourcePositions() ? SourcePositionsState::Calculated : SourcePositionsState::Failed; + } + + return (SourcePositionsState::Calculated == m_sourcePositionsState) ? &m_sourcePositions : nullptr; +} + +static auto sourceURLWasmString = MAKE_STATIC_STRING_IMPL("[wasm code]"); +static auto sourceURLNativeString = MAKE_STATIC_STRING_IMPL("[native code]"); +ALWAYS_INLINE JSC::JSString* JSCStackFrame::retrieveSourceURL() +{ + if (m_isWasmFrame) { + return jsOwnedString(m_vm, sourceURLWasmString); + } + + if (!m_codeBlock) { + return jsOwnedString(m_vm, sourceURLNativeString); + } + + String sourceURL = m_codeBlock->ownerExecutable()->sourceURL(); + return sourceURL.isNull() ? m_vm.smallStrings.emptyString() : JSC::jsString(m_vm, sourceURL); +} + +static auto functionNameEvalCodeString = MAKE_STATIC_STRING_IMPL("eval code"); +static auto functionNameModuleCodeString = MAKE_STATIC_STRING_IMPL("module code"); +static auto functionNameGlobalCodeString = MAKE_STATIC_STRING_IMPL("global code"); +ALWAYS_INLINE JSC::JSString* JSCStackFrame::retrieveFunctionName() +{ + if (m_isWasmFrame) { + return jsString(m_vm, JSC::Wasm::makeString(m_wasmFunctionIndexOrName)); + } + + if (m_codeBlock) { + switch (m_codeBlock->codeType()) { + case JSC::EvalCode: + return JSC::jsOwnedString(m_vm, functionNameEvalCodeString); + case JSC::ModuleCode: + return JSC::jsOwnedString(m_vm, functionNameModuleCodeString); + case JSC::FunctionCode: + break; + case JSC::GlobalCode: + return JSC::jsOwnedString(m_vm, functionNameGlobalCodeString); + default: + ASSERT_NOT_REACHED(); + } + } + + if (!m_callee || !m_callee->isObject()) { + return m_vm.smallStrings.emptyString(); + } + + JSC::JSObject* calleeAsObject = JSC::jsCast<JSC::JSObject*>(m_callee); + + // First, try the "displayName" property + JSC::JSValue displayName = calleeAsObject->getDirect(m_vm, m_vm.propertyNames->displayName); + if (displayName && isJSString(displayName)) { + return JSC::asString(displayName); + } + + // Our addition - if there's no "dispalyName" property, try the "name" property + JSC::JSValue name = calleeAsObject->getDirect(m_vm, m_vm.propertyNames->name); + if (name && isJSString(name)) { + return JSC::asString(name); + } + + /* For functions (either JSFunction or InternalFunction), fallback to their "native" name property. + * Based on JSC::getCalculatedDisplayName, "inlining" the + * JSFunction::calculatedDisplayName\InternalFunction::calculatedDisplayName calls */ + if (JSC::JSFunction* function = JSC::jsDynamicCast<JSC::JSFunction*>(calleeAsObject)) { + // Based on JSC::JSFunction::calculatedDisplayName, skipping the "displayName" property check + WTF::String actualName = function->name(m_vm); + if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) { + return JSC::jsString(m_vm, actualName); + } + + return JSC::jsString(m_vm, function->jsExecutable()->name().string()); + } + if (JSC::InternalFunction* function = JSC::jsDynamicCast<JSC::InternalFunction*>(calleeAsObject)) { + // Based on JSC::InternalFunction::calculatedDisplayName, skipping the "displayName" property check + return JSC::jsString(m_vm, function->name()); + } + + return m_vm.smallStrings.emptyString(); +} + +ALWAYS_INLINE JSC::JSString* JSCStackFrame::retrieveTypeName() +{ + JSC::JSObject* calleeObject = JSC::jsCast<JSC::JSObject*>(m_callee); + // return JSC::jsTypeStringForValue(m_globalObjectcalleeObject->toThis() + return jsString(m_vm, makeString(calleeObject->className())); +} + +// General flow here is based on JSC's appendSourceToError (ErrorInstance.cpp) +bool JSCStackFrame::calculateSourcePositions() +{ + if (!m_codeBlock) { + return false; + } + + JSC::BytecodeIndex bytecodeIndex = hasBytecodeIndex() ? m_bytecodeIndex : JSC::BytecodeIndex(); + + /* Get the "raw" position info. + * Note that we're using m_codeBlock->unlinkedCodeBlock()->expressionRangeForBytecodeOffset rather than m_codeBlock->expressionRangeForBytecodeOffset + * in order get the "raw" offsets and avoid the CodeBlock's expressionRangeForBytecodeOffset modifications to the line and column numbers, + * (we don't need the column number from it, and we'll calculate the line "fixes" ourselves). */ + int startOffset = 0; + int endOffset = 0; + int divotPoint = 0; + unsigned line = 0; + unsigned unusedColumn = 0; + m_codeBlock->unlinkedCodeBlock()->expressionRangeForBytecodeIndex(bytecodeIndex, divotPoint, startOffset, endOffset, line, unusedColumn); + divotPoint += m_codeBlock->sourceOffset(); + + /* On the first line of the source code, it seems that we need to "fix" the column with the starting + * offset. We currently use codeBlock->source()->startPosition().m_column.oneBasedInt() as the + * offset in the first line rather than codeBlock->firstLineColumnOffset(), which seems simpler + * (and what CodeBlock::expressionRangeForBytecodeOffset does). This is because firstLineColumnOffset + * values seems different from what we expect (according to v8's tests) and I haven't dove into the + * relevant parts in JSC (yet) to figure out why. */ + unsigned columnOffset = line ? 0 : m_codeBlock->source().startColumn().zeroBasedInt(); + + // "Fix" the line number + JSC::ScriptExecutable* executable = m_codeBlock->ownerExecutable(); + line = executable->overrideLineNumber(m_vm).value_or(line + executable->firstLine()); + + // Calculate the staring\ending offsets of the entire expression + int expressionStart = divotPoint - startOffset; + int expressionStop = divotPoint + endOffset; + + // Make sure the range is valid + StringView sourceString = m_codeBlock->source().provider()->source(); + if (!expressionStop || expressionStart > static_cast<int>(sourceString.length())) { + return false; + } + + // Search for the beginning of the line + unsigned int lineStart = expressionStart; + while ((lineStart > 0) && ('\n' != sourceString[lineStart - 1])) { + lineStart--; + } + // Search for the end of the line + unsigned int lineStop = expressionStop; + unsigned int sourceLength = sourceString.length(); + while ((lineStop < sourceLength) && ('\n' != sourceString[lineStop])) { + lineStop++; + } + + /* Finally, store the source "positions" info. + * Notes: + * - The retrieved column seem to point the "end column". To make sure we're current, we'll calculate the + * columns ourselves, since we've already found where the line starts. Note that in v8 it should be 0-based + * here (in contrast the 1-based column number in v8::StackFrame). + * - The static_casts are ugly, but comes from differences between JSC and v8's api, and should be OK + * since no source should be longer than "max int" chars. + */ + m_sourcePositions.expressionStart = WTF::OrdinalNumber::fromZeroBasedInt(expressionStart); + m_sourcePositions.expressionStop = WTF::OrdinalNumber::fromZeroBasedInt(expressionStop); + m_sourcePositions.line = WTF::OrdinalNumber::fromZeroBasedInt(static_cast<int>(line)); + m_sourcePositions.startColumn = WTF::OrdinalNumber::fromZeroBasedInt((expressionStart - lineStart) + columnOffset); + m_sourcePositions.endColumn = WTF::OrdinalNumber::fromZeroBasedInt(m_sourcePositions.startColumn.zeroBasedInt() + (expressionStop - expressionStart)); + m_sourcePositions.lineStart = WTF::OrdinalNumber::fromZeroBasedInt(static_cast<int>(lineStart)); + m_sourcePositions.lineStop = WTF::OrdinalNumber::fromZeroBasedInt(static_cast<int>(lineStop)); + + return true; +} + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/ErrorStackTrace.h b/src/bun.js/bindings/ErrorStackTrace.h new file mode 100644 index 000000000..65ee60852 --- /dev/null +++ b/src/bun.js/bindings/ErrorStackTrace.h @@ -0,0 +1,180 @@ +/** + * This source code is licensed under the terms found in the LICENSE file in + * node-jsc's root directory. + */ + +#pragma once + +#include <JavaScriptCore/StackVisitor.h> +#include <JavaScriptCore/CodeBlock.h> +#include <JavaScriptCore/WasmIndexOrName.h> + +#include "ZigGlobalObject.h" + +using namespace JSC; +using namespace WebCore; + +namespace Zig { + +/* JSCStackFrame is an alternative to JSC::StackFrame, which provides the following advantages\changes: + * - Also hold the call frame (ExecState). This is mainly used by CallSite to get "this value". + * - More detailed and v8 compatible "source offsets" caculations: JSC::StackFrame only provides the + * line number and column numbers. It's column calculation seems to be different than v8's column. + * According to v8's unit tests, it seems that their column number points to the beginning of + * the expression which raised the exception, while in JSC the column returned by computeLineAndColumn + * seem to point to the end of the expression. Thus, we'll do the calculations ourselves. + * Here, we'll also provide more information which is needed by jscshim (mainly by Message): + * - Full expression range in the source code + * - Line number + * - Start\end columns (before the "throw", and after the "throw <x>") + * - Line start\stop offsets in the source code + * Also, to avoid zero\one base confusions, we'll store all offsets as WTF::OrdinalNumber. + * - Function name "calculation" also checks the function's "name" property. See retrieveFunctionName's + * documentation bellow for more information. + * - String properties are exposed (and cached) as JSStrings, instead of WTF::String. + * - Helper functions like isEval and isConstructor. + * + * Note that this is not a heap allocated, garbage collected, JSCell. It must be stack allocated, as it doens't + * use any write barriers and rely on the GC to see the stored JSC object pointers on the stack. + */ +class JSCStackFrame { +public: + struct SourcePositions { + WTF::OrdinalNumber expressionStart; + WTF::OrdinalNumber expressionStop; + WTF::OrdinalNumber line; + WTF::OrdinalNumber startColumn; + WTF::OrdinalNumber endColumn; + WTF::OrdinalNumber lineStart; + WTF::OrdinalNumber lineStop; + }; + +private: + JSC::VM& m_vm; + JSC::JSCell* m_callee; + + // May be null + JSC::CallFrame* m_callFrame; + + // May be null + JSC::CodeBlock* m_codeBlock; + JSC::BytecodeIndex m_bytecodeIndex; + + // Lazy-initialized + JSC::JSString* m_sourceURL; + JSC::JSString* m_functionName; + JSC::JSString* m_typeName; + + // m_wasmFunctionIndexOrName has meaning only when m_isWasmFrame is set + JSC::Wasm::IndexOrName m_wasmFunctionIndexOrName; + bool m_isWasmFrame; + + enum class SourcePositionsState { + NotCalculated, + Failed, + Calculated + }; + + SourcePositions m_sourcePositions; + SourcePositionsState m_sourcePositionsState; + +public: + JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor); + JSCStackFrame(JSC::VM& vm, const JSC::StackFrame& frame); + + JSC::JSCell* callee() const { return m_callee; } + JSC::CallFrame* callFrame() const { return m_callFrame; } + JSC::CodeBlock* codeBlock() const { return m_codeBlock; } + + intptr_t sourceID() const; + JSC::JSString* sourceURL(); + JSC::JSString* functionName(); + JSC::JSString* typeName(); + + bool hasBytecodeIndex() const { return (m_bytecodeIndex.offset() != UINT_MAX) && !m_isWasmFrame; } + JSC::BytecodeIndex bytecodeIndex() const + { + ASSERT(hasBytecodeOffset()); + return m_bytecodeIndex; + } + + // Returns null if can't retreive the source positions + SourcePositions* getSourcePositions(); + + bool isWasmFrame() const { return m_isWasmFrame; } + bool isEval() const { return m_codeBlock && (JSC::EvalCode == m_codeBlock->codeType()); } + bool isConstructor() const { return m_codeBlock && (JSC::CodeForConstruct == m_codeBlock->specializationKind()); } + +private: + ALWAYS_INLINE JSC::JSString* retrieveSourceURL(); + + /* Regarding real functions (not eval\module\global code), both v8 and JSC seem to follow + * the same logic, which is to first try the function's "display name", and if it's not defined, + * the function's name. In JSC, StackFrame::functionName uses JSC::getCalculatedDisplayName, + * which will internally call the JSFunction\InternalFunction's calculatedDisplayName function. + * But, those function don't check the function's "name" property if the "dispaly name" isn't defined. + * See JSFunction::name()'s and InternalFunction::name()'s implementation. According to v8's unit tests, + * v8 does check the name property in StackFrame::GetFunctionName (see the last part of the + * "CaptureStackTrace" test in test-api.cc). + * Thus, we'll reimplement the general flow of JSC::getCalculatedDisplayName and it's internal calls, + * and just try to use the "name" property when needed, so our lookup will be: + * "display name" property -> "name" property -> JSFunction\InternalFunction "name" methods. + */ + ALWAYS_INLINE JSC::JSString* retrieveFunctionName(); + + ALWAYS_INLINE JSC::JSString* retrieveTypeName(); + + bool calculateSourcePositions(); +}; + +class JSCStackTrace { +private: + WTF::Vector<JSCStackFrame> m_frames; + +public: + JSCStackTrace() + { + } + + size_t size() const { return m_frames.size(); } + bool isEmpty() const { return m_frames.isEmpty(); } + JSCStackFrame& at(size_t i) { return m_frames.at(i); } + + static JSCStackTrace fromExisting(JSC::VM& vm, const WTF::Vector<JSC::StackFrame>& existingFrames); + + /* This is based on JSC::Interpreter::getStackTrace, but skips native (non js and not wasm) + * frames, which is what v8 does. Note that we could have just called JSC::Interpreter::getStackTrace + * and and filter it later (or let our callers filter it), but that would have been both inefficient, and + * problematic with the requested stack size limit (as it should only refer to the non-native frames, + * thus we would have needed to pass a large limit to JSC::Interpreter::getStackTrace, and filter out + * maxStackSize non-native frames). + * + * Return value must remain stack allocated. */ + static JSCStackTrace captureCurrentJSStackTrace(Zig::GlobalObject* globalObject, JSC::CallFrame* callFrame, size_t frameLimit, JSC::JSValue caller); + + /* In JSC, JSC::Exception points to the actual value that was thrown, usually + * a JSC::ErrorInstance (but could be any JSValue). In v8, on the other hand, + * TryCatch::Exception returns the thrown value, and we follow the same rule in jscshim. + * This is a problem, since JSC::Exception is the one that holds the stack trace. + * ErrorInstances might also hold the stack trace (until the error properties are + * "materialized" and it is no longer needed). So, to try to get the stack trace for a thrown JSValue, + * we'll try two things: + * - If the current JSC (vm) exception points to our value, it means our value is probably the current + * exception and we could take the stack trace from the vm's current JSC::Exception. The downside + * of doing this is that we'll get the last stack trace of the thrown value, meaning that if the value + * was thrown, stored in the api and than re-thrown, we'll get the latest stack trace and not the one + * that was available when we stored it. For now it'll do. + * - If that failed and our thrown value is a JSC::ErrorInstance, we'll try to use it's stack trace, + * if it currently has one. + * + * Return value must remain stack allocated */ + static JSCStackTrace getStackTraceForThrownValue(JSC::VM& vm, JSC::JSValue thrownValue); + +private: + JSCStackTrace(WTF::Vector<JSCStackFrame>& frames) + : m_frames(WTFMove(frames)) + { + } +}; + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 84e63802b..34663c228 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -68,6 +68,8 @@ // #include "JavaScriptCore/CachedType.h" #include "JavaScriptCore/JSCallbackObject.h" #include "JavaScriptCore/JSClassRef.h" +#include "JavaScriptCore/CallData.h" +#include "GCDefferalContext.h" #include "BunClientData.h" @@ -167,6 +169,8 @@ using JSBuffer = WebCore::JSBuffer; #include "OnigurumaRegExp.h" +constexpr size_t DEFAULT_ERROR_STACK_TRACE_LIMIT = 10; + #ifdef __APPLE__ #include <sys/sysctl.h> #else @@ -2104,11 +2108,224 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotaskVariadic, (JSGlobalObject * g return JSValue::encode(jsUndefined()); } +void GlobalObject::createCallSitesFromFrames(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ObjectInitializationScope& objectScope, JSCStackTrace& stackTrace, JSC::JSArray* callSites) +{ + /* From v8's "Stack Trace API" (https://github.com/v8/v8/wiki/Stack-Trace-API): + * "To maintain restrictions imposed on strict mode functions, frames that have a + * strict mode function and all frames below (its caller etc.) are not allow to access + * their receiver and function objects. For those frames, getFunction() and getThis() + * will return undefined."." */ + bool encounteredStrictFrame = false; + GlobalObject* globalObject = reinterpret_cast<GlobalObject*>(lexicalGlobalObject); + + JSC::Structure* callSiteStructure = globalObject->callSiteStructure(); + JSC::IndexingType callSitesIndexingType = callSites->indexingType(); + size_t framesCount = stackTrace.size(); + for (size_t i = 0; i < framesCount; i++) { + /* Note that we're using initializeIndex and not callSites->butterfly()->contiguous().data() + * directly, since if we're "having a bad time" (globalObject->isHavingABadTime()), + * the array won't be contiguous, but a "slow put" array. + * See https://github.com/WebKit/webkit/commit/1c4a32c94c1f6c6aa35cf04a2b40c8fe29754b8e for more info + * about what's a "bad time". */ + CallSite* callSite = CallSite::create(lexicalGlobalObject, callSiteStructure, stackTrace.at(i), encounteredStrictFrame); + callSites->initializeIndex(objectScope, i, callSite, callSitesIndexingType); + + if (!encounteredStrictFrame) { + encounteredStrictFrame = callSite->isStrict(); + } + } +} + +JSC::JSValue GlobalObject::formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites, ZigStackFrame remappedStackFrames[]) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue errorValue = this->get(this, JSC::Identifier::fromString(vm, "Error"_s)); + if (UNLIKELY(scope.exception())) { + return JSValue(); + } + + if (!errorValue || errorValue.isUndefined() || !errorValue.isObject()) { + return JSValue(jsEmptyString(vm)); + } + + auto* errorConstructor = jsDynamicCast<JSC::JSObject*>(errorValue); + + /* If the user has set a callable Error.prepareStackTrace - use it to format the stack trace. */ + JSC::JSValue prepareStackTrace = errorConstructor->getIfPropertyExists(lexicalGlobalObject, JSC::Identifier::fromString(vm, "prepareStackTrace"_s)); + if (prepareStackTrace && prepareStackTrace.isCallable()) { + JSC::CallData prepareStackTraceCallData = JSC::getCallData(prepareStackTrace); + + if (prepareStackTraceCallData.type != JSC::CallData::Type::None) { + JSC::MarkedArgumentBuffer arguments; + arguments.append(errorObject); + arguments.append(callSites); + ASSERT(!arguments.hasOverflowed()); + + JSC::JSValue result = profiledCall( + lexicalGlobalObject, + JSC::ProfilingReason::Other, + prepareStackTrace, + prepareStackTraceCallData, + errorConstructor, + arguments); + RETURN_IF_EXCEPTION(scope, JSC::jsUndefined()); + return result; + } + } + + // default formatting + size_t framesCount = callSites->length(); + + WTF::StringBuilder sb; + if (JSC::JSValue errorMessage = errorObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->message)) { + sb.append("Error: "_s); + sb.append(errorMessage.getString(lexicalGlobalObject)); + } else { + sb.append("Error"_s); + } + + if (framesCount > 0) { + sb.append("\n"_s); + } + + for (size_t i = 0; i < framesCount; i++) { + JSC::JSValue callSiteValue = callSites->getIndex(lexicalGlobalObject, i); + CallSite* callSite = JSC::jsDynamicCast<CallSite*>(callSiteValue); + + JSString* typeName = jsTypeStringForValue(lexicalGlobalObject, callSite->thisValue()); + JSString* function = callSite->functionName().toString(lexicalGlobalObject); + JSString* functionName = callSite->functionName().toString(lexicalGlobalObject); + JSString* sourceURL = callSite->sourceURL().toString(lexicalGlobalObject); + JSString* columnNumber = remappedStackFrames[i].position.column_start >= 0 ? jsNontrivialString(vm, String::number(remappedStackFrames[i].position.column_start)) : jsEmptyString(vm); + JSString* lineNumber = remappedStackFrames[i].position.line >= 0 ? jsNontrivialString(vm, String::number(remappedStackFrames[i].position.line)) : jsEmptyString(vm); + bool isConstructor = callSite->isConstructor(); + + sb.append(" at "_s); + if (functionName->length() > 0) { + if (isConstructor) { + sb.append("new "_s); + } else { + // TODO: print type or class name if available + // sb.append(typeName->getString(lexicalGlobalObject)); + // sb.append(" "_s); + } + sb.append(functionName->getString(lexicalGlobalObject)); + } else { + sb.append("<anonymous>"_s); + } + sb.append(" ("_s); + if (callSite->isNative()) { + sb.append("native"_s); + } else { + sb.append(sourceURL->getString(lexicalGlobalObject)); + sb.append(":"_s); + sb.append(lineNumber->getString(lexicalGlobalObject)); + sb.append(":"_s); + sb.append(columnNumber->getString(lexicalGlobalObject)); + } + sb.append(")"_s); + if (i != framesCount - 1) { + sb.append("\n"_s); + } + } + + return JSC::JSValue(jsString(vm, sb.toString())); +} + +extern "C" void Bun__remapStackFramePositions(JSC::JSGlobalObject*, ZigStackFrame*, size_t); + +JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace); +JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + GlobalObject* globalObject = reinterpret_cast<GlobalObject*>(lexicalGlobalObject); + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSC::JSValue objectArg = callFrame->argument(0); + if (!objectArg.isObject()) { + return JSC::JSValue::encode(throwTypeError(lexicalGlobalObject, scope, "invalid_argument"_s)); + } + + JSC::JSObject* errorObject = objectArg.asCell()->getObject(); + JSC::JSValue caller = callFrame->argument(1); + + JSValue errorValue = lexicalGlobalObject->get(lexicalGlobalObject, vm.propertyNames->Error); + auto* errorConstructor = jsDynamicCast<JSC::JSObject*>(errorValue); + + size_t stackTraceLimit = DEFAULT_ERROR_STACK_TRACE_LIMIT; + if (JSC::JSValue stackTraceLimitProp = errorConstructor->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->stackTraceLimit)) { + if (stackTraceLimitProp.isNumber()) { + stackTraceLimit = std::min(std::max(static_cast<size_t>(stackTraceLimitProp.toIntegerOrInfinity(lexicalGlobalObject)), 0ul), 2048ul); + if (stackTraceLimit == 0) { + stackTraceLimit = 2048; + } + } + } + JSCStackTrace stackTrace = JSCStackTrace::captureCurrentJSStackTrace(globalObject, callFrame, stackTraceLimit, caller); + + // Create an (uninitialized) array for our "call sites" + JSC::GCDeferralContext deferralContext(vm); + JSC::ObjectInitializationScope objectScope(vm); + JSC::JSArray* callSites = JSC::JSArray::tryCreateUninitializedRestricted(objectScope, + &deferralContext, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + stackTrace.size()); + RELEASE_ASSERT(callSites); + + // Create the call sites (one per frame) + GlobalObject::createCallSitesFromFrames(lexicalGlobalObject, objectScope, stackTrace, callSites); + + /* Foramt the stack trace. + * Note that v8 won't actually format the stack trace here, but will create a "stack" accessor + * on the error object, which will format the stack trace on the first access. For now, since + * we're not being used internally by JSC, we can assume callers of Error.captureStackTrace in + * node are interested in the (formatted) stack. */ + + size_t framesCount = stackTrace.size(); + ZigStackFrame remappedFrames[framesCount]; + for (int i = 0; i < framesCount; i++) { + remappedFrames[i].source_url = Zig::toZigString(stackTrace.at(i).sourceURL(), lexicalGlobalObject); + if (JSCStackFrame::SourcePositions* sourcePositions = stackTrace.at(i).getSourcePositions()) { + remappedFrames[i].position.line = sourcePositions->line.zeroBasedInt(); + remappedFrames[i].position.column_start = sourcePositions->startColumn.zeroBasedInt() + 1; + } else { + remappedFrames[i].position.line = -1; + remappedFrames[i].position.column_start = -1; + } + } + + // remap line and column start to original source + Bun__remapStackFramePositions(lexicalGlobalObject, remappedFrames, framesCount); + + JSC::JSValue formattedStackTrace = globalObject->formatStackTrace(vm, lexicalGlobalObject, errorObject, callSites, remappedFrames); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode({})); + + if (errorObject->hasProperty(lexicalGlobalObject, vm.propertyNames->stack)) { + errorObject->deleteProperty(lexicalGlobalObject, vm.propertyNames->stack); + } + if (formattedStackTrace.isUndefinedOrNull()) { + errorObject->putDirect(vm, vm.propertyNames->stack, jsUndefined(), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + } else { + errorObject->putDirect(vm, vm.propertyNames->stack, formattedStackTrace.toString(lexicalGlobalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + } + + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSValue {})); + + return JSC::JSValue::encode(JSC::jsUndefined()); +} + void GlobalObject::finishCreation(VM& vm) { Base::finishCreation(vm); ASSERT(inherits(info())); + JSC::JSObject* errorConstructor = this->errorConstructor(); + errorConstructor->putDirectNativeFunctionWithoutTransition(vm, this, JSC::Identifier::fromString(vm, "captureStackTrace"_s), 2, errorConstructorFuncCaptureStackTrace, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); + + // JSC default is 100 + errorConstructor->deleteProperty(this, vm.propertyNames->stackTraceLimit); + errorConstructor->putDirect(vm, vm.propertyNames->stackTraceLimit, jsNumber(DEFAULT_ERROR_STACK_TRACE_LIMIT), JSC::PropertyAttribute::DontEnum | 0); + // Change prototype from null to object for synthetic modules. m_moduleNamespaceObjectStructure.initLater( [](const Initializer<Structure>& init) { @@ -2318,6 +2535,14 @@ void GlobalObject::finishCreation(VM& vm) init.setConstructor(constructor); }); + m_callSiteStructure.initLater( + [](LazyClassStructure::Initializer& init) { + auto* prototype = CallSitePrototype::create(init.vm, CallSitePrototype::createStructure(init.vm, init.global, init.global->objectPrototype()), init.global); + auto* structure = CallSite::createStructure(init.vm, init.global, prototype); + init.setPrototype(prototype); + init.setStructure(structure); + }); + m_JSStringDecoderClassStructure.initLater( [](LazyClassStructure::Initializer& init) { auto* prototype = JSStringDecoderPrototype::create( @@ -3037,6 +3262,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_processObject.visit(visitor); thisObject->m_subtleCryptoObject.visit(visitor); thisObject->m_JSHTTPResponseController.visit(visitor); + thisObject->m_callSiteStructure.visit(visitor); for (auto& barrier : thisObject->m_thenables) { visitor.append(barrier); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 8613bc912..68fa452b2 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -34,6 +34,10 @@ class EventLoopTask; #include "DOMIsoSubspaces.h" #include "BunPlugin.h" +#include "ErrorStackTrace.h" +#include "CallSite.h" +#include "CallSitePrototype.h" + extern "C" void Bun__reportError(JSC__JSGlobalObject*, JSC__JSValue); // defined in ModuleLoader.cpp extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultResolve(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame); @@ -58,6 +62,8 @@ extern "C" JSC::EncodedJSValue jsFunctionOnLoadObjectResultReject(JSC::JSGlobalO namespace Zig { +class JSCStackTrace; + using JSDOMStructureMap = HashMap<const JSC::ClassInfo*, JSC::WriteBarrier<JSC::Structure>>; using DOMGuardedObjectSet = HashSet<WebCore::DOMGuardedObject*>; @@ -153,6 +159,9 @@ public: void clearDOMGuardedObjects(); + static void createCallSitesFromFrames(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ObjectInitializationScope& objectScope, JSCStackTrace& stackTrace, JSC::JSArray* callSites); + JSC::JSValue formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites, ZigStackFrame remappedStackFrames[]); + static void reportUncaughtExceptionAtEventLoop(JSGlobalObject*, JSC::Exception*); static JSGlobalObject* deriveShadowRealmGlobalObject(JSGlobalObject* globalObject); static void queueMicrotaskToEventLoop(JSC::JSGlobalObject& global, Ref<JSC::Microtask>&& task); @@ -218,6 +227,8 @@ public: JSC::JSMap* requireMap() { return m_requireMap.getInitializedOnMainThread(this); } JSC::JSObject* encodeIntoObjectPrototype() { return m_encodeIntoObjectPrototype.getInitializedOnMainThread(this); } + JSC::Structure* callSiteStructure() const { return m_callSiteStructure.getInitializedOnMainThread(this); } + JSC::JSObject* performanceObject() { return m_performanceObject.getInitializedOnMainThread(this); } JSC::JSObject* primordialsObject() { return m_primordialsObject.getInitializedOnMainThread(this); } @@ -441,6 +452,7 @@ private: LazyClassStructure m_JSStringDecoderClassStructure; LazyClassStructure m_NapiClassStructure; LazyClassStructure m_OnigurumaRegExpClassStructure; + LazyClassStructure m_callSiteStructure; /** * WARNING: You must update visitChildrenImpl() if you add a new field. diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index b6641ea4d..cd292d938 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -30,6 +30,7 @@ public: std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForReadableState; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForPendingVirtualModuleResult; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForOnigurumaRegExp; + std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForCallSite; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 32c367014..568b4e978 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -30,6 +30,7 @@ public: std::unique_ptr<IsoSubspace> m_subspaceForReadableState; std::unique_ptr<IsoSubspace> m_subspaceForPendingVirtualModuleResult; std::unique_ptr<IsoSubspace> m_subspaceForOnigurumaRegExp; + std::unique_ptr<IsoSubspace> m_subspaceForCallSite; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 1fd6783be..8768b3180 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1551,6 +1551,25 @@ pub const VirtualMachine = struct { } } + pub export fn Bun__remapStackFramePositions(globalObject: *JSC.JSGlobalObject, frames: [*]JSC.ZigStackFrame, frames_count: usize) void { + globalObject.bunVM().remapStackFramePositions(frames, frames_count); + } + + pub fn remapStackFramePositions(this: *VirtualMachine, frames: [*]JSC.ZigStackFrame, frames_count: usize) void { + var i: usize = 0; + while (i < frames_count) : (i += 1) { + if (frames[i].position.isInvalid()) continue; + if (this.source_mappings.resolveMapping( + frames[i].source_url.slice(), + @maximum(frames[i].position.line, 0), + @maximum(frames[i].position.column_start, 0), + )) |mapping| { + frames[i].position.line = mapping.original.lines; + frames[i].position.column_start = mapping.original.columns; + } + } + } + pub fn remapZigException( this: *VirtualMachine, exception: *ZigException, @@ -1831,6 +1850,11 @@ pub const VirtualMachine = struct { try writer.print(comptime Output.prettyFmt("<r><red>error<r>\n", allow_ansi_color), .{}); } } + + comptime { + if (!JSC.is_bindgen) + _ = Bun__remapStackFramePositions; + } }; const GetterFn = fn ( |