TaggedTemplateNode: call (a?.b)... with |this| = a, and super.tag... with the method's this - #534
TaggedTemplateNode: call (a?.b)... with |this| = a, and super.tag... with the method's this#534robobun wants to merge 2 commits into
... with |this| = a, and super.tag... with the method's this#534Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review. WalkthroughChangesThe compiler now preserves Tagged template receiver semantics
Merge Risk: ⚪ Minimal · up to 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)
✅ Passed checks (3 passed)
Full details: Description checkExplanation 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.
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 Comment |
84b5a66 to
bc9542d
Compare
Preview Builds
|
|
Results with the preview build
|
…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.
bc9542d to
492e8dd
Compare
... with |this| = a... with |this| = a, and super.tag... with the method's this
…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.
|
Results with the combined preview build
|
Symptom
TaggedTemplateNode::emitBytecode(bytecompiler/NodesCodegen.cpp) gets thethisof the tag call wrong in two cases whereFunctionCallDotNode/FunctionCallBracketNodeget it right for the equivalent call:(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 referenceo?.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``....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`, andsuper[key()]`x`evaluatedkey()before thethis` TDZ check in a derived constructor.V8 and SpiderMonkey give the spec result in both cases. Found through Bun; the repros go through
new Function, so it is the engine. Upstreammainhas the same code.Cause
The function sets up the call's
thisonly for a tag that is aResolveNode,BracketAccessorNodeorDotAccessorNode, and reuses the lookup base as the receiver.(o?.tag)parses to anOptionalChainNodearound the dot node, which is not a location, so the tag was evaluated as a plain value and the call gotundefined(ASTBuilder::makeFunctionCallNodelooks through theOptionalChainNodefor(a?.b)(), so calls were fine). Forsuper.tagthe 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:
ensureThis()first (also thethisTDZ check, so it runs before the key), thenemitSuperBaseForCallee()for the lookup, and the call receives thethisregister (commit from TaggedTemplateNode: call super.tag...and super[key]...with the method's this #438).TaggedTemplateNode::emitBytecodelooks through anOptionalChainNodetag 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'sthis. The optional chain target is pushed before the base is evaluated and popped right after the tag is looked up, and a base flaggedisOptionalChainBase()gets the nullish check (as inDotAccessorNode). So only the lookup short-circuits: when the base is nullish, the tag register and the base register are both loaded withundefinedat the target (a short-circuited chain is the plain valueundefined, 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 byop_enter.BracketAccessorNode::emitBytecodeandDotAccessorNode::emitBytecodedo. 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.Verification
JSTests/stress/tagged-templates-optional-chain-this.js: dot and bracket tags (variable, string literal, symbol and index keys), double parentheses,this?.tagin 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 theirthis; 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 atestLoopCountloop through the tiers.JSTests/stress/tagged-templates-super-this.jsfrom TaggedTemplateNode: call super.tag...and super[key]...with the method's this #438 (its description lists what it covers).jscshell of theautobuild-ceb9f90frelease prebuilt (unfixed) both tests fail. On a local Debug build of this branch both pass, also with--useJIT=false,--useLLInt=false,--useConcurrentJIT=falseand the DFG/FTL eager thresholds, with--validateGraph=true --validateBytecode=true. The optional chain test also passes on node v26 (withnoInlineandtestLoopCountsupplied).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 prebuiltjsc.JSTests/stresscomparison 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 underbun.