Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
75 changes: 44 additions & 31 deletions packages/compiler/src/frontend/lowering/expressions/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,37 +861,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<string> = 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.
Expand Down Expand Up @@ -948,6 +919,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<string> = 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, "/"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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";
import { builtinGlobalValue } from "./property-access.js";

/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
Expand Down Expand Up @@ -3284,6 +3285,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;
Comment thread
vercel[bot] marked this conversation as resolved.
const sig = lowerer.fnSigsBySymbol.get(resolved);
const decl = resolved.declarations[0];
if (
Expand Down Expand Up @@ -3312,22 +3317,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);
}
// A standard-library global (`{ Date }`): the value the long form
// `{ Date: Date }` reads.
if (lowerer.isStdlibSymbol(resolved)) return lowerer.lowerExpr(propName);
Expand Down
43 changes: 21 additions & 22 deletions packages/compiler/src/frontend/lowering/expressions/relational.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,22 +471,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))) {
Expand Down Expand Up @@ -827,19 +819,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, 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[] = [{ 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6585-class-value-shorthand.cjs"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6586-class-value-shorthand.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6587-class-value-shorthand-exports/channel.cjs",
"<repo>/tests/corpus/6587-class-value-shorthand-exports/main.cjs"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6588-class-value-shorthand-early-reads.ts"
],
"diags": []
}
30 changes: 30 additions & 0 deletions tests/corpus/6585-class-value-shorthand.cjs
Original file line number Diff line number Diff line change
@@ -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));
30 changes: 30 additions & 0 deletions tests/corpus/6586-class-value-shorthand.ts
Original file line number Diff line number Diff line change
@@ -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(","));
17 changes: 17 additions & 0 deletions tests/corpus/6587-class-value-shorthand-exports/channel.cjs
Original file line number Diff line number Diff line change
@@ -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 };
15 changes: 15 additions & 0 deletions tests/corpus/6587-class-value-shorthand-exports/main.cjs
Original file line number Diff line number Diff line change
@@ -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());
31 changes: 31 additions & 0 deletions tests/corpus/6588-class-value-shorthand-early-reads.ts
Original file line number Diff line number Diff line change
@@ -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());
7 changes: 0 additions & 7 deletions tests/diagnostics/inheritance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading