mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Let constructor functions inherit from EventEmitter and the stream classes (#1512)
This commit is contained in:
@@ -16,7 +16,11 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
|
||||
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
|
||||
|
||||
`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments.
|
||||
Class constructors passed to `util.inherits` or `util.deprecate` are refused.
|
||||
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
|
||||
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
|
||||
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
|
||||
Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
|
||||
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
|
||||
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
|
||||
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
|
||||
In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused.
|
||||
|
||||
@@ -32,6 +32,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
bigint: features.bigint,
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
dynBuiltinClass: features.dynBuiltinClass,
|
||||
parseArgs: features.parseArgs,
|
||||
stream: features.stream,
|
||||
net: features.net,
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function compilePackedLibrary(
|
||||
bigint: options.bigint ?? false,
|
||||
searchParams: options.searchParams ?? false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -1437,6 +1437,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.classPrototype": "scr_dyn_class_prototype",
|
||||
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
|
||||
"dyn.classInherit": "scr_dyn_class_inherit",
|
||||
"dyn.builtinClass": "scr_dyn_builtin_class",
|
||||
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
|
||||
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
|
||||
"dyn.classSuper": "scr_dyn_class_super",
|
||||
"dyn.assignPrototype": "scr_dyn_assign_prototype",
|
||||
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface NativeLinkFeatures {
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
dynBuiltinClass: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
|
||||
@@ -152,6 +152,11 @@ export interface CcOptions {
|
||||
* line (escape-only programs ride the always-linked component encoder
|
||||
* and never flip this). */
|
||||
qs?: boolean;
|
||||
/** The program uses EventEmitter or a stream class as an untyped
|
||||
* constructor value (a constructor function's base): compiles
|
||||
* scr_dyn_builtin_class.c, which also references the inspection,
|
||||
* dyn-invoke and symbol units. */
|
||||
dynBuiltinClass?: boolean;
|
||||
/** The program uses native util parsing, comparison, or styling. Optional
|
||||
* utility units also require their symbol, inspection, and warning features. */
|
||||
parseArgs?: boolean;
|
||||
|
||||
@@ -236,6 +236,15 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
|
||||
"scr_util_callable.c",
|
||||
),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynBuiltinClass &&
|
||||
u.opts.inspect &&
|
||||
u.opts.dynInvoke &&
|
||||
u.opts.symbol &&
|
||||
(u.opts.emitter || u.opts.workers),
|
||||
"scr_dyn_builtin_class.c",
|
||||
),
|
||||
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
|
||||
// The zlib stream classes reference both the zlib engine and the
|
||||
// stream unit.
|
||||
|
||||
@@ -107,6 +107,7 @@ const features = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function warmNativeCaches(
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
dynBuiltinClass: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
@@ -190,6 +191,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"bigint",
|
||||
"searchParams",
|
||||
"qs",
|
||||
"dynBuiltinClass",
|
||||
"parseArgs",
|
||||
"stream",
|
||||
"net",
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { numLit } from "../../../ir/build.js";
|
||||
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
|
||||
/* EventEmitter and the stream classes as untyped constructor values, the
|
||||
* bases of JavaScript constructor functions:
|
||||
*
|
||||
* function Bus() { EventEmitter.call(this); }
|
||||
* util.inherits(Bus, EventEmitter);
|
||||
*
|
||||
* A compiled `class Bus extends EventEmitter` gets a native emitter prefix,
|
||||
* but `new Bus()` on a constructor function builds an untyped object, so
|
||||
* the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a
|
||||
* function whose prototype carries Node's EventEmitter methods over the
|
||||
* object's own `_events` table. The stream classes have prototype chains
|
||||
* for util.inherits and instanceof; constructing over them refuses where
|
||||
* it runs. These values appear only where a constructor function uses its
|
||||
* base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`,
|
||||
* `Base.prototype` and `instanceof Base` on an untyped value. */
|
||||
|
||||
/** scr_dyn_builtin_class's kind for a runtime class, or null. */
|
||||
export function builtinClassKind(className: string): number | null {
|
||||
switch (className) {
|
||||
case RUNTIME_EMITTER_CLASS:
|
||||
return 0;
|
||||
case "%Readable":
|
||||
return 2;
|
||||
case "%Writable":
|
||||
return 3;
|
||||
case "%Duplex":
|
||||
return 4;
|
||||
case "%Transform":
|
||||
return 5;
|
||||
case "%PassThrough":
|
||||
return 6;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The classes by builtin module member. JavaScript sources bind
|
||||
* `require("events").EventEmitter` as a module member rather than a class
|
||||
* value, and `require("stream")` is the legacy Stream constructor. */
|
||||
const MODULE_CLASS_KINDS: Readonly<Record<string, Readonly<Record<string, number>>>> = {
|
||||
events: { EventEmitter: 0, default: 0 },
|
||||
stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 },
|
||||
};
|
||||
|
||||
/** The kind of a builtin module member that is one of those classes. */
|
||||
export function builtinMemberClassKind(module: string, member: string): number | null {
|
||||
return MODULE_CLASS_KINDS[module]?.[member] ?? null;
|
||||
}
|
||||
|
||||
function unparenthesized(node: ts.Expression): ts.Expression {
|
||||
let current = node;
|
||||
while (ts.isParenthesizedExpression(current)) current = current.expression;
|
||||
return current;
|
||||
}
|
||||
|
||||
/** The kind of a node that names one of those classes: a class value, a
|
||||
* builtin module member, or the events and stream modules themselves. */
|
||||
export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
|
||||
const expr = unparenthesized(node);
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (mapped?.kind === "classval") return builtinClassKind(mapped.className);
|
||||
const member = ts.isIdentifier(expr)
|
||||
? lowerer.builtinImportOf(expr)
|
||||
: ts.isPropertyAccessExpression(expr)
|
||||
? lowerer.builtinMemberOf(expr)
|
||||
: null;
|
||||
if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null;
|
||||
const module = lowerer.builtinNamespaceModuleOf(expr);
|
||||
return module === "events" ? 0 : module === "stream" ? 1 : null;
|
||||
}
|
||||
|
||||
/** Whether evaluating the node can be skipped: a binding or a member of
|
||||
* one, or of a builtin require. */
|
||||
function referenceOnly(node: ts.Expression): boolean {
|
||||
const expr = unparenthesized(node);
|
||||
if (ts.isIdentifier(expr)) return true;
|
||||
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
|
||||
const base = unparenthesized(expr.expression);
|
||||
return (
|
||||
ts.isIdentifier(base) ||
|
||||
(ts.isCallExpression(base) &&
|
||||
ts.isIdentifier(base.expression) &&
|
||||
base.expression.text === "require" &&
|
||||
base.arguments.nodes.length === 1 &&
|
||||
ts.isStringLiteral(base.arguments.nodes[0]!))
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The untyped constructor named by a node, or null when it names none of
|
||||
* those classes. */
|
||||
export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
const kind = builtinClassKindOf(lowerer, node);
|
||||
if (kind === null) return null;
|
||||
const loc = locOf(node);
|
||||
const value = builtinClassValue(kind, loc);
|
||||
if (referenceOnly(node)) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }],
|
||||
result: value,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
|
||||
* values: Function.prototype.call/apply over the untyped constructor. */
|
||||
export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
|
||||
const method = callee.name.text;
|
||||
if (method !== "call" && method !== "apply") return null;
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
|
||||
const receiver = builtinClassDynValueOf(lowerer, callee.expression);
|
||||
if (receiver === null) return null;
|
||||
const loc = locOf(call);
|
||||
return {
|
||||
kind: "dynInvoke",
|
||||
recv: receiver,
|
||||
method,
|
||||
calleeName: callee.getText(),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `value instanceof Base` for an untyped value. */
|
||||
export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.builtinInstanceOf",
|
||||
args: [value, numLit(kind, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -450,6 +451,19 @@ export function lowerClassPrototypeData(
|
||||
exactClassOfReceiver(lowerer, expr.expression) ??
|
||||
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
|
||||
if (!info) return null;
|
||||
// EventEmitter.prototype and the stream prototypes: the untyped
|
||||
// constructor's prototype object (builtin-class-dyn.ts).
|
||||
const builtinKind = builtinClassKind(info.def.name);
|
||||
if (builtinKind !== null) {
|
||||
const loc = locOf(expr);
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: builtinClassValue(builtinKind, loc),
|
||||
key: { kind: "strLit", value: "prototype", type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const parent = expr.parent;
|
||||
const descriptorKey =
|
||||
ts.isCallExpression(parent) &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
|
||||
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
|
||||
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import * as posix from "node:path/posix";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
@@ -539,6 +540,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// the same identity-token story as stdlib globals above (the
|
||||
// harness adds worker_threads.Worker to its identity Set).
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
// EventEmitter and the stream classes are the runtime's untyped
|
||||
// constructors, so constructor functions can inherit from them.
|
||||
const classKind = builtinMemberClassKind(bi.module, bi.member);
|
||||
if (classKind !== null) return builtinClassValue(classKind, loc);
|
||||
// A function member is a function value that fences when called.
|
||||
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
|
||||
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
|
||||
|
||||
@@ -1,5 +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 {
|
||||
checkedPrivateIn,
|
||||
localPrivateBrandClass,
|
||||
@@ -473,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return checkedClassInstanceOf(lowerer, value, constructor, loc);
|
||||
}
|
||||
// `this instanceof Glob` in a JavaScript constructor function: the
|
||||
// function's prototype against the value's prototype chain.
|
||||
if (
|
||||
constructor?.type.kind === "func" &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
lowerer.dynConvertible(constructor.type)
|
||||
) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.functionInstanceOf",
|
||||
args: [value, { kind: "dynFrom", value: constructor, type: DYN, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.right,
|
||||
@@ -581,6 +598,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
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.
|
||||
const builtinKind = builtinClassKind(target.def.name);
|
||||
if (left.type.kind === "dyn" && builtinKind !== null) {
|
||||
return builtinClassInstanceOf(left, builtinKind, loc);
|
||||
}
|
||||
if (left.type.kind === "dyn") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
|
||||
import { refuseModuleMemberThis } from "./module-member-this.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
|
||||
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
|
||||
@@ -266,6 +267,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
if (object) return object;
|
||||
}
|
||||
|
||||
// `EventEmitter.call(this)` in a constructor function: the untyped
|
||||
// EventEmitter constructor initializes the receiver.
|
||||
const builtinClassCall = lowerBuiltinClassCall(lowerer, expr);
|
||||
if (builtinClassCall) return builtinClassCall;
|
||||
|
||||
// require() of a JSON document: the shared parsed object.
|
||||
const json = lowerJsonRequireCall(lowerer, expr);
|
||||
if (json) return json;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
@@ -2609,8 +2610,15 @@ function lowerUtilCallableCall(
|
||||
const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2;
|
||||
// Program classes keep static prototypes: there is no function object
|
||||
// whose prototype chain the runtime could relink or wrap.
|
||||
// EventEmitter and the stream classes are the exception as superCtor:
|
||||
// the runtime has untyped constructors for them (builtin-class-dyn.ts).
|
||||
const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0;
|
||||
for (const arg of expr.arguments.nodes.slice(0, classArgs)) {
|
||||
const builtinBase =
|
||||
member === "inherits" && expr.arguments.nodes[1] !== undefined
|
||||
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
|
||||
: null;
|
||||
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
|
||||
if (index === 1 && builtinBase !== null) continue;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
|
||||
lowerer.noLowering(
|
||||
`util.${member} over a class constructor`,
|
||||
@@ -2629,9 +2637,12 @@ function lowerUtilCallableCall(
|
||||
const stmts: IrStmt[] = [];
|
||||
const args: IrExpr[] = [];
|
||||
for (let i = 0; i < arity; i++) {
|
||||
const input: IrExpr = expr.arguments.nodes[i]
|
||||
? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN)
|
||||
: dynUndefinedExpr(loc);
|
||||
const node = expr.arguments.nodes[i];
|
||||
const input: IrExpr = !node
|
||||
? dynUndefinedExpr(loc)
|
||||
: i === 1 && builtinBase !== null
|
||||
? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN))
|
||||
: lowerer.lowerExprExpecting(node, DYN);
|
||||
const saved = lowerer.declareHiddenLocal("%utilInput", DYN);
|
||||
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
|
||||
args.push(varRef(saved.id, DYN, loc));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { needsGeneralNodeStreams, stdioIntoGeneralStream } 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 {
|
||||
lifetimeRegionType,
|
||||
lowerLifetimeRegionCall,
|
||||
@@ -12686,6 +12687,12 @@ export class Lowerer {
|
||||
}
|
||||
// A bare `fs.constants` read (not one of the baked bits above).
|
||||
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
|
||||
// `stream.Readable` as a JavaScript value: the runtime's untyped
|
||||
// constructor, the base a constructor function inherits.
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
const classKind = builtinMemberClassKind(bi.module, bi.member);
|
||||
if (classKind !== null) return builtinClassValue(classKind, loc);
|
||||
}
|
||||
if (builtinModuleFnOf(this, bi.module, bi.member)) {
|
||||
this.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -551,6 +551,7 @@ async function compileExecutableNative(
|
||||
bigint: features.bigint,
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
dynBuiltinClass: features.dynBuiltinClass,
|
||||
parseArgs: features.parseArgs,
|
||||
stream: features.stream,
|
||||
net: features.net,
|
||||
|
||||
@@ -576,6 +576,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.objCreateWithProperties",
|
||||
"dyn.getPrototype",
|
||||
"dyn.setPrototype",
|
||||
// a proxy in the prototype chain can throw
|
||||
"dyn.builtinInstanceOf",
|
||||
"dyn.functionInstanceOf",
|
||||
"dyn.getOwnPropertyNames",
|
||||
"dyn.ownKeys",
|
||||
"dyn.getOwnPropertySymbols",
|
||||
|
||||
@@ -3536,6 +3536,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN },
|
||||
"dyn.classBasePrototype": { argTypes: [DYN], result: DYN },
|
||||
"dyn.classInherit": { argTypes: [DYN, DYN], result: DYN },
|
||||
/** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4),
|
||||
* Transform (5) and PassThrough (6) as untyped constructor values, one
|
||||
* per thread (scr_dyn_builtin_class.c). */
|
||||
"dyn.builtinClass": { argTypes: [F64], result: DYN },
|
||||
/** `value instanceof C` for those constructors, over the value's
|
||||
* prototype chain. */
|
||||
"dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL },
|
||||
/** `value instanceof f` for an untyped constructor function: the
|
||||
* prototype chain against f.prototype. */
|
||||
"dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL },
|
||||
"dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
"dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
|
||||
@@ -241,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
/** libCall name prefixes that link the child-process unit. */
|
||||
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
|
||||
|
||||
/** The entry points scr_dyn_builtin_class.c defines. The unit calls
|
||||
* listeners through the dyn-invoke unit, renders unhandled 'error'
|
||||
* payloads through the inspect unit, creates EventEmitter's private
|
||||
* symbols through the symbol unit and shares defaultMaxListeners with the
|
||||
* emitter unit, so each of them links all five. */
|
||||
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
|
||||
"dyn.builtinClass",
|
||||
"dyn.builtinInstanceOf",
|
||||
"dyn.functionInstanceOf",
|
||||
];
|
||||
|
||||
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
|
||||
const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// EventEmitter's kShapeMode and kCapture properties
|
||||
...BUILTIN_CLASS_LIB_FNS,
|
||||
"dyn.usingDispose",
|
||||
"dyn.usingMethod",
|
||||
"util.isDeepStrictEqual",
|
||||
@@ -519,9 +532,16 @@ const RUNTIME_FEATURE_RULES = {
|
||||
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
|
||||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
|
||||
}),
|
||||
/** EventEmitter and the stream classes as untyped constructor values
|
||||
* (scr_dyn_builtin_class.c), which call listeners through the dyn-invoke
|
||||
* unit, render unhandled 'error' payloads through the inspect unit and
|
||||
* create EventEmitter's private symbols through the symbol unit. */
|
||||
dynBuiltinClass: rule({
|
||||
fns: [...BUILTIN_CLASS_LIB_FNS],
|
||||
}),
|
||||
dynInvoke: rule({
|
||||
initial: (mod) => mod.workers === true,
|
||||
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS],
|
||||
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
|
||||
// Constant-key reads of Object.prototype member names answer the
|
||||
// inherited member through scr_dyn_invoke.c.
|
||||
expr: (node, _tables) =>
|
||||
@@ -538,7 +558,7 @@ const RUNTIME_FEATURE_RULES = {
|
||||
emitter: rule({
|
||||
initial: (mod) =>
|
||||
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
|
||||
fns: [...STREAM_LIB_FNS],
|
||||
fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
|
||||
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
|
||||
}),
|
||||
/** Subclasses of the runtime EventTarget/Event bases tear down and box
|
||||
@@ -572,7 +592,13 @@ const RUNTIME_FEATURE_RULES = {
|
||||
* terminal unit prints completion errors through it when there is a
|
||||
* completer. */
|
||||
inspect: rule({
|
||||
fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS],
|
||||
fns: [
|
||||
"global.native",
|
||||
"util.styleText",
|
||||
"crypto.cipherNew",
|
||||
...BUILTIN_CLASS_LIB_FNS,
|
||||
...UTIL_CALLABLE_LIB_FNS,
|
||||
],
|
||||
prefixes: ["insp.", "console.", "test."],
|
||||
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
|
||||
}),
|
||||
@@ -678,6 +704,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dynAsync: false,
|
||||
dynBuiltinClass: false,
|
||||
dynInvoke: false,
|
||||
dynTier: false,
|
||||
emitter: false,
|
||||
|
||||
@@ -89,6 +89,7 @@ async function main(): Promise<number> {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
|
||||
"bigint",
|
||||
"searchParams",
|
||||
"qs",
|
||||
"dynBuiltinClass",
|
||||
"parseArgs",
|
||||
"stream",
|
||||
"net",
|
||||
|
||||
@@ -96,6 +96,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5817-constructor-function-instanceof.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5818-constructor-function-emitter-defaults.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6453-constructor-function-emitter.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6454-constructor-function-stream-bases.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -88,6 +88,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
["scr_util_compare.c", all("parseArgs", "symbol")],
|
||||
["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")],
|
||||
["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")],
|
||||
// Untyped EventEmitter and stream constructor values call listeners
|
||||
// through the dyn-invoke unit, inspect unhandled 'error' payloads and
|
||||
// create EventEmitter's private symbols.
|
||||
["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")],
|
||||
["scr_stream.c", "stream"],
|
||||
// The zlib stream classes reference both the zlib engine and the stream unit.
|
||||
["scr_zlib_stream.c", all("zlib", "stream")],
|
||||
|
||||
@@ -1153,6 +1153,12 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr
|
||||
* must be static literals (the box never frees them; name may be NULL). */
|
||||
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
|
||||
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
|
||||
/* EventEmitter and the stream constructors as untyped constructor values,
|
||||
* and instanceof over untyped constructor functions
|
||||
* (scr_dyn_builtin_class.c, linked with the dynBuiltinClass feature). */
|
||||
ScrDyn *scr_dyn_builtin_class(double kind);
|
||||
bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind);
|
||||
bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee);
|
||||
bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key);
|
||||
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key);
|
||||
/* Calls a dyn value: a non-function kind throws the catchable TypeError
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
// `this instanceof F` in a constructor function, in a program that uses no
|
||||
// EventEmitter or stream constructor value: the call-without-new idiom
|
||||
// returns a new instance, and instanceof follows the function's prototype.
|
||||
"use strict";
|
||||
function Glob(pattern) {
|
||||
if (!(this instanceof Glob)) return new Glob(pattern);
|
||||
this.pattern = pattern;
|
||||
}
|
||||
Glob.prototype.describe = function () {
|
||||
return `glob ${this.pattern}`;
|
||||
};
|
||||
const called = Glob("*.ts");
|
||||
const constructed = new Glob("*.js");
|
||||
console.log(called.describe(), constructed.describe());
|
||||
console.log(called instanceof Glob, {} instanceof Glob, Object.create(Glob.prototype) instanceof Glob);
|
||||
@@ -0,0 +1,53 @@
|
||||
// EventEmitter.defaultMaxListeners read and written through the untyped
|
||||
// EventEmitter constructor (Node's accessor and its validation), and a
|
||||
// legacy pipe whose destination emits 'close' synchronously from end().
|
||||
"use strict";
|
||||
const util = require("util");
|
||||
const EventEmitter = require("events");
|
||||
const Stream = require("stream");
|
||||
|
||||
function Bus() {
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
util.inherits(Bus, EventEmitter);
|
||||
|
||||
console.log(EventEmitter.defaultMaxListeners);
|
||||
console.log(new Bus().getMaxListeners());
|
||||
EventEmitter.defaultMaxListeners = 3;
|
||||
console.log(EventEmitter.defaultMaxListeners, new Bus().getMaxListeners());
|
||||
for (const value of [-1, "4", Number.NaN]) {
|
||||
try {
|
||||
EventEmitter.defaultMaxListeners = value;
|
||||
} catch (error) {
|
||||
console.log(error.name, error.code, error.message);
|
||||
}
|
||||
}
|
||||
console.log(EventEmitter.defaultMaxListeners);
|
||||
EventEmitter.defaultMaxListeners = 10;
|
||||
|
||||
function Source() {
|
||||
Stream.call(this);
|
||||
this.readable = true;
|
||||
}
|
||||
util.inherits(Source, Stream);
|
||||
|
||||
function Sink() {
|
||||
Stream.call(this);
|
||||
this.writable = true;
|
||||
}
|
||||
util.inherits(Sink, Stream);
|
||||
Sink.prototype.write = function (chunk) {
|
||||
console.log("write", chunk);
|
||||
return true;
|
||||
};
|
||||
Sink.prototype.end = function () {
|
||||
console.log("sink end");
|
||||
this.emit("close");
|
||||
};
|
||||
|
||||
const source = new Source();
|
||||
const sink = new Sink();
|
||||
source.pipe(sink);
|
||||
source.emit("data", "a");
|
||||
console.log("emit end", source.emit("end"));
|
||||
console.log("listeners", source.listenerCount("end"), source.listenerCount("data"), sink.listenerCount("close"));
|
||||
@@ -0,0 +1,114 @@
|
||||
// Constructor functions that inherit from EventEmitter the pre-class way:
|
||||
// util.inherits and EventEmitter.call(this), the call-without-new guard,
|
||||
// prototype methods that emit, and the listener API over the instance.
|
||||
"use strict";
|
||||
const util = require("util");
|
||||
const { EventEmitter } = require("events");
|
||||
|
||||
util.inherits(Bus, EventEmitter);
|
||||
function Bus(name) {
|
||||
if (!(this instanceof Bus)) return new Bus(name);
|
||||
EventEmitter.call(this);
|
||||
this.name = name;
|
||||
this.entries = [];
|
||||
}
|
||||
Bus.prototype.add = function (entry) {
|
||||
this.entries.push(entry);
|
||||
this.emit("added", entry, this.entries.length);
|
||||
return this;
|
||||
};
|
||||
|
||||
// A second level: Sub inherits Bus, and Bus's constructor runs through call.
|
||||
function Sub(name) {
|
||||
Bus.call(this, name);
|
||||
this.sub = true;
|
||||
}
|
||||
util.inherits(Sub, Bus);
|
||||
Sub.prototype.add = function (entry) {
|
||||
return Bus.prototype.add.call(this, entry.toUpperCase());
|
||||
};
|
||||
|
||||
// Object.setPrototypeOf instead of util.inherits.
|
||||
function Legacy() {
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
Object.setPrototypeOf(Legacy.prototype, EventEmitter.prototype);
|
||||
|
||||
function main() {
|
||||
const bus = Bus("main");
|
||||
console.log(bus instanceof Bus, bus instanceof EventEmitter, Bus.super_ === EventEmitter);
|
||||
console.log(Object.keys(bus).join(","), bus._eventsCount, bus._maxListeners);
|
||||
bus.on("added", function (entry, count) {
|
||||
console.log("added", entry, count, this === bus);
|
||||
});
|
||||
bus.once("added", (entry) => console.log("first only", entry));
|
||||
bus.prependListener("added", () => console.log("prepended"));
|
||||
bus.prependOnceListener("added", () => console.log("prepended once"));
|
||||
bus.add("a").add("b");
|
||||
console.log(bus.listenerCount("added"), bus.eventNames(), bus.getMaxListeners());
|
||||
|
||||
const handler = () => console.log("never");
|
||||
bus.on("x", handler);
|
||||
bus.on("x", () => console.log("kept"));
|
||||
bus.off("x", handler);
|
||||
console.log(bus.emit("x"), bus.emit("nothing"), bus.listeners("x").length, bus.rawListeners("added").length);
|
||||
|
||||
try {
|
||||
bus.emit("error", new Error("boom"));
|
||||
} catch (err) {
|
||||
console.log("caught", err.message);
|
||||
}
|
||||
try {
|
||||
bus.emit("error", "plain");
|
||||
} catch (err) {
|
||||
console.log("caught", err.code, err.message);
|
||||
}
|
||||
bus.on("error", (err) => console.log("handled", err.message));
|
||||
bus.emit("error", new Error("quiet"));
|
||||
|
||||
bus.on("newListener", (event, listener) => console.log("newListener", event, typeof listener));
|
||||
bus.on("removeListener", (event) => console.log("removeListener", event));
|
||||
bus.on("later", () => {});
|
||||
bus.removeAllListeners("added");
|
||||
console.log(bus.listenerCount("added"), bus.listenerCount("later"));
|
||||
bus.removeAllListeners();
|
||||
console.log(bus.eventNames().length);
|
||||
|
||||
const sub = new Sub("child");
|
||||
sub.on("added", (entry) => console.log("sub added", entry));
|
||||
sub.add("c");
|
||||
console.log(sub instanceof Sub, sub instanceof Bus, sub instanceof EventEmitter, sub.entries);
|
||||
|
||||
const legacy = new Legacy();
|
||||
console.log("legacy", legacy instanceof EventEmitter, legacy.getMaxListeners());
|
||||
try {
|
||||
legacy.setMaxListeners(-1);
|
||||
} catch (err) {
|
||||
console.log(err.name, err.code);
|
||||
}
|
||||
try {
|
||||
legacy.on("bad", "not a function");
|
||||
} catch (err) {
|
||||
console.log(err.name, err.code);
|
||||
}
|
||||
console.log(new Legacy());
|
||||
|
||||
// The leak warning goes to stderr with the process id: run it in a child
|
||||
// and print its output with the id normalized.
|
||||
const { spawnSync } = require("child_process");
|
||||
const child = spawnSync(process.execPath, [process.argv[1], "leak"], { encoding: "utf8" });
|
||||
console.log(child.stdout + child.stderr.replace(/\(node:\d+\)/g, "(node:<pid>)"));
|
||||
}
|
||||
|
||||
function leak() {
|
||||
const legacy = new Legacy();
|
||||
legacy.setMaxListeners(1);
|
||||
legacy.on("many", () => {});
|
||||
legacy.on("many", () => {});
|
||||
legacy.on("many", () => {});
|
||||
console.log("leak", legacy.listenerCount("many"));
|
||||
setTimeout(() => console.log("leak done"), 1);
|
||||
}
|
||||
|
||||
if (process.argv[2] === "leak") leak();
|
||||
else main();
|
||||
@@ -0,0 +1,65 @@
|
||||
// The legacy Stream constructor as a constructor function's base: its
|
||||
// EventEmitter state and the legacy pipe between two such streams. The
|
||||
// stream classes take util.inherits, and instanceof follows the chain.
|
||||
"use strict";
|
||||
const util = require("util");
|
||||
const Stream = require("stream");
|
||||
const { Readable, Writable, Duplex, Transform, PassThrough } = require("stream");
|
||||
|
||||
function Source(items) {
|
||||
Stream.call(this);
|
||||
this.items = items;
|
||||
this.readable = true;
|
||||
}
|
||||
util.inherits(Source, Stream);
|
||||
Source.prototype.start = function () {
|
||||
for (const item of this.items) this.emit("data", item);
|
||||
this.emit("end");
|
||||
};
|
||||
|
||||
function Sink() {
|
||||
Stream.call(this);
|
||||
this.writable = true;
|
||||
this.chunks = [];
|
||||
}
|
||||
util.inherits(Sink, Stream);
|
||||
Sink.prototype.write = function (chunk) {
|
||||
this.chunks.push(chunk);
|
||||
return true;
|
||||
};
|
||||
Sink.prototype.end = function () {
|
||||
console.log("sink end", this.chunks.join("+"));
|
||||
};
|
||||
|
||||
const source = new Source(["a", "b", "c"]);
|
||||
const sink = new Sink();
|
||||
sink.on("pipe", (from) => console.log("piped", from === source));
|
||||
console.log(source.pipe(sink) === sink, sink.listenerCount("pipe"));
|
||||
source.start();
|
||||
console.log("after end", source.listenerCount("data"), sink.listenerCount("drain"));
|
||||
|
||||
const kept = new Sink();
|
||||
const second = new Source(["x"]);
|
||||
second.pipe(kept, { end: false });
|
||||
second.start();
|
||||
console.log("kept open", kept.chunks);
|
||||
|
||||
function Reader() {}
|
||||
util.inherits(Reader, Readable);
|
||||
function Both() {}
|
||||
util.inherits(Both, Duplex);
|
||||
function Shift() {}
|
||||
util.inherits(Shift, Transform);
|
||||
function Through() {}
|
||||
util.inherits(Through, PassThrough);
|
||||
function Out() {}
|
||||
util.inherits(Out, Writable);
|
||||
console.log(Reader.super_ === Readable, Through.super_ === PassThrough, Out.super_ === Writable);
|
||||
const reader = Object.create(Reader.prototype);
|
||||
const through = Object.create(Through.prototype);
|
||||
const out = Object.create(Out.prototype);
|
||||
const both = Object.create(Both.prototype);
|
||||
console.log(reader instanceof Readable, reader instanceof Writable);
|
||||
console.log(through instanceof Transform, through instanceof Duplex, through instanceof Readable);
|
||||
console.log(out instanceof Writable, out instanceof Readable, both instanceof Readable);
|
||||
console.log(typeof Shift.prototype.constructor, Shift.prototype instanceof Duplex);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2655, "san": 7944 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2746, "san": 51663 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4300, "san": 51474 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3371, "san": 3844 }
|
||||
@@ -30,6 +30,7 @@ const features: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ const featureCalls: readonly [string, IrLibFn][] = [
|
||||
["bigint", "bigint.parse"],
|
||||
["searchParams", "sp.new"],
|
||||
["qs", "qs.parse"],
|
||||
["dynBuiltinClass", "dyn.builtinClass"],
|
||||
["parseArgs", "util.parseArgs"],
|
||||
["parseArgs", "util.isDeepStrictEqual"],
|
||||
["parseArgs", "util.styleText"],
|
||||
|
||||
@@ -31,6 +31,7 @@ const base: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
Reference in New Issue
Block a user