Skip to content

TaggedTemplateNode: call (a?.b)... with |this| = a, and super.tag... with the method's this - #534

Open
robobun wants to merge 2 commits into
mainfrom
farm/c0522873/tagged-template-optional-chain-this
Open

TaggedTemplateNode: call (a?.b)... with |this| = a, and super.tag... with the method's this#534
robobun wants to merge 2 commits into
mainfrom
farm/c0522873/tagged-template-optional-chain-this

Conversation

@robobun

@robobun robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Symptom

TaggedTemplateNode::emitBytecode (bytecompiler/NodesCodegen.cpp) gets the this of the tag call wrong in two cases where FunctionCallDotNode / FunctionCallBracketNode get it right for the equivalent call:

  1. A parenthesized optional chain as the tag: (o?.tag)\x`invokestagwiththis= undefined, while(o?.tag)()ando.tag`x`invoke it withthis=o. The parentheses end the optional chain, but the expression they wrap is still the property reference o?.tag, and EvaluateCall step 1.a.i uses GetThisValue of that reference. Same for (o?.["tag"])`x`and longer chains such as(o?.inner.tag)`x``.
  2. A super property as the tag (TaggedTemplateNode: call super.tag... and super[key]... with the method's this #438, carried here as its own commit): super.tag\x`inside a method invokestagwiththis= the home object's prototype instead of the receiver, whilesuper.tag()uses the receiver. Same forsuper[key]`x`, and super[key()]`x`evaluatedkey()before thethis` TDZ check in a derived constructor.
const o = { tag() { return this === o; } };
[(o?.tag)`x`, (o?.tag)(), o.tag`x`];   // jsc: [false, true, true]    node: [true, true, true]

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

V8 and SpiderMonkey give the spec result in both cases. Found through Bun; the repros go through new Function, so it is the engine. Upstream main has the same code.

Cause

The function sets up the call's this only for a tag that is a ResolveNode, BracketAccessorNode or DotAccessorNode, and reuses the lookup base as the receiver. (o?.tag) parses to an OptionalChainNode around the dot node, which is not a location, so the tag was evaluated as a plain value and the call got undefined (ASTBuilder::makeFunctionCallNode looks through the OptionalChainNode for (a?.b)(), so calls were fine). For super.tag the lookup base (the home object's prototype) was also used as the receiver, although the lookup itself already used the right receiver.

Change

Both branches now compute the receiver the way the call nodes do:

  • A super base: ensureThis() first (also the this TDZ check, so it runs before the key), then emitSuperBaseForCallee() for the lookup, and the call receives the this register (commit from TaggedTemplateNode: call super.tag... and super[key]... with the method's this #438).
  • An optional chain: TaggedTemplateNode::emitBytecode looks through an OptionalChainNode tag whose expression is a property access and compiles that access with the same bracket/dot branches, so the base ends up in its own register and becomes the call's this. The optional chain target is pushed before the base is evaluated and popped right after the tag is looked up, and a base flagged isOptionalChainBase() gets the nullish check (as in DotAccessorNode). So only the lookup short-circuits: when the base is nullish, the tag register and the base register are both loaded with undefined at the target (a short-circuited chain is the plain value undefined, with no base) and the call throws the same TypeError as before. The base register has to be written there because nothing else writes it on that path; temporaries are not initialized by op_enter.
  • The bracket and dot branches emit the expression info of the property access before the lookup, as BracketAccessorNode::emitBytecode and DotAccessorNode::emitBytecode do. Without it the TypeError of a failing lookup used whatever expression info came last: a.b.c\x`witha.bundefined saidevaluating 'a.b', and the unwrapped (a?.b[k])`x`would have saidevaluating 'k'`. Both now name the whole access. This only changes the side table used for error messages, not the emitted instructions.
  • Tags that are neither super references nor parenthesized optional chains emit the same instructions as before.

Verification

  • JSTests/stress/tagged-templates-optional-chain-this.js: dot and bracket tags (variable, string literal, symbol and index keys), double parentheses, this?.tag in a method, longer chains with the ?. at each position, a getter on the base, a base expression evaluated once, the template object, raw strings, substitution values and per-site caching, evaluation order of the lookup and the substitutions, primitive receivers seen by a strict tag, private fields, methods and static fields in the chain, a super property as the base of the chain, generator, eval, new Function, arrow, callback and condition contexts; tags that are not parenthesized chains keeping their this; nullish bases throwing a TypeError with the key and the rest of the chain skipped, the substitutions still evaluated and the tag never called; a missing tag on a present base; and a testLoopCount loop through the tiers.
  • JSTests/stress/tagged-templates-super-this.js from TaggedTemplateNode: call super.tag... and super[key]... with the method's this #438 (its description lists what it covers).
  • On the jsc shell of the autobuild-ceb9f90f release prebuilt (unfixed) both tests fail. On a local Debug build of this branch both pass, also with --useJIT=false, --useLLInt=false, --useConcurrentJIT=false and the DFG/FTL eager thresholds, with --validateGraph=true --validateBytecode=true. The optional chain test also passes on node v26 (with noInline and testLoopCount supplied).
  • The existing optional-chaining*, tagged-template*, *optional*, *template*, *super* and *this* stress tests (168 files) give the same results on the Debug build of this branch as on the unfixed prebuilt jsc.
  • Results with the preview build, including a full JSTests/stress comparison against the unfixed shell, are in a comment below. Bump WebKit (oven-sh/WebKit#534 preview): call the tag of (a?.b)... with this = a bun#40848 pins the preview and runs both files under bun.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 61b75d86-78d3-4c2e-b39f-5405be4638c8

📥 Commits

Reviewing files that changed from the base of the PR and between ceb9f90 and 492e8dd.

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

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

The compiler now preserves this for tagged templates that use super properties or parenthesized optional-chain property references. New stress tests cover lookup, evaluation order, short-circuiting, execution contexts, and JIT tiers.

Tagged template receiver semantics

Layer / File(s) Summary
Receiver-aware tagged template bytecode
Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp
TaggedTemplateNode tracks property lookup and call receivers separately. Optional chains short-circuit to undefined, while super tags use the current this value.
Optional-chain validation
JSTests/stress/tagged-templates-optional-chain-this.js
Tests cover property, computed, private, super, primitive, getter, nested-chain, nullish, evaluation-order, execution-context, and JIT cases.
Super receiver validation
JSTests/stress/tagged-templates-super-this.js
Tests cover computed and direct super tags across methods, fields, constructors, generators, async functions, eval, receiver types, ordinary tags, and optimized execution.

Merge Risk: ⚪ Minimal · up to 492e8

The change corrects receiver handling for tagged templates involving optional chains and super properties, with dedicated regression coverage. No actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides detailed symptom, cause, implementation, affected behavior, changed tests, and verification results. It does not include the required Bugzilla bug title and link, the “Reviewe… Add the associated Bugzilla URL and bug title, include the required reviewer line, and format the changed files and relevant functions according to the repository template.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies both primary fixes: preserving the receiver for parenthesized optional-chain tags and for super-property tagged templates.
Full details: Description check

Explanation

The description provides detailed symptom, cause, implementation, affected behavior, changed tests, and verification results. It does not include the required Bugzilla bug title and link, the “Reviewed by NOBODY (OOPS!).” line, or a template-formatted changed-path/function list.

  • Fix all pre-merge checks with AI

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


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

@robobun
robobun force-pushed the farm/c0522873/tagged-template-optional-chain-this branch 2 times, most recently from 84b5a66 to bc9542d Compare August 28, 2026 18:52
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
492e8dd6 autobuild-preview-pr-534-492e8dd6 2026-08-29 02:25:30 UTC
bc9542d2 autobuild-preview-pr-534-bc9542d2 2026-08-28 19:23:41 UTC

@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.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Results with the preview build autobuild-preview-pr-534-bc9542d2:

  • JSTests/stress/tagged-templates-optional-chain-this.js passes on its release jsc shell and fails at its first assertion on the autobuild-ceb9f90f shell (unfixed main).
  • Full JSTests/stress (5745 files, default options plus --useDollarVM=true, 120s per file, 14 in parallel) on both release shells: 132 non-zero exits before, 131 after. 10 files differ. The new test goes from exception to pass. The other 9 are load- or option-dependent and give the same result on both shells when rerun alone: big-int-strict-spec-to-this.js (needs --useConcurrentJIT=false, fails on both alone), stack-overflow-in-syntax-checker.js (needs --ignoreUncaughtExceptions), ffi-pointers-and-buffers.js, array-prototype-flat-reentrant-mutation.js (memoryHog!), deep-StructureStubClearingWatchpoint-destructor-recursion.js (//@ skip), bigint-terminate-tostring.js (needs --watchdog=300), get-own-property-descriptors-oom.js and re-enter-resolve-rope-string.js (memoryHog! with a watchdog, timeout vs. kill depends on load), and delete-property-check-structure-transition.js (//@ skip, flaky on both: 5 of 15 runs fail before, 6 of 15 after).
  • Bump WebKit (oven-sh/WebKit#534 preview): call the tag of (a?.b)... with this = a bun#40848 pins this preview. Under bun (debug + ASAN), the new file passes as a jsc-stress fixture, and the other 116 fixtures of test/js/bun/jsc-stress/jsc-stress.test.ts still pass.

robobun added a commit to oven-sh/bun that referenced this pull request Aug 28, 2026
…ession info

oven-sh/WebKit#534 emits the expression info of a tagged template's
property-access tag before the lookup. The cached bytecode of every
corpus file with a `String.raw`...`` style tag moves by that entry:
records.js (so also all.js), libraries.js and source-forms. CI produced
the same new hashes on every platform, which is the update case the
file's header describes.
…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.
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 force-pushed the farm/c0522873/tagged-template-optional-chain-this branch from bc9542d to 492e8dd Compare August 29, 2026 01:55
@robobun robobun changed the title TaggedTemplateNode: call (a?.b)... with |this| = a TaggedTemplateNode: call (a?.b)... with |this| = a, and super.tag... with the method's this Aug 29, 2026

@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.

Code review found no issues

No high-confidence issues detected in this change.

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.
@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Results with the combined preview build autobuild-preview-pr-534-492e8dd6 (#438's commit plus the optional chain fix):

  • JSTests/stress/tagged-templates-optional-chain-this.js and JSTests/stress/tagged-templates-super-this.js pass on its release jsc shell. Both fail on the autobuild-ceb9f90f shell (unfixed main): the first at its first assertion, the second with super.tag\x` is called with the receiver as this: got [object Base], expected [object C]`.
  • Full JSTests/stress (5746 files, default options plus --useDollarVM=true, 120s per file, 14 in parallel) on both release shells: 132 non-zero exits before, 129 after. Besides the two new tests, 10 files differ, and each gives the same result on both shells when rerun alone or is a known load-dependent one: big-int-spec-to-this.js (skip if not $jitTests, fails on both alone), big-int-strict-spec-to-this.js (needs --useConcurrentJIT=false, fails on both alone), int8-repeat-in-then-out-of-bounds.js (fails on both alone), math-pow-coherency.js (//@ skip, an FTL timing check that fails 8 and 12 of 12 runs alone and 11 of 12 on each under load), ffi-pointers-and-buffers.js, array-prototype-flat-reentrant-mutation.js and regress-84402043.js (memoryHog!, pass on both alone), deep-StructureStubClearingWatchpoint-destructor-recursion.js (//@ skip, passes on both alone), get-own-property-descriptors-oom.js and re-enter-resolve-rope-string.js (memoryHog! with a watchdog, timeout vs. kill depends on load).
  • Bump WebKit (oven-sh/WebKit#534 preview): call the tag of (a?.b)... with this = a bun#40848 pins this preview: under bun (debug + ASAN) both files pass as jsc-stress fixtures together with the other 116, and the bytecode portability snapshot is unchanged from the previous preview (the super fix changes instructions only for super.tag\...`` forms, which the corpus does not contain).

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