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 = "5a61071f79134bf41844e7ea31a8449147d85dd4";
export const WEBKIT_VERSION = "autobuild-preview-pr-1018-6ada7bda";

/**
* WebKit (JavaScriptCore) — the JS engine, with WTF and bmalloc.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// A store through `super` in an arrow function of a derived constructor, with the super() in its key or in its value.
// Plain Bun asks for `this` in front of the key: the store throws and the base constructor does not run.
// Todo: an executable asks for `this` at the store, behind the key and the value, so the base constructor runs and the store is done. The Super arm of lower_member_reference (src/aot/lower/member/reference.rs) gets the check with WebKit/WebKit#76967
let baseCalls = 0;
class Base {
constructor() {
baseCalls++;
}
set setter(value) {
this.stored = value;
}
}
Base.prototype.count = 1;

class Derived extends Base {
constructor(name) {
const initialize = () => {
super();
};
const stores = {
byName: () => {
super.setter = (initialize(), 1);
},
byKey: () => {
super[(initialize(), "setter")] = 1;
},
byKeyWithTheCallInTheValue: () => {
super["setter"] = (initialize(), 1);
},
compound: () => {
super[(initialize(), "count")] += 1;
},
increment: () => {
super[(initialize(), "count")]++;
},
deleted: () => delete super[(initialize(), "count")],
pattern: () => {
[super[(initialize(), "a")]] = [1];
},
forOf: () => {
for (super[(initialize(), "last")] of [1]);
},
forIn: () => {
for (super[(initialize(), "key")] in { k: 0 });
},
};
let said = "stored";
try {
stores[name]();
} catch (error) {
said = error.constructor.name;
}
console.log(name + ":", said + ", the base constructor ran", baseCalls, "times");
if (!baseCalls) initialize();
baseCalls = 0;
}
}

for (const name of [
"byName",
"byKey",
"byKeyWithTheCallInTheValue",
"compound",
"increment",
"deleted",
"pattern",
"forOf",
"forIn",
]) {
new Derived(name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// An arrow function of a derived constructor, and the text of a direct eval in one, read `this` where they use it:
// another arrow function of the constructor may have called super() since they started.
class Base {
constructor() {
this.own = 1;
}
}

class Derived extends Base {
constructor(name) {
const initialize = () => {
super();
};
const readers = {
arrow: () => {
initialize();
return this.own;
},
nested: () =>
(() => {
initialize();
return this.own;
})(),
text: () => eval("initialize(); this.own"),
readBeforeAndAfter: () => {
try {
this.own;
} catch (error) {
console.log(name, "before super():", error.constructor.name);
}
initialize();
return this.own;
},
};
console.log(name, readers[name]());
}
}

for (const name of ["arrow", "nested", "text", "readBeforeAndAfter"]) {
console.log(new Derived(name) instanceof Derived);
}
34 changes: 34 additions & 0 deletions test/bundler/bundler_bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ describe("bundler", () => {
});
}

// The bytecode has the constructor and not the code of its arrow functions, or their code and not the code of their
// eval. The cache keeps what the constructor told them: another arrow function can call super().
for (const bytecodeDepth of [1, 2, 3]) {
itBundled(`bun/bytecode-depth-${bytecodeDepth}-derived-constructor-this-in-arrow-function`, {
target: "bun",
format: "cjs",
bytecode: true,
bytecodeDepth,
outdir: "/out",
files: {
"/entry.ts": /* js */ `
class Base {}
class Derived extends Base {
constructor() {
const initialize = () => {
super();
};
const read = () => {
initialize();
return this;
};
const readInEval = () => eval("initialize(); this");
console.log(new.target === Derived ? read() === this : readInEval() === this);
}
}
class EvalCode extends Derived {}
new Derived();
new EvalCode();
`,
},
run: { stdout: "true\ntrue\n" },
});
}

