Skip to content

TaggedTemplateNode: call super.tag... and super[key]... with the method's this - #438

Open
robobun wants to merge 1 commit into
mainfrom
farm/49eb5711/tagged-template-super-this
Open

TaggedTemplateNode: call super.tag... and super[key]... with the method's this#438
robobun wants to merge 1 commit into
mainfrom
farm/49eb5711/tagged-template-super-this

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Symptom

A tagged template whose tag is a super property is called with the wrong this. super.tag\x`inside a method invokestagwiththis = the object the property was found on (Base.prototypefor a class, the[[Prototype]]for an object literal method), whilesuper.tag()in the same method invokes it with the receiver. The spec gives both the samethis(EvaluateCall step 1.a.i uses GetThisValue of the Super Reference, which is the method'sthis`), and V8 does that.

class Base { tag() { return this; } f() { return this; } }
class C extends Base {
    m() { return [super.tag`x` === this, super.f() === this]; }
}
new C().m();   // jsc: [false, true]    node: [true, true]

Same for super[key]\x`. Found through Bun, whose transpiler leaves tagged templates alone, so any untranspiled super.tag`...`` in user code hits this.

A second, smaller consequence: in a derived constructor that has not called super() yet, super[key()]\x`evaluatedkey()before throwing the ReferenceError for the uninitializedthis, while superkey()and a plainsuper[key()]read throw first (the SuperProperty evaluation rule fetches thethisbinding before the key). Upstreammain` has the same code.

Cause

TaggedTemplateNode::emitBytecode (bytecompiler/NodesCodegen.cpp) keeps one register, base, for both the object the tag is looked up on and the this value of the call. For the super forms it already looked the property up with the right receiver (emitGetByVal(..., base, thisValue, property) / emitGetPropertyValue with a super base), but then moved base, the home object's prototype, into the call's this register. FunctionCallDotNode and FunctionCallBracketNode keep the two apart for super.

Change

  • The two super branches now do what the call nodes do: ensureThis() first (which is also the this TDZ check), then emitSuperBaseForCallee() for the lookup, and they record the this register in a separate thisValue; the call uses thisValue when set and base otherwise. The dot branch hands thisValue to the existing four argument emitGetPropertyValue overload so the lookup and the call share it.
  • Non-super tags generate the same bytecode as before: thisValue stays null and the call still receives base (or undefined when there is no base).

Verification

  • JSTests/stress/tagged-templates-super-this.js: super.tag and super[key] (variable, string literal, symbol and index keys) in instance methods, through an intermediate class, static methods, object literal methods (also called through an inheriting object), arrow functions, direct eval, instance and static field initializers, generator and async methods, and a derived constructor after super(); a getter on the super base; the template object, raw strings and substitution values still being passed and the template object still being cached per site; substitutions seeing the same this; primitive receivers passed through unconverted from a strict method and boxed by a sloppy one, both compared against super.tag(); a replaced super base; the TDZ case, asserting that neither the key nor the substitutions run and the tag is never called; and tags that are not super references (o.tag, o["tag"], o[0], base()[key()] with its evaluation order, a plain binding, this.tag inside a method) keeping their this; plus a testLoopCount loop through the tiers.
  • On the jsc shell of the current autobuild-f0f60fd2 release prebuilt (unfixed): 18 of the 20 groups fail, every super form in every context plus the TDZ ordering (evaluated: key,key); the two non-super groups pass. The same expectations pass on node v26 (V8), apart from the async group, which needs the shell's drainMicrotasks and was checked there separately.
  • The modified NodesCodegen.cpp compiles cleanly (-fsyntax-only, clang 21) against the engine headers; results from this PR's preview build (the new test and the existing super-* / tagged-template* stress tests on its jsc) will be added below once it is published, and oven-sh/bun will pin the preview to run the same file under bun.

…this

TaggedTemplateNode::emitBytecode passed the register holding the tag's
base as the call's this value. For a super property reference that base
is the home object's prototype, so super.tag`x` invoked the tag with
this = Base.prototype instead of the receiver, unlike super.tag() in
FunctionCallDotNode / FunctionCallBracketNode (spec: EvaluateCall step
1.a.i, GetThisValue of a Super Reference).

Emit the super cases the way the call nodes do: ensureThis() first,
then the super base for the lookup, and pass the this register to the
call. Doing the this TDZ check first also stops super[key]`...` from
evaluating the key before throwing in a derived constructor that has
not called super() yet, matching super[key]() and plain super[key].

