Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "4d45755fed4aeef19e95beb90d3d0c884a2812bd";
export const WEBKIT_VERSION = "autobuild-preview-pr-950-c5dabb29";

/**
* WebKit (JavaScriptCore) — the JS engine, with WTF and bmalloc.
Expand Down
5 changes: 5 additions & 0 deletions src/aot/lower/class/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ impl FileLowering<'_, '_, '_, '_> {
} else {
header.name = name;
header.flags = header.flags.without(FunctionFlags::HAS_EXPLICIT_NAME);
// `export default class {}` has the name `default` from the start, as `export default function () {}` has.
let is_declaration = matches!(self.bound.class_owner[class.idx()], ClassOwner::Stmt(_));
if is_declaration && self.is_default_export_name_its_own(node.start) {
header.flags |= FunctionFlags::HAS_EXPLICIT_NAME;
}
}
let end = match self.bound.class_owner[class.idx()] {
ClassOwner::Expr(expression) => hir[expression].end,
Expand Down
8 changes: 1 addition & 7 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5404,13 +5404,7 @@ BunString JSC__JSValue__getNameProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlo
#endif

if (JSC::JSFunction* function = dynamicDowncast<JSC::JSFunction>(obj)) {

WTF::String actualName = function->name(vm);
if (!actualName.isEmpty() || function->isHostOrBuiltinFunction()) {
return toStringAdopt(WTF::move(actualName));
}

return Bun::toStringRef(function->jsExecutable()->name().string());
return toStringAdopt(function->name(vm));
}

if (JSC::InternalFunction* function = dynamicDowncast<JSC::InternalFunction>(obj)) {
Expand Down
4 changes: 3 additions & 1 deletion test/aot/ir/lower/classes/anonymous-class-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

// RUN(INFERRED): lower
// INFERRED-NOT: name="A"{{*}}HasExplicitName
// INFERRED-NOT: name="default"{{*}}HasExplicitName

// RUN(DECLARED): lower
// DECLARED: name="default"{{*}}HasExplicitName

declare const k: string;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default class extends Object {
constructor() {
super();
return 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import arrow from "./arrow.mjs";
import asyncFunction from "./async-function.mjs";
import asyncGenerator from "./async-generator.mjs";
import DerivedClass from "./derived-class.mjs";
import index from "./folder/index.mjs";
import generator from "./generator.mjs";
import named from "./named.mjs";
Expand All @@ -15,6 +16,21 @@ console.log("async function |", asyncFunction.name, "|", await asyncFunction());
console.log("generator |", generator.name, "|", generator().next().value);
console.log("async generator |", asyncGenerator.name, "|", (await asyncGenerator().next()).value);
console.log("class |", SomeClass.name, "|", new SomeClass().place);
const messageOf = run => {
try {
run();
} catch (error) {
return error.message;
}
};
console.log(
"class that is called without new |",
messageOf(() => SomeClass()),
);
console.log(
"derived class whose constructor returns a number |",
messageOf(() => new DerivedClass()),
);
console.log("arrow function |", arrow.name, "|", arrow());
console.log("function in parentheses |", parenthesized.name, "|", parenthesized());
console.log("function in a TypeScript file |", typed.name, "|", typed());
Expand All @@ -32,6 +48,8 @@ async function | default | at default (async-function.mjs:3:14)
generator | default | at default (generator.mjs:2:13)
async generator | default | at default (async-generator.mjs:3:13)
class | default | at new default (some-class.mjs:3:22)
class that is called without new | Cannot call a class constructor default without |new|
derived class whose constructor returns a number | Cannot return a non-object type in the constructor of a derived class default.
arrow function | default | at default (arrow.mjs:2:7)
function in parentheses | default | at default (parenthesized.mjs:2:14)
function in a TypeScript file | default | at default (typed.ts:2:14)
Expand Down
134 changes: 134 additions & 0 deletions test/cli/inspect/inspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,137 @@ test("LifecycleReporter.getModuleGraph reports an empty argv when process.argv i
expect(child.exitCode).toBe(null);
webSocket.close();
});

// JavaScriptCore binds an anonymous default export to the private name `*default*`, whose text is
// "starDefault". The debugger shows the name of the function or class, "default".
describe("the debugger names an anonymous default export default", () => {
type Scope = { type: string; name?: string; object: { objectId: string } };
type CallFrame = { functionName: string; scopeChain: Scope[] };

// Runs entry.mjs until a `debugger` statement pauses it.
async function pause(files: Record<string, string>) {
const dir = tempDir("inspect-default-export", files);
const child = spawn({
cmd: [bunExe(), "--inspect-wait=ws://127.0.0.1:0/", "entry.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "ignore",
stderr: "pipe",
});

let stderr = "";
let url: string | undefined;
const decoder = new TextDecoder();
for await (const chunk of child.stderr as ReadableStream) {
stderr += decoder.decode(chunk);
url = stderr.match(/(ws:\/\/[^\s]+)/)?.[1];
if (url) break;
}
if (!url) throw new Error(`Unable to find the inspector URL in:\n${stderr}`);

const pending = new Map<number, (result: any) => void>();
const closed = Promise.withResolvers<never>();
closed.promise.catch(() => {});
const opened = Promise.withResolvers<void>();
const paused = Promise.withResolvers<{ callFrames: CallFrame[] }>();
const webSocket = new WebSocket(url);
webSocket.addEventListener("open", () => opened.resolve());
webSocket.addEventListener("close", () => closed.reject(new Error("the inspector closed the socket")));
webSocket.addEventListener("error", () => closed.reject(new Error("the inspector socket failed")));
webSocket.addEventListener("message", ({ data }) => {
const message = JSON.parse(data.toString());
if (message.method === "Debugger.paused") paused.resolve(message.params);
pending.get(message.id)?.(message.result);
pending.delete(message.id);
});

let nextId = 1;
function send(method: string, params: object = {}): Promise<any> {
const id = nextId++;
const { promise, resolve } = Promise.withResolvers<any>();
pending.set(id, resolve);
webSocket.send(JSON.stringify({ id, method, params }));
return Promise.race([promise, closed.promise]);
}

await Promise.race([opened.promise, closed.promise]);
await send("Inspector.enable");
await send("Debugger.enable");
await send("Debugger.setBreakpointsActive", { active: true });
await send("Debugger.setPauseOnDebuggerStatements", { enabled: true });
await send("Inspector.initialized");

const { callFrames } = await Promise.race([paused.promise, closed.promise]);
return {
callFrames,
send,
async [Symbol.asyncDispose]() {
webSocket.close();
child.kill();
await child.exited;
dir[Symbol.dispose]();
},
};
}

test("the call frame and the scopes of its variables", async () => {
await using session = await pause({
"paused.mjs": `
export default () => {
let captured = 1;
const read = () => captured;
debugger;
return read();
};
`,
"entry.mjs": `
import paused from "./paused.mjs";
paused();
`,
});
const frame = session.callFrames[0];
expect({
functionName: frame.functionName,
// A scope of a function has the name of the function. The scope of the module has none.
scopeNames: frame.scopeChain.flatMap(scope => (scope.name ? [scope.name] : [])),
}).toEqual({ functionName: "default", scopeNames: ["default", "default"] });
});

// The constructor that JavaScriptCore makes for a class with none is a function expression with the name of
// the class. Under a debugger it binds itself to that name in a scope of its own, as a class with a written
// name does. For `export default class {}` the name is "default".
test("the scope that holds the constructor of a class under its name", async () => {
await using session = await pause({
"paused.mjs": `
export default class {
field = (() => {
debugger;
return 1;
})();
}
`,
"entry.mjs": `
import Paused from "./paused.mjs";
new Paused();
`,
});
const names: string[][] = [];
for (const frame of session.callFrames) {
for (const scope of frame.scopeChain) {
if (scope.type !== "functionName") continue;
const { properties } = await session.send("Runtime.getProperties", {
objectId: scope.object.objectId,
ownProperties: true,
});
names.push(properties.map((property: { name: string }) => property.name));
}
}
expect({
functionNames: session.callFrames.map(frame => frame.functionName),
namesInFunctionNameScopes: names,
}).toEqual({
functionNames: expect.arrayContaining(["default"]),
namesInFunctionNameScopes: [["default"]],
});
});
});
80 changes: 80 additions & 0 deletions test/cli/run/cpu-prof.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,86 @@ describe.concurrent("--cpu-prof", () => {
expect(exitCode).toBe(0);
});

// The DFG inlines the default export into caller(). A frame of an inlined function has no function
// object, so the profiler names it from its code. JavaScriptCore binds an anonymous default export
// to the private name `*default*`, whose text is "starDefault". The name of the function is "default".
test("names an inlined anonymous default export default", async () => {
const files = {
"inlined.mjs": `export default work => work() + 1;`,
"main.mjs": `
import { noInline, numberOfDFGCompiles } from "bun:jsc";
import inlined from "./inlined.mjs";

function work() {
let sum = 0;
for (let i = 0; i < 1000; i++) sum += i % 7;
return sum;
}
noInline(work);

function caller() {
return inlined(work) + 1;
}
noInline(caller);

while (numberOfDFGCompiles(caller) === 0) caller();
const end = performance.now() + 100;
while (performance.now() < end) caller();
`,
};

async function profileOfOneRun() {
using dir = tempDir("cpu-prof-default-export", files);
await using proc = Bun.spawn({
cmd: [bunExe(), "--cpu-prof", "--cpu-prof-md", "main.mjs"],
cwd: String(dir),
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;

const written = readdirSync(String(dir));
const profile = JSON.parse(
readFileSync(
join(
String(dir),
written.find(f => f.endsWith(".cpuprofile"))!,
),
"utf-8",
),
);
const markdown = readFileSync(
join(
String(dir),
written.find(f => f.endsWith(".md"))!,
),
"utf-8",
);

const nodes = new Map<number, any>(profile.nodes.map((node: any) => [node.id, node]));
const callersOfWork = new Set<string>();
for (const node of profile.nodes) {
for (const child of node.children ?? []) {
if (nodes.get(child).callFrame.functionName === "work") callersOfWork.add(node.callFrame.functionName);
}
}
return {
callersOfWork: [...callersOfWork],
markdown: ["`default`", "`starDefault`"].filter(name => markdown.includes(name)),
exitCode,
};
}

// The profiler can miss work() in one window of 100 ms (see the comment at the top of this file).
// A run that has a sample in work() decides.
let result = await profileOfOneRun();
for (let run = 1; run < 5 && result.exitCode === 0 && result.callersOfWork.length === 0; run++) {
result = await profileOfOneRun();
}
expect(result).toEqual({ callersOfWork: ["default"], markdown: ["`default`"], exitCode: 0 });
});

test("--cpu-prof-md generates markdown format profile", async () => {
using dir = tempDir("cpu-prof-md", {
"test.js": `
Expand Down
37 changes: 37 additions & 0 deletions test/cli/test/test-aot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,43 @@ describe.skipIf(!supported)("bun test --aot", () => {
);
});

// JavaScriptCore binds an anonymous default export to the private name `*default*`. The class has the name "default"
// from the start, as a class with a written name has its name, and compiled code says the same.
test.concurrent("an anonymous default class is named, mocked and snapshotted as without --aot", async () => {
const files = {
"anonymous-class.ts": `export default class { m() {} }`,
"derived-class.ts": `export default class extends Object { constructor() { super(); return 1 as any; } }`,
"names.test.ts": `
import { expect, mock, test } from "bun:test";
import AnonymousClass from "./anonymous-class";
import DerivedClass from "./derived-class";
const element = (type: unknown) => ({ $$typeof: Symbol.for("react.element"), type, key: null, ref: null, props: {} });
const messageOf = (run: () => unknown) => { try { run(); } catch (error) { return (error as Error).message; } };
test("the name property", () => { expect(AnonymousClass.name).toBe("default"); });
test("a mock", () => { const mocked = mock(AnonymousClass as unknown as () => unknown); console.log(JSON.stringify([mocked.name, mocked.getMockName()])); });
test("the errors that name the class", () => { console.log(messageOf(() => (AnonymousClass as any)()) + " | " + messageOf(() => new DerivedClass())); });
test("a snapshot", () => { expect({ AnonymousClass, instance: new AnonymousClass(), element: element(AnonymousClass) }).toMatchSnapshot(); });
`,
};
using expectedDir = tempDir("test-aot", files);
using actualDir = tempDir("test-aot", files);
const [expected, actual] = await Promise.all([
runTests(String(expectedDir), false, ["--update-snapshots"]),
runTests(String(actualDir), true, ["--update-snapshots"]),
]);
expect(actual).toEqual(expected);
expect(actual.stdout).toContain(`["default","default"]`);
expect(actual.stdout).toContain(
"Cannot call a class constructor default without |new| | Cannot return a non-object type in the constructor of a derived class default.",
);
expect(actual.stderr).toContain(" 4 pass");
const snapshot = join("__snapshots__", "names.test.ts.snap");
expect(readFileSync(join(String(actualDir), snapshot), "utf8")).toBe(
readFileSync(join(String(expectedDir), snapshot), "utf8"),
);
expect(readFileSync(join(String(actualDir), snapshot), "utf8")).toContain("[class default]");
});

test.concurrent("other options work the same as without --aot", async () => {
const { stderr } = await expectSameAsBunTest(
{
Expand Down
30 changes: 30 additions & 0 deletions test/js/bun/test/describe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,36 @@ describe("shows first arg name correctly in test output", () => {
expect(fullOutput).toInclude("MyClass > should pass");
expect(fullOutput).not.toInclude("[object Object] > should pass");
});

// JavaScriptCore binds an anonymous default export to the private name `*default*`, whose text is
// "starDefault". The name of the class is "default".
test("describe block shows an anonymous default class as default in test output", async () => {
await using test_dir = tempDir(".", {
"anonymous-default-class.js": `export default class {}`,
"describe-test.test.js": `
import { describe, test, expect } from "bun:test";
import AnonymousDefaultClass from "./anonymous-default-class.js";

describe(AnonymousDefaultClass, () => {
test("should pass", () => {
expect(true).toBe(true);
});
});
`,
});
const { stdout, stderr } = spawnSync({
cmd: [bunExe(), "test", "describe-test.test.js"],
cwd: test_dir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});

const fullOutput = stdout.toString() + stderr.toString();

expect(fullOutput).toInclude("default > should pass");
expect(fullOutput).not.toInclude("starDefault > should pass");
});
});

describe("passing arrow function as args", () => {
Expand Down
Loading
Loading