Repository navigation
[JSC] Arrow functions and eval code of a derived constructor read this at each use when other code of the constructor can call super() - #1018
Draft
robobun wants to merge 1 commit into
Conversation
…is` at each use when other code of the constructor can call super()
In a derived constructor, an arrow function copies `this` from the scope of the constructor when it starts,
and eval code gets `this` from its caller. Other code of the constructor can call super() after that:
class K extends B {
constructor()
{
const callSuper = () => { super(); };
const read = () => { callSuper(); return this; };
read();
}
}
read() throws "ReferenceError: 'super()' must be called in derived constructor before accessing |this| or
returning non-object." `this` of the constructor is one binding (ECMA-262 GetThisBinding), and V8 returns the
object. esbuild's `__super` helper for lowered class fields has this shape.
BytecodeGenerator::ensureThis() reads `this` again from the scope only where the code block itself has a
super() or an eval, or an arrow function with one. A code block does not know what its parents and the other
arrow functions of the constructor have.
The derived constructor knows: isSuperCallUsedInInnerArrowFunction(). It tells its arrow functions with one
bit, derivedContextHasInnerSuperCall, and they hand it on to their arrow functions. Eval code gets the bit
from its caller: from an arrow function or eval code what that has, and from a constructor whether it has a
parent class. The bit goes through UnlinkedFunctionExecutable, DirectEvalExecutable, ExecutableInfo,
UnlinkedCodeBlock and the bytecode cache.
Code with the bit that uses `this` resolves the scope that has `this` once, into a variable, and makes no copy
at its start. ensureThis() reads `this` from that scope each time: one get_from_scope in front of the check.
The body of an async arrow function keeps its form, which reads `this` at each use already. Code without the
bit compiles to the bytecode it had: an arrow function of a derived constructor in which no arrow function and
no eval can call super(), and eval code in the constructor of a class that has no parent.
In code with the bit, a super property checks `this` before its key and the assigned value are evaluated
(SuperNode::emitBytecode). `super[(callSuper(), key)] = value` threw only because the check at the store saw
the old copy. The two lines are from WebKit#76967, which makes that check in all code. The body of the
constructor, and code without the bit, have only the check at the store, as before.
The debugger takes `this` of a frame from the frame. A frame of code with the bit has a copy only after a
read. The frame of the constructor has none between a super() in its arrow function or eval code and its next
use of `this`, which the debugger showed as undefined. DebuggerCallFrame::thisValue() reads the scope for both.
Console text, and the condition and the action of a breakpoint, are eval code of the paused frame:
DebuggerCallFrame::evaluateWithScopeExtension() gives them the bit of that frame.
cachedTypesFormatRevision is 15: the flags of a function executable and of a code block have the bit. The
decoder's Scalars are value-initialized, so a flag that a later change does not unpack is 0 and not what the
stack had.
Test: JSTests/stress/derived-constructor-this-initialized-by-another-arrow-function.js
Co-authored-by: SUZUKI Sosuke <sosuke@bun.com>
robobun
force-pushed
the
robobun/e0501bca/derived-constructor-this-read-at-use
branch
from
November 9, 2026 00:53
c2d6f47 to
6ada7bd
Compare
This was referenced Nov 9, 2026
Open
Collaborator
Author
|
Status Reproduced on the base (5a61071) with class B {}
class K extends B {
constructor() {
const callSuper = () => { super(); };
const f = () => { callSuper(); return this; };
const r = f();
console.log("K ok", r === this);
}
}
try { new K(); } catch (e) { console.log("K THROWS", e.constructor.name + ": " + e.message); }
Related PRs: #1016 (a register that a read of |
|
Preview build of 6ada7bd: |
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Nov 9, 2026
…ived 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft: an upstream candidate (Route in Notes).
Problem
thisafter another one calledsuper()throwsReferenceError: 'super()' must be called in derived constructor before accessing |this| or returning non-object.Node returns the object. esbuild's__superhas this shape (fix: do not readthisbeforesuper()in wrapped constructors nodejs/orchestrion-js#101).thisat its start.ensureThis()(BytecodeGenerator.cpp:5442) reads again only in code that itself holdssuper()or an eval.Fix
super(). Flagged code readsthisfrom the scope at each use.thisfirst (from [JSC] Super property should checkthisbinding before evaluating the property expression WebKit/WebKit#76967).thisof a flagged or constructor frame from the scope.JSTests/stress/derived-constructor-this-initialized-by-another-arrow-function.js(new, fails on the base). Self-reviewed: 24 concerns raised, 22 addressed (Notes).Background
super()initializesthisof a derived constructor. JSC keeps it in a scope slot for its arrow functions.Downsides
thisafter the first costs a flagged arrow function one instruction: 4 reads take 1298 → 1386 (LLInt), 204 → 226 (DFG). Eval code with avarpays more.super()in its key or value can now throw (13 forms), as in Node.super()([JSC] Predict the empty value for an unprofiled load ofthisof a derived constructor from its scope #1017).Notes
Route. This PR does not ask for a merge into the fork as it stands.
ensureThis()atBytecodeGenerator.cpp:5296. Fork-only parts: theCachedTypes.cpphunk, the line inaot/runtime/AOTDirectEvalOperations.cpp, and thebuiltinFromBytecodeCacheblock of the test.ensureThis(), which costs every arrow function of a derived constructor? I cannot file the bugs.webkit.org entry or the WebKit/WebKit PR.SuperNode::emitBytecodeare from your [JSC] Super property should checkthisbinding before evaluating the property expression WebKit/WebKit#76967, under a condition. When that PR arrives, the condition andreadsThisAtEachUse()go and your lines stay.aot/), eval code as a second PR on top.cachedTypesFormatRevisionis 15 here. Upgrade to upstream WebKit 1d4bed41aca8 #992, [JSC] Store the namedefaultfor an anonymous default export #950 and [JSC] The control flow profiler counts the branches of a decision #894 take 15 too. The one that merges later takes the next number.thisafter another one called super() bun#48304 pins the preview build of this PR and has the tests on the Bun side. typescript: match tsc for experimentalDecorators and emitDecoratorMetadata bun#46553 makes Bun's TypeScript lowering emit the__superhelper, so it needs this change.thisof a derived constructor from its scope #1017 are independent of this PR. Each fixes a fault thatmainhas today.What changes. The code is in the constructor of
class K extends B, andcsis() => { super(); }. The base is 5a61071.const f = () => { cs(); return this; }; f()ReferenceErrorconst f = () => (() => { cs(); return this; })(); f()ReferenceErroreval("cs(); this")ReferenceErrorconst f = () => { super[(cs(), "x")] = 1; }; f()ReferenceError, after the constructor ofBranReferenceError, the constructor ofBdoes not runconst f = () => { super[(super(), "x")] = 1; }; f()ReferenceError, the constructor ofBdoes not runconst f = () => this; f()before anysuper()ReferenceErrorReferenceErrorReferenceErrorsuper(): stores, compound assignments,++,delete, a template tag, destructuring,for-ofandfor-intargets. Such an arrow function readsthisagain at the store on the base too. 13 forms store there and 1 throws for another reason. Here all 14 throw before the key is evaluated.await, and an async arrow function before and after anawait: this PR differs from Node in one,delete super[key], where V8 evaluates the key first.Which code has the bit.
super()or a direct eval, or the constructor has a direct eval (isSuperCallUsedInInnerArrowFunction()).thisnor a super property compiles as before.Cost. Release
jscof both commits. The counts areinstructions:ufor one call of the arrow function from a loop, the difference of two call counts. Baseline varies by about 3 between runs.this.v, base and this PRmaximumFunctionForCallInlineCandidateBytecodeCostForDFG). With the limit at 120 it is 33.thisbinding before evaluating the property expression WebKit/WebKit#76967.this.vin a derived constructor: 18 → 34 bytes.this.v + this.v: 33 → 58 bytes. The same text in the constructor of a class with no parent: 18 and 33 bytes, unchanged.this.ownin a loop, instructions for one iteration:varor a function of its ownthis, 1 flagged (the__superhelper in prettier 3.6.2). Constructors with a direct eval: 0. Stores to a super property in such code: 0.sizeof releasejsc: text 51,700,256 → 51,701,984.sizeof(UnlinkedFunctionExecutable),sizeof(UnlinkedCodeBlock)andsizeof(EvalExecutable)do not change: the bit is in bit-fields with room.Other code.
jsc --useJIT=0 --dumpGeneratedBytecodes=1over 545 files ofJSTests/stress(arrow functions, classes,super, eval, TDZ): 21,277 of 21,297 code blocks are the same on both builds. The 20 others are 14 arrow functions of flagged constructors and 6 eval code blocks of derived constructors.The bit and not a fourth
DerivedContextType. A fourth value needs a change at each comparison withDerivedConstructorContext, in the parser, the generator and the interpreter. A comparison that a later sync brings would be wrong for the new value, with no build error. A separate bit changes no comparison.The debugger.
DebuggerCallFrame::thisValue()reads the scope for a frame whose code block has the bit. Such a frame has a copy ofthisonly after a read. Before that its slot has the receiver of the call.thisasundefinedthere after an arrow function calledsuper(), until the constructor usesthis. That is the state of a constructor right after a call of esbuild's__super. The walk ends at the scope of the callee, so a constructor finds only athisof its own activation.DebuggerCallFrame::evaluateWithScopeExtension()hands the bit of the paused frame to the eval code that it makes: console text, the condition and the action of a breakpoint. Text that callssuper()and then readsthisgets the object.boundThisof an arrow function in the inspector reads the scope of the arrow function, and the request aborts the process whilethishas no value, on the base too. [JSC] ShowboundThisof an arrow function as undefined in the inspector whilethisof its derived constructor has no value #1019 fixes the abort.The bytecode cache. The block with
builtinFromBytecodeCachein the test makes code after a decode. It fails when either flag is not packed or not unpacked: the function executable's flag at depth 1, the code block's flag at depth 2 in the eval case.Scalarsis value-initialized in the two decoder views for that: without it a flag that is not unpacked is what the stack had. With it the block fails in 5 of 5 runs when either unpack line is removed.Not in this PR.
thisafter the key and the value of a super property store. The four test262 tests for that stay inexpectations.yaml. [JSC] Super property should checkthisbinding before evaluating the property expression WebKit/WebKit#76967 fixes them.super()is compiled by the DFG again and again, before and after this PR: 8 compiles and 25,508 exits in 60,000 constructions of the__supershape, 6 compiles for each of 12 other forms in 12,000 (--useConcurrentJIT=0). With--useBytecodeOptimizer=1: 1 compile and no exit on both. [JSC] Predict the empty value for an unprofiled load ofthisof a derived constructor from its scope #1017 makes it 1 compile with default flags. With this PR, [JSC] Hold the value and the key of a super property store in a RefPtr across ensureThis() #1016 and [JSC] Predict the empty value for an unprofiled load ofthisof a derived constructor from its scope #1017 together a construction of that shape takes 80 instructions against 1073.async key => { await 0; return super[key] = 21; }still gives the scope object, as on the base.thisat each use. It checksthisafter the key and the value of a store to a super property. A program with a// Todo:line in Bump WebKit (oven-sh/WebKit#1018 preview): an arrow function of a derived constructor seesthisafter another one called super() bun#48304 holds that difference.Self-review. Two rounds, 24 concerns, 22 addressed.
thisof a derived constructor from its scope #1017, for the condition on the early check, for the eval frame in the debugger, for loads ofthisat thesuper()site that the bytecode optimizer still resolves, for the cost per tier, for the test cases that move the scope register, and for a test of the cached bit.Scalars, the early check in the body of an async arrow function, the test of the bit for console text (Bump WebKit (oven-sh/WebKit#1018 preview): an arrow function of a derived constructor seesthisafter another one called super() bun#48304), the eval code cases with avar, and this description. It also found three faults that the base has. Each has a place now: the frame of the constructor is fixed here, the abort ofboundThisis [JSC] ShowboundThisof an arrow function as undefined in the inspector whilethisof its derived constructor has no value #1019, and the order of the check in compiled code is the// Todo:program.readsThisAtEachUse()keeps its name, because it now is that property for all code with the bit.Test runs.
run-jsc-stress-tests, and fails on the base at its first case.test/languageandtest/staging/sm/class, default and strict, 43,864 runs on release builds of both commits: the same 43,333 pass.thisof a derived constructor from its scope #1017 passes the three new tests in every mode.