JSTests/stress/tagged-templates-super-this.js covers both forms in
instance, static, object literal, arrow, eval, class field, generator,
async and derived constructor contexts, the TDZ ordering, and checks
that tags which are not super references are unaffected.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40576c51-0f78-412a-a78b-89bce90b9ba3

📥 Commits

Reviewing files that changed from the base of the PR and between 1f44d5c and 31f493c.

📒 Files selected for processing (2)
  • JSTests/stress/tagged-templates-super-this.js
  • Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it changes bytecode generation in TaggedTemplateNode::emitBytecode — a core engine path whose output feeds all four JIT tiers — and the preview-build test results mentioned in the description are still pending, a human look would still be worthwhile.

What was reviewed:

  • The super branches now mirror FunctionCallDotNode / FunctionCallBracketNode / BracketAccessorNode::emitBytecodeensureThis() before the subscript, emitSuperBaseForCallee() for lookup, receiver passed as call this.
  • Non-super paths are unchanged: thisValue stays null, and the 4-arg emitGetPropertyValue with a null thisValue is identical to the 3-arg overload it replaces.
  • Checked that ensureThis()emitSuperBaseForCallee() ordering matches existing call sites and is not observably different from the spec's actualThis → key → superBase order (home objects are ordinary, no proxy trap on the prototype fetch).
  • The new stress test covers dot/bracket, static/instance, arrows, eval, field initializers, generators/async, TDZ ordering, primitive receivers, and non-super regressions.
Extended reasoning...

Overview

This PR fixes a spec-compliance bug in TaggedTemplateNode::emitBytecode (Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp). Previously, super.tag\...`andsuper[key]`...`used a singlebaseregister for both the property lookup target (the home object's prototype) and the call'sthis, so the tag was invoked with the prototype as thisinstead of the method's receiver. The fix introduces a separatethisValueregister for the two super forms, populated viagenerator.ensureThis()beforeemitSuperBaseForCallee(), and moves thisValueinto the call'sthisregister when set. A 346-line stress test inJSTests/stress/tagged-templates-super-this.js` exercises the fix across instance/static methods, object-literal methods, arrow functions, direct eval, class field initializers, generators/async, derived-constructor TDZ ordering, primitive receivers, getters, replaced prototypes, and JIT tiers, plus regression cases confirming non-super tags are unaffected.

Security risks

None identified. This is a semantic correctness fix to which this value is passed to a tagged-template tag function. There is no new attack surface: the code already computed both the super base and the receiver (ensureThis() was already called inside the old emitGetPropertyValue path); the change only routes the receiver into the call's this register instead of the base. No parsing, allocation, or bounds logic is touched.

Level of scrutiny

High. Although the diff is small (~20 net lines in one function), it lives in the bytecode compiler, which is the single source of truth feeding LLInt, Baseline, DFG, and FTL. A mistake here would affect every JavaScript program run under Bun/JSC. The PR description also notes that preview-build results (running the new test and the existing super-* / tagged-template* stress tests on the built jsc) are still to be posted. Given the criticality of the file and the pending verification step, a human reviewer with JSC bytecode-generation experience should confirm.

