Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/jsc/bindings/CallSite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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)
void CallSite::finishCreation(VM& vm, JSCStackFrame& stackFrame, bool encounteredStrictFrame)
{
Base::finishCreation(vm);

Expand All @@ -39,20 +39,13 @@ void CallSite::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSCStac
}
}

// Initialize "this" and "function" (and set the "IsStrict" flag if needed)
JSC::CallFrame* callFrame = stackFrame.callFrame();
// Initialize "this" and "function" (and set the "IsStrict" flag if needed).
// JSC::StackFrame does not record the receiver, so getThis() is always undefined.
Comment thread
robobun marked this conversation as resolved.
Outdated
m_thisValue.set(vm, this, JSC::jsUndefined());
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 && callFrame->thisValue()) {
// 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());
}

Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/CallSite.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CallSite final : public JSC::JSNonFinalObject {
{
auto& vm = JSC::getVM(globalObject);
CallSite* callSite = new (NotNull, JSC::allocateCell<CallSite>(vm)) CallSite(vm, structure);
callSite->finishCreation(vm, globalObject, stackFrame, encounteredStrictFrame);
callSite->finishCreation(vm, stackFrame, encounteredStrictFrame);
return callSite;
}

Expand Down Expand Up @@ -101,7 +101,7 @@ class CallSite final : public JSC::JSNonFinalObject {
{
}

void finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSCStackFrame& stackFrame, bool encounteredStrictFrame);
void finishCreation(VM& vm, JSCStackFrame& stackFrame, bool encounteredStrictFrame);

DECLARE_VISIT_CHILDREN;
};
Expand Down
50 changes: 1 addition & 49 deletions src/jsc/bindings/ErrorStackTrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,57 +213,9 @@ static bool isVisibleBuiltinFunction(JSC::CodeBlock* codeBlock)
return !Zig::sourceURL(source).isEmpty();
}

JSCStackFrame::JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor)
: m_vm(vm)
, m_codeBlock(nullptr)
, m_bytecodeIndex(JSC::BytecodeIndex())
, m_sourceURL()
, m_functionName()
, m_isWasmFrame(false)
, m_isAsync(false)
, m_sourcePositionsState(SourcePositionsState::NotCalculated)
{
m_callee = visitor->callee().asCell();
m_callFrame = visitor->callFrame();

if (auto* codeBlock = visitor->codeBlock()) {
auto codeType = codeBlock->codeType();
if (codeType == JSC::FunctionCode || codeType == JSC::EvalCode) {
m_isFunctionOrEval = true;
}
}

// Based on JSC's GetStackTraceFunctor (Interpreter.cpp)
if (visitor->isNativeCalleeFrame()) {
auto* nativeCallee = visitor->callee().asNativeCallee();
switch (nativeCallee->category()) {
case NativeCallee::Category::Wasm: {
m_wasmFunctionIndexOrName = visitor->wasmFunctionIndexOrName();
m_isWasmFrame = true;
break;
}
case NativeCallee::Category::InlineCache: {
break;
}
}
} else if (auto* codeBlock = visitor->codeBlock()) {
auto* unlinkedCodeBlock = codeBlock->unlinkedCodeBlock();
if (!unlinkedCodeBlock->isBuiltinFunction() || isVisibleBuiltinFunction(codeBlock)) {
m_codeBlock = codeBlock;
m_bytecodeIndex = visitor->bytecodeIndex();
}
}

if (!m_bytecodeIndex && visitor->hasLineAndColumnInfo()) {
auto lineColumn = visitor->computeLineAndColumn();
m_sourcePositions = { OrdinalNumber::fromOneBasedInt(lineColumn.line), OrdinalNumber::fromOneBasedInt(lineColumn.column) };
m_sourcePositionsState = SourcePositionsState::Calculated;
}
}

JSCStackFrame::JSCStackFrame(JSC::VM& vm, const JSC::StackFrame& frame)
: m_vm(vm)
, m_callFrame(nullptr)
, m_stackFrame(&frame)
, m_codeBlock(nullptr)
, m_bytecodeIndex(JSC::BytecodeIndex())
, m_sourceURL()
Expand Down
14 changes: 7 additions & 7 deletions src/jsc/bindings/ErrorStackTrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

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".
/* JSCStackFrame is a view over a JSC::StackFrame, which provides the following advantages\changes:
* - More detailed and v8 compatible "source offsets" calculations: 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
Expand Down Expand Up @@ -46,11 +45,10 @@

private:
JSC::VM& m_vm;
// Points into the vector this frame was built from (JSCStackTrace::fromExisting).
const JSC::StackFrame* m_stackFrame;
JSC::JSCell* m_callee { nullptr };

// May be null
JSC::CallFrame* m_callFrame;

// May be null
JSC::CodeBlock* m_codeBlock { nullptr };
JSC::BytecodeIndex m_bytecodeIndex;
Expand All @@ -77,11 +75,10 @@
SourcePositionsState m_sourcePositionsState;

public:
JSCStackFrame(JSC::VM& vm, JSC::StackVisitor& visitor);
JSCStackFrame(JSC::VM& vm, const JSC::StackFrame& frame);

const JSC::StackFrame& stackFrame() const { return *m_stackFrame; }
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;
Expand Down Expand Up @@ -179,6 +176,9 @@

WTF::Vector<JSCStackFrame>&& frames() { return WTF::move(m_frames); }

/* Skips private-visibility frames, which JSC only leaves in `existingFrames` while
* Options::showPrivateScriptsInStackTraces() is on (debug builds), so the result can be shorter
* than `existingFrames`; reach the JSC frame through JSCStackFrame::stackFrame(), not by index. */

