From 1f748bf744f2b94729a78757fd63e74d8dc5c632 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 00:24:07 +0000 Subject: [PATCH 1/2] Call the tag of super.tag`...` and super[key]`...` with the method's 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. --- JSTests/stress/tagged-templates-super-this.js | 346 ++++++++++++++++++ .../bytecompiler/NodesCodegen.cpp | 33 +- 2 files changed, 370 insertions(+), 9 deletions(-) create mode 100644 JSTests/stress/tagged-templates-super-this.js diff --git a/JSTests/stress/tagged-templates-super-this.js b/JSTests/stress/tagged-templates-super-this.js new file mode 100644 index 0000000000000..3438e73a2dc79 --- /dev/null +++ b/JSTests/stress/tagged-templates-super-this.js @@ -0,0 +1,346 @@ +// A tagged template whose tag is a super property reference, super.tag`...` or super[key]`...`, must call +// the tag with the same |this| as super.tag(...) would: the |this| of the method containing it +// (EvaluateCall, https://tc39.es/ecma262/#sec-evaluatecall, step 1.a.i: GetThisValue of a Super Reference). +// It used to be called with the object the tag was found on, the home object's prototype, as if it were +// written Base.prototype.tag`...`. + +function describe(value) { + if (typeof value === "function") + return `function ${value.name}`; + if (typeof value === "object" && value !== null) + return `[object ${value.constructor ? value.constructor.name : "?"}]`; + return `${typeof value} ${String(value)}`; +} + +function shouldBe(actual, expected, message) { + if (actual !== expected) + throw new Error(`${message}: got ${describe(actual)}, expected ${describe(expected)}`); +} + +function shouldThrowReferenceError(func, message) { + let error; + try { + func(); + } catch (e) { + error = e; + } + if (!(error instanceof ReferenceError)) + throw new Error(`${message}: expected a ReferenceError, got ${String(error)}`); +} + +const failures = []; +function test(name, func) { + try { + func(); + } catch (e) { + failures.push(`${name}: ${e.message}`); + } +} + +// Every tag records the |this| it was called with and returns it, so callers can compare it with `===`. +// All of them are strict, so they see exactly the value they were passed. +function tagStrict(strings) { + "use strict"; + tagStrict.lastCall = { thisValue: this, strings, values: Array.prototype.slice.call(arguments, 1) }; + return this; +} + +class Base { + tag(strings, ...values) { + Base.lastCall = { thisValue: this, strings, values }; + return this; + } + get getterTag() { + Base.lastGetterReceiver = this; + return tagStrict; + } + static tag() { + return this; + } +} +const key = "tag"; +const symbolKey = Symbol("tag"); +Base.prototype[symbolKey] = tagStrict; +Base.prototype[0] = tagStrict; + +test("super.tag in a method", () => { + class C extends Base { + m() { return super.tag`x`; } + call() { return super.tag(); } + } + const c = new C; + shouldBe(c.m(), c, "super.tag`x` is called with the receiver as this"); + shouldBe(Base.lastCall.strings.length, 1, "the template object is still passed"); + shouldBe(Base.lastCall.strings[0], "x", "the template strings are still passed"); + shouldBe(c.call(), c, "super.tag() is called with the receiver as this"); +}); + +test("super.tag is still looked up on the super base, not on this", () => { + class C extends Base { + tag() { throw new Error("C.prototype.tag must not be called by super.tag`x`"); } + m() { return super.tag`x`; } + } + const c = new C; + shouldBe(c.m(), c, "super.tag`x` resolves to Base.prototype.tag and is called with the receiver"); +}); + +test("super.tag two levels up", () => { + class Middle extends Base { } + class C extends Middle { + m() { return super.tag`x`; } + } + const c = new C; + shouldBe(c.m(), c, "a tag inherited through an intermediate class is called with the receiver"); +}); + +test("super[key] with a variable, a string literal, a symbol and an index", () => { + class C extends Base { + variable() { return super[key]`x`; } + literal() { return super["tag"]`x`; } + symbol() { return super[symbolKey]`x`; } + index() { return super[0]`x`; } + } + const c = new C; + shouldBe(c.variable(), c, "super[key]`x`"); + shouldBe(c.literal(), c, "super[\"tag\"]`x`"); + shouldBe(c.symbol(), c, "super[symbolKey]`x`"); + shouldBe(c.index(), c, "super[0]`x`"); +}); + +test("template object and substitutions", () => { + class C extends Base { + dot(a, b) { return super.tag`a${a}b${b}c`; } + bracket(a, b) { return super[key]`a${a}b${b}c`; } + } + const c = new C; + for (const [name, method] of [["super.tag", c.dot], ["super[key]", c.bracket]]) { + shouldBe(method.call(c, 1, 2), c, `${name} with substitutions is called with the receiver`); + const { strings, values } = Base.lastCall; + shouldBe(strings.length, 3, `${name}: strings.length`); + shouldBe(strings.join(","), "a,b,c", `${name}: strings`); + shouldBe(strings.raw.join(","), "a,b,c", `${name}: strings.raw`); + shouldBe(values.length, 2, `${name}: values.length`); + shouldBe(values[0], 1, `${name}: values[0]`); + shouldBe(values[1], 2, `${name}: values[1]`); + method.call(c, 3, 4); + shouldBe(Base.lastCall.strings, strings, `${name}: the template object is cached per call site`); + } +}); + +test("substitutions are evaluated after the tag is looked up and see the same this", () => { + const log = []; + class C extends Base { + get probe() { log.push("substitution"); return this; } + m() { return super.tag`${this.probe}${log.push("second")}`; } + } + const c = new C; + shouldBe(c.m(), c, "this value"); + shouldBe(log.join(), "substitution,second", "substitution order"); + shouldBe(Base.lastCall.values[0], c, "the substitution saw the receiver"); + shouldBe(Base.lastCall.values[1], 2, "the second substitution"); +}); + +test("getter on the super base", () => { + class C extends Base { + dot() { return super.getterTag`x`; } + bracket() { return super["getterTag"]`x`; } + } + const c = new C; + shouldBe(c.dot(), c, "the function a super getter returns is called with the receiver"); + shouldBe(Base.lastGetterReceiver, c, "super.getterTag invokes the getter with the receiver"); + Base.lastGetterReceiver = null; + shouldBe(c.bracket(), c, "the same through super[\"getterTag\"]"); + shouldBe(Base.lastGetterReceiver, c, "super[\"getterTag\"] invokes the getter with the receiver"); +}); + +test("static method", () => { + class C extends Base { + static dot() { return super.tag`x`; } + static bracket() { return super[key]`x`; } + } + shouldBe(C.dot(), C, "static super.tag`x` is called with the class as this"); + shouldBe(C.bracket(), C, "static super[key]`x` is called with the class as this"); +}); + +test("object literal method", () => { + const proto = { tag: tagStrict, [symbolKey]: tagStrict }; + const object = { + __proto__: proto, + dot() { return super.tag`x`; }, + bracket() { return super[symbolKey]`x`; }, + }; + shouldBe(object.dot(), object, "super.tag`x` in an object literal method"); + shouldBe(object.bracket(), object, "super[key]`x` in an object literal method"); + + const inheriting = { __proto__: object }; + shouldBe(inheriting.dot(), inheriting, "the receiver, not the home object, when the method is called through an inheriting object"); + shouldBe(inheriting.bracket(), inheriting, "the same for super[key]"); +}); + +test("primitive receivers are passed through like super.tag() passes them", () => { + class C extends Base { + dot() { return super.tag`x`; } + bracket() { return super[key]`x`; } + call() { return super.tag(); } + } + // Class code is strict, so the primitive reaches the method unconverted and is passed on as is. + shouldBe(C.prototype.call.call(5), 5, "sanity: super.tag() from a strict method passes the primitive through"); + shouldBe(C.prototype.dot.call(5), 5, "super.tag`x` from a strict method passes the primitive through"); + shouldBe(C.prototype.bracket.call(5), 5, "super[key]`x` from a strict method passes the primitive through"); + + // A sloppy method (made with Function so that it is sloppy whatever mode this file runs in) boxes its + // receiver on entry; the tag has to be called with that box. + const proto = { tag: tagStrict }; + const sloppy = Function("proto", "return { __proto__: proto, dot() { return super.tag`x`; }, call() { return super.tag(); } };")(proto); + const boxedByCall = sloppy.call.call(5); + shouldBe(typeof boxedByCall, "object", "sanity: super.tag() from a sloppy method passes the boxed receiver"); + const boxed = sloppy.dot.call(5); + shouldBe(typeof boxed, "object", "super.tag`x` from a sloppy method passes the boxed receiver"); + shouldBe(boxed instanceof Number, true, "the box is a Number object"); + shouldBe(boxed.valueOf(), 5, "the box holds the receiver"); +}); + +test("super base replaced after the class was defined", () => { + class C extends Base { + m() { return super.tag`x`; } + } + Object.setPrototypeOf(C.prototype, { tag: tagStrict }); + const c = new C; + tagStrict.lastCall = null; + shouldBe(c.m(), c, "this is still the receiver when the tag comes from the replacement super base"); + shouldBe(tagStrict.lastCall.thisValue, c, "the replacement tag is the one that was called"); +}); + +test("arrow functions inside a method", () => { + class C extends Base { + dot() { return (() => super.tag`x`)(); } + bracket() { return (() => (() => super[key]`x`)())(); } + } + const c = new C; + shouldBe(c.dot(), c, "super.tag`x` inside an arrow function uses the method's this"); + shouldBe(c.bracket(), c, "super[key]`x` inside nested arrow functions uses the method's this"); +}); + +test("direct eval inside a method", () => { + class C extends Base { + dot() { return eval("super.tag`x`"); } + bracket() { return eval("super[key]`x`"); } + } + const c = new C; + shouldBe(c.dot(), c, "super.tag`x` in eval code"); + shouldBe(c.bracket(), c, "super[key]`x` in eval code"); +}); + +test("class field initializers", () => { + class C extends Base { + dot = super.tag`x`; + bracket = super[key]`x`; + static staticDot = super.tag`x`; + static staticBracket = super[key]`x`; + } + const c = new C; + shouldBe(c.dot, c, "instance field initializer, super.tag"); + shouldBe(c.bracket, c, "instance field initializer, super[key]"); + shouldBe(C.staticDot, C, "static field initializer, super.tag"); + shouldBe(C.staticBracket, C, "static field initializer, super[key]"); +}); + +test("generator and async methods", () => { + class C extends Base { + *generator() { yield super.tag`x`; yield super[key]`x`; } + async asyncMethod() { return [super.tag`x`, await null, super[key]`x`]; } + } + const c = new C; + const results = Array.from(c.generator()); + shouldBe(results[0], c, "generator method, super.tag"); + shouldBe(results[1], c, "generator method, super[key]"); + + let asyncResult; + let asyncError; + c.asyncMethod().then((value) => { asyncResult = value; }, (error) => { asyncError = error; }); + drainMicrotasks(); + if (asyncError) + throw asyncError; + shouldBe(asyncResult[0], c, "async method, super.tag before an await"); + shouldBe(asyncResult[2], c, "async method, super[key] after an await"); +}); + +test("derived constructor after super()", () => { + let seen; + class C extends Base { + constructor() { + super(); + seen = [super.tag`x`, super[key]`x`, this]; + } + } + const c = new C; + shouldBe(seen[2], c, "sanity: this is the constructed object"); + shouldBe(seen[0], c, "super.tag`x` after super() is called with the new object"); + shouldBe(seen[1], c, "super[key]`x` after super() is called with the new object"); +}); + +test("derived constructor before super(): this is in its TDZ", () => { + // Nothing belonging to the tagged template may run before the uninitialized |this| is detected: neither the + // key of super[key] nor the substitutions. This is the order super[key]() and a plain super[key] read already use. + const log = []; + const evaluatedKey = () => { log.push("key"); return "tag"; }; + const substitution = () => { log.push("substitution"); return 1; }; + Base.lastCall = null; + + class Dot extends Base { + constructor() { super.tag`${substitution()}`; super(); } + } + shouldThrowReferenceError(() => new Dot, "super.tag`...` before super()"); + + class Bracket extends Base { + constructor() { super[evaluatedKey()]`${substitution()}`; super(); } + } + shouldThrowReferenceError(() => new Bracket, "super[key]`...` before super()"); + + class InArrow extends Base { + constructor() { (() => super[evaluatedKey()]`${substitution()}`)(); super(); } + } + shouldThrowReferenceError(() => new InArrow, "super[key]`...` in an arrow function before super()"); + + shouldBe(log.length, 0, `neither the key nor the substitutions were evaluated (evaluated: ${log.join()})`); + shouldBe(Base.lastCall, null, "the tag was never called"); +}); + +test("tags that are not super references keep their this", () => { + const object = { tag: tagStrict, 0: tagStrict }; + const log = []; + function base() { log.push("base"); return object; } + function property() { log.push("key"); return "tag"; } + shouldBe(object.tag`x`, object, "object.tag`x`"); + shouldBe(object["tag"]`x`, object, "object[\"tag\"]`x`"); + shouldBe(object[0]`x`, object, "object[0]`x`"); + shouldBe(base()[property()]`x`, object, "base()[key()]`x` is called with the base"); + shouldBe(log.join(), "base,key", "base()[key()]`x` evaluates the base before the key"); + shouldBe(tagStrict`x`, undefined, "tag`x` through a binding is called with undefined"); + class C extends Base { + other() { return object.tag`x`; } + own() { return this.tag`x`; } + } + const c = new C; + shouldBe(c.other(), object, "object.tag`x` inside a method is called with the object"); + shouldBe(c.own(), c, "this.tag`x` is called with this"); +}); + +test("optimized tiers", () => { + class C extends Base { + dot() { return super.tag`x`; } + bracket() { return super[key]`x`; } + } + noInline(C.prototype.dot); + noInline(C.prototype.bracket); + const c = new C; + for (let i = 0; i < testLoopCount; ++i) { + if (c.dot() !== c) + throw new Error(`super.tag\`x\` was called with the wrong this on iteration ${i}`); + if (c.bracket() !== c) + throw new Error(`super[key]\`x\` was called with the wrong this on iteration ${i}`); + } +}); + +if (failures.length) + throw new Error(`FAIL:\n${failures.join("\n")}`); diff --git a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp index 69ab2d5765151..ae6b00caef5e4 100644 --- a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp +++ b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp @@ -366,6 +366,10 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis ExpectedFunction expectedFunction = NoExpectedFunction; RefPtr tag = nullptr; RefPtr base = nullptr; + // Set for super.tag`...` and super[tag]`...`. A super property reference looks the tag up on the home object's + // prototype (|base|) but is called with the current |this| (https://tc39.es/ecma262/#sec-evaluatecall step 1.a.i, + // GetThisValue of a Super Reference), exactly like super.tag(...) in FunctionCallDotNode / FunctionCallBracketNode. + RefPtr thisValue = nullptr; if (!m_tag->isLocation()) { tag = generator.newTemporary(); tag = generator.emitNode(tag.get(), m_tag); @@ -390,21 +394,30 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis } } else if (m_tag->isBracketAccessorNode()) { BracketAccessorNode* bracket = static_cast(m_tag); - base = generator.newTemporary(); - base = generator.emitNode(base.get(), bracket->base()); - RefPtr property = generator.emitNodeForProperty(bracket->subscript()); if (bracket->base()->isSuperNode()) { - RefPtr thisValue = generator.ensureThis(); + thisValue = generator.ensureThis(); + base = emitSuperBaseForCallee(generator); + } else { + base = generator.newTemporary(); + base = generator.emitNode(base.get(), bracket->base()); + } + RefPtr property = generator.emitNodeForProperty(bracket->subscript()); + if (thisValue) tag = generator.emitGetByVal(generator.newTemporary(), base.get(), thisValue.get(), property.get()); - } else + else tag = generator.emitGetByVal(generator.newTemporary(), base.get(), property.get()); } else { ASSERT(m_tag->isDotAccessorNode()); DotAccessorNode* dot = static_cast(m_tag); tag = generator.newTemporary(); - base = generator.newTemporary(); - base = generator.emitNode(base.get(), dot->base()); - tag = dot->emitGetPropertyValue(generator, tag.get(), base.get()); + if (dot->base()->isSuperNode()) { + thisValue = generator.ensureThis(); + base = emitSuperBaseForCallee(generator); + } else { + base = generator.newTemporary(); + base = generator.emitNode(base.get(), dot->base()); + } + tag = dot->emitGetPropertyValue(generator, tag.get(), base.get(), thisValue); } RefPtr templateObject = generator.emitGetTemplateObject(nullptr, this); @@ -414,7 +427,9 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis ++expressionsCount; CallArguments callArguments(generator, nullptr, 1 + expressionsCount); - if (base) + if (thisValue) + generator.move(callArguments.thisRegister(), thisValue.get()); + else if (base) generator.move(callArguments.thisRegister(), base.get()); else generator.emitLoad(callArguments.thisRegister(), jsUndefined()); From 492e8dd695ae1cc42989342ea5fe683fb670dc5f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:32:51 +0000 Subject: [PATCH 2/2] TaggedTemplateNode: call (a?.b)`...` with |this| = a 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 oven-sh/WebKit#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. --- .../tagged-templates-optional-chain-this.js | 224 ++++++++++++++++++ .../bytecompiler/NodesCodegen.cpp | 51 +++- 2 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 JSTests/stress/tagged-templates-optional-chain-this.js diff --git a/JSTests/stress/tagged-templates-optional-chain-this.js b/JSTests/stress/tagged-templates-optional-chain-this.js new file mode 100644 index 0000000000000..6ad38238017b0 --- /dev/null +++ b/JSTests/stress/tagged-templates-optional-chain-this.js @@ -0,0 +1,224 @@ +// A tagged template whose tag is a parenthesized optional chain, (a?.b)`...` or (a?.[k])`...`, must call the +// tag with |this| = a, the same |this| that (a?.b)(...) and a.b`...` get. The parentheses end the optional +// chain, but the expression they wrap is still the property reference a?.b, so EvaluateCall +// (https://tc39.es/ecma262/#sec-evaluatecall, step 1.a.i) uses GetThisValue of that reference. The tag used to +// be called with |this| = undefined, as if the chain had produced a plain value. +// +// Only the lookup of the tag short-circuits. When a is nullish, (a?.b)`...` is a call of undefined: the template +// object and the substitutions are still evaluated (ArgumentListEvaluation comes before the IsCallable check), +// and then a TypeError is thrown. + +function shouldBe(actual, expected, message) { + if (actual !== expected) + throw new Error(`${message}: got ${String(actual)}, expected ${String(expected)}`); +} + +function shouldThrowTypeError(func, message) { + let error; + try { + func(); + } catch (e) { + error = e; + } + if (!(error instanceof TypeError)) + throw new Error(`${message}: expected a TypeError, got ${String(error)}`); +} + +// The tag is strict so it sees exactly the |this| it was called with, and it records the call. +function tag(strings) { + "use strict"; + tag.lastCall = { thisValue: this, strings, values: Array.prototype.slice.call(arguments, 1) }; + return this; +} + +const key = "tag"; +const symbolKey = Symbol("tag"); +const inner = { tag, [symbolKey]: tag, 0: tag }; +const object = { + tag, + [symbolKey]: tag, + 0: tag, + inner, + get getterTag() { + object.lastGetterReceiver = this; + return tag; + }, + getTag() { + return tag; + }, + method() { + return (this?.tag)`x`; + }, +}; + +shouldBe((object?.tag)`x`, object, "(object?.tag)`x`"); +shouldBe((object?.["tag"])`x`, object, "(object?.[\"tag\"])`x`"); +shouldBe((object?.[key])`x`, object, "(object?.[key])`x`"); +shouldBe((object?.[symbolKey])`x`, object, "(object?.[symbolKey])`x`"); +shouldBe((object?.[0])`x`, object, "(object?.[0])`x`"); +shouldBe(((object?.tag))`x`, object, "((object?.tag))`x`"); +shouldBe(object.method(), object, "(this?.tag)`x` in a method"); + +// Longer chains: |this| is the base of the last property access. +shouldBe((object?.inner.tag)`x`, inner, "(object?.inner.tag)`x`"); +shouldBe((object?.inner?.tag)`x`, inner, "(object?.inner?.tag)`x`"); +shouldBe((object.inner?.tag)`x`, inner, "(object.inner?.tag)`x`"); +shouldBe((object?.inner?.[key])`x`, inner, "(object?.inner?.[key])`x`"); +shouldBe((object?.inner[symbolKey])`x`, inner, "(object?.inner[symbolKey])`x`"); + +// A getter on the base runs with the base as receiver, and the tag it returns is called with the base too. +shouldBe((object?.getterTag)`x`, object, "(object?.getterTag)`x`"); +shouldBe(object.lastGetterReceiver, object, "the getter's receiver"); + +// The base of the chain can be any expression; it is evaluated once. +let getObjectCalls = 0; +function getObject() { + getObjectCalls++; + return object; +} +shouldBe((getObject()?.tag)`x`, object, "(getObject()?.tag)`x`"); +shouldBe(getObjectCalls, 1, "the base is evaluated once"); + +// The template object, its raw strings and the substitutions are passed as for any tagged template, and the +// template object is cached per call site. +function withSubstitutions(a, b) { + return (object?.tag)`a${a}b${b}c`; +} +shouldBe(withSubstitutions(1, 2), object, "(object?.tag)`a${a}b${b}c`"); +{ + const { strings, values } = tag.lastCall; + shouldBe(strings.length, 3, "strings.length"); + shouldBe(strings.join(","), "a,b,c", "strings"); + shouldBe(strings.raw.join(","), "a,b,c", "strings.raw"); + shouldBe(values.length, 2, "values.length"); + shouldBe(values[0], 1, "values[0]"); + shouldBe(values[1], 2, "values[1]"); + withSubstitutions(3, 4); + shouldBe(tag.lastCall.strings, strings, "the template object is cached per call site"); + shouldBe(tag.lastCall.values[0], 3, "values[0] of the second call"); +} + +// Substitutions are evaluated after the tag is looked up, and see the same |this| as the enclosing code. +{ + const log = []; + const probe = { + get tag() { log.push("tag"); return tag; }, + m() { return (this?.tag)`${log.push("first")}${this}`; }, + }; + shouldBe(probe.m(), probe, "(this?.tag)`${...}` with substitutions"); + shouldBe(log.join(), "tag,first", "evaluation order"); + shouldBe(tag.lastCall.values[1], probe, "the substitution saw the enclosing this"); +} + +// Primitive receivers: a strict tag sees the primitive itself. +String.prototype.tag = tag; +Number.prototype.tag = tag; +shouldBe(("str"?.tag)`x`, "str", "(\"str\"?.tag)`x`"); +shouldBe(((1)?.tag)`x`, 1, "((1)?.tag)`x`"); +shouldBe(("str"?.["tag"])`x`, "str", "(\"str\"?.[\"tag\"])`x`"); +delete String.prototype.tag; +delete Number.prototype.tag; + +// Private names in the chain. +class WithPrivate { + #tag = tag; + #method(strings) { "use strict"; return this; } + static #staticTag = tag; + field() { return (this?.#tag)`x`; } + method() { return (this?.#method)`x`; } + static staticField() { return (WithPrivate?.#staticTag)`x`; } + static onInstance(instance) { return (instance?.#tag)`x`; } +} +{ + const instance = new WithPrivate; + shouldBe(instance.field(), instance, "(this?.#tag)`x`"); + shouldBe(instance.method(), instance, "(this?.#method)`x`"); + shouldBe(WithPrivate.staticField(), WithPrivate, "(WithPrivate?.#staticTag)`x`"); + shouldBe(WithPrivate.onInstance(instance), instance, "(instance?.#tag)`x`"); +} + +// A super property can be the base of the chain, but not the chain itself. +class Base { + get inner() { return inner; } +} +class Derived extends Base { + m() { return (super.inner?.tag)`x`; } +} +shouldBe(new Derived().m(), inner, "(super.inner?.tag)`x`"); + +// Other contexts that compile the same node. +function* generator() { + yield (object?.tag)`x`; +} +shouldBe(generator().next().value, object, "(object?.tag)`x` in a generator"); +globalThis.taggedTemplateThisObject = object; +shouldBe((0, eval)("(taggedTemplateThisObject?.tag)`x`"), object, "(object?.tag)`x` in indirect eval"); +shouldBe(eval("(object?.tag)`x`"), object, "(object?.tag)`x` in direct eval"); +shouldBe(new Function("object", "return (object?.tag)`x`")(object), object, "(object?.tag)`x` in new Function"); +shouldBe((() => (object?.tag)`x`)(), object, "(object?.tag)`x` in an arrow function"); +shouldBe([1].map(() => (object?.tag)`x`)[0], object, "(object?.tag)`x` in a callback"); +{ + let result; + if ((object?.tag)`x`) + result = "truthy"; + shouldBe(result, "truthy", "(object?.tag)`x` in a condition"); +} +shouldBe((object?.tag)`x`.tag, tag, "a property of the result"); + +// Tags that are not parenthesized optional chains keep their |this|. +shouldBe((object?.inner).tag`x`, inner, "(object?.inner).tag`x`"); +shouldBe((object?.inner)[key]`x`, inner, "(object?.inner)[key]`x`"); +shouldBe((object?.getTag())`x`, undefined, "(object?.getTag())`x` is a call of a plain value"); +shouldBe((object?.tag ?? tag)`x`, undefined, "(object?.tag ?? tag)`x` is a call of a plain value"); +shouldBe((object?.tag, tag)`x`, undefined, "(object?.tag, tag)`x` is a call of a plain value"); +shouldBe(object.tag`x`, object, "object.tag`x`"); +shouldBe(tag`x`, undefined, "tag`x`"); + +// A nullish base: the tag is undefined and the call throws a TypeError. The key and the rest of the chain are +// skipped, the substitutions are still evaluated, and the tag function is never called. +{ + const log = []; + tag.lastCall = null; + const nullish = null; + shouldThrowTypeError(() => (nullish?.tag)`x`, "(null?.tag)`x`"); + shouldThrowTypeError(() => (undefined?.tag)`x`, "(undefined?.tag)`x`"); + shouldThrowTypeError(() => (nullish?.[log.push("key")])`${log.push("substitution")}`, "(null?.[key])`${...}`"); + shouldBe(log.join(), "substitution", "the key is skipped, the substitution is evaluated"); + shouldThrowTypeError(() => (nullish?.inner.tag)`x`, "(null?.inner.tag)`x`"); + shouldThrowTypeError(() => (object?.missing?.tag)`x`, "(object?.missing?.tag)`x`"); + shouldThrowTypeError(() => (object?.missing.tag)`x`, "(object?.missing.tag)`x` throws on the plain access"); + shouldBe(tag.lastCall, null, "the tag is never called"); +} + +// A missing tag on a present base is a call of undefined too. +shouldThrowTypeError(() => (object?.missing)`x`, "(object?.missing)`x`"); +shouldThrowTypeError(() => (object?.[Symbol("missing")])`x`, "(object?.[missing symbol])`x`"); + +// Through the JIT tiers. +function dot(o) { + return (o?.tag)`x`; +} +noInline(dot); +function bracket(o, k) { + return (o?.[k])`x`; +} +noInline(bracket); +function chain(o) { + return (o?.inner?.tag)`x`; +} +noInline(chain); +function nullishBase(o) { + try { + (o?.tag)`x`; + } catch (e) { + return e instanceof TypeError; + } + return false; +} +noInline(nullishBase); +for (let i = 0; i < testLoopCount; ++i) { + shouldBe(dot(object), object, `dot, iteration ${i}`); + shouldBe(bracket(object, key), object, `bracket, iteration ${i}`); + shouldBe(chain(object), inner, `chain, iteration ${i}`); + shouldBe(nullishBase(i & 1 ? null : undefined), true, `nullish base, iteration ${i}`); +} diff --git a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp index ae6b00caef5e4..d35c2619a4ef4 100644 --- a/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp +++ b/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp @@ -370,11 +370,29 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis // prototype (|base|) but is called with the current |this| (https://tc39.es/ecma262/#sec-evaluatecall step 1.a.i, // GetThisValue of a Super Reference), exactly like super.tag(...) in FunctionCallDotNode / FunctionCallBracketNode. RefPtr thisValue = nullptr; - if (!m_tag->isLocation()) { + + // The parentheses in (a?.b)`...` end the optional chain, but the parenthesized expression is still the + // property reference a?.b, so the tag is called with |this| = a, exactly like (a?.b)() is + // (https://tc39.es/ecma262/#sec-evaluatecall step 1.a.i). Look through the OptionalChainNode to get at the + // base. Only the lookup of the tag short-circuits, so the chain target is pushed here and popped right + // after the lookup: a nullish base leaves the tag undefined and the call throws, as the spec requires. + ExpressionNode* tagNode = m_tag; + bool tagIsOptionalChain = false; + if (m_tag->isOptionalChain()) { + ExpressionNode* expr = static_cast(m_tag)->expr(); + if (expr->isLocation()) { + ASSERT(!expr->isResolveNode()); + tagNode = expr; + tagIsOptionalChain = true; + generator.pushOptionalChainTarget(); + } + } + + if (!tagNode->isLocation()) { tag = generator.newTemporary(); - tag = generator.emitNode(tag.get(), m_tag); - } else if (m_tag->isResolveNode()) { - ResolveNode* resolve = static_cast(m_tag); + tag = generator.emitNode(tag.get(), tagNode); + } else if (tagNode->isResolveNode()) { + ResolveNode* resolve = static_cast(tagNode); const Identifier& identifier = resolve->identifier(); expectedFunction = generator.expectedFunctionForIdentifier(identifier); @@ -392,23 +410,26 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis generator.emitGetFromScope(tag.get(), base.get(), var, ThrowIfNotFound); generator.emitTDZCheckIfNecessary(var, tag.get(), nullptr); } - } else if (m_tag->isBracketAccessorNode()) { - BracketAccessorNode* bracket = static_cast(m_tag); + } else if (tagNode->isBracketAccessorNode()) { + BracketAccessorNode* bracket = static_cast(tagNode); if (bracket->base()->isSuperNode()) { thisValue = generator.ensureThis(); base = emitSuperBaseForCallee(generator); } else { base = generator.newTemporary(); base = generator.emitNode(base.get(), bracket->base()); + if (bracket->base()->isOptionalChainBase()) + generator.emitOptionalCheck(base.get()); } RefPtr property = generator.emitNodeForProperty(bracket->subscript()); + generator.emitExpressionInfo(bracket->divot(), bracket->divotStart(), bracket->divotEnd()); if (thisValue) tag = generator.emitGetByVal(generator.newTemporary(), base.get(), thisValue.get(), property.get()); else tag = generator.emitGetByVal(generator.newTemporary(), base.get(), property.get()); } else { - ASSERT(m_tag->isDotAccessorNode()); - DotAccessorNode* dot = static_cast(m_tag); + ASSERT(tagNode->isDotAccessorNode()); + DotAccessorNode* dot = static_cast(tagNode); tag = generator.newTemporary(); if (dot->base()->isSuperNode()) { thisValue = generator.ensureThis(); @@ -416,10 +437,24 @@ RegisterID* TaggedTemplateNode::emitBytecode(BytecodeGenerator& generator, Regis } else { base = generator.newTemporary(); base = generator.emitNode(base.get(), dot->base()); + if (dot->base()->isOptionalChainBase()) + generator.emitOptionalCheck(base.get()); } + generator.emitExpressionInfo(dot->divot(), dot->divotStart(), dot->divotEnd()); tag = dot->emitGetPropertyValue(generator, tag.get(), base.get(), thisValue); } + if (tagIsOptionalChain) { + // A short-circuited chain is the plain value undefined: there is no tag and no base to use as |this|. + // |base| has to be written here as well, since nothing else writes it on this path. + Ref