mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Answer EventEmitter methods read as values off compiled emitters
Reading an EventEmitter.prototype method off a compiled emitter (a program class extending EventEmitter, or new EventEmitter()) through an untyped value answered undefined (`typeof o.on`, `const f = o.on; f.call(o, ...)`, `o.emit.bind(o)`), and EventEmitter.prototype.on in JavaScript was a fence. Such reads, typed value reads, computed keys, `in`, and EventEmitter.prototype.<method> now answer the runtime's EventEmitter.prototype functions, the ones constructor-function emitters inherit, so `o.on === o.addListener === EventEmitter.prototype.on` and names and lengths match Node. The first read installs a program dispatch that those functions call for a compiled emitter receiver; it invokes the method through the existing untyped dispatch with the call's argument count (emit forwards up to 16 arguments after the name, then fences). An untyped emit on, or read of emit from, a class that overrides emit now fences instead of bypassing the override for the native emit. A subclass visited under the boxed EventEmitter capsule keeps its own dispatch plan for values boxed as itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
73ae23a5eb
commit
0cf62ebecf
@@ -5,6 +5,9 @@
|
||||
An untyped `emit` of an event that has a listener with typed parameters converts its arguments to the event's typed tuple before any listener runs, and throws a `TypeError` when they do not convert.
|
||||
Through an untyped value, `listeners()` and `rawListeners()` of an event with a listener that has typed parameters are unsupported, and `emit('error')` of an object that is not an Error with no `error` listener is unsupported.
|
||||
Untyped `newListener` and `removeListener` listeners receive `undefined` in place of a listener registered with typed parameters.
|
||||
Reading EventEmitter methods as properties through an untyped value (`typeof emitter.listeners`) answers `undefined`.
|
||||
An untyped `emit` on, or an untyped read of `emit` from, an instance of a class that overrides `emit` is unsupported.
|
||||
Calling `EventEmitter.prototype.emit` as a function value on a compiled emitter with more than 16 arguments after the event name is unsupported.
|
||||
Calling EventEmitter.prototype's methods with an instance of a compiled class that does not extend EventEmitter as the receiver throws a `TypeError`.
|
||||
Through an untyped value, a stream's `emit` and listener-introspection methods read as `undefined`.
|
||||
`events.once` accepts EventEmitter and stream instances with a literal event name and an optional object-literal `{ signal }`; a stream's `data` event, other options values, and EventTarget emitters are unsupported.
|
||||
Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
|
||||
|
||||
@@ -1465,6 +1465,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.classInherit": "scr_dyn_class_inherit",
|
||||
"dyn.builtinClass": "scr_dyn_builtin_class",
|
||||
"dyn.builtinStreamClass": "scr_dyn_builtin_stream_class",
|
||||
"dyn.emitterMethod": "scr_dyn_emitter_method",
|
||||
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
|
||||
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
|
||||
"dyn.classSuper": "scr_dyn_class_super",
|
||||
|
||||
@@ -74,6 +74,12 @@ import {
|
||||
lowerDynamicEmitterOn,
|
||||
} from "./lower-event-emitter.js";
|
||||
import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js";
|
||||
import {
|
||||
EMITTER_PROTOTYPE_METHODS,
|
||||
emitterMethodValue,
|
||||
inheritedEmitterMethod,
|
||||
overriddenEmit,
|
||||
} from "./emitter-method-values.js";
|
||||
import { STREAM_OPERATORS } from "./lower-stream.js";
|
||||
import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js";
|
||||
|
||||
@@ -2771,6 +2777,7 @@ export class ClassDynamicDispatch {
|
||||
!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
(!(info.builtinStream !== undefined && streamProperty(dispatch.name)) &&
|
||||
!eventTargetInheritedMember(info, dispatch.name) &&
|
||||
!inheritedEmitterMethod(lowerer, info, dispatch.name) &&
|
||||
!findMethodOn(lowerer, info, dispatch.name) &&
|
||||
!findGenericMethodOn(lowerer, info, dispatch.name) &&
|
||||
dispatch.name !== "constructor" &&
|
||||
@@ -2889,6 +2896,12 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
},
|
||||
];
|
||||
// An inherited EventEmitter.prototype method: `in` sees it, the
|
||||
// own-property probes see only an own property that shadows it.
|
||||
if (dispatch.probe === "in" && inheritedEmitterMethod(lowerer, info, name))
|
||||
return [
|
||||
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
|
||||
];
|
||||
if (!dispatch.probe)
|
||||
return dispatch.reflect
|
||||
? this.reflectPropertyBody(lowerer, { ...dispatch, name }, info, receiver)
|
||||
@@ -3101,6 +3114,8 @@ export class ClassDynamicDispatch {
|
||||
for (const name of [...STREAM_BOOL_PROPERTIES, ...STREAM_NUM_PROPERTIES]) names.add(name);
|
||||
const inherited = eventTargetApiMembers(info);
|
||||
if (inherited) for (const name of inherited) names.add(name);
|
||||
for (const name of EMITTER_PROTOTYPE_METHODS.keys())
|
||||
if (inheritedEmitterMethod(lowerer, info, name)) names.add(name);
|
||||
|
||||
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
|
||||
const methodNames = [...owner.methods.keys()];
|
||||
@@ -3548,6 +3563,10 @@ export class ClassDynamicDispatch {
|
||||
for (const name of c.methods.keys())
|
||||
names.add(name.startsWith("get:") || name.startsWith("set:") ? name.slice(4) : name);
|
||||
}
|
||||
// EventEmitter.prototype's methods on compiled emitters.
|
||||
for (const { info } of this.propertyReceivers(lowerer, true))
|
||||
for (const name of EMITTER_PROTOTYPE_METHODS.keys())
|
||||
if (inheritedEmitterMethod(lowerer, info, name)) names.add(name);
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -4308,7 +4327,9 @@ export class ClassDynamicDispatch {
|
||||
(!canonical || !seen.has(info.def.name)) &&
|
||||
(info === capsule || lowerer.classCanBeConstructed(info))
|
||||
) {
|
||||
seen.add(info.def.name);
|
||||
// A subclass visited under a runtime capsule (boxed as that
|
||||
// base) keeps its own canonical plan for values boxed as itself.
|
||||
if (canonical) seen.add(info.def.name);
|
||||
plans.push({
|
||||
info,
|
||||
capsule: canonical ? info : capsule,
|
||||
@@ -4728,6 +4749,45 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
},
|
||||
];
|
||||
// An emit override has no function value (it is specialized per event
|
||||
// name and payload type).
|
||||
if (
|
||||
!write &&
|
||||
name === "emit" &&
|
||||
overriddenEmit(info) &&
|
||||
inheritedEmitterMethod(lowerer, info, name)
|
||||
)
|
||||
return [
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC2020",
|
||||
message:
|
||||
"reading an overridden EventEmitter emit through an untyped value is not supported yet",
|
||||
loc,
|
||||
},
|
||||
];
|
||||
// An inherited EventEmitter.prototype method: an own property shadows
|
||||
// it, otherwise the read answers the function
|
||||
// `EventEmitter.prototype.<name>` reads.
|
||||
if (!write && inheritedEmitterMethod(lowerer, info, name)) {
|
||||
const instance = lowerer.coerceToExpected(receiver, DYN);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
then: { kind: "libCall", fn: "dyn.bagGet", args: [bag, key, instance], type: DYN, loc },
|
||||
else_: emitterMethodValue(lowerer, name, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
const field = info.fields.get(name);
|
||||
const member = `${write ? "set" : "get"}:${name}`;
|
||||
const accessor = field ? null : findMethodOn(lowerer, info, member);
|
||||
@@ -5159,6 +5219,11 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
},
|
||||
];
|
||||
// An emit override is specialized per event name and payload type; an
|
||||
// untyped emit names neither, so it refuses rather than bypassing the
|
||||
// override for the native emit.
|
||||
if (method === "emit" && overriddenEmit(info) && inheritedEmitterMethod(lowerer, info, method))
|
||||
return fence();
|
||||
if (
|
||||
emitterRooted(lowerer, info) &&
|
||||
!streamRooted(info) &&
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* EventEmitter.prototype's methods as values. A read of `o.on` on a
|
||||
* compiled emitter (a program class extending EventEmitter, or
|
||||
* `new EventEmitter()`) through an untyped value or as a value, and
|
||||
* `EventEmitter.prototype.on` itself, answer the runtime's
|
||||
* EventEmitter.prototype functions (scr_dyn_builtin_class.c), the same
|
||||
* functions JavaScript constructor functions inherit: `on` and
|
||||
* `addListener` are one function, as are `off` and `removeListener`, and
|
||||
* each carries Node's `name` and `length`. Called on a compiled emitter
|
||||
* (`f.call(o, …)`, `f.bind(o)`, `f.apply(o, …)`), such a function hands
|
||||
* the call to the program's dispatch below, which invokes the method on
|
||||
* the receiver through the same untyped dispatch a direct `o.on(…)` call
|
||||
* reaches.
|
||||
*/
|
||||
import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
|
||||
import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
|
||||
/** EventEmitter.prototype's method names. */
|
||||
export const EMITTER_PROTOTYPE_METHODS: ReadonlySet<string> = new Set([
|
||||
"setMaxListeners",
|
||||
"getMaxListeners",
|
||||
"emit",
|
||||
"addListener",
|
||||
"on",
|
||||
"prependListener",
|
||||
"once",
|
||||
"prependOnceListener",
|
||||
"removeListener",
|
||||
"off",
|
||||
"removeAllListeners",
|
||||
"listeners",
|
||||
"rawListeners",
|
||||
"listenerCount",
|
||||
"eventNames",
|
||||
]);
|
||||
|
||||
/** `emit` forwards this many arguments after the event name; a call with
|
||||
* more fences. */
|
||||
const EMIT_ARGUMENTS = 16;
|
||||
|
||||
/** The runtime's SCR_EE_M_* order: each method's name and the argument
|
||||
* counts its call forwards. Node's methods that look at
|
||||
* `arguments.length` (removeAllListeners, emit, listenerCount's optional
|
||||
* listener) get exactly the arguments given; the rest get their declared
|
||||
* parameters, an absent one as undefined. */
|
||||
const DISPATCHED: readonly {
|
||||
method: string;
|
||||
fixed?: number;
|
||||
counts?: (given: number) => number;
|
||||
}[] = [
|
||||
{ method: "addListener", fixed: 2 },
|
||||
{ method: "prependListener", fixed: 2 },
|
||||
{ method: "once", fixed: 2 },
|
||||
{ method: "prependOnceListener", fixed: 2 },
|
||||
{ method: "removeListener", fixed: 2 },
|
||||
{ method: "removeAllListeners", counts: (given) => Math.min(given, 1) },
|
||||
{ method: "emit", counts: (given) => Math.max(given, 1) },
|
||||
{ method: "listeners", fixed: 1 },
|
||||
{ method: "rawListeners", fixed: 1 },
|
||||
{ method: "listenerCount", counts: (given) => (given < 2 ? 1 : 2) },
|
||||
{ method: "eventNames", fixed: 0 },
|
||||
{ method: "setMaxListeners", fixed: 1 },
|
||||
{ method: "getMaxListeners", fixed: 0 },
|
||||
];
|
||||
|
||||
const DISPATCH = "%emitter.method.dispatch";
|
||||
|
||||
/** An EventEmitter.prototype method a non-stream emitter class inherits
|
||||
* (no field, method or accessor of that name in its chain; streams keep
|
||||
* their own surface). An `emit` override is not a method, so `emit` counts
|
||||
* here too; overriddenEmit separates it. */
|
||||
export function inheritedEmitterMethod(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
|
||||
if (!EMITTER_PROTOTYPE_METHODS.has(name)) return false;
|
||||
let emitter = false;
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
if (c.builtinStream !== undefined) return false;
|
||||
if (c.builtinEmitter) emitter = true;
|
||||
}
|
||||
return (
|
||||
emitter &&
|
||||
!info.fields.has(name) &&
|
||||
!findMethodOn(lowerer, info, name) &&
|
||||
!findGenericMethodOn(lowerer, info, name) &&
|
||||
!findMethodOn(lowerer, info, `get:${name}`) &&
|
||||
!findMethodOn(lowerer, info, `set:${name}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** True when the class or an ancestor overrides `emit` (lowered as
|
||||
* per-event specializations rather than a method). */
|
||||
export function overriddenEmit(info: ClassInfo): boolean {
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) if (c.emitOverride) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A typed read of an inherited EventEmitter.prototype method as a value
|
||||
* (`const f = emitter.on`, `typeof emitter.emit`) on a compiled emitter or
|
||||
* a union of them: the receiver is evaluated, the read answers the
|
||||
* interned method value. */
|
||||
export function lowerEmitterInstanceMethod(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
const name = expr.name.text;
|
||||
if (!EMITTER_PROTOTYPE_METHODS.has(name) || expr.questionDotToken) return null;
|
||||
if (ts.isCallExpression(expr.parent) && expr.parent.expression === expr) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr.name);
|
||||
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
|
||||
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
if (!receiverType) return null;
|
||||
const arms =
|
||||
receiverType.kind === "union"
|
||||
? (lowerer.unions.get(receiverType.unionId)?.arms ?? [])
|
||||
: [receiverType];
|
||||
if (
|
||||
!arms.length ||
|
||||
!arms.every((arm) => {
|
||||
const info = arm.kind === "object" ? lowerer.classes.get(arm.className) : undefined;
|
||||
return info !== undefined && inheritedEmitterMethod(lowerer, info, name);
|
||||
})
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(expr);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.expression), loc }],
|
||||
result: emitterMethodValue(lowerer, name, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The program's dispatch for EventEmitter.prototype's functions on
|
||||
* compiled emitters: `this` is the emitter, the first argument the
|
||||
* method's index, the rest the call's arguments. */
|
||||
function emitterMethodDispatch(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === DISPATCH)) {
|
||||
const self = varRef("self", DYN, loc);
|
||||
const values = varRef("values", DYN, loc);
|
||||
const index = varRef("index", F64, loc);
|
||||
const given = varRef("given", F64, loc);
|
||||
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const equals = (left: IrExpr, value: number): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left,
|
||||
right: number(value),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const element = (i: number): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value: values,
|
||||
key: lowerer.coerceToExpected(number(i), DYN),
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const invoke = (method: string, n: number): IrStmt => ({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynInvoke",
|
||||
recv: self,
|
||||
method,
|
||||
calleeName: method,
|
||||
args: Array.from({ length: n }, (_, i) => element(i + 1)),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "self",
|
||||
init: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "index",
|
||||
init: { kind: "dynCheck", value: element(0), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "given",
|
||||
init: {
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "dynKeyGet", value: values, key: strLit("length", loc), type: DYN, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
right: number(1),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
DISPATCHED.forEach(({ method, fixed, counts }, i) => {
|
||||
const arms: IrStmt[] = [];
|
||||
if (fixed !== undefined || !counts) arms.push(invoke(method, fixed ?? 0));
|
||||
else {
|
||||
// One arm per argument count up to the last one that matters.
|
||||
const limit = method === "emit" ? EMIT_ARGUMENTS + 1 : 2;
|
||||
for (let n = 0; n < limit; n++)
|
||||
arms.push({
|
||||
kind: "if",
|
||||
cond: equals(given, n),
|
||||
then: [invoke(method, counts(n))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
if (method === "emit")
|
||||
arms.push(
|
||||
{
|
||||
kind: "if",
|
||||
cond: equals(given, limit),
|
||||
then: [invoke(method, limit)],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC2020",
|
||||
message: `calling EventEmitter.prototype.emit through a method value with more than ${EMIT_ARGUMENTS} arguments after the event name is not supported yet`,
|
||||
loc,
|
||||
},
|
||||
);
|
||||
else arms.push(invoke(method, counts(limit)));
|
||||
}
|
||||
body.push({ kind: "if", cond: equals(index, i), then: arms, else_: null, loc });
|
||||
});
|
||||
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
|
||||
lowerer.liftedFns.push({
|
||||
name: DISPATCH,
|
||||
params: [{ localId: "values", name: "values", type: DYN }],
|
||||
returnType: DYN,
|
||||
locals: [
|
||||
{ id: "values", name: "values", type: DYN, mutable: false },
|
||||
{ id: "self", name: "self", type: DYN, mutable: false },
|
||||
{ id: "index", name: "index", type: F64, mutable: false },
|
||||
{ id: "given", name: "given", type: F64, mutable: false },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: DISPATCH,
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: DYN, rest: true, argumentsAll: true },
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** EventEmitter.prototype[name] (DYN). */
|
||||
export function emitterMethodValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.emitterMethod",
|
||||
args: [{ kind: "strLit", value: name, type: STRING, loc }, emitterMethodDispatch(lowerer, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `EventEmitter.prototype.<method>` (any import spelling of the class):
|
||||
* the interned method value. */
|
||||
export function lowerEmitterPrototypeMethod(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (!EMITTER_PROTOTYPE_METHODS.has(expr.name.text)) return null;
|
||||
const prototype = expr.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(prototype) ||
|
||||
prototype.questionDotToken ||
|
||||
prototype.name.text !== "prototype"
|
||||
)
|
||||
return null;
|
||||
const owner = prototype.expression;
|
||||
const symbol = ts.isIdentifier(owner)
|
||||
? lowerer.resolveValueSymbol(owner)
|
||||
: ts.isPropertyAccessExpression(owner)
|
||||
? lowerer.checker.getSymbolAtLocation(owner.name)
|
||||
: null;
|
||||
if (!lowerer.builtinEmitterInfoOf(symbol)) return null;
|
||||
return emitterMethodValue(lowerer, expr.name.text, locOf(expr));
|
||||
}
|
||||
@@ -128,6 +128,10 @@ import { lowerReadlineProperty } from "../builtins/readline.js";
|
||||
import { lowerEventStaticProperty } from "../builtins/numbers.js";
|
||||
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js";
|
||||
import { lowerEventObjectProperty } from "../lower-event-target.js";
|
||||
import {
|
||||
lowerEmitterInstanceMethod,
|
||||
lowerEmitterPrototypeMethod,
|
||||
} from "../emitter-method-values.js";
|
||||
import { iteratorResultValueRead } from "../lower-generators.js";
|
||||
import { lowerGlobalValue } from "../lower-global-value.js";
|
||||
import { nullishGenericBindingUnitOf } from "../binding-analysis.js";
|
||||
@@ -329,6 +333,12 @@ export function lowerPropertyAccessExpr(
|
||||
const ctor = runtimeConstructorReceiver(lowerer, expr);
|
||||
if (ctor) return lowerRuntimeConstructor(lowerer, expr, ctor.info);
|
||||
}
|
||||
// EventEmitter.prototype's methods as values (emitter-method-values.ts).
|
||||
{
|
||||
const method =
|
||||
lowerEmitterPrototypeMethod(lowerer, expr) ?? lowerEmitterInstanceMethod(lowerer, expr);
|
||||
if (method) return method;
|
||||
}
|
||||
// `C.name` through a runtime-provided class VALUE (a `typeof Readable`
|
||||
// parameter): the class object's stored name.
|
||||
if (
|
||||
|
||||
@@ -3611,6 +3611,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* functions over them get native stream state
|
||||
* (scr_dyn_builtin_stream.c). */
|
||||
"dyn.builtinStreamClass": { argTypes: [F64], result: DYN },
|
||||
/** EventEmitter.prototype[name] off EventEmitter (0) above; the first
|
||||
* read installs the program's dispatch (the DYN function) that runs
|
||||
* those methods on compiled emitters. */
|
||||
"dyn.emitterMethod": { argTypes: [STRING, DYN], result: DYN },
|
||||
/** `value instanceof C` for those constructors, over the value's
|
||||
* prototype chain. */
|
||||
"dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL },
|
||||
|
||||
@@ -259,6 +259,8 @@ const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
|
||||
"dyn.builtinClass",
|
||||
// the stream classes, through scr_dyn_builtin_stream.c
|
||||
"dyn.builtinStreamClass",
|
||||
// EventEmitter.prototype's methods read off compiled emitters
|
||||
"dyn.emitterMethod",
|
||||
"dyn.builtinInstanceOf",
|
||||
"dyn.functionInstanceOf",
|
||||
];
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6600-emitter-method-values-untyped.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6601-emitter-method-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6602-emitter-method-values-typed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1159,6 +1159,11 @@ ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
|
||||
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);
|
||||
/* EventEmitter.prototype[name] (+1); the first call installs DISPATCH,
|
||||
* the program's function that runs those methods on compiled emitters
|
||||
* (typed refs): called with the emitter as `this`, the method's
|
||||
* SCR_EE_M_* index, then the call's arguments. */
|
||||
ScrDyn *scr_dyn_emitter_method(const ScrStr *name, ScrDyn *dispatch);
|
||||
/* The stream half of those constructors (scr_dyn_builtin_stream.c, linked
|
||||
* with the dynBuiltinStream feature), registered with
|
||||
* scr_dyn_builtin_class_streams before the first stream class value is
|
||||
|
||||
@@ -66,9 +66,35 @@ static ScrDyn *bc_class(int kind);
|
||||
/* The stream half, once registered (scr_dyn_builtin_stream.c). */
|
||||
static SCR_TL const ScrDynStreamOps *bc_stream_ops;
|
||||
|
||||
/* EventEmitter.prototype's method on a receiver with stream state. */
|
||||
/* The program's dispatch for compiled emitters (a program class extending
|
||||
* EventEmitter, or `new EventEmitter()`), which reach these methods as
|
||||
* typed refs whose listeners live in the native registry; installed by
|
||||
* scr_dyn_emitter_method. */
|
||||
static SCR_TL ScrDyn *bc_compiled_dispatch;
|
||||
|
||||
/* method(...args) on a compiled emitter: the dispatch runs with the
|
||||
* receiver as `this` and the method's SCR_EE_M_* index before the
|
||||
* arguments. */
|
||||
static ScrDyn *bc_compiled_event(int method, ScrDyn *self, ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn **argv = malloc((argc + 1) * sizeof *argv);
|
||||
if (!argv) scr_trap("out of memory");
|
||||
argv[0] = scr_dyn_new_num(method);
|
||||
for (size_t i = 0; i < argc; i++) argv[i + 1] = args[i];
|
||||
scr_dyn_this_push_dyn(self);
|
||||
ScrDyn *result = scr_dyn_call(bc_compiled_dispatch, argv, argc + 1, bc_names[BC_EMITTER]);
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(argv[0]);
|
||||
free(argv);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* EventEmitter.prototype's method on a receiver with stream state, or on
|
||||
* a compiled emitter. */
|
||||
static bool bc_stream_event(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result) {
|
||||
return bc_stream_ops != NULL && bc_stream_ops->event(method, self, args, argc, result);
|
||||
if (bc_stream_ops != NULL && bc_stream_ops->event(method, self, args, argc, result)) return true;
|
||||
if (bc_compiled_dispatch == NULL || self->kind != SCR_DYN_TYPED_REF) return false;
|
||||
*result = bc_compiled_event(method, self, args, argc);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── property helpers ───────────────────────────────────────────────── */
|
||||
@@ -1436,6 +1462,8 @@ static void bc_teardown(void) {
|
||||
if (bc_symbols[i]) scr_dyn_release(bc_symbols[i]);
|
||||
bc_symbols[i] = NULL;
|
||||
}
|
||||
if (bc_compiled_dispatch) scr_dyn_release(bc_compiled_dispatch);
|
||||
bc_compiled_dispatch = NULL;
|
||||
if (scr_exc_pending()) scr_exc_clear();
|
||||
}
|
||||
|
||||
@@ -1542,6 +1570,19 @@ ScrDyn *scr_dyn_builtin_class(double kind) {
|
||||
return scr_dyn_retain(bc_class(index));
|
||||
}
|
||||
|
||||
/* EventEmitter.prototype[name], +1, read off a compiled emitter or the
|
||||
* class; the first read installs the program's dispatch for compiled
|
||||
* emitters (borrowed, retained). */
|
||||
ScrDyn *scr_dyn_emitter_method(const ScrStr *name, ScrDyn *dispatch) {
|
||||
ScrDyn *ctor = bc_class(BC_EMITTER);
|
||||
if (!bc_compiled_dispatch) bc_compiled_dispatch = scr_dyn_retain(dispatch);
|
||||
ScrDyn *prototype = bc_get(ctor, "prototype");
|
||||
if (!prototype) return NULL;
|
||||
ScrDyn *method = bc_get(prototype, name->data);
|
||||
scr_dyn_release(prototype);
|
||||
return method;
|
||||
}
|
||||
|
||||
/* OrdinaryHasInstance over the value's prototype chain. */
|
||||
static bool bc_chain_has(ScrDyn *value, ScrDyn *prototype) {
|
||||
if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_ARR && value->kind != SCR_DYN_FUNC &&
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// EventEmitter.prototype's methods read as values through untyped
|
||||
// references to compiled emitters: a program class extending EventEmitter
|
||||
// and `new EventEmitter()` seen through any and unknown.
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
class Bus extends EventEmitter {
|
||||
label = "bus";
|
||||
ping(): string {
|
||||
return `${this.label} ping`;
|
||||
}
|
||||
}
|
||||
|
||||
const bus = new Bus();
|
||||
const o: any = bus;
|
||||
console.log(typeof o.on, typeof o.emit, typeof o.listeners, typeof o.ping);
|
||||
console.log(o.on === o.addListener, o.off === o.removeListener, o.on === o.once);
|
||||
console.log(o.on === EventEmitter.prototype.on, o.emit === EventEmitter.prototype.emit);
|
||||
console.log(o.on.name, o.off.name, o.emit.name, o.once.name);
|
||||
console.log(o.on.length, o.emit.length, o.listeners.length, o.eventNames.length);
|
||||
|
||||
// call, apply and bind with the emitter as the receiver
|
||||
const add = o.on;
|
||||
console.log(add.call(o, "a", (x: unknown, y: unknown) => console.log("a:", x, y)) === o);
|
||||
console.log(o.emit.apply(o, ["a", 1, 2]));
|
||||
const emit = o.emit.bind(o);
|
||||
console.log(emit("a", "x"), emit("missing"));
|
||||
console.log(o.listenerCount.call(o, "a"), o.eventNames.call(o));
|
||||
const once = o.once;
|
||||
once.call(o, "b", () => console.log("b once"));
|
||||
console.log(emit("b"), emit("b"));
|
||||
const prepend = o.prependListener;
|
||||
prepend.call(o, "a", () => console.log("a first"));
|
||||
emit("a", 3, 4);
|
||||
console.log(Reflect.apply(o.listenerCount, o, ["a"]));
|
||||
|
||||
// the argument count matters to removeAllListeners and emit
|
||||
o.on("c", () => {});
|
||||
o.on("d", () => {});
|
||||
console.log(o.removeAllListeners.call(o, "c") === o, o.eventNames());
|
||||
o.removeAllListeners.call(o);
|
||||
console.log(o.eventNames());
|
||||
o.on("many", (...args: unknown[]) => console.log("many:", args.length, args.join(",")));
|
||||
console.log(emit("many", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));
|
||||
console.log(o.setMaxListeners.call(o, 3) === o, o.getMaxListeners.call(o));
|
||||
|
||||
// off through a detached method value
|
||||
const listener = () => console.log("never");
|
||||
o.on("e", listener);
|
||||
const off = o.off;
|
||||
off.call(o, "e", listener);
|
||||
console.log(o.emit("e"), o.listenerCount("e"));
|
||||
|
||||
// membership: inherited, not own
|
||||
console.log("on" in o, "emit" in o, Object.prototype.hasOwnProperty.call(o, "on"));
|
||||
const key = ["emit", "listeners"][Number(o.listenerCount("a") > 5)]!;
|
||||
console.log(typeof o[key], o[key] === EventEmitter.prototype.emit);
|
||||
|
||||
// the class's own members still answer
|
||||
console.log(o.ping(), o.ping.call(o));
|
||||
|
||||
// new EventEmitter() through unknown
|
||||
const plain = new EventEmitter();
|
||||
const seen: unknown = plain;
|
||||
const p = seen as any;
|
||||
const pOn = p.on;
|
||||
pOn.call(p, "z", (v: unknown) => console.log("z:", v));
|
||||
console.log(p.emit.call(p, "z", 9), p.listeners.call(p, "z").length);
|
||||
console.log(p.on === o.on, p.rawListeners === EventEmitter.prototype.rawListeners);
|
||||
|
||||
// a detached method called without a receiver throws a TypeError
|
||||
try {
|
||||
add("x", () => {});
|
||||
} catch (err) {
|
||||
console.log("detached:", err instanceof TypeError);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
// EventEmitter.prototype's methods as values in JavaScript: read off a
|
||||
// compiled subclass, off `new EventEmitter()`, off EventEmitter.prototype
|
||||
// by name, and shared with constructor-function emitters.
|
||||
const EventEmitter = require("events");
|
||||
const util = require("util");
|
||||
|
||||
class Queue extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
}
|
||||
push(v) {
|
||||
this.items.push(v);
|
||||
this.emit("push", v);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const q = new Queue();
|
||||
const on = q.on;
|
||||
on.call(q, "push", (v) => console.log("pushed", v));
|
||||
q.push(1).push(2);
|
||||
for (const m of ["on", "addListener", "emit", "off", "once", "prependOnceListener", "listeners", "listenerCount", "eventNames"]) {
|
||||
console.log(m, typeof q[m], q[m] === EventEmitter.prototype[m], q[m].name, q[m].length);
|
||||
}
|
||||
|
||||
// EventEmitter.prototype's functions called on compiled emitters
|
||||
const proto = EventEmitter.prototype;
|
||||
proto.on.call(q, "drain", () => console.log("drained"));
|
||||
console.log(proto.emit.call(q, "drain"), proto.listenerCount.call(q, "push"));
|
||||
console.log(proto.eventNames.call(q));
|
||||
proto.removeAllListeners.call(q, "drain");
|
||||
console.log(q.eventNames(), Reflect.apply(proto.listenerCount, q, ["drain"]));
|
||||
|
||||
// new EventEmitter()
|
||||
const plain = new EventEmitter();
|
||||
const emit = plain.emit;
|
||||
plain.on("hi", (a, b, c) => console.log("hi", a, b, c));
|
||||
console.log(emit.call(plain, "hi", 1, "two", [3]));
|
||||
console.log(emit.call(plain, "hi", 1, 2, 3, 4, 5, 6, 7, 8, 9));
|
||||
const fn = () => console.log("never");
|
||||
plain.once("x", fn);
|
||||
const off = plain.off;
|
||||
off.call(plain, "x", fn);
|
||||
console.log(plain.emit("x"), plain.listenerCount("x"));
|
||||
const bound = plain.prependListener.bind(plain);
|
||||
bound("hi", () => console.log("hi first"));
|
||||
plain.emit("hi", "a");
|
||||
|
||||
// a constructor-function emitter shares the same functions
|
||||
function Legacy() {
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
util.inherits(Legacy, EventEmitter);
|
||||
const legacy = new Legacy();
|
||||
console.log(legacy.on === q.on, legacy.emit === plain.emit);
|
||||
legacy.on("tick", (n) => console.log("legacy tick", n));
|
||||
q.on.call(legacy, "tick", (n) => console.log("legacy tick again", n));
|
||||
console.log(plain.emit.call(legacy, "tick", 7), legacy.listenerCount("tick"));
|
||||
|
||||
// method values of a class expression whose instances escape
|
||||
function makeEmitter() {
|
||||
return new (class extends EventEmitter {
|
||||
fire() {
|
||||
return this.emit("fire", "local");
|
||||
}
|
||||
})();
|
||||
}
|
||||
const local = makeEmitter();
|
||||
const localOn = local.on;
|
||||
localOn.call(local, "fire", (v) => console.log("fire", v));
|
||||
console.log(local.fire(), "on" in local, Object.keys(local).includes("on"));
|
||||
@@ -0,0 +1,34 @@
|
||||
// EventEmitter.prototype's methods read as values from typed references
|
||||
// and from EventEmitter.prototype in TypeScript.
|
||||
import { EventEmitter } from "node:events";
|
||||
import * as events from "node:events";
|
||||
|
||||
class Counter extends EventEmitter {
|
||||
count = 0;
|
||||
bump(): void {
|
||||
this.count++;
|
||||
this.emit("bump", this.count);
|
||||
}
|
||||
}
|
||||
|
||||
const counter = new Counter();
|
||||
const plain = new EventEmitter();
|
||||
console.log(typeof counter.on, typeof plain.emit, typeof EventEmitter.prototype.listeners);
|
||||
console.log(counter.on === plain.on, counter.addListener === EventEmitter.prototype.on);
|
||||
console.log(events.EventEmitter.prototype.off === plain.removeListener);
|
||||
|
||||
const onValue: unknown = counter.on;
|
||||
const emitValue: unknown = plain.emit;
|
||||
console.log(typeof onValue, onValue === EventEmitter.prototype.addListener, emitValue === counter.emit);
|
||||
|
||||
const register = counter.on as any;
|
||||
register.call(counter, "bump", (n: number) => console.log("bump", n));
|
||||
counter.bump();
|
||||
EventEmitter.prototype.on.call(counter, "bump", (n: number) => console.log("bump again", n));
|
||||
counter.bump();
|
||||
console.log(EventEmitter.prototype.listenerCount.call(counter, "bump"));
|
||||
console.log(EventEmitter.prototype.emit.call(plain, "nothing"));
|
||||
|
||||
const pick = (emitter: EventEmitter, name: "on" | "emit"): unknown =>
|
||||
name === "on" ? emitter.on : emitter.emit;
|
||||
console.log(pick(counter, "on") === pick(plain, "on"), pick(counter, "emit") === pick(plain, "on"));
|
||||
Reference in New Issue
Block a user