From 69d93430fb88c202ecf0c9bdf8fce08db769ea7f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 13:41:44 -0700 Subject: [PATCH 1/2] Compile object-literal shorthand of class declarations `{ A, B }` where `B` is a class declaration failed: the shorthand path resolved locals, globals and functions but not class names, so a class extending EventEmitter (and, in JavaScript, a plain class declaration) reported "the reference to 'B' (a binding form with no lowering)" or a deferred SC2002 shape mismatch, breaking `module.exports = { A, B }`. The shorthand now lowers a class name exactly like the identifier (shared classNameValue), so the property holds the class value. A statically decided instanceof with computed operands (`new o.B() instanceof B`, `p instanceof lib.Plain`) was refused because folding dropped the operands' evaluation; it now evaluates both operands, left then right, and answers the folded result. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/identifiers.ts | 75 +++++++++++-------- .../lowering/expressions/object-literals.ts | 5 ++ .../lowering/expressions/relational.ts | 43 ++++++----- .../6585-class-value-shorthand.cjs.json | 6 ++ .../corpus/6586-class-value-shorthand.ts.json | 6 ++ .../main.cjs.json | 7 ++ tests/corpus/6585-class-value-shorthand.cjs | 30 ++++++++ tests/corpus/6586-class-value-shorthand.ts | 30 ++++++++ .../channel.cjs | 17 +++++ .../main.cjs | 15 ++++ tests/diagnostics/inheritance.ts | 7 -- .../harness/__snapshots__/inheritance.ts.txt | 39 ++++------ 12 files changed, 197 insertions(+), 83 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6585-class-value-shorthand.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6586-class-value-shorthand.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6587-class-value-shorthand-exports/main.cjs.json create mode 100644 tests/corpus/6585-class-value-shorthand.cjs create mode 100644 tests/corpus/6586-class-value-shorthand.ts create mode 100644 tests/corpus/6587-class-value-shorthand-exports/channel.cjs create mode 100644 tests/corpus/6587-class-value-shorthand-exports/main.cjs diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d6826..2acccc45a1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -842,37 +842,8 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // reach here — their hooks claim the property/new forms first). { const sym = lowerer.resolveValueSymbol(expr); - const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined; - // A GENERIC class name whose CONTEXTUAL type pins one instantiation - // (`const B: new (v: string) => Counter = Counter`): the - // value is that instantiation's class object — the generic-fn - // pinning rule on the static side. Unpinned references fall - // through to classValueRef's family fence. - if (classInfo?.generic) { - const ctxT = lowerer.checker.getContextualType(expr); - const mapped = ctxT ? lowerer.mapTypeOf(ctxT) : null; - const instInfo = - mapped?.kind === "classval" ? lowerer.classes.get(mapped.className) : undefined; - if (instInfo && instInfo.genericInstance?.family === classInfo) { - return lowerer.classValueRef(instInfo, expr); - } - } - if (classInfo) { - // A decorated name a replacing decorator can REBIND: the value - // is the decoration result — the mutable classval global %init - // assigned at the class statement (TC39's name binding), never - // the declaration's own class object. - const decoratedGlobal = classInfo.classDecorators?.valueGlobalId; - if (decoratedGlobal !== undefined) { - return { - kind: "varRef", - localId: decoratedGlobal, - type: { kind: "classval", className: classInfo.def.name }, - loc, - }; - } - return lowerer.classValueRef(classInfo, expr); - } + const value = sym ? classNameValue(lowerer, sym, expr, loc) : null; + if (value) return value; // The enum OBJECT as a value (member reads never reach here — the // access hooks fold them): iteration, storage, reverse lookups // through variables all land on this pointed fence. @@ -929,6 +900,48 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: ); } +/** A program CLASS NAME as a value (`sym` is the name's value symbol; an + * identifier or a shorthand property `{ Name }` spells it): the classRef + * over the class's immortal class object, or null when `sym` names no + * program class. */ +export function classNameValue( + lowerer: Lowerer, + sym: ts.Symbol, + expr: ts.Identifier, + loc: SrcLoc, +): IrExpr | null { + const classInfo = lowerer.classBySymbol.get(sym); + if (!classInfo) return null; + // A GENERIC class name whose CONTEXTUAL type pins one instantiation + // (`const B: new (v: string) => Counter = Counter`): the + // value is that instantiation's class object — the generic-fn + // pinning rule on the static side. Unpinned references fall + // through to classValueRef's family fence. + if (classInfo.generic) { + const ctxT = lowerer.checker.getContextualType(expr); + const mapped = ctxT ? lowerer.mapTypeOf(ctxT) : null; + const instInfo = + mapped?.kind === "classval" ? lowerer.classes.get(mapped.className) : undefined; + if (instInfo && instInfo.genericInstance?.family === classInfo) { + return lowerer.classValueRef(instInfo, expr); + } + } + // A decorated name a replacing decorator can REBIND: the value + // is the decoration result — the mutable classval global %init + // assigned at the class statement (TC39's name binding), never + // the declaration's own class object. + const decoratedGlobal = classInfo.classDecorators?.valueGlobalId; + if (decoratedGlobal !== undefined) { + return { + kind: "varRef", + localId: decoratedGlobal, + type: { kind: "classval", className: classInfo.def.name }, + loc, + }; + } + return lowerer.classValueRef(classInfo, expr); +} + function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string { return lowerer.targetPlatform === "wasi" ? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")) diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 303b6ed55c..c0e465eeef 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -35,6 +35,7 @@ import { tryLowerExpression } from "./try-lower-expression.js"; import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; +import { classNameValue } from "./identifiers.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -3165,6 +3166,10 @@ export function lowerShorthandValue( if (g) { return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName); } + // A class declaration's name (`{ Name }`): the class value, exactly + // like the long form `{ Name: Name }`. + const classValue = classNameValue(lowerer, resolved, propName, loc); + if (classValue) return classValue; const sig = lowerer.fnSigsBySymbol.get(resolved); const decl = resolved.declarations[0]; if ( diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index f8b8c76042..90fced5dd3 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -452,22 +452,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const value = left.type.className === targetInfo.def.name || lowerer.isSubclassOf(left.type.className, targetInfo.def.name); + const answer: IrExpr = { kind: "boolLit", value, type: BOOL, loc }; if ( left.kind === "varRef" && (ts.isIdentifier(expr.right) || expr.right.kind === ts.SyntaxKind.ThisKeyword) ) { - return readsClassAfter(lowerer, targetInfo, expr.right, [], { - kind: "boolLit", - value, - type: BOOL, - loc, - }); + return readsClassAfter(lowerer, targetInfo, expr.right, [], answer); } - lowerer.unsupported( - "SC1090", - expr, - "statically-decided 'instanceof' on computed operands (bind the values to variables first)", - ); + return evaluatedOperandsThen(lowerer, left, expr.right, answer); } const constructor = tryLowerExpression(lowerer, expr.right); if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { @@ -750,19 +742,26 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc leftClass === className || lowerer.isSubclassOf(leftClass, className); const value = isInstance(target.def.name) || (duplex !== undefined && isInstance(duplex.def.name)); + const answer: IrExpr = { kind: "boolLit", value, type: BOOL, loc }; if (left.kind === "varRef") { - return readsClassAfter(lowerer, target, expr.right, [], { - kind: "boolLit", - value, - type: BOOL, - loc, - }); + return readsClassAfter(lowerer, target, expr.right, [], answer); } - lowerer.unsupported( - "SC1090", - expr, - "statically-decided 'instanceof' on computed operands (bind the value to a variable first)", - ); + return evaluatedOperandsThen(lowerer, left, expr.right, answer); +} + +/** A statically decided `instanceof` over computed operands: both operands + * still evaluate, left then right, for their effects (a construction, a + * call, a property read), and the folded answer follows. */ +function evaluatedOperandsThen( + lowerer: Lowerer, + left: IrExpr, + right: ts.Expression, + answer: IrExpr, +): IrExpr { + const stmts: IrStmt[] = []; + if (left.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: left, loc: left.loc }); + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(right), loc: locOf(right) }); + return { kind: "seqExpr", stmts, result: answer, type: answer.type, loc: answer.loc }; } /** `x instanceof K` for a union-typed operand whose arms are class diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6585-class-value-shorthand.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6585-class-value-shorthand.cjs.json new file mode 100644 index 0000000000..220dfa3964 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6585-class-value-shorthand.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6585-class-value-shorthand.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6586-class-value-shorthand.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6586-class-value-shorthand.ts.json new file mode 100644 index 0000000000..3d6dede39c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6586-class-value-shorthand.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6586-class-value-shorthand.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6587-class-value-shorthand-exports/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6587-class-value-shorthand-exports/main.cjs.json new file mode 100644 index 0000000000..b15756051f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6587-class-value-shorthand-exports/main.cjs.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6587-class-value-shorthand-exports/channel.cjs", + "/tests/corpus/6587-class-value-shorthand-exports/main.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6585-class-value-shorthand.cjs b/tests/corpus/6585-class-value-shorthand.cjs new file mode 100644 index 0000000000..74d9be45eb --- /dev/null +++ b/tests/corpus/6585-class-value-shorthand.cjs @@ -0,0 +1,30 @@ +// Object-literal shorthand of class values: class declarations (plain and +// extending EventEmitter) and class expressions stored as properties. The +// property holds the class itself: construction through it, its name, +// identity with the binding, instanceof, and the emitter surface of its +// instances. +"use strict"; +const { EventEmitter } = require("node:events"); + +class Base { + hi() { + return "base"; + } +} +class Bus extends EventEmitter { + constructor(name) { + super(); + this.name = name; + } +} +const Anon = class extends EventEmitter {}; +const api = { Base, Bus, Anon, version: 1 }; +const bus = new api.Bus("main"); +console.log(api.Base.name, api.Bus.name, api.Anon.name, api.version); +console.log(bus.name, bus instanceof Bus, bus instanceof EventEmitter, bus instanceof api.Bus); +console.log(new api.Base().hi(), new api.Base() instanceof Base, new api.Bus("x") instanceof Base); +bus.on("x", (v) => console.log("x", v)); +console.log(bus.emit("x", 1), bus.listenerCount("x")); +const anon = new api.Anon(); +console.log(anon instanceof EventEmitter, anon instanceof Anon, typeof api.Anon); +console.log(api.Bus === Bus, typeof api.Bus, Object.keys(api)); diff --git a/tests/corpus/6586-class-value-shorthand.ts b/tests/corpus/6586-class-value-shorthand.ts new file mode 100644 index 0000000000..91bb2bc182 --- /dev/null +++ b/tests/corpus/6586-class-value-shorthand.ts @@ -0,0 +1,30 @@ +// Object-literal shorthand of class values in TypeScript, including a +// class extending EventEmitter, and statically decided instanceof whose +// operands are computed: both operands still evaluate, left first. +import { EventEmitter } from "node:events"; + +const A = class { + x = 1; +}; +class B extends EventEmitter { + n = 2; +} +class C { + label(): string { + return "c"; + } +} +const registry = { A, B, C }; +console.log(registry.A.name, registry.B.name, registry.C.name); +console.log(new registry.A().x, new registry.B().n, new registry.C().label()); +const order: string[] = []; +function make(): C { + order.push("make"); + return new C(); +} +function target(): typeof C { + order.push("target"); + return registry.C; +} +console.log(make() instanceof target(), new registry.B() instanceof EventEmitter); +console.log(new registry.B() instanceof B, make() instanceof registry.C, order.join(",")); diff --git a/tests/corpus/6587-class-value-shorthand-exports/channel.cjs b/tests/corpus/6587-class-value-shorthand-exports/channel.cjs new file mode 100644 index 0000000000..77197f1e88 --- /dev/null +++ b/tests/corpus/6587-class-value-shorthand-exports/channel.cjs @@ -0,0 +1,17 @@ +const { EventEmitter } = require("node:events"); + +class Channel extends EventEmitter { + constructor(name) { + super(); + this.name = name; + } + send(message) { + this.emit("message", message, this.name); + } +} +const Plain = class { + constructor() { + this.kind = "plain"; + } +}; +module.exports = { Channel, Plain }; diff --git a/tests/corpus/6587-class-value-shorthand-exports/main.cjs b/tests/corpus/6587-class-value-shorthand-exports/main.cjs new file mode 100644 index 0000000000..18ff83b9c9 --- /dev/null +++ b/tests/corpus/6587-class-value-shorthand-exports/main.cjs @@ -0,0 +1,15 @@ +// A module exporting classes through object-literal shorthand +// (`module.exports = { Channel, Plain }`), one of them extending +// EventEmitter; the importer constructs, compares and tests instances +// through the exported values. +const { EventEmitter } = require("node:events"); +const lib = require("./channel.cjs"); +const { Channel, Plain } = lib; + +const ch = new lib.Channel("alpha"); +ch.on("message", (message, from) => console.log("message", message, from)); +ch.send("hello"); +console.log(lib.Channel.name, Channel === lib.Channel, ch instanceof Channel, ch instanceof EventEmitter); +const p = new Plain(); +console.log(p.kind, p instanceof lib.Plain, lib.Plain.name, new lib.Plain() instanceof Plain); +console.log(ch.listenerCount("message"), ch.eventNames()); diff --git a/tests/diagnostics/inheritance.ts b/tests/diagnostics/inheritance.ts index a2a09e7323..6eb91358f9 100644 --- a/tests/diagnostics/inheritance.ts +++ b/tests/diagnostics/inheritance.ts @@ -72,13 +72,6 @@ class Conditional extends Animal { const rec = { name: "r" }; console.log(rec instanceof Animal); -// ...and folds statically for standalone classes, so a computed operand -// whose effects would be dropped is rejected -class Point { - x: number = 0; -} -console.log(new Point() instanceof Point); - // Reached: unreached bodies never lower, so their rejections only exist // when something on the entry path uses them. new Conditional(true); diff --git a/tests/harness/__snapshots__/inheritance.ts.txt b/tests/harness/__snapshots__/inheritance.ts.txt index 1fc3312d7c..b3c64c6ea7 100644 --- a/tests/harness/__snapshots__/inheritance.ts.txt +++ b/tests/harness/__snapshots__/inheritance.ts.txt @@ -26,39 +26,32 @@ inheritance.ts:55:3 - error SC1090: overriding a method with a different signatu | ^~~~ 56 | return other.name; -inheritance.ts:80:13 - error SC1090: statically-decided 'instanceof' on computed operands (bind the value to a variable first) are not supported yet +inheritance.ts:82:15 - error SC2004: uses of 'GenericAbstract' inherit the blocker on its declaration - 79 | } - 80 | console.log(new Point() instanceof Point); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 81 | - -inheritance.ts:89:15 - error SC2004: uses of 'GenericAbstract' inherit the blocker on its declaration - - 88 | new Impl().usesLimit(); - 89 | const gaRef = GenericAbstract; + 81 | new Impl().usesLimit(); + 82 | const gaRef = GenericAbstract; | ^~~~~~~~~~~~~~~ - 90 | new Aliased(); + 83 | new Aliased(); hint: the declaration of 'GenericAbstract' did not compile — fix the diagnostic reported there and these sites clear with it -inheritance.ts:91:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet +inheritance.ts:84:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - 90 | new Aliased(); - 91 | new BareRepeat(); + 83 | new Aliased(); + 84 | new BareRepeat(); | ^~~~~~~~~~~~~~~~ - 92 | new RetypedRepeat(); + 85 | new RetypedRepeat(); -inheritance.ts:92:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet +inheritance.ts:85:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - 91 | new BareRepeat(); - 92 | new RetypedRepeat(); + 84 | new BareRepeat(); + 85 | new RetypedRepeat(); | ^~~~~~~~~~~~~~~~~~~ - 93 | new Bivariant(); + 86 | new Bivariant(); -inheritance.ts:93:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet +inheritance.ts:86:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - 92 | new RetypedRepeat(); - 93 | new Bivariant(); + 85 | new RetypedRepeat(); + 86 | new Bivariant(); | ^~~~~~~~~~~~~~~ - 94 | \ No newline at end of file + 87 | \ No newline at end of file From 0b3d713a3a81e0924f5a8c5f9ad1b7a8c82c53df Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:33:46 -0500 Subject: [PATCH 2/2] Throw for early reads through class shorthand and folded instanceof A class read through object-literal shorthand (`{ K }`) from code that runs before the class statement returned the class value: the reference search resolved the shorthand's name to the property's symbol, so the read carried no initialization check. Shorthand names now resolve to the binding they read, and the read throws Node's ReferenceError. A statically decided `instanceof` with a computed constructor operand skipped a variable on its left, so a read of a binding that was not yet initialized did not throw and the constructor operand ran instead. The left operand now always evaluates first. The shorthand class branch at the end of lowerShorthandValue is removed: classNameValue, which runs earlier, returns for every class. --- .../frontend/lowering/cycle-initialization.ts | 7 ++++- .../lowering/expressions/object-literals.ts | 16 ---------- .../lowering/expressions/relational.ts | 6 ++-- ...-class-value-shorthand-early-reads.ts.json | 6 ++++ .../6588-class-value-shorthand-early-reads.ts | 31 +++++++++++++++++++ ...-class-value-shorthand-early-reads.ts.json | 1 + 6 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6588-class-value-shorthand-early-reads.ts.json create mode 100644 tests/corpus/6588-class-value-shorthand-early-reads.ts create mode 100644 tests/harness/corpus-costs/6588-class-value-shorthand-early-reads.ts.json diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 4b1e74fcf0..3b865e6c1e 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -240,7 +240,12 @@ function resolvesTo( symbol: ts.Symbol, aliases: boolean, ): boolean { - let referenced = lowerer.checker.getSymbolAtLocation(node); + // A shorthand property's name (`{ K }`) carries the property's symbol; + // the binding it reads is the shorthand's value symbol. + let referenced = + ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node + ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) + : lowerer.checker.getSymbolAtLocation(node); if (aliases && referenced !== undefined && referenced.flags & ts.SymbolFlags.Alias) referenced = lowerer.checker.getAliasedSymbol(referenced); return referenced === symbol; diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 4679ee8e05..a879478154 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -3232,22 +3232,6 @@ export function lowerShorthandValue( `generic functions as values (call '${propName.text}' directly)`, ); } - // A program class name (`{ parse, TomlError }`, an imported class - // included): the class object, exactly the long form `{ C: C }`. - // Generic classes keep the identifier path's contextual pinning. - const classInfo = lowerer.classBySymbol.get(resolved); - if (classInfo && !classInfo.generic) { - const decoratedGlobal = classInfo.classDecorators?.valueGlobalId; - if (decoratedGlobal !== undefined) { - return { - kind: "varRef", - localId: decoratedGlobal, - type: { kind: "classval", className: classInfo.def.name }, - loc, - }; - } - return lowerer.classValueRef(classInfo, propName); - } } lowerer.rejectUnresolvedSymbol( symbol diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 026a1d64ee..bd29cee22e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -821,15 +821,15 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc /** A statically decided `instanceof` over computed operands: both operands * still evaluate, left then right, for their effects (a construction, a - * call, a property read), and the folded answer follows. */ + * call, a property read, a binding read that throws before the binding is + * initialized), and the folded answer follows. */ function evaluatedOperandsThen( lowerer: Lowerer, left: IrExpr, right: ts.Expression, answer: IrExpr, ): IrExpr { - const stmts: IrStmt[] = []; - if (left.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: left, loc: left.loc }); + const stmts: IrStmt[] = [{ kind: "exprStmt", expr: left, loc: left.loc }]; stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(right), loc: locOf(right) }); return { kind: "seqExpr", stmts, result: answer, type: answer.type, loc: answer.loc }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6588-class-value-shorthand-early-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6588-class-value-shorthand-early-reads.ts.json new file mode 100644 index 0000000000..d94b0b6a93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6588-class-value-shorthand-early-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6588-class-value-shorthand-early-reads.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6588-class-value-shorthand-early-reads.ts b/tests/corpus/6588-class-value-shorthand-early-reads.ts new file mode 100644 index 0000000000..d021a82656 --- /dev/null +++ b/tests/corpus/6588-class-value-shorthand-early-reads.ts @@ -0,0 +1,31 @@ +// Reads that run before their binding is initialized throw Node's +// ReferenceError: a class read through object-literal shorthand from a +// function called before the class statement, and the left operand of a +// statically decided instanceof whose constructor operand is computed. +class Plain {} +const lib = { Plain }; + +function early(): { Late: typeof Late } { + return { Late }; +} +function earlyInstance(): boolean { + return pending instanceof lib.Plain; +} + +function attempt(label: string, run: () => unknown): void { + try { + run(); + console.log(label, "returned"); + } catch (error) { + console.log(label, (error as Error).name, (error as Error).message); + } +} +attempt("shorthand", early); +attempt("instanceof", earlyInstance); + +class Late { + static tag = "late"; +} +let pending = new Plain(); +const late = early().Late; +console.log(late.tag, late === Late, earlyInstance()); diff --git a/tests/harness/corpus-costs/6588-class-value-shorthand-early-reads.ts.json b/tests/harness/corpus-costs/6588-class-value-shorthand-early-reads.ts.json new file mode 100644 index 0000000000..b38645e4a2 --- /dev/null +++ b/tests/harness/corpus-costs/6588-class-value-shorthand-early-reads.ts.json @@ -0,0 +1 @@ +{ "plain": 2110, "san": 3160 }