Other factors

  • The new super handling is not novel: it copies the exact pattern from BracketAccessorNode::emitBytecode (lines ~1039–1050), FunctionCallBracketNode::emitBytecode (lines ~2196–2233), and FunctionCallDotNode::emitBytecode (lines ~2250–2265) — ensureThis() for the receiver/TDZ check, emitSuperBaseForCallee() for the lookup base, and the receiver as the call's this.
  • The non-super code paths are byte-for-byte equivalent to before: thisValue remains null, so the resolve/dot/bracket branches emit the same bytecode, and the call falls through to the existing else if (base) / else arms. The dot branch's switch from the 3-arg to the 4-arg emitGetPropertyValue overload with a null thisValue is a no-op — the 3-arg overload is a thin wrapper that does exactly that.
  • I checked the evaluation-order change in the bracket-super branch: ensureThis() now runs before both emitSuperBaseForCallee() and the subscript, whereas the old code emitted the subscript before ensureThis(). The new order matches the spec (SuperProperty step 2 fetches the this binding before evaluating the Expression) and matches BracketAccessorNode::emitBytecode. Placing emitSuperBaseForCallee() before the subscript (rather than after, as the spec's MakeSuperPropertyReference would strictly imply) is not observable because the home object is always an ordinary object, and this ordering already exists in FunctionCallBracketNode.
  • Test coverage is unusually thorough for a JSC stress test and includes a testLoopCount loop with noInline to push the code through the optimizing tiers.
  • No prior human or bot review comments to address; CodeRabbit was rate-limited and did not review.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
31f493c1 autobuild-preview-pr-438-31f493c1 2026-08-15 00:56:52 UTC

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Results with the autobuild-preview-pr-438-31f493c1 build, as promised in the description:

  • JSTests/stress/tagged-templates-super-this.js passes on the preview's release jsc (all 20 groups); on the autobuild-f0f60fd2 release jsc it still fails 18 of them, with the 2 non-super groups passing.
  • The 103 existing stress tests whose names mention super, template, class fields or derived classes give identical results on both shells (the same 2 pre-existing failures on each: super-sampler-intrinsic-exposed-to-user-scripts.js needs --exposePrivateIdentifiers, superclass-expression-strictness.js fails on both). The only difference is the new test going from fail to pass.
  • All 5614 JSTests/stress files, default variant (cwd JSTests/stress, --useDollarVM=1, no per-test //@ options, 16 in parallel, 120s timeout): f0f60fd2 5483 pass / 131 fail, preview 5487 pass / 127 fail. Of the 12 files whose result differs, the new test and the 4 ffi-* tests updated by FFI: convert Number arguments of i64/u64 parameters modulo 2^64 #421 (which is between Bun's current pin and this branch) are the deterministic ones; the other 7 (big-int-spec-to-this, big-int-strict-spec-to-this, int8-repeat-in-then-out-of-bounds, which depend on --useConcurrentJIT=false and fail 4/4 on both shells when rerun; destroy-GCAwareJITStubRoutineWithExceptionHandler-in-gc-end-phase, regress-84402043, shared-array-buffer-sort-while-different-thread-is-modifying, stack-overflow-in-syntax-checker, which pass on both when rerun alone and only hit the timeout under load; plus try-get-value-without-gc, killed on both) contain no tagged templates and behave the same on both engines when rerun. Non-super tags take the unchanged code path (thisValue stays null), which is what the suite shows.
  • Bump WebKit (oven-sh/WebKit#438 preview): call the tag of super.tag... with the method's this bun#38920 pins this preview: the same file runs as a jsc-stress fixture there, failing on the f0f60fd2 pin and passing on this one, and the rest of that suite (116 fixtures) passes on the preview.

Upstream main has the same TaggedTemplateNode::emitBytecode, so this is a candidate for sending to bugs.webkit.org as well.

robobun added a commit that referenced this pull request Aug 29, 2026
The tag of a tagged template can be a parenthesized optional chain.
The parentheses end the chain, but the expression they wrap is still
the property reference a?.b, so EvaluateCall (step 1.a.i) calls the tag
with |this| = a, as it does for (a?.b)(). TaggedTemplateNode::emitBytecode
treated the OptionalChainNode as a plain value and called the tag with
|this| = undefined.

Look through the OptionalChainNode when its expression is a property
access, evaluate the base into its own register and use it as |this|.
The chain target is pushed and popped around the lookup of the tag, so
a nullish base still short-circuits the lookup only: the tag and the
base are both undefined then, and the call throws a TypeError as before.
Tags that are not parenthesized optional chains emit the same bytecode
as before.

The bracket and dot branches now also emit the expression info of the
property access before the lookup, as BracketAccessorNode and
DotAccessorNode do, so a failing lookup names the whole access in its
TypeError ('a.b.c' and 'a?.b[k]' instead of the stale 'a.b' or 'k').

This sits on top of the super.tag`...` fix from #438, which
rewrites the same two branches: the receiver of the tag call is now
computed the way FunctionCallDotNode and FunctionCallBracketNode compute
it, for a super base, an optional chain base and a plain base alike.
@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

#534 (the (a?.b)...`` fix) rewrites the same two branches of TaggedTemplateNode::emitBytecode and conflicts with this PR in three hunks, so #534 now carries this PR's commit (31f493c, cherry-picked as 1f748bf with its test) underneath its own change. The combined function computes the tag call's receiver the way the call nodes do for a super base, an optional chain base and a plain base. Landing #534 lands this fix too, and this PR can be closed at that point. If this PR lands first instead, #534 rebases onto it.

robobun added a commit to oven-sh/bun that referenced this pull request Aug 29, 2026
…fixture

oven-sh/WebKit#534 now also carries oven-sh/WebKit#438's commit, which
fixes the this of super.tag`...`: both rewrite the same two branches of
TaggedTemplateNode::emitBytecode and conflicted. The preview moves to
autobuild-preview-pr-534-492e8dd6, and the jsc-stress fixture of #38920
(tagged-templates-super-this.js, its registration and the drainMicrotasks
preload global it uses) comes along.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant