Resolve builtin classes through const aliases (#1327)

This commit is contained in:
Chris Tate
2026-10-18 15:42:15 -05:00
committed by GitHub
22 changed files with 541 additions and 141 deletions
@@ -0,0 +1,159 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { type ClassInfo } from "../lower-classes.js";
import { isJsSourceFile } from "../../program.js";
import { builtinMemberRequireDecl } from "./module-bindings.js";
import { stripTypeCasts } from "./arguments.js";
import { streamClassAliasDecl } from "../lower-stream.js";
import { zlibClassName, zlibStreamModeOf } from "./compression.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
/** A builtin class an expression names at compile time: the error
* classes, node:events EventEmitter, the node:stream classes (each a
* runtime ClassInfo), the node:zlib stream classes (by node_zlib_mode),
* and the Map and Set constructors. `name` is the class's `name`
* property. */
export type BuiltinClassRef =
| { kind: "error" | "emitter" | "stream"; info: ClassInfo; name: string }
| { kind: "zlib"; mode: number; name: string }
| { kind: "collection"; which: "Map" | "Set"; name: string };
/** The builtin class `expr` names: a direct spelling (`EventEmitter`,
* `stream.Readable`, `zlib.Gzip`, `Map`, `globalThis.TypeError`) or a
* `const` binding of one (`const E = EventEmitter`, an alias of an alias,
* an imported alias), through parentheses and type assertions. Null for
* anything else. */
export function builtinClassRefOf(lowerer: Lowerer, expr: ts.Expression): BuiltinClassRef | null {
const e = stripTypeCasts(expr);
// Checker lookups only: answering must not run the fences and deferred
// diagnostics of a lowered reference (resolveValueSymbol).
let symbol: ts.Symbol | undefined;
if (ts.isIdentifier(e)) {
symbol = lowerer.checker.getSymbolAtLocation(e);
} else if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
symbol = lowerer.checker.getSymbolAtLocation(e.name);
} else {
return null;
}
if (!symbol) return null;
const alias = builtinClassAliasOf(lowerer, symbol);
if (alias !== null) return alias;
if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
const which = stdlibGlobalNameOf(lowerer, e);
if (which === "Map" || which === "Set") return { kind: "collection", which, name: which };
const error = lowerer.builtinErrorInfoOf(symbol);
if (error) return { kind: "error", info: error, name: runtimeClassName(error) };
const emitter = lowerer.builtinEmitterInfoOf(symbol);
if (emitter) return { kind: "emitter", info: emitter, name: runtimeClassName(emitter) };
const stream = lowerer.builtinStreamInfoOf(symbol);
if (stream) return { kind: "stream", info: stream, name: runtimeClassName(stream) };
const mode = zlibStreamModeOf(lowerer, symbol);
if (mode !== null) return { kind: "zlib", mode, name: zlibClassName(mode) };
return null;
}
/** The builtin class a `const` binding names (`const E = EventEmitter`,
* `const M = Map`, `const C = zlib.Gzip`, or an alias of such a binding),
* or null. `let` and `var` bindings could be reassigned, and annotated
* bindings give the value another type, so they are not aliases. Resolved from the declaration, so uses lowered before the
* declaration statement (hoisted functions) see the same answer. */
export function builtinClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): BuiltinClassRef | null {
const cached = lowerer.builtinClassAliases.get(symbol);
if (cached !== undefined) return cached;
// An import of an exported alias names the exporting binding.
if (symbol.flags & ts.SymbolFlags.Alias) {
lowerer.builtinClassAliases.set(symbol, null);
const target = lowerer.checker.getAliasedSymbol(symbol);
const ref = target === symbol ? null : builtinClassAliasOf(lowerer, target);
lowerer.builtinClassAliases.set(symbol, ref);
return ref;
}
const init = constInitializerOf(lowerer, symbol);
if (init === null) {
lowerer.builtinClassAliases.set(symbol, null);
return null;
}
// Marked first: a cycle through other bindings answers null.
lowerer.builtinClassAliases.set(symbol, null);
const ref = builtinClassRefOf(lowerer, init);
lowerer.builtinClassAliases.set(symbol, ref);
return ref;
}
/** True when `symbol` is a builtin class alias whose declaration owns no
* runtime slot: new, instanceof, typeof, .name and === lower at each use,
* and other uses report SC1090. In JavaScript files, a binding whose
* initializer has a value (`const E = EventEmitter`, `const M =
* globalThis.Map`, both identity tokens) keeps its slot; only builtin
* module members (`zlib.Gzip`) and copies of slotless aliases own none.
* The `require("events").EventEmitter` and `stream.Writable` spellings
* keep their own declaration handling. */
export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol): boolean {
if (builtinClassAliasOf(lowerer, symbol) === null) return false;
const binding =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const decl = lowerer.checker.valueDeclarationOf(binding);
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
if (!isJsSourceFile(decl.getSourceFile())) return true;
let init = decl.initializer;
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === undefined) return true;
let source: ts.Symbol | undefined;
if (ts.isPropertyAccessExpression(init)) {
if (lowerer.builtinNamespaceModuleOf(init.expression) !== null) return true;
source = lowerer.checker.getSymbolAtLocation(init.name);
} else {
source = lowerer.checker.getSymbolAtLocation(init);
}
return source !== undefined && builtinClassAliasOwnsNoSlot(lowerer, source);
}
/** True for a `const` declaration that is a builtin class alias owning no
* runtime slot (see builtinClassAliasOwnsNoSlot): both declaration walks
* skip it. */
export function registerBuiltinClassAlias(
lowerer: Lowerer,
nameNode: ts.Node,
init: ts.Expression | undefined,
): boolean {
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol);
}
/** True when two references name the same builtin class. */
export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean {
if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode;
if (a.kind === "collection") return b.kind === "collection" && a.which === b.which;
return b.kind === a.kind && b.info === a.info;
}
/** The initializer of an unannotated `const` binding whose initializer
* can name a class (an identifier or a property access), or null. */
function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null {
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null;
const decl = lowerer.checker.valueDeclarationOf(symbol);
if (
decl === undefined ||
!ts.isVariableDeclaration(decl) ||
!ts.isIdentifier(decl.name) ||
decl.type !== undefined
) {
return null;
}
const list = decl.parent;
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
let init = decl.initializer;
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === undefined) return null;
if (ts.isIdentifier(init)) return init;
if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) return init;
return null;
}
/** Node's constructor name of a runtime class (`%TypeError` → TypeError). */
function runtimeClassName(info: ClassInfo): string {
return info.def.name.startsWith("%") ? info.def.name.slice(1) : info.def.name;
}
@@ -132,8 +132,8 @@ export function zlibStreamModeOf(
symbol: ts.Symbol | null | undefined,
): number | null {
if (!symbol) return null;
const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0);
if (aliasMode !== undefined) return aliasMode;
const alias = lowerer.builtinClassAliasOf(symbol);
if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
const target =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const mode = ZLIB_STREAM_MODES.get(target.name);
@@ -499,80 +499,12 @@ export function lowerZlibModuleCall(
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
}
/** The zlib stream class an expression names (`zlib.Gzip`, an imported
* `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */
export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null {
let e = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e));
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name));
}
return null;
}
/** The class name of a zlib stream mode (Node's constructor name). */
export function zlibClassName(mode: number): string {
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
return "Zlib";
}
/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a
* binding), null when the symbol is a const binding of something else,
* undefined when it is not a const binding. Resolved from the declaration,
* so uses lowered before the declaration statement see the same answer. */
function zlibClassAliasModeOf(
lowerer: Lowerer,
symbol: ts.Symbol,
depth: number,
): number | null | undefined {
const cached = lowerer.zlibClassAliases.get(symbol);
if (cached !== undefined) return cached;
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined;
const decl = lowerer.checker.valueDeclarationOf(symbol);
if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
return undefined;
}
// const only: a var or let binding could be reassigned
const list = decl.parent;
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
return undefined;
}
let init = decl.initializer;
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === undefined || depth > 16) return null;
let mode: number | null = null;
if (ts.isIdentifier(init)) {
const next = lowerer.checker.getSymbolAtLocation(init);
if (next !== undefined) {
const nested = zlibClassAliasModeOf(lowerer, next, depth + 1);
mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested;
}
} else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) {
mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name));
}
if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode);
return mode;
}
/** True for `const C = zlib.Gzip` (or an alias of an alias): a
* compile-time zlib class alias that owns no runtime slot, so the
* statement lowering skips it. */
export function registerZlibClassAlias(
lowerer: Lowerer,
nameNode: ts.Node,
init: ts.Expression | undefined,
): boolean {
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null;
}
/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */
export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null {
return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null;
}
/** `value instanceof zlib.Gzip` (any spelling of the class, aliases
* included). zlib streams are runtime Transforms: a Transform operand asks
* the runtime for the stream's mode, and an operand typed as one of
@@ -12,6 +12,11 @@ import { InternalCompilerError } from "../../errors.js";
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
import * as ts from "../ts7/adapter.js";
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
import {
type BuiltinClassRef,
builtinClassAliasOf,
builtinClassRefOf,
} from "./builtins/class-values.js";
import type { FnCtx, Lowerer } from "./lowerer.js";
import {
BOOL,
@@ -924,12 +929,15 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void {
* "stream" ambient module, EXCLUDING @types/node's (whose stream.Readable
* also types child stdio — under @types/node the childStream mapping
* keeps priority and the static stream classes stand down; the shipped
* fallback declarations are the supported surface). */
* fallback declarations are the supported surface). A const binding of
* one (`const R = Readable`) names the same class (builtinClassAliasOf). */
export function builtinStreamInfoOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): ClassInfo | null {
if (!symbol) return null;
const alias = builtinClassAliasOf(lowerer, symbol);
if (alias !== null) return alias.kind === "stream" ? alias.info : null;
if (!lowerer.isStdlibSymbol(symbol)) {
// A const ALIAS of a namespace member (`const Writable =
// stream.Writable` — the two-step spelling; the one-step
@@ -1284,12 +1292,16 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
* `extends EventEmitter`, `x instanceof EventEmitter`) — any import
* spelling (named/default/namespace member, CJS require) resolves to
* the ambient class. Provenance-checked like the error classes: only a
* stdlib-file declaration inside the "events" ambient module counts. */
* stdlib-file declaration inside the "events" ambient module counts. A
* const binding of it (`const E = EventEmitter`) names the same class
* (builtinClassAliasOf). */
export function builtinEmitterInfoOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): ClassInfo | null {
if (!symbol) return null;
const alias = builtinClassAliasOf(lowerer, symbol);
if (alias !== null) return alias.kind === "emitter" ? alias.info : null;
if (!lowerer.isStdlibSymbol(symbol)) {
// A const ALIAS of the emitter class member (`const EventEmitter =
// require('node:events').EventEmitter` — commander's spelling; the
@@ -1330,12 +1342,17 @@ export function builtinEmitterInfoOf(
/** The builtin error ClassInfo a VALUE symbol refers to (`new Error`,
* `extends TypeError`, `x instanceof RangeError`), or null. Provenance-
* checked: only the standard library's declarations count — a user's own
* `class Error` resolves through classBySymbol instead. */
* `class Error` resolves through classBySymbol instead. A const binding
* of one (`const E = TypeError`) names the same class
* (builtinClassAliasOf). */
export function builtinErrorInfoOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): ClassInfo | null {
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
if (!symbol) return null;
const alias = builtinClassAliasOf(lowerer, symbol);
if (alias !== null) return alias.kind === "error" ? alias.info : null;
if (!lowerer.isStdlibSymbol(symbol)) return null;
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null;
}
@@ -8443,6 +8460,12 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null;
}
/** Map, Set and the error classes construct in lowerNew's identifier
* branch, which a module member alias of one (`new mod.M()`) also takes. */
function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
return ref?.kind === "collection" || ref?.kind === "error";
}
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
const worker = lowerWorkerNew(lowerer, expr);
@@ -8737,7 +8760,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
ts.isIdentifier(expr.expression) ||
(ts.isPropertyAccessExpression(expr.expression) &&
ts.isIdentifier(expr.expression.name) &&
lowerer.isStdlibGlobal(expr.expression.expression, "globalThis"))
(lowerer.isStdlibGlobal(expr.expression.expression, "globalThis") ||
// `new mod.M()` over an exported `const M = Map` or `const E =
// TypeError`
isCollectionOrErrorClass(builtinClassRefOf(lowerer, expr.expression))))
) {
// `import C = N.C; new C()` — the alias's own source-order guards
// (a no-op for every non-import= binding).
@@ -9102,7 +9128,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) {
return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []);
}
if (symbol?.name === "Map" && lowerer.isStdlibSymbol(symbol)) {
// A const alias (`const M = Map`) constructs the same Map.
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) {
const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null;
const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression =>
ts.isArrayLiteralExpression(el) &&
@@ -9234,7 +9261,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
}
// `new Set<T>()`: construct + bulk add from supported values,
// including live collection cursors. Duplicates collapse in insertion order.
if (symbol?.name === "Set" && lowerer.isStdlibSymbol(symbol)) {
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
const tsType = lowerer.typeOf(expr);
let mapped = lowerer.mapTypeOf(tsType);
const fieldType = assignedThisFieldType(lowerer, expr);
@@ -35,10 +35,12 @@ import {
isZlibStreamMember,
lowerZlibInstanceOf,
lowerZlibStreamBytesWritten,
zlibClassAliasOf,
zlibClassModeOfExpr,
zlibClassName,
} from "./builtins/compression.js";
import {
builtinClassAliasOwnsNoSlot,
builtinClassRefOf,
sameBuiltinClass,
} from "./builtins/class-values.js";
import { constituentTypes } from "../ts7/checker.js";
import { dirname } from "node:path";
import * as posix from "node:path/posix";
@@ -616,8 +618,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (isNodeModuleValue(lowerer, expr.expression)) {
return { kind: "strLit", value: "object", type: STRING, loc };
}
// `typeof zlib.Gzip`, `typeof C` over a zlib class alias.
if (zlibClassModeOfExpr(lowerer, expr.expression) !== null) {
// `typeof EventEmitter`, `typeof zlib.Gzip`, `typeof C` over a
// builtin class alias.
if (builtinClassRefOf(lowerer, expr.expression) !== null) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
if (ts.isPropertyAccessExpression(expr.expression)) {
@@ -902,13 +905,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
`a builtin function alias as an escaping value (call '${expr.text}' directly)`,
);
}
// `const C = zlib.Gzip` owns no runtime slot: construction,
// `const E = EventEmitter` owns no runtime slot: construction,
// instanceof, typeof and .name lower at their use sites.
if (sym && zlibClassAliasOf(lowerer, sym) !== null) {
if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) {
lowerer.unsupported(
"SC1090",
expr,
`a zlib class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
`a builtin class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
);
}
}
@@ -1849,10 +1852,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const table = lowerInternTableProperty(lowerer, expr);
if (table) return table;
}
// `zlib.Gzip.name`, `C.name` over a zlib class alias.
// `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin
// class alias.
if (expr.name.text === "name" && !expr.questionDotToken) {
const mode = zlibClassModeOfExpr(lowerer, expr.expression);
if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc };
const ref = builtinClassRefOf(lowerer, expr.expression);
if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc };
}
// Expando statics may still be absent even when JS inference sees
// the eventual assignment's scalar type. Preserve the checked value.
@@ -10266,17 +10270,18 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
lowerUnionTypeofTest(lowerer, expr, loc);
if (test) return test;
}
// `C === zlib.Gzip` between two zlib class references: the classes
// are compile-time values, so the answer folds.
// `C === EventEmitter` between two builtin class references: the
// classes are compile-time values, so the answer folds.
if (
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
) {
const leftClass = zlibClassModeOfExpr(lowerer, expr.left);
const rightClass = leftClass === null ? null : zlibClassModeOfExpr(lowerer, expr.right);
const leftClass = builtinClassRefOf(lowerer, expr.left);
const rightClass = leftClass === null ? null : builtinClassRefOf(lowerer, expr.right);
if (leftClass !== null && rightClass !== null) {
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
return { kind: "boolLit", value: (leftClass === rightClass) !== negated, type: BOOL, loc };
const same = sameBuiltinClass(leftClass, rightClass);
return { kind: "boolLit", value: same !== negated, type: BOOL, loc };
}
}
@@ -12352,11 +12357,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
if (sockTest !== null) return sockTest;
const zlibMode = zlibClassModeOfExpr(lowerer, expr.right);
if (zlibMode !== null) return lowerZlibInstanceOf(lowerer, expr, zlibMode, loc);
const rightClass = builtinClassRefOf(lowerer, expr.right);
if (rightClass?.kind === "zlib") return lowerZlibInstanceOf(lowerer, expr, rightClass.mode, loc);
if (lowerer.isStdlibGlobal(expr.right, "Set") || lowerer.isStdlibGlobal(expr.right, "Map")) {
const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set";
if (rightClass?.kind === "collection") {
const collection = rightClass.which === "Map" ? "map" : "set";
const value = lowerer.lowerExpr(expr.left);
if (value.type.kind === "union") {
const union = lowerer.unions.get(value.type.unionId);
@@ -80,7 +80,7 @@ import {
createRequireProgramModuleDecl,
} from "./builtins/module-bindings.js";
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { registerZlibClassAlias } from "./builtins/compression.js";
import { registerBuiltinClassAlias } from "./builtins/class-values.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import {
bindingContextualGenericFnNodeOf,
@@ -1701,7 +1701,7 @@ export function collectGlobals(
// resolution and value reads reuse the source's interned closure.
if (isConst && registerOverloadedCallableAlias(lowerer, decl)) continue;
if (isConst && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) continue;
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) continue;
if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) continue;
// `const f = (x) => ...` at file scope of an npm-static JS file
// whose params are implicit-any: the implicit-monomorphization
// twin of the generic binding above — no global exists; calls and
@@ -148,7 +148,7 @@ import {
} from "./builtins/module-bindings.js";
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
import { registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { registerZlibClassAlias } from "./builtins/compression.js";
import { builtinClassAliasOwnsNoSlot, registerBuiltinClassAlias } from "./builtins/class-values.js";
import { lowerEnumDeclaration } from "./lower-enums.js";
import {
abstractPropertyDeclOf,
@@ -853,6 +853,9 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b
if (!decl.initializer) return false;
const flags = ts.getCombinedNodeFlags(decl);
if ((flags & ts.NodeFlags.BlockScoped) === 0 || (flags & ts.NodeFlags.Using) !== 0) return false;
// `const E = EventEmitter` has no storage to box: uses resolve at
// compile time (builtinClassAliasOf).
if (builtinClassAliasOwnsNoSlot(lowerer, symbol)) return false;
// MODULE-scope bindings are pre-registered globals (collectGlobals):
// references resolve through globalOf after the local search fails, so
// a TDZ box here would SHADOW the global and never fill (the top-level
@@ -4429,7 +4432,7 @@ export function lowerVarDecl(
// reads reuse its interned closure identity.
if (!isLet && registerOverloadedCallableAlias(lowerer, decl)) return null;
if (!isLet && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) return null;
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) return null;
if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) return null;
// `const knownBy = (cmd) => ...` — an IMPLICIT-ANY function-value
// binding in npm-static JS (function-scope bindings register here, in
@@ -261,6 +261,7 @@ import {
stdlibGlobalMember,
nodeTypesOnlySymbol,
} from "./surfaces.js";
import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js";
import {
type FileParts,
splitFiles,
@@ -1858,11 +1859,11 @@ export class Lowerer {
* typeParamBindings — the checker has no `T` to substitute, so the
* binding rides the node-type accessor instead of mapType. */
implicitParamTypes: Map<ts.Symbol, ts.Type> | null = null;
/** Immutable bindings that name a zlib stream class (`const C =
* zlib.Gzip`), keyed by the binding's symbol: the class's
* node_zlib_mode. No runtime slot exists: new, typeof, .name and ===
* resolve at compile time, and instanceof checks the stream's mode. */
readonly zlibClassAliases = new Map<ts.Symbol, number>();
/** The builtin class each binding names (`const E = EventEmitter`,
* `const C = zlib.Gzip`), keyed by the binding's symbol; null for
* bindings that name none (builtinClassAliasOf). new, instanceof,
* typeof, .name and === resolve at compile time. */
readonly builtinClassAliases = new Map<ts.Symbol, BuiltinClassRef | null>();
/** Whether some net.Socket in this program may get an encoding: a
* setEncoding call whose receiver is a socket or an untyped value.
* Computed once on first use (null until then). */
@@ -3073,6 +3074,10 @@ export class Lowerer {
return builtinStreamInfoOf(this, symbol);
}
builtinClassAliasOf(symbol: ts.Symbol): BuiltinClassRef | null {
return builtinClassAliasOf(this, symbol);
}
/** Program-wide qualified name for a top-level declaration. */
qualify(sf: ts.SourceFile, name: string): string {
return `${this.fileTag.get(sf) ?? ""}${name}`;
@@ -3207,7 +3207,9 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined):
* `global.process`) — `declare var` globals ARE properties of
* `typeof globalThis`, same symbol either way;
* - a local alias binding (`const process = globalThis.process`, the
* tamper-guard prologue) registered in stdlibGlobalAliases. */
* tamper-guard prologue) registered in stdlibGlobalAliases;
* - a const binding of the Map or Set constructor (`const M = Map`,
* builtinClassAliasOf). */
export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null {
if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression);
if (ts.isIdentifier(expr)) {
@@ -3225,6 +3227,8 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
if (!symbol) return null;
const alias = lowerer.stdlibGlobalAliases.get(symbol);
if (alias !== undefined) return alias;
const classAlias = lowerer.builtinClassAliasOf(symbol);
if (classAlias?.kind === "collection") return classAlias.which;
const decl = lowerer.checker.declarationsOf(symbol)[0];
if (decl !== undefined && ts.isImportClause(decl) && decl.name !== undefined) {
const importDecl = decl.parent;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5976-builtin-class-aliases.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5977-builtin-class-aliases.cjs"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/5978-builtin-class-alias-modules/classes.ts",
"<repo>/tests/corpus/5978-builtin-class-alias-modules/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/builtin-class-alias-escape.ts"
],
"diags": []
}
+109
View File
@@ -0,0 +1,109 @@
// Builtin classes through const aliases: EventEmitter, the stream classes,
// the error classes, Map and Set construct, extend and test instanceof
// through an alias, and typeof, .name and === answer as in Node for the
// aliases and the direct spellings.
import { EventEmitter } from "node:events";
import * as events from "node:events";
import { PassThrough, Readable, Transform } from "node:stream";
import * as stream from "node:stream";
import { AssertionError } from "node:assert";
import * as assert from "node:assert";
function lateEmitter(): boolean {
return new LateEmitter() instanceof EventEmitter && typeof LateMap === "function";
}
function localHoist(): string {
function inner(): string {
return `${typeof LocalMap} ${LocalMap.name} ${new LocalMap<string, number>().size}`;
}
const LocalMap = Map;
return inner();
}
const Emitter = EventEmitter;
const NsEmitter = events.EventEmitter;
const Again = Emitter;
const LateEmitter = events.EventEmitter;
const LateMap = Map;
const e = new Emitter();
e.on("ping", (n: number) => console.log("ping", n));
e.emit("ping", 1);
console.log("emitter", e instanceof Emitter, e instanceof NsEmitter, e instanceof Again);
console.log("hoisted use", lateEmitter(), localHoist());
class Bus extends Again {
send(text: string): void {
this.emit("text", text);
}
}
const bus = new Bus();
bus.on("text", (text: string) => console.log("bus", text));
bus.send("hello");
console.log("subclass", bus instanceof Emitter, bus instanceof Bus);
const Pass = PassThrough;
const R = Readable;
const T = stream.Transform;
const pass = new Pass();
console.log("stream", pass instanceof Pass, pass instanceof R, pass instanceof T);
console.log("readable", new R({ read() {} }) instanceof Pass, e instanceof R);
class Upper extends T {
override _transform(
chunk: Buffer,
_encoding: BufferEncoding,
callback: (error: Error | null, data?: string) => void,
): void {
callback(null, chunk.toString().toUpperCase());
}
}
const upper = new Upper();
upper.on("data", (chunk: Buffer) => console.log("upper", chunk.toString()));
upper.end("quiet");
const TE = TypeError;
const RE = RangeError;
class LimitError extends RE {
constructor(limit: number) {
super(`over ${limit}`);
this.name = "LimitError";
}
}
try {
throw new TE("bad input");
} catch (err) {
console.log("error", err instanceof TE, err instanceof RE, err instanceof Error, (err as Error).message);
}
try {
throw new LimitError(3);
} catch (err) {
console.log("subclass error", err instanceof RE, err instanceof LimitError, (err as Error).message);
}
const AE = AssertionError;
try {
assert.strictEqual(1, 2);
} catch (err) {
console.log("assertion", err instanceof AE, err instanceof TE);
}
const viaNamespace = new assert.AssertionError({ message: "custom" });
console.log("namespace error", viaNamespace.message, viaNamespace instanceof AE);
const M = Map;
const S = Set;
const m = new M<string, number>([
["a", 1],
["b", 2],
]);
m.set("c", 3);
const s = new S([1, 2, 2, 3]);
console.log("collections", m.get("b"), m.size, s.size, m instanceof M, s instanceof S, s instanceof M);
const groups = M.groupBy([1, 2, 3, 4, 5], (n: number) => (n % 2 === 0 ? "even" : "odd"));
console.log("groupBy", groups.get("even"), groups.get("odd"));
console.log("typeof", typeof Emitter, typeof R, typeof TE, typeof M, typeof EventEmitter, typeof Readable);
console.log("name", Emitter.name, NsEmitter.name, Pass.name, T.name, TE.name, AE.name, M.name, S.name);
console.log("direct name", EventEmitter.name, stream.Duplex.name, Error.name, RangeError.name, Map.name, Set.name);
console.log("identity", Emitter === EventEmitter, Again === NsEmitter, T === Transform, R === stream.Readable);
console.log("other class", (Pass as unknown) === (R as unknown), (M as unknown) !== (S as unknown), (TE as unknown) === (RE as unknown));
@@ -0,0 +1,45 @@
// Builtin classes through const aliases in a CommonJS file. A copy of a
// name (or of a globalThis property) keeps its value, so it can be stored
// and compared; construction, instanceof, typeof and .name resolve to the
// class. Builtin module members construct through the alias.
const { EventEmitter } = require("node:events");
const events = require("node:events");
const zlib = require("node:zlib");
const { PassThrough } = require("node:stream");
const Emitter = EventEmitter;
const NsEmitter = events.EventEmitter;
const Gz = zlib.Gzip;
const Gz2 = Gz;
const M = Map;
const GlobalSet = globalThis.Set;
const Pass = PassThrough;
const TE = TypeError;
const e = new Emitter();
e.on("ping", (n) => console.log("ping", n));
e.emit("ping", 2);
console.log("emitter", e instanceof Emitter, e instanceof NsEmitter, e instanceof EventEmitter);
const m = new M();
m.set("k", 1);
const s = new GlobalSet(["a", "b", "a"]);
console.log("collections", m.get("k"), s.size, m instanceof M, s instanceof GlobalSet, m instanceof GlobalSet);
const gz = new Gz2();
console.log("zlib", gz instanceof Gz, gz instanceof zlib.Gunzip, Gz2 === zlib.Gzip);
const pass = new Pass();
console.log("stream", pass instanceof Pass, pass instanceof EventEmitter);
try {
throw new TE("typed");
} catch (err) {
console.log("error", err instanceof TE, err.message);
}
const stored = [Emitter, M, GlobalSet, Pass];
console.log("stored", stored.length, stored[0] === EventEmitter, stored[1] === Map, stored[2] === Set);
console.log("typeof", typeof Emitter, typeof NsEmitter, typeof Gz2, typeof M, typeof GlobalSet, typeof TE);
console.log("name", Emitter.name, NsEmitter.name, Gz2.name, M.name, GlobalSet.name, Pass.name, TE.name);
console.log("identity", Emitter === NsEmitter, M === Map, GlobalSet === Set, M === GlobalSet);
@@ -0,0 +1,8 @@
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
export const Emitter = EventEmitter;
export const Pass = PassThrough;
export const Dict = Map;
export const Bag = Set;
export const Failure = TypeError;
@@ -0,0 +1,33 @@
// Builtin class aliases exported from one module and used through named
// and namespace imports: construction, instanceof, typeof, .name and ===
// resolve to the builtin class in the importing module.
import { EventEmitter } from "node:events";
import { Emitter, Pass, Dict, Bag, Failure } from "./classes.ts";
import * as classes from "./classes.ts";
const e = new Emitter();
e.on("tick", (n: number) => console.log("tick", n));
e.emit("tick", 7);
console.log("emitter", e instanceof Emitter, e instanceof classes.Emitter, e instanceof EventEmitter);
const viaNs = new classes.Emitter();
console.log("namespace emitter", viaNs instanceof Emitter, Emitter === classes.Emitter);
const pass = new classes.Pass();
console.log("stream", pass instanceof Pass, pass instanceof classes.Emitter);
const dict = new Dict<string, number>([["k", 1]]);
const viaNsDict = new classes.Dict<string, string>();
viaNsDict.set("a", "b");
const bag = new classes.Bag<number>([1, 1, 2]);
console.log("collections", dict.get("k"), viaNsDict.get("a"), bag.size, dict instanceof classes.Dict, bag instanceof Bag);
try {
throw new classes.Failure("wrong type");
} catch (err) {
console.log("error", err instanceof Failure, err instanceof classes.Failure, (err as Error).message);
}
console.log("typeof", typeof Emitter, typeof classes.Pass, typeof Dict, typeof classes.Bag, typeof Failure);
console.log("name", Emitter.name, classes.Pass.name, Dict.name, classes.Bag.name, Failure.name);
console.log("identity", Dict === Map, classes.Bag === Set, Failure === TypeError, (Dict as unknown) === (Bag as unknown));
@@ -0,0 +1,15 @@
// A builtin class alias owns no runtime value in a TypeScript file:
// construction, instanceof, typeof and .name compile, other uses report
// at the use. A let binding of a class is not an alias.
import { EventEmitter } from "node:events";
const Emitter = EventEmitter;
const kinds = [Emitter];
console.log(kinds.length);
const M = Map;
const boxed: unknown = M;
console.log(boxed !== undefined);
let Mutable = EventEmitter;
console.log(typeof Mutable);
+3 -1
View File
@@ -18,8 +18,10 @@ class Unrelated {
static tag = "unrelated";
}
// Builtin classes as values: their construction is libCall-shaped.
// Builtin classes as values: a const alias resolves at compile time, and
// an array of builtin classes has no element type.
const E = Error;
const errorClasses = [E];
// Classes extending builtin bases as values (the error-message completion
// rule has no thunk ABI).
@@ -0,0 +1,22 @@
builtin-class-alias-escape.ts:7:16 - error SC1090: a builtin class alias as an escaping value (use 'Emitter' with new, instanceof, typeof or .name) is not supported yet
6 | const Emitter = EventEmitter;
7 | const kinds = [Emitter];
| ^~~~~~~
8 | console.log(kinds.length);
builtin-class-alias-escape.ts:11:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet
10 | const M = Map;
11 | const boxed: unknown = M;
| ^
12 | console.log(boxed !== undefined);
builtin-class-alias-escape.ts:14:15 - error SC2020: 'events.EventEmitter' is part of the standard library types but has no scriptc lowering yet
13 |
14 | let Mutable = EventEmitter;
| ^~~~~~~~~~~~
15 | console.log(typeof Mutable);
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
+32 -32
View File
@@ -1,52 +1,52 @@
class-values.ts:22:7 - error SC2020: 'ErrorConstructor' is part of the standard library types but has no scriptc lowering yet
class-values.ts:24:7 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile
21 | // Builtin classes as values: their construction is libCall-shaped.
22 | const E = Error;
| ^
23 |
23 | const E = Error;
24 | const errorClasses = [E];
| ^~~~~~~~~~~~
25 |
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
class-values.ts:22:11 - error SC2020: 'Error' is part of the standard library types but has no scriptc lowering yet
class-values.ts:24:22 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile
21 | // Builtin classes as values: their construction is libCall-shaped.
22 | const E = Error;
| ^~~~~
23 |
23 | const E = Error;
24 | const errorClasses = [E];
| ^~~
25 |
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
class-values.ts:44:1 - error SC1090: assigning the static 'tag' through a class value (JS creates an OWN property on the runtime class — assign through the declaring class's name, which subclasses here) is not supported yet
class-values.ts:46:1 - error SC1090: assigning the static 'tag' through a class value (JS creates an OWN property on the runtime class — assign through the declaring class's name, which subclasses here) is not supported yet
43 | let someClass: typeof Base = Base;
44 | someClass.tag = "write";
45 | let someClass: typeof Base = Base;
46 | someClass.tag = "write";
| ^~~~~~~~~~~~~
45 |
47 |
class-values.ts:48:13 - error SC1090: reading the static member 'tag' through a class value (a subclass of 'Base' redeclares it, so the runtime class decides which declaration answers) is not supported yet
class-values.ts:50:13 - error SC1090: reading the static member 'tag' through a class value (a subclass of 'Base' redeclares it, so the runtime class decides which declaration answers) is not supported yet
47 | // runtime class decides which declaration answers.
48 | console.log(someClass.tag);
49 | // runtime class decides which declaration answers.
50 | console.log(someClass.tag);
| ^~~~~~~~~~~~~
49 |
51 |
class-values.ts:52:10 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
class-values.ts:54:10 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
51 | function make(): unknown {
52 | return class { static value = 1; };
53 | function make(): unknown {
54 | return class { static value = 1; };
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
53 | }
55 | }
class-values.ts:59:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet
class-values.ts:61:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet
58 | // evaluates.
59 | const pair = [1, class WithStatic { static x = 1; }] as const;
60 | // evaluates.
61 | const pair = [1, class WithStatic { static x = 1; }] as const;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60 |
62 |
class-values.ts:79:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet
class-values.ts:81:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet
78 | class OtherThis extends UsesThis {}
79 | function self(value: typeof UsesThis): string { return value.self(); }
80 | class OtherThis extends UsesThis {}
81 | function self(value: typeof UsesThis): string { return value.self(); }
| ^~~~~~~~~~
80 | console.log(self(OtherThis));
82 | console.log(self(OtherThis));
@@ -1,4 +1,4 @@
zlib-class-alias-escape.ts:7:18 - error SC1090: a zlib class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet
zlib-class-alias-escape.ts:7:18 - error SC1090: a builtin class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet
6 | const Gz = zlib.Gzip;
7 | const classes = [Gz];
+6
View File
@@ -2190,6 +2190,9 @@
"5973-large-closure-environment.ts": 4499,
"5974-typeof-module-exports.cjs": 2000,
"5975-zlib-class-values.ts": 2800,
"5976-builtin-class-aliases.ts": 3100,
"5977-builtin-class-aliases.cjs": 2950,
"5978-builtin-class-alias-modules/main.ts": 2750,
"5990-worker-uncloneable.mjs": 3283,
"5991-worker-async-dispose.mjs": 5699,
"5992-worker-thread-name-lifetime.mjs": 2687,
@@ -5226,6 +5229,9 @@
"5973-large-closure-environment.ts": 5145,
"5974-typeof-module-exports.cjs": 2500,
"5975-zlib-class-values.ts": 4200,
"5976-builtin-class-aliases.ts": 5450,
"5977-builtin-class-aliases.cjs": 4300,
"5978-builtin-class-alias-modules/main.ts": 4450,
"5990-worker-uncloneable.mjs": 22432,
"5991-worker-async-dispose.mjs": 25768,
"5992-worker-thread-name-lifetime.mjs": 28932,