From c9edc11ac3eeb4ef01636ed5f5a2f5ee7e55cdca Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 01:03:52 +0000 Subject: [PATCH] Bump WebKit (oven-sh/WebKit#1018 preview): an arrow function of a derived constructor sees `this` after another one called super() In a derived constructor, an arrow function that reads `this` after another arrow function of the constructor called super() threw "ReferenceError: 'super()' must be called in derived constructor before accessing |this| or returning non-object." oven-sh/WebKit#1018 makes such arrow functions, and eval code, read `this` at each use. WEBKIT_VERSION is the preview build of that pull request. The tests: - test/js/bun/jsc-stress/fixtures/derived-constructor-this-initialized-by-another-arrow-function.js is the stress test of that pull request. - test/cli/inspect/inspect.test.ts pauses in such an arrow function, in eval code of one and in the constructor, and reads `this` of the frame and from console text. - test/bundler/bundler_bun.test.ts builds bytecode of depth 1, 2 and 3: the cache keeps what the constructor told its arrow functions. - test/aot/parallel/classes has a program that plain Bun and an executable of --aot print alike, and one with a Todo line for the order of the check in a store to a super property. --- scripts/build/deps/webkit.ts | 2 +- ...unction-asks-for-this-ahead-of-its-key.mjs | 71 +++ ...sees-this-once-another-has-called-super.js | 41 ++ test/bundler/bundler_bun.test.ts | 34 + test/cli/inspect/inspect.test.ts | 179 ++++++ ...s-initialized-by-another-arrow-function.js | 595 ++++++++++++++++++ test/js/bun/jsc-stress/jsc-stress.test.ts | 2 + test/js/bun/jsc-stress/preload.js | 3 + 8 files changed, 926 insertions(+), 1 deletion(-) create mode 100644 test/aot/parallel/classes/a-store-through-super-in-an-arrow-function-asks-for-this-ahead-of-its-key.mjs create mode 100644 test/aot/parallel/classes/an-arrow-function-sees-this-once-another-has-called-super.js create mode 100644 test/js/bun/jsc-stress/fixtures/derived-constructor-this-initialized-by-another-arrow-function.js diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 7db502dd17c7..f95fcd3c0af7 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` 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. diff --git a/test/aot/parallel/classes/a-store-through-super-in-an-arrow-function-asks-for-this-ahead-of-its-key.mjs b/test/aot/parallel/classes/a-store-through-super-in-an-arrow-function-asks-for-this-ahead-of-its-key.mjs new file mode 100644 index 000000000000..615bb9b3c4e8 --- /dev/null +++ b/test/aot/parallel/classes/a-store-through-super-in-an-arrow-function-asks-for-this-ahead-of-its-key.mjs @@ -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); +} diff --git a/test/aot/parallel/classes/an-arrow-function-sees-this-once-another-has-called-super.js b/test/aot/parallel/classes/an-arrow-function-sees-this-once-another-has-called-super.js new file mode 100644 index 000000000000..30e004f02cab --- /dev/null +++ b/test/aot/parallel/classes/an-arrow-function-sees-this-once-another-has-called-super.js @@ -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); +} diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 08f86e9d1216..76ccc53dd745 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -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", diff --git a/test/cli/inspect/inspect.test.ts b/test/cli/inspect/inspect.test.ts index 0e3e7eb58fb1..b8c2309adf5e 100644 --- a/test/cli/inspect/inspect.test.ts +++ b/test/cli/inspect/inspect.test.ts @@ -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 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(); + closed.promise.catch(() => {}); + const opened = Promise.withResolvers(); + 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 { + const id = nextId++; + const { promise, resolve } = Promise.withResolvers(); + pending.set(id, resolve); + webSocket.send(JSON.stringify({ id, method, params })); + return Promise.race([promise, closed.promise]); + } + async function nextPause(): Promise { + while (!pauses.length) { + const { promise, resolve } = Promise.withResolvers(); + 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(); +}); diff --git a/test/js/bun/jsc-stress/fixtures/derived-constructor-this-initialized-by-another-arrow-function.js b/test/js/bun/jsc-stress/fixtures/derived-constructor-this-initialized-by-another-arrow-function.js new file mode 100644 index 000000000000..6f99123e45a8 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/derived-constructor-this-initialized-by-another-arrow-function.js @@ -0,0 +1,595 @@ +// @bun +//@ defaultRun; run("bytecode-optimizer", "--useBytecodeOptimizer=1") + +// |this| of a derived constructor is one binding. An arrow function or eval code of the constructor reads the binding +// where it evaluates |this| or a super property. So it has the value once super() has returned, whichever code of the +// constructor called super(), also if that call came after the arrow function or the eval code started. + +const uninitialized = "'super()' must be called in derived constructor before accessing |this| or returning non-object."; +const calledTwice = "'super()' can't be called more than once in a constructor."; + +function shouldBe(actual, expected, name) +{ + if (actual !== expected) + throw new Error(`${name}: expected ${String(expected)} but got ${String(actual)}`); +} + +function shouldThrow(run, message, name) +{ + let error = null; + try { + run(); + } catch (e) { + error = e; + } + if (!(error instanceof ReferenceError) || error.message !== message) + throw new Error(`${name}: expected ReferenceError "${message}" but got ${String(error)}`); +} + +let baseCalls = 0; + +class Base { + constructor() + { + baseCalls++; + this.own = 1; + } + method() { return this; } + get getter() { return this; } + set setter(value) { this.stored = value; } + get tag() { return () => "tagged"; } +} +Base.prototype.count = 1; + +// Eval code that reads |this| before and after initialize(). +const readBeforeAndAfter = ` + const seen = []; + for (let i = 0; i < 3; ++i) { + if (i === 1) + initialize(); + try { + seen.push(typeof this); + } catch (error) { + seen.push(error.message === uninitialized ? "uninitialized" : String(error)); + } + } + seen.join(); +`; + +// The constructor calls test(readers) before any super(). If test is a string, the constructor evaluates it as eval code. +// initialize() is an arrow function that calls super(). A reader calls it, and then reads. +let evalResult; + +class Derived extends Base { + #field = 7; + constructor(test) + { + const initialize = () => { super(); }; + const initializeInEval = () => eval("super()"); + if (typeof test === "string") { + evalResult = eval(test); + return; + } + return test({ + initialize, + read: () => this, + + // Each kind of code that has the |this| of the constructor. + body: () => { initialize(); return this; }, + nested: () => (() => { initialize(); return this; })(), + withInnerArrow: () => { const inner = () => 1; initialize(); return inner() && this; }, + initializedInNestedArrow: () => { const inner = () => { super(); }; return (() => { inner(); return this; })(); }, + initializedInEval: () => { initializeInEval(); return this; }, + initializedInGetter: () => { ({ get x() { return initialize(); } }).x; return this; }, + recursive: depth => { + if (!depth) { + initialize(); + return this; + } + readers.recursive(depth - 1); + return this; + }, + parameter: (value = (initialize(), this)) => value, + evalCode: () => eval("initialize(); this"), + evalCodeBeforeAndAfter: () => eval(readBeforeAndAfter), + asyncBody: async () => { initialize(); return this; }, + asyncParameter: async (value = (initialize(), this)) => value, + asyncBodyAfterAwait: async () => { initialize(); await 0; return this; }, + arrowInAsyncBody: async () => { await 0; return (() => { initialize(); return this; })(); }, + + // Each kind of read. + thisProperty: () => { initialize(); return this.own; }, + thisCall: () => { initialize(); return this.method(); }, + thisStore: () => { initialize(); this.value = 2; return this.value; }, + thisPrivateField: () => { initialize(); return this.#field; }, + typeofThis: () => { initialize(); return typeof this; }, + superGet: () => { initialize(); return super.getter; }, + superGetByVal: key => { initialize(); return super[key]; }, + superCall: () => { initialize(); return super.method(); }, + superCallByVal: key => { initialize(); return super[key](); }, + superCallCall: () => { initialize(); return super.method.call(this); }, + superCallApply: () => { initialize(); return super.method.apply(this, []); }, + superTag: () => { initialize(); return super.tag`x`; }, + superPut: value => { initialize(); const result = (super.setter = value); return result + this.stored; }, + superPutByVal: (key, value) => { initialize(); const result = (super[key] = value); return result + this.stored; }, + superPutByValConstant: key => { initialize(); return super[key] = 21; }, + superAdd: () => { initialize(); return super.count += 1; }, + superAddByVal: key => { initialize(); return super[key] += 1; }, + superOr: key => { initialize(); return super[key] ||= 5; }, + superIncrement: key => { initialize(); super[key]++; return this.count; }, + superDecrement: () => { initialize(); return --super.count; }, + superDestructure: () => { initialize(); [super.a, { b: super["b"] }] = [3, { b: 4 }]; return this.a + this.b; }, + superForOf: key => { initialize(); for (super[key] of [5, 6]); return this.last; }, + superForIn: () => { initialize(); for (super.key in { k: 0 }); return this.key; }, + + // A read that comes before super() in the order of evaluation. + readThenInitialize: (first = this, second = initialize()) => first, + storeThenInitialize: () => { this.early = initialize(); }, + argumentThenInitialize: () => [this, initialize()], + superCallThenInitialize: () => super.method(initialize()), + superGetByValThenInitialize: () => super[(initialize(), "count")], + superPutThenInitialize: () => { super.setter = (initialize(), 1); }, + superPutByValThenInitialize: () => { super[(initialize(), "setter")] = 1; }, + superPutByValValueThenInitialize: key => { super[key] = (initialize(), 1); }, + superAddByValThenInitialize: () => { super[(initialize(), "count")] += 1; }, + superIncrementThenInitialize: () => { super[(initialize(), "count")]++; }, + superDeleteThenInitialize: () => delete super[(initialize(), "count")], + superTagThenInitialize: () => super[(initialize(), "tag")]`x`, + superDestructureThenInitialize: () => { [super[(initialize(), "a")]] = [1]; }, + superForOfThenInitialize: () => { for (super[(initialize(), "last")] of [1]); }, + superForInThenInitialize: () => { for (super[(initialize(), "key")] in { k: 0 }); }, + asyncSuperPutByValThenInitialize: async () => { super[(initialize(), "setter")] = 1; }, + asyncSuperPutByValAfterAwaitThenInitialize: async () => { await 0; super[(initialize(), "setter")] = 1; }, + + readBeforeAndAfter: () => { + const seen = []; + for (let i = 0; i < 3; ++i) { + if (i === 1) + initialize(); + try { + seen.push(typeof this); + } catch (error) { + seen.push(error.message === uninitialized ? "uninitialized" : String(error)); + } + } + return seen.join(); + }, + initializeTwice: () => { initialize(); super(); }, + }); + } +} + +let readers; + +// Calls the reader in a new construction, and checks what it gives against the new object. +function test(name, read, check = (result, instance) => result === instance) +{ + let result; + const instance = new Derived(typeof read === "string" ? read : scope => { + readers = scope; + result = read(scope); + }); + shouldBe(check(typeof read === "string" ? evalResult : result, instance), true, name); + shouldBe(instance instanceof Derived && instance.own === 1, true, `${name}: the new object`); +} + +function testOnce() +{ + for (const name of ["body", "nested", "withInnerArrow", "initializedInNestedArrow", "initializedInEval", "initializedInGetter", "parameter", "evalCode"]) + test(name, scope => scope[name]()); + test("recursive", scope => scope.recursive(3)); + + test("eval code of the constructor", "initialize(); this"); + test("eval code of eval code", "initialize(); eval('this')"); + test("arrow function of eval code", "(() => { initialize(); return this; })()"); + test("eval code of an arrow function of eval code", "(() => eval('initialize(); this'))()"); + test("super property in eval code", "initialize(); super.getter"); + test("eval code, super() in eval code", "initializeInEval(); this"); + test("eval code, assignment to a super property", "initialize(); (super['setter'] = 21)", result => result === 21); + + test("this.property", scope => scope.thisProperty(), result => result === 1); + test("this.method()", scope => scope.thisCall()); + test("this.property = value", scope => scope.thisStore(), result => result === 2); + test("this.#field", scope => scope.thisPrivateField(), result => result === 7); + test("typeof this", scope => scope.typeofThis(), result => result === "object"); + test("super.property", scope => scope.superGet()); + test("super[key]", scope => scope.superGetByVal("getter")); + test("super.method()", scope => scope.superCall()); + test("super[key]()", scope => scope.superCallByVal("method")); + test("super.method.call()", scope => scope.superCallCall()); + test("super.method.apply()", scope => scope.superCallApply()); + test("super.tag`x`", scope => scope.superTag(), result => result === "tagged"); + test("super.property = value", scope => scope.superPut(20), result => result === 40); + test("super[key] = value", scope => scope.superPutByVal("setter", 21), result => result === 42); + test("super[key] = 21", scope => scope.superPutByValConstant("setter"), (result, instance) => result === 21 && instance.stored === 21); + test("super.property += value", scope => scope.superAdd(), (result, instance) => result === 2 && instance.count === 2); + test("super[key] += value", scope => scope.superAddByVal("count"), (result, instance) => result === 2 && instance.count === 2); + test("super[key] ||= value", scope => scope.superOr("absent"), (result, instance) => result === 5 && instance.absent === 5); + test("super[key]++", scope => scope.superIncrement("count"), result => result === 2); + test("--super.property", scope => scope.superDecrement(), (result, instance) => result === 0 && instance.count === 0); + test("destructuring to super properties", scope => scope.superDestructure(), result => result === 7); + test("for-of to a super property", scope => scope.superForOf("last"), result => result === 6); + test("for-in to a super property", scope => scope.superForIn(), result => result === "k"); + + // A read that comes before super() throws, and the super() behind it does not run. + const early = [ + "read", "readThenInitialize", "storeThenInitialize", "argumentThenInitialize", "superCallThenInitialize", "superGetByValThenInitialize", + "superPutThenInitialize", "superPutByValThenInitialize", "superPutByValValueThenInitialize", "superAddByValThenInitialize", "superIncrementThenInitialize", + "superDeleteThenInitialize", "superTagThenInitialize", "superDestructureThenInitialize", "superForOfThenInitialize", "superForInThenInitialize", + ]; + for (const name of early) { + baseCalls = 0; + shouldThrow(() => new Derived(scope => { scope[name](name === "superPutByValValueThenInitialize" ? "setter" : undefined); }), uninitialized, name); + shouldBe(baseCalls, 0, `${name}: calls of the base constructor`); + } + shouldThrow(() => new Derived("this"), uninitialized, "eval code before super()"); + shouldThrow(() => new Derived("(() => this)()"), uninitialized, "arrow function of eval code before super()"); + + for (const code of ["super[(initialize(), 'setter')] = 1", "super.setter = (initialize(), 1)", "delete super[(initialize(), 'count')]"]) { + baseCalls = 0; + shouldThrow(() => new Derived(code), uninitialized, `eval code: ${code}`); + shouldBe(baseCalls, 0, `eval code: ${code}: calls of the base constructor`); + } + + // One activation has |this| in both states, and a second super() does not change it. + test("read, super(), read", scope => scope.readBeforeAndAfter(), result => result === "uninitialized,object,object"); + test("eval code: read, super(), read", readBeforeAndAfter, result => result === "uninitialized,object,object"); + test("eval code of eval code: read, super(), read", "eval(readBeforeAndAfter)", result => result === "uninitialized,object,object"); + test("eval code of an arrow function: read, super(), read", scope => scope.evalCodeBeforeAndAfter(), result => result === "uninitialized,object,object"); + baseCalls = 0; + let first; + shouldThrow(() => new Derived(scope => { + try { + scope.initializeTwice(); + } finally { + first = scope.read(); + } + }), calledTwice, "second super()"); + shouldBe(baseCalls, 2, "second super(): calls of the base constructor"); + shouldBe(first instanceof Derived, true, "second super(): |this| stays the first object"); + + // The constructor returns an object and never calls super(). Its arrow functions still have its |this|. + let escaped; + const replacement = { }; + shouldBe(new Derived(scope => { escaped = scope; return replacement; }), replacement, "the returned object"); + shouldThrow(() => escaped.read(), uninitialized, "escaped arrow function before super()"); + const late = escaped.body(); + shouldBe(late instanceof Derived && late !== replacement, true, "escaped arrow function calls super() and reads"); + shouldBe(escaped.read(), late, "escaped arrow function after super()"); + + // Each construction has its own |this|. + const scopes = []; + const initialized = new Derived(scope => { + scopes.push(scope); + scope.initialize(); + }); + new Derived(scope => { + scopes.push(scope); + return replacement; + }); + shouldBe(scopes[0].read(), initialized, "the construction that called super()"); + shouldThrow(() => scopes[1].read(), uninitialized, "the construction that did not call super()"); +} + +for (let i = 0; i < 100; ++i) + testOnce(); + +// An async arrow function evaluates its parameters, and a body without await, in the call itself. +{ + const names = ["asyncBody", "asyncParameter", "asyncBodyAfterAwait", "arrowInAsyncBody"]; + const settled = []; + for (const name of names) { + for (let i = 0; i < 100; ++i) { + new Derived(scope => { + scope[name]().then(value => settled.push(value), error => settled.push(error)); + return { }; + }); + } + } + drainMicrotasks(); + shouldBe(settled.length, 100 * names.length, "async arrow functions settle"); + for (const value of settled) + shouldBe(value instanceof Derived && value.own === 1, true, `async arrow function gives the new object, got ${String(value)}`); +} + +// In an async arrow function too, a super property that comes before super() rejects, and the super() behind it does not run. +{ + const names = ["asyncSuperPutByValThenInitialize", "asyncSuperPutByValAfterAwaitThenInitialize"]; + const settled = []; + for (const name of names) { + for (let i = 0; i < 100; ++i) { + baseCalls = 0; + new Derived(scope => { + scope[name]().then(() => settled.push("resolved"), error => settled.push(error.message === uninitialized && !baseCalls ? "rejected" : String(error))); + return { }; + }); + drainMicrotasks(); + } + } + shouldBe(settled.length, 100 * names.length, "async arrow functions with a store to a super property settle"); + for (const outcome of settled) + shouldBe(outcome, "rejected", "async arrow function with a store to a super property before super()"); +} + +// Eval code in the constructor of a class without a parent has |this| from the start. +{ + class WithoutParent { + constructor() + { + this.fromEval = eval("this"); + this.fromArrowInEval = eval("(() => this)()"); + } + } + for (let i = 0; i < 100; ++i) { + const instance = new WithoutParent; + shouldBe(instance.fromEval, instance, "eval code in the constructor of a class without a parent"); + shouldBe(instance.fromArrowInEval, instance, "arrow function of eval code in the constructor of a class without a parent"); + } +} + +// The reader keeps the scope that has |this| from its start. The scopes that the reader makes and leaves do not change it. +{ + let result; + class BlockScope extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = () => { + initialize(); + { + let captured = 1; + const inner = () => captured; + inner(); + return this; + } + }; + result = read(); + } + } + class LoopScope extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = () => { + const closures = []; + const seen = []; + for (let i = 0; i < 3; ++i) { + closures.push(() => i); + if (i === 1) + initialize(); + try { + seen.push(typeof this); + } catch (error) { + seen.push(error.message === uninitialized ? "uninitialized" : String(error)); + } + } + return seen.join(); + }; + shouldBe(read(), "uninitialized,object,object", "reads in a loop with a scope for each iteration"); + result = this; + } + } + class CatchScope extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = () => { + initialize(); + try { + throw 1; + } catch (error) { + const inner = () => error; + inner(); + return super.getter; + } + }; + result = read(); + } + } + class ParameterScope extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = captured => { + const inner = () => captured; + initialize(); + return inner() && this; + }; + result = read(1); + } + } + // The scope of the constructor has |this| and no variable. + class NoCapturedVariable extends Base { + constructor() + { + result = ((initialize, enterBlock) => { + initialize(); + if (enterBlock) { + let captured = 1; + const inner = () => captured; + inner(); + } + return this; + })(() => { super(); }, true); + } + } + // Only an eval tells the constructor that its arrow functions read |this| at each use. + class EvalInArrowFunction extends Base { + constructor() + { + const initialize = () => eval("super()"); + const read = () => { initialize(); return this; }; + result = read(); + } + } + class EvalInConstructor extends Base { + constructor() + { + let initialize; + eval("initialize = () => { super(); }"); + const read = () => { initialize(); return this; }; + result = read(); + } + } + // The variable of the eval code is in a scope that is not known before the eval code runs. + class ArrowFunctionOfEvalCodeWithVariable extends Base { + constructor() + { + const initialize = () => { super(); }; + result = eval("var variable = 1; (() => { initialize(); return this; })()"); + } + } + class EvalCodeWithVariable extends Base { + constructor() + { + const initialize = () => { super(); }; + result = eval("var variable = 1; initialize(); this"); + } + } + class EvalCodeWithFunction extends Base { + constructor() + { + const initialize = () => { super(); }; + result = eval("function declared() { return 1; } initialize(); declared() && this"); + } + } + for (const Class of [BlockScope, LoopScope, CatchScope, ParameterScope, NoCapturedVariable, EvalInArrowFunction, EvalInConstructor, ArrowFunctionOfEvalCodeWithVariable, EvalCodeWithVariable, EvalCodeWithFunction]) { + for (let i = 0; i < 100; ++i) { + result = null; + shouldBe(new Class, result, Class.name); + } + } +} + +// A bytecode cache keeps what the constructor told its arrow functions: for an arrow function whose code is generated after +// the decode, and for the eval code of a decoded arrow function. The depth is the count of function levels in the cache. +if (typeof builtinFromBytecodeCache === "function") { + const source = `(function (Base, name) { + "use strict"; + class Cached extends Base { + constructor() + { + const initialize = () => { super(); }; + const readers = { + body: () => { initialize(); return this; }, + nested: () => (() => { initialize(); return this; })(), + evalCode: () => eval("initialize(); this"), + }; + if (readers[name]() !== this) + throw new Error("bytecode cache, " + name + ": not the new object"); + } + } + return new Cached; + })`; + for (const depth of [1, 2]) { + for (const name of ["body", "nested", "evalCode"]) { + const create = builtinFromBytecodeCache(source, true, depth); + for (let i = 0; i < 2; ++i) + shouldBe(create(Base, name).own, 1, `bytecode cache of depth ${depth}, ${name}`); + } + } +} + +// The same in optimized code. One activation of the reader starts before super(), the others after it. +{ + let result; + class HotArrow extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = shouldInitialize => { + if (shouldInitialize) + initialize(); + return this; + }; + noInline(read); + result = read(true); + for (let i = 0; i < 4; ++i) + shouldBe(read(false), result, "hot arrow function, a later activation"); + } + } + class HotInlinedArrow extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = () => { initialize(); return this.own + super.count; }; + shouldBe(read(), 2, "hot inlined arrow function"); + result = this; + } + } + class HotUninitialized extends Base { + constructor(callSuperFirst) + { + const initialize = () => { super(); }; + const read = () => this; + if (callSuperFirst) + initialize(); + else { + shouldThrow(read, uninitialized, "hot arrow function before super()"); + initialize(); + } + result = read(); + } + } + class HotEval extends Base { + constructor() + { + const initialize = () => { super(); }; + result = eval("initialize(); this"); + } + } + class HotEvalInitializedInEval extends Base { + constructor() + { + const initialize = () => eval("super()"); + result = eval("initialize(); this"); + } + } + // One activation gets to optimized code in its loop, after its first reads threw and super() ran. + class HotLoop extends Base { + constructor() + { + const initialize = () => { super(); }; + const read = count => { + let failed = 0; + let sum = 0; + for (let i = 0; i < count; ++i) { + if (i === 3) + initialize(); + try { + sum += this.own; + } catch { + failed++; + } + } + return `${failed},${sum}`; + }; + noInline(read); + const count = 2 * testLoopCount; + shouldBe(read(count), `3,${count - 3}`, "hot loop in an arrow function"); + result = this; + } + } + // The same for eval code with a variable of its own. + class HotLoopInEvalCodeWithVariable extends Base { + constructor() + { + const initialize = () => { super(); }; + const count = 2 * testLoopCount; + shouldBe(eval("var sum = 0; initialize(); for (let i = 0; i < count; ++i) sum += this.own; sum"), count, "hot loop in eval code with a variable"); + result = this; + } + } + for (const Hot of [HotLoop, HotLoopInEvalCodeWithVariable]) { + for (let i = 0; i < 3; ++i) { + result = null; + shouldBe(new Hot, result, Hot.name); + } + } + for (const Hot of [HotArrow, HotInlinedArrow, HotUninitialized, HotEval, HotEvalInitializedInEval]) { + for (let i = 0; i < testLoopCount; ++i) { + result = null; + shouldBe(new Hot(i % 3 === 0), result, Hot.name); + } + } +} diff --git a/test/js/bun/jsc-stress/jsc-stress.test.ts b/test/js/bun/jsc-stress/jsc-stress.test.ts index 3b6e14146858..84073ad7fbfd 100644 --- a/test/js/bun/jsc-stress/jsc-stress.test.ts +++ b/test/js/bun/jsc-stress/jsc-stress.test.ts @@ -132,6 +132,8 @@ const jsFixtures = [ "regexp-many-single-character-alternatives.js", // Builtins "function-prototype-call-not-called-as-a-method.js", + // Bytecode generator + "derived-constructor-this-initialized-by-another-arrow-function.js", ]; const wasmFixtures = [ diff --git a/test/js/bun/jsc-stress/preload.js b/test/js/bun/jsc-stress/preload.js index b0b991f5d067..bee70ca47ef8 100644 --- a/test/js/bun/jsc-stress/preload.js +++ b/test/js/bun/jsc-stress/preload.js @@ -32,3 +32,6 @@ globalThis.fullGC = jsc.fullGC; globalThis.edenGC = jsc.edenGC; globalThis.numberOfDFGCompiles = jsc.numberOfDFGCompiles; globalThis.noDFG = jsc.noFTL; + +// drainMicrotasks: runs the queued microtasks before it returns. +globalThis.drainMicrotasks = jsc.drainMicrotasks;