Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/content/limitations/builtin-function-values.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Some builtins can be stored and passed as functions:
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
Calls through these values bypass `node:test` mock timers.
Other builtin globals stored in JavaScript remain opaque identity values.
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
A class can extend `require("events")` directly.

Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { numLit } from "../../../ir/build.js";
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
import {
BOOL,
DYN,
RUNTIME_EMITTER_CLASS,
type IrExpr,
type IrStmt,
type SrcLoc,
} from "../../../ir/ir.js";
import * as ts from "../../ts7/adapter.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
import type { ClassInfo } from "../lower-classes.js";
import { classInstanceOf } from "../class-dynamic-dispatch.js";
import { isSafeToDiscard, isSafeToRepeat } from "../expressions/evaluation-safety.js";

/* EventEmitter and the stream classes as untyped constructor values, the
* bases of JavaScript constructor functions:
Expand Down Expand Up @@ -157,3 +167,57 @@ export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc)
loc,
};
}

/** `value instanceof Base` for an untyped value that may also hold a
* compiled instance: an object built by a constructor function inherits
* the untyped constructor's prototype, and an instance of a program class
* extending Base boxes as a typed reference the class test recognizes.
* With `callee`, the right-hand side is an untyped value and the class
* test applies only when it is Base's untyped constructor. */
export function builtinClassInstanceOfAny(
lowerer: Lowerer,
value: IrExpr,
kind: number,
info: ClassInfo,
loc: SrcLoc,
callee?: IrExpr,
): IrExpr {
const stmts: IrStmt[] = [];
const once = (expr: IrExpr, name: string, snapshot = false): IrExpr => {
if (!snapshot && isSafeToRepeat(expr)) return expr;
const local = lowerer.declareHiddenLocal(name, DYN);
stmts.push({ kind: "varDecl", localId: local.id, init: expr, loc });
return { kind: "varRef", localId: local.id, type: DYN, loc };
};
// The left operand evaluates first: a variable read keeps its value in
// a local when the right-hand side can assign the variable.
const tested = once(value, "%instanceValue", callee !== undefined && !isSafeToDiscard(callee));
const ctor = callee ? once(callee, "%instanceCallee") : null;
const compiled = classInstanceOf(lowerer, tested, info, loc);
const result: IrExpr = {
kind: "logical",
op: "||",
left: ctor
? { kind: "libCall", fn: "dyn.functionInstanceOf", args: [tested, ctor], type: BOOL, loc }
: builtinClassInstanceOf(tested, kind, loc),
right: ctor
? {
kind: "logical",
op: "&&",
left: {
kind: "dynScalarEq",
left: ctor,
right: builtinClassValue(kind, loc),
type: BOOL,
loc,
},
right: compiled,
type: BOOL,
loc,
}
: compiled,
type: BOOL,
loc,
};
return stmts.length ? { kind: "seqExpr", stmts, result, type: BOOL, loc } : result;
}
20 changes: 20 additions & 0 deletions packages/compiler/src/frontend/lowering/expressions/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { lowerElementCompoundParts } from "./element-access.js";
import { InternalCompilerError } from "../../../errors.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { storedClassValueType } from "../lower-classes.js";
import { builtinClassKind } from "../builtins/builtin-class-dyn.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { fenceNodeModuleMutation, lowerRequireCacheHas } from "../lower-node-module.js";
Expand Down Expand Up @@ -1486,6 +1487,25 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
}
// EventEmitter or a stream class against an untyped value in
// JavaScript: the untyped value can hold the class's untyped
// constructor (coerceToExpected), which compares by identity.
if (
dynSide.type.kind === "dyn" &&
isJsSourceFile(expr.getSourceFile()) &&
((scalarSide.type.kind === "moduleNs" && scalarSide.type.moduleId === "builtin:events") ||
(scalarSide.kind === "classRef" && builtinClassKind(scalarSide.className) !== null))
) {
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
return {
kind: "dynScalarEq",
left: scalarSide === left ? boxed : left,
right: scalarSide === right ? boxed : right,
...(negated ? { negated: true as const } : {}),
type: BOOL,
loc,
};
}
if (
dynSide.type.kind === "dyn" &&
(scalarSide.type.kind === "f64" ||
Expand Down
18 changes: 14 additions & 4 deletions packages/compiler/src/frontend/lowering/expressions/relational.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
Expand Down Expand Up @@ -401,6 +401,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
}
const left = lowerer.lowerExpr(expr.left);
if (left.type.kind === "dyn") {
// An untyped binding the checker narrowed to EventEmitter or a
// stream class (`ReleaseEmitter = EventEmitter` in JavaScript):
// the binding holds an untyped constructor, so the test is
// Function.prototype[Symbol.hasInstance] over it.
const builtinKind = targetInfo.def.runtime ? builtinClassKind(targetInfo.def.name) : null;
if (builtinKind !== null) {
const right = lowerer.lowerExpr(expr.right);
if (right.type.kind === "dyn")
return builtinClassInstanceOfAny(lowerer, left, builtinKind, targetInfo, loc, right);
}
if (targetInfo.def.runtime) {
lowerer.unsupported(
"SC1090",
Expand Down Expand Up @@ -600,11 +610,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
}
// `u instanceof EventEmitter` (or a stream class): an untyped object
// built by a constructor function inherits the untyped constructor's
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
// no untyped form, so the prototype chain answers for every value.
// prototype (builtin-class-dyn.ts), and a compiled emitter or stream
// boxes as a typed reference the class test answers for.
const builtinKind = builtinClassKind(target.def.name);
if (left.type.kind === "dyn" && builtinKind !== null) {
return builtinClassInstanceOf(left, builtinKind, loc);
return builtinClassInstanceOfAny(lowerer, left, builtinKind, target, loc);
}
if (left.type.kind === "dyn") {
lowerer.unsupported(
Expand Down
23 changes: 22 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,17 @@ import {
withUnitArm,
} from "../type-mapper.js";
import {
canonicalBuiltinModule,
cjsClassExprWholeExportOf,
isCjsJsFile,
isJsSourceFile,
isModuleExportsAccess,
locOf,
requireSpecOf,
} from "../program.js";
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
import { isAssertClassNew } from "./assert-instances.js";
import { isCjsRequireGlobal } from "./lower-node-module.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
Expand Down Expand Up @@ -979,6 +982,19 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void {
lowerer.classes.set(RUNTIME_EMITTER_CLASS, info);
}

/** `class Bus extends require("events") {}` in a CommonJS module: the
* events module is the EventEmitter class itself, so the base is the
* runtime emitter, exactly as through a `require("events")` binding. */
export function builtinRequireBaseOf(lowerer: Lowerer, node: ts.Expression): ClassInfo | null {
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isCallExpression(expr) || !isJsSourceFile(expr.getSourceFile())) return null;
const spec = requireSpecOf(expr);
if (spec === null || canonicalBuiltinModule(spec) !== "events") return null;
if (!isCjsRequireGlobal(lowerer, expr.expression)) return null;
return lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null;
}

/** The runtime-provided node:stream classes as eagerly-registered
* ClassInfos (the emitter story): mapType names `%Readable` et al the
* moment a stream type appears, so the infos must exist before any
Expand Down Expand Up @@ -2099,6 +2115,11 @@ export function collectClassShapeInner(
// callee is NOT a mixin function keeps the computed-expression
// fence below.
if (t && ts.isCallExpression(t.expression) && !t.typeArguments?.nodes) {
const requiredBase = builtinRequireBaseOf(lowerer, t.expression);
if (requiredBase) {
base = requiredBase;
continue;
}
const mixinBase = lowerer.mixinCallClassInfoOf(t.expression);
if (mixinBase) {
base = mixinBase;
Expand Down Expand Up @@ -5765,7 +5786,7 @@ export function lowerClassExpressionInfo(
)?.types?.nodes[0]?.expression;
if (
heritage &&
(ts.isCallExpression(heritage) ||
((ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage)) ||
(ts.isIdentifier(heritage) &&
(lowerer.peekLocal(heritage) ?? lowerer.globalOf(heritage)) != null))
) {
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import { recordTextCodecClass } from "../../ir/ir.js";
import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
import type { ClassInfo } from "./lower-classes.js";
import {
builtinRequireBaseOf,
collectInheritedJsFieldWrites,
collectVirtualJsMethods,
decoratorNodesOf,
Expand Down Expand Up @@ -432,7 +433,8 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
const heritage = decl.heritageClauses?.nodes.find(
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
)?.types?.nodes[0]?.expression;
if (heritage && ts.isCallExpression(heritage)) computed.add(decl);
if (heritage && ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage))
computed.add(decl);
}
// Descendants need the computed ancestor's completed layout before
// their own fields and inherited methods can be collected.
Expand Down
51 changes: 50 additions & 1 deletion packages/compiler/src/frontend/lowering/lowerer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
} from "./general-streams.js";
import { extendAssertedRecordMembers } from "./assertion-members.js";
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
import { isJsSourceFileName } from "../tsc-codes.js";
import {
builtinClassKind,
builtinClassValue,
builtinMemberClassKind,
} from "./builtins/builtin-class-dyn.js";
import {
lifetimeRegionType,
lowerLifetimeRegionCall,
Expand Down Expand Up @@ -9004,7 +9009,33 @@ export class Lowerer {
}
// A runtime-provided class value has no checked-dynamic form that
// inspects as Node's does (`[Function: EventEmitter] { ... }`).
// JavaScript sources already read EventEmitter and the stream
// classes as the runtime's untyped constructors (builtin-class-dyn.ts),
// so one of those classes turning untyped there becomes the same
// constructor. `require("events")` is the EventEmitter class itself,
// so its module binding is too (`ReleaseEmitter = EventEmitter` over
// `const EventEmitter = require("events")`).
if (
expr.type.kind === "moduleNs" &&
expr.type.moduleId === "builtin:events" &&
isJsSourceFileName(expr.loc.file)
) {
const value = builtinClassValue(0, expr.loc);
if (expr.kind === "moduleNsRef") return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr, loc: expr.loc }],
result: value,
type: DYN,
loc: expr.loc,
};
}
if (expr.type.kind === "classval" && this.classes.get(expr.type.className)?.def.runtime) {
// Only the class itself: a slot of the class's type may hold a
// program subclass, which this constructor would misidentify.
const kind = builtinClassKind(expr.type.className);
if (kind !== null && expr.kind === "classRef" && isJsSourceFileName(expr.loc.file))
return builtinClassValue(kind, expr.loc);
this.pushDiag(
unsupportedDiag(
"SC1090",
Expand Down Expand Up @@ -9084,6 +9115,24 @@ export class Lowerer {
);
const handleOk = DYN_HANDLE_KINDS.has(expected.kind);
noteUntypedStreamFlow(this, expected);
// `[EventEmitter]` in JavaScript builds an untyped array of the
// untyped constructor, which a typed array of the class cannot
// hold: no class check admits that constructor.
if (
expr.kind === "dynArrLit" &&
expected.kind === "array" &&
expected.elem.kind === "classval" &&
expr.elems.some((elem) => elem.kind === "libCall" && elem.fn === "dyn.builtinClass")
) {
this.pushDiag(
unsupportedDiag(
"SC1090",
expr.loc,
`holding runtime-provided class values ('${expected.elem.className.slice(1)}') in a typed array`,
),
);
throw new PoisonError();
}
if (
canDynCheckTo(
expected,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6571-events-module-class-value.cjs"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6572-extends-require-events.cjs"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6573-semaphore-release-emitter.cjs"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/2595-bundled-chunk-emitter/main.ts"
],
"diags": []
}
35 changes: 35 additions & 0 deletions tests/corpus/6571-events-module-class-value.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// require("events") is the EventEmitter class. Assigned to an untyped
// binding or stored in an object, it stays a constructor: construction
// through it builds an emitter with the whole listener API, and identity,
// instanceof, typeof and .name agree with the class.
var EventEmitter = require("events"), ReleaseEmitter;
ReleaseEmitter = EventEmitter;

const e = new ReleaseEmitter();
e.on("x", (v) => console.log("x", v));
e.prependListener("x", (v) => console.log("first x", v));
e.once("x", (v) => console.log("once x", v));
console.log("emit", e.emit("x", 1), e.emit("x", 2), e.emit("missing"));
console.log("count", e.listenerCount("x"), e.eventNames(), e.getMaxListeners());
console.log("instanceof", e instanceof EventEmitter, e instanceof ReleaseEmitter);
console.log("identity", ReleaseEmitter === EventEmitter, ReleaseEmitter !== EventEmitter);
console.log("typeof", typeof ReleaseEmitter, ReleaseEmitter.name);

const holder = { E: EventEmitter };
const h = new holder.E();
h.on("error", (err) => console.log("error", err.message));
h.emit("error", new Error("boom"));
console.log("holder", h instanceof EventEmitter, h instanceof holder.E, holder.E === EventEmitter);

try {
new ReleaseEmitter().emit("error", new Error("unhandled"));
} catch (err) {
console.log("caught", err.message);
}
e.removeAllListeners("x");
console.log("removed", e.listenerCount("x"), e.emit("x", 3));

// The left operand evaluates before the right-hand side assigns its
// variable.
var tested = new ReleaseEmitter();
console.log("left first", tested instanceof ((tested = 1), ReleaseEmitter), tested);
Loading
Loading