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
5 changes: 5 additions & 0 deletions docs/content/limitations/builtin-apis/eventemitter.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
- **EventEmitter:** Literal event names use fixed argument tuples.
Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion.
Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
An emitter reached through an untyped value shares one listener list with its typed uses, so untyped registrations and emits reach typed listeners and typed emits reach untyped listeners.
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`.
`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.
53 changes: 50 additions & 3 deletions packages/compiler/src/backend/llvm/lib-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
host.emitPendingCheck();
return { name: t, type: e.type };
}
if (e.fn === "emitter.emitFlex") {
if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") {
const args = e.args.map((a) => host.emitExpr(a));
const n = args.length - 2;
let payload = "null";
Expand All @@ -867,10 +867,57 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
}
payload = arr;
}
host.declare(`declare zeroext i1 @scr_emitter_emit_flex(ptr, ptr, ptr, ${host.sizeType})`);
const sym = e.fn === "emitter.emitAny" ? "scr_emitter_emit_any" : "scr_emitter_emit_flex";
host.declare(`declare zeroext i1 @${sym}(ptr, ptr, ptr, ${host.sizeType})`);
const t = B.tmp();
B.line(
`${t} = call i1 @scr_emitter_emit_flex(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${payload}, ${host.sizeType} ${n})`,
`${t} = call i1 @${sym}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${payload}, ${host.sizeType} ${n})`,
);
host.emitPendingCheck();
return { name: t, type: e.type };
}
if (e.fn === "emitter.emitMixed") {
// (recv, name, k, ...k dyn arguments, ...converted tuple): the dyn
// arguments ride an exact-length array like emitFlex, the tuple the
// pointer-classed variadic tail like emitter.emit. Borrowed; may throw.
const count = e.args[2]!;
const k = count.kind === "numLit" ? count.value : 0;
const args = [e.args[0]!, e.args[1]!, ...e.args.slice(3)].map((a) => host.emitExpr(a));
let payload = "null";
if (k > 0) {
const arr = B.slot();
B.entryAllocas.push(`${arr} = alloca [${k} x ptr]`);
for (let i = 0; i < k; i++) {
const slot = B.tmp();
B.line(`${slot} = getelementptr inbounds [${k} x ptr], ptr ${arr}, i64 0, i64 ${i}`);
B.line(`store ptr ${args[i + 2]!.name}, ptr ${slot}`);
}
payload = arr;
}
const tuple = args.slice(2 + k).map((a) => {
const ty = host.llType(a.type);
if (ty === "double" || ty === "i1") {
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${ty} ; EventEmitter scalar argument`);
B.line(`store ${ty} ${a.name}, ptr ${slot}`);
return `ptr ${slot}`;
}
return `ptr ${a.name}`;
});
host.declare(
`declare zeroext i1 @scr_emitter_emit_mixed(ptr, ptr, ptr, ${host.sizeType}, ...)`,
);
const t = B.tmp();
B.line(
`${t} = call zeroext i1 (ptr, ptr, ptr, ${host.sizeType}, ...) @scr_emitter_emit_mixed(` +
[
`ptr ${args[0]!.name}`,
`ptr ${args[1]!.name}`,
`ptr ${payload}`,
`${host.sizeType} ${k}`,
...tuple,
].join(", ") +
`)`,
);
host.emitPendingCheck();
return { name: t, type: e.type };
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"emitter.off": "scr_emitter_off",
"emitter.offDyn": "scr_emitter_off_dyn",
"emitter.onFlex": "scr_emitter_on_flex",
"emitter.onAny": "scr_emitter_on_any",
"emitter.checkListener": "scr_emitter_check_listener",
"emitter.removeAll": "scr_emitter_remove_all",
"emitter.emitError": "scr_emitter_emit_error",
Expand All @@ -1076,6 +1077,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"emitter.countDyn": "scr_emitter_listener_count_dyn",
"emitter.names": "scr_emitter_event_names",
"emitter.listeners": "scr_emitter_listeners",
"emitter.listenersDyn": "scr_emitter_listeners_dyn",
"emitter.hasTyped": "scr_emitter_has_typed",
"emitter.setMax": "scr_emitter_set_max",
"emitter.setMaxChk": "scr_emitter_set_max_chk",
"emitter.setDefaultMaxChk": "scr_emitter_set_default_max_chk",
Expand Down
134 changes: 125 additions & 9 deletions packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ import { isClassCallback } from "./class-callbacks.js";
import { CaughtValueDispatch } from "./caught-value-dispatch.js";
import { refreshDescriptorGuards } from "./class-descriptors.js";
import { ClassConstructionDispatch } from "./class-construction.js";
import { emitterRooted } from "./lower-event-emitter.js";
import {
emitterRooted,
lowerDynamicEmitterEmit,
lowerDynamicEmitterOn,
} from "./lower-event-emitter.js";
import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js";
import { STREAM_OPERATORS } from "./lower-stream.js";
import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js";
Expand Down Expand Up @@ -97,6 +101,24 @@ const EMITTER_METHODS = new Set([
"off",
"removeListener",
]);
/** The rest of EventEmitter.prototype, answered through an untyped value
* on emitters that are not streams (streams keep their own surface). */
const EMITTER_INTROSPECTION_METHODS = new Set([
"listenerCount",
"eventNames",
"removeAllListeners",
"listeners",
"rawListeners",
"setMaxListeners",
"getMaxListeners",
]);
/** True when the class is or extends one of the runtime stream classes. */
function streamRooted(info: ClassInfo): boolean {
for (let c: ClassInfo | null = info; c; c = c.base) {
if (c.builtinStream !== undefined) return true;
}
return false;
}
const STREAM_BOOL_PROPERTIES = new Set([
"readable",
"readableEnded",
Expand Down Expand Up @@ -1223,6 +1245,13 @@ export class ClassDynamicDispatch {
const matching = this.propertyReceivers(lowerer, true).flatMap(({ info }) => {
if (!info || info.fields.has(method) || info.builtinError) return [];
if (emitterRooted(lowerer, info) && EMITTER_METHODS.has(method)) return [info];
if (
emitterRooted(lowerer, info) &&
!streamRooted(info) &&
EMITTER_INTROSPECTION_METHODS.has(method) &&
!findMethodOn(lowerer, info, method)
)
return [info];
if (info.builtinStream) {
return STREAM_METHODS.has(method) ||
(STREAM_OPERATORS.has(method) && info.builtinStream !== "w")
Expand Down Expand Up @@ -5055,6 +5084,66 @@ export class ClassDynamicDispatch {
return [store, { kind: "return", value: null, loc }];
}

/** EventEmitter.prototype's introspection and bookkeeping members on an
* emitter seen through an untyped value: Node's results over the native
* listener registry. */
private emitterIntrospection(lowerer: Lowerer, dispatch: Dispatch, receiver: IrExpr): IrStmt[] {
const { method, loc } = dispatch.source;
const incoming = dispatch.source.args.map((_, i) => varRef(`p.${i + 1}`, DYN, loc));
const ret = (value: IrExpr): IrStmt[] => [
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
];
const call = (fn: IrLibFn, args: IrExpr[], type: IrType): IrExpr => ({
kind: "libCall",
fn,
args: [receiver, ...args],
type,
loc,
});
// Node indexes _events with the argument itself: an omitted name is
// the key "undefined".
const eventName = (): IrExpr =>
incoming.length
? lowerer.coerceToExpected(incoming[0]!, STRING)
: { kind: "strLit", value: "undefined", type: STRING, loc };
switch (method) {
case "listenerCount":
return ret(
incoming.length >= 2
? call("emitter.countDyn", [eventName(), incoming[1]!], F64)
: call("emitter.count", [eventName()], F64),
);
case "eventNames":
return ret(call("emitter.names", [], { kind: "array", elem: STRING }));
case "removeAllListeners":
return ret(
incoming.length
? call(
"emitter.removeAll",
[eventName(), { kind: "boolLit", value: false, type: BOOL, loc }],
receiver.type,
)
: call(
"emitter.removeAll",
[
{ kind: "strLit", value: "", type: STRING, loc },
{ kind: "boolLit", value: true, type: BOOL, loc },
],
receiver.type,
),
);
case "listeners":
case "rawListeners":
return ret(call("emitter.listenersDyn", [eventName()], DYN));
case "setMaxListeners":
return ret(
call("emitter.setMaxChk", [incoming[0] ?? dynUndefinedExpr(loc)], receiver.type),
);
default:
return ret(call("emitter.getMax", [], F64));
}
}

private methodBody(
lowerer: Lowerer,
dispatch: Dispatch,
Expand All @@ -5070,6 +5159,14 @@ export class ClassDynamicDispatch {
loc,
},
];
if (
emitterRooted(lowerer, info) &&
!streamRooted(info) &&
EMITTER_INTROSPECTION_METHODS.has(method) &&
!findMethodOn(lowerer, info, method)
) {
return this.emitterIntrospection(lowerer, dispatch, receiver);
}
if (
info.builtinStream ||
(emitterRooted(lowerer, info) &&
Expand All @@ -5090,18 +5187,37 @@ export class ClassDynamicDispatch {
];
if (method === "emit") {
if (!incoming.length) return fence();
return ret({
kind: "libCall",
fn: "emitter.emitFlex",
args: [receiver, lowerer.coerceToExpected(incoming[0]!, STRING), ...incoming.slice(1)],
type: BOOL,
loc,
});
const event = lowerer.coerceToExpected(incoming[0]!, STRING);
const bridged = streamRooted(info)
? null
: lowerDynamicEmitterEmit(lowerer, receiver, event, incoming.slice(1), loc);
return ret(
bridged ?? {
kind: "libCall",
fn: "emitter.emitAny",
args: [receiver, event, ...incoming.slice(1)],
type: BOOL,
loc,
},
);
}
if (registering || removing) {
if (incoming.length < 2) return fence();
const event = lowerer.coerceToExpected(incoming[0]!, STRING);
const cb = incoming[1]!;
const bridged =
registering && !streamRooted(info)
? lowerDynamicEmitterOn(
lowerer,
receiver,
event,
cb,
method === "once" || method === "prependOnceListener",
method.startsWith("prepend"),
loc,
)
: null;
if (bridged) return ret(bridged);
return [
{
kind: "exprStmt",
Expand All @@ -5113,7 +5229,7 @@ export class ClassDynamicDispatch {
fn: registering
? info.builtinStream
? "stream.onDyn"
: "emitter.onFlex"
: "emitter.onAny"
: "emitter.offDyn",
args: [
receiver,
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4090,6 +4090,12 @@ const DYN_DISPATCH_METHODS = new Set([
"prependOnceListener",
"listeners",
"listenerCount",
// The rest of EventEmitter.prototype, which compiled emitters answer
// through the class dispatch (class-dynamic-dispatch.ts).
"rawListeners",
"eventNames",
"setMaxListeners",
"getMaxListeners",
"write",
"end",
"destroy",
Expand Down
Loading
Loading