// https://github.com/oven-sh/bun/issues/18899
itBundled("bun/import-bun-format-cjs", {
target: "bun",
Expand Down
179 changes: 179 additions & 0 deletions test/cli/inspect/inspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,182 @@ test("LifecycleReporter.getModuleGraph reports an empty argv when process.argv i
expect(child.exitCode).toBe(null);
webSocket.close();
});

// An arrow function or eval code of a derived constructor reads `this` from a scope at each use when other code of the
// constructor can call super(). Its frame has a copy of `this` only after a read, so the debugger reads the scope too.
// It does the same for the frame of the constructor, which has no copy between that super() and its next use of `this`.
test("Debugger.paused has `this` of an arrow function after another arrow function of the derived constructor called super()", async () => {
using dir = tempDir("inspect-derived-constructor-this", {
"entry.js": `
class Base {
constructor() {
this.own = 1;
}
}
class Derived extends Base {
constructor() {
const initialize = () => {
super();
};
const reader = () => {
debugger;
initialize();
debugger;
return this;
};
const evaluator = () => {
eval("debugger;");
return this;
};
reader();
evaluator.call({});
}
}
new Derived();
// The console text calls super() at these pauses, so each is in a constructor of its own.
class ConsoleInArrow extends Base {
constructor() {
const initialize = () => {
super();
};
const reader = () => {
debugger;
return [this, initialize][0];
};
reader();
}
}
new ConsoleInArrow();
class ConsoleInEval extends Base {
constructor() {
const initialize = () => {
super();
};
const evaluator = () => {
eval("debugger;");
return this;
};
evaluator.call({});
}
}
new ConsoleInEval();
// The frame of the constructor itself, after an arrow function called super() and before the constructor used this.
class InConstructor extends Base {
constructor() {
const initialize = () => {
super();
};
initialize();
debugger;
}
}
new InConstructor();
setInterval(() => {}, 60_000);
`,
});

await using child = spawn({
cmd: [bunExe(), "--inspect-wait=ws://127.0.0.1:0/", "entry.js"],
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}`);

type Reply = { result?: any; error?: { message: string } };
const pending = new Map<number, (reply: Reply) => void>();
const pauses: any[] = [];
let onPause: (() => void) | undefined;
// A crash closes the socket. Reject, so that the test does not wait for a pause that cannot come.
const closed = Promise.withResolvers<never>();
closed.promise.catch(() => {});
const opened = Promise.withResolvers<void>();
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 (typeof message.id === "number") {
pending.get(message.id)?.(message);
pending.delete(message.id);
} else if (message.method === "Debugger.paused") {
pauses.push(message.params);
onPause?.();
}
});

let nextId = 1;
function send(method: string, params: object = {}): Promise<Reply> {
const id = nextId++;
const { promise, resolve } = Promise.withResolvers<Reply>();
pending.set(id, resolve);
webSocket.send(JSON.stringify({ id, method, params }));
return Promise.race([promise, closed.promise]);
}
async function nextPause(): Promise<any> {
while (!pauses.length) {
const { promise, resolve } = Promise.withResolvers<void>();
onPause = resolve;
await Promise.race([promise, closed.promise]);
}
return pauses.shift();
}

await Promise.race([opened.promise, closed.promise]);
await Promise.all([
send("Inspector.enable"),
send("Debugger.enable"),
send("Debugger.setBreakpointsActive", { active: true }),
send("Debugger.setPauseOnDebuggerStatements", { enabled: true }),
]);
// The reply comes when the program has run to its first pause.
send("Inspector.initialized").catch(() => {});

const seen: unknown[] = [];
// The console text is eval code of the paused frame. Its arrow functions read `this` at each use too.
const readOwn = "(() => { try { return this.own; } catch (error) { return error.constructor.name; } })()";
const initializeAndReadOwn =
"(() => { try { return (initialize(), this.own); } catch (error) { return error.constructor.name; } })()";
for (let i = 0; i < 6; i++) {
const { callFrames } = await nextPause();
const frame = callFrames[0];
const evaluated = await send("Debugger.evaluateOnCallFrame", {
callFrameId: frame.callFrameId,
expression: i === 3 || i === 4 ? `[${readOwn}, ${initializeAndReadOwn}].join()` : readOwn,
returnByValue: true,
});
seen.push({
function: frame.functionName,
this: frame.this.className ?? frame.this.type,
evaluated: evaluated.result?.result?.value,
});
await send("Debugger.resume");
}

expect(seen).toEqual([
// No super() yet.
{ function: "reader", this: "undefined", evaluated: "ReferenceError" },
// initialize() called super(). The frame of reader has not read `this` since.
{ function: "reader", this: "Derived", evaluated: 1 },
// The eval code of evaluator, which was called with another receiver and has not read `this` yet.
{ function: "eval code", this: "Derived", evaluated: 1 },
// Console text that reads `this`, calls super() and reads again: in an arrow function and in eval code of one.
{ function: "reader", this: "undefined", evaluated: "ReferenceError,1" },
{ function: "eval code", this: "undefined", evaluated: "ReferenceError,1" },
// The constructor has no copy of `this` yet: the super() was in its arrow function.
{ function: "InConstructor", this: "InConstructor", evaluated: 1 },
]);

webSocket.close();
});
Loading
Loading