Check warning on line 181 in src/jsc/bindings/ErrorStackTrace.h

View check run for this annotation

Claude / Claude Code Review

getStackTraceForThrownValue not removed as PR description states

The PR description says `JSCStackTrace::getStackTraceForThrownValue` "is removed instead of being adapted to the new signature", but the definition (ErrorStackTrace.cpp:185), the declaration here, and its ~15-line doc comment are still in the tree. A repo-wide grep confirms zero callers, so please follow through and delete all three — leaving it around now that `JSCStackFrame` holds a raw pointer into the source vector is a latent lifetime footgun for whoever wires up a caller later.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
static JSCStackTrace fromExisting(JSC::VM& vm, const WTF::Vector<JSC::StackFrame>& existingFrames);

static void getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, JSC::JSCell* owner, JSC::JSValue caller, WTF::Vector<JSC::StackFrame>& stackTrace, size_t stackTraceLimit);
Expand Down
5 changes: 3 additions & 2 deletions src/jsc/bindings/FormatStackTraceForJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,8 @@ static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObj

for (int i = 0; i < n; i++) {
ZigStackFrame& frame = remappedFrames[i];
auto& stackFrame = stackFrames.at(i);
JSCStackFrame& visibleFrame = stackTrace.at(i);
const JSC::StackFrame& stackFrame = visibleFrame.stackFrame();
sourceURLs[i] = Zig::sourceURL(vm, stackFrame);
didRemap[i] = false;
frame.position.line_zero_based = -1;
Expand All @@ -478,7 +479,7 @@ static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObj
}

if (globalObjectForFrame == globalObject) {
if (JSCStackFrame::SourcePositions* sourcePositions = stackTrace.at(i).getSourcePositions()) {
if (JSCStackFrame::SourcePositions* sourcePositions = visibleFrame.getSourcePositions()) {
frame.position.line_zero_based = sourcePositions->line.zeroBasedInt();
frame.position.column_zero_based = sourcePositions->column.zeroBasedInt();
}
Expand Down
75 changes: 74 additions & 1 deletion test/js/node/v8/capture-stack-trace.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { nativeFrameForTesting } from "bun:internal-for-testing";
import { noInline } from "bun:jsc";
import { afterEach, expect, mock, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, tempDir } from "harness";
const origPrepareStackTrace = Error.prepareStackTrace;
afterEach(() => {
Error.prepareStackTrace = origPrepareStackTrace;
Expand Down Expand Up @@ -1121,3 +1121,76 @@ test("lazy error-info materialization does not store an empty stack value when t
});
expect(exitCode).toBe(0);
});

test("Error.prepareStackTrace call sites keep their own file when a hidden frame is on the stack", async () => {
// A bound function call is a frame with private implementation visibility. JSC omits it from the
// trace unless showPrivateScriptsInStackTraces is on (debug builds turn it on); Bun then has to
// drop it while building the CallSites, and the frames after it must keep their own file.
using dir = tempDir("prepare-stack-trace-hidden-frame", {
"main.cjs": [
`const { outer, viaAsyncLocalStorage } = require("./callers.cjs");`,
`function inner() {`,
` const error = new Error("boom");`,
` return error;`,
`}`,
`function main() {`,
` const error = outer(inner.bind(null));`,
` return error;`,
`}`,
`const { basename } = require("node:path");`,
`const wanted = new Set(["inner", "outer", "main", "run", "viaAsyncLocalStorage"]);`,
`Error.prepareStackTrace = (_error, callSites) =>`,
` callSites`,
` .filter(callSite => wanted.has(callSite.getFunctionName()))`,
` .map(callSite => ({`,
` name: callSite.getFunctionName(),`,
` file: basename(callSite.getFileName()),`,
` line: callSite.getLineNumber(),`,
` }));`,
`console.log(JSON.stringify({ bound: main().stack, asyncLocalStorage: viaAsyncLocalStorage(inner.bind(null)).stack }));`,
].join("\n"),
"callers.cjs": [
`const { AsyncLocalStorage } = require("node:async_hooks");`,
`exports.outer = function outer(callback) {`,
` const error = callback();`,
` return error;`,
`};`,
`const storage = new AsyncLocalStorage();`,
`exports.viaAsyncLocalStorage = function viaAsyncLocalStorage(callback) {`,
` const error = storage.run("store", callback);`,
` return error;`,
`};`,
].join("\n"),
});

const results = await Promise.all(
["0", "1"].map(async showPrivateScriptsInStackTraces => {
await using proc = Bun.spawn({
cmd: [bunExe(), "main.cjs"],
cwd: String(dir),
env: { ...bunEnv, BUN_JSC_showPrivateScriptsInStackTraces: showPrivateScriptsInStackTraces },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { showPrivateScriptsInStackTraces, callSites: stdout && JSON.parse(stdout), stderr, exitCode };
}),
);

const expected = {
bound: [
{ name: "inner", file: "main.cjs", line: 3 },
{ name: "outer", file: "callers.cjs", line: 3 },
{ name: "main", file: "main.cjs", line: 7 },
],
asyncLocalStorage: [
{ name: "inner", file: "main.cjs", line: 3 },
{ name: "run", file: "node:async_hooks", line: expect.any(Number) },
{ name: "viaAsyncLocalStorage", file: "callers.cjs", line: 8 },
],
};
expect(results).toEqual([
{ showPrivateScriptsInStackTraces: "0", callSites: expected, stderr: "", exitCode: 0 },
{ showPrivateScriptsInStackTraces: "1", callSites: expected, stderr: "", exitCode: 0 },
]);
});