From 48dd898db6382f2a8542512e176183be3df613fd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 19:16:06 -0500 Subject: [PATCH] Support non-Error 'error' payloads and checked-dynamic listeners in listeners() and meta events --- .../limitations/builtin-apis/eventemitter.mdx | 5 +- .../compiler/src/backend/llvm/lib-process.ts | 28 ++ .../frontend/lowering/lower-event-emitter.ts | 303 +++++++++++++++--- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/runtime-features.ts | 2 + packages/compiler/src/ir/validate.ts | 8 + .../compiler/test/emitter-flex-events.test.ts | 11 - .../emitter-computed-name-listeners.ts.json | 6 + .../emitter-listeners-dynamic-typed.ts.json | 6 + .../corpus/emitter-listeners-dynamic.cjs.json | 6 + .../emitter-meta-listeners-dynamic.cjs.json | 6 + ...ter-unhandled-error-payloads-typed.ts.json | 6 + .../emitter-unhandled-error-payloads.cjs.json | 6 + .../diagnostics/ee-computed-listeners.ts.json | 6 - packages/runtime/src/scr_events_emitter.c | 72 ++++- packages/runtime/src/scr_events_emitter.h | 4 + .../corpus/emitter-computed-name-listeners.ts | 14 + .../corpus/emitter-listeners-dynamic-typed.ts | 15 + tests/corpus/emitter-listeners-dynamic.cjs | 32 ++ .../corpus/emitter-meta-listeners-dynamic.cjs | 37 +++ .../emitter-unhandled-error-payloads-typed.ts | 46 +++ .../emitter-unhandled-error-payloads.cjs | 67 ++++ tests/diagnostics/ee-computed-listeners.ts | 6 - .../ee-computed-listeners.ts.txt | 8 - .../emitter-computed-name-listeners.ts.json | 1 + .../emitter-listeners-dynamic-typed.ts.json | 1 + .../emitter-listeners-dynamic.cjs.json | 1 + .../emitter-meta-listeners-dynamic.cjs.json | 1 + ...ter-unhandled-error-payloads-typed.ts.json | 1 + .../emitter-unhandled-error-payloads.cjs.json | 1 + 31 files changed, 614 insertions(+), 96 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-computed-name-listeners.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-dynamic.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads.cjs.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/ee-computed-listeners.ts.json create mode 100644 tests/corpus/emitter-computed-name-listeners.ts create mode 100644 tests/corpus/emitter-listeners-dynamic-typed.ts create mode 100644 tests/corpus/emitter-listeners-dynamic.cjs create mode 100644 tests/corpus/emitter-meta-listeners-dynamic.cjs create mode 100644 tests/corpus/emitter-unhandled-error-payloads-typed.ts create mode 100644 tests/corpus/emitter-unhandled-error-payloads.cjs delete mode 100644 tests/diagnostics/ee-computed-listeners.ts delete mode 100644 tests/harness/__snapshots__/ee-computed-listeners.ts.txt create mode 100644 tests/harness/corpus-costs/emitter-computed-name-listeners.ts.json create mode 100644 tests/harness/corpus-costs/emitter-listeners-dynamic-typed.ts.json create mode 100644 tests/harness/corpus-costs/emitter-listeners-dynamic.cjs.json create mode 100644 tests/harness/corpus-costs/emitter-meta-listeners-dynamic.cjs.json create mode 100644 tests/harness/corpus-costs/emitter-unhandled-error-payloads-typed.ts.json create mode 100644 tests/harness/corpus-costs/emitter-unhandled-error-payloads.cjs.json diff --git a/docs/content/limitations/builtin-apis/eventemitter.mdx b/docs/content/limitations/builtin-apis/eventemitter.mdx index 0e085c56f..6bd1dab79 100644 --- a/docs/content/limitations/builtin-apis/eventemitter.mdx +++ b/docs/content/limitations/builtin-apis/eventemitter.mdx @@ -1,10 +1,11 @@ - **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. + Unrestricted names, symbols, and names that may reach internal events 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. + A `newListener` or `removeListener` listener registered through an untyped value receives `undefined` in place of a listener registered with typed parameters. + A typed `error` or `errorMonitor` listener throws a `TypeError` when an emitted payload that is not an Error does not convert to its parameter. 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. diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index d8b94fac0..fe0ad7dc7 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -951,6 +951,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr host.emitPendingCheck(); return { name: t, type: e.type }; } + if (e.fn === "emitter.emitErrorValue") { + // (recv, ...payload as dyn): emit('error') with a non-Error payload. + // The unhandled case renders the payload with util.inspect + // (scr_insp_dyn), which this call links. + const args = e.args.map((a) => host.emitExpr(a)); + const n = args.length - 1; + let payload = "null"; + if (n > 0) { + const arr = B.slot(); + B.entryAllocas.push(`${arr} = alloca [${n} x ptr]`); + for (let i = 0; i < n; i++) { + const slot = B.tmp(); + B.line(`${slot} = getelementptr inbounds [${n} x ptr], ptr ${arr}, i64 0, i64 ${i}`); + B.line(`store ptr ${args[i + 1]!.name}, ptr ${slot}`); + } + payload = arr; + } + host.declare(`declare ptr @scr_insp_dyn(ptr, double, double)`); + host.declare( + `declare zeroext i1 @scr_emitter_emit_error_value(ptr, ptr, ${host.sizeType}, ptr)`, + ); + const t = B.tmp(); + B.line( + `${t} = call i1 @scr_emitter_emit_error_value(ptr ${args[0]!.name}, ptr ${payload}, ${host.sizeType} ${n}, ptr @scr_insp_dyn)`, + ); + host.emitPendingCheck(); + return { name: t, type: e.type }; + } if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") { const args = e.args.map((a) => host.emitExpr(a)); const n = args.length - 2; diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index 92cc7f23d..07efba81d 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -44,6 +44,7 @@ * super.emit as the prototype chain). */ import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; +import { stripTypeCasts } from "./builtins/arguments.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { dynFallbackType, newFnCtx } from "./lowerer.js"; @@ -120,6 +121,54 @@ export interface EventSig { unmappedEmit?: true; /** A listener with any concrete parameter still needs the fixed ABI. */ typedListener?: true; + /** listeners(), rawListeners() or events.getEventListeners() reads the + * event's listener array. With a dyn-flavored listener present, every + * registration of the event goes through the dyn adapter, so the array + * holds checked-dynamic function values (listenerArrayIsDyn). */ + listenersRead?: true; + /** ('error' only) An emit passes a payload that is not statically an + * Error, which typed listeners of 'error' and errorMonitor receive as a + * checked-dynamic value. */ + valueEmit?: true; +} + +/** Whether `t` is an instance type of the Error hierarchy. */ +function rootsAtError(lowerer: Lowerer, t: IrType | null): boolean { + if (t?.kind !== "object") return false; + for (let c: ClassInfo | null = lowerer.classes.get(t.className) ?? null; c; c = c.base) { + if (c.def.name === "%Error") return true; + } + return false; +} + +/** Whether a typed listener of `name` registers through the dyn adapter, so + * its entry holds a checked-dynamic function value: a dyn-flavored + * 'newListener' or 'removeListener' listener receives every added or + * removed listener as one, and a read listener array beside dyn-flavored + * listeners holds them. Listeners of 'error' and errorMonitor do when an + * emit passes a payload that is not an Error: the adapter's checked + * conversion admits it or throws a TypeError. */ +function registersAsDyn(lowerer: Lowerer, name: string, streamForced: boolean): boolean { + const table = emitterEvents(lowerer); + if ([...META_EVENTS].some((meta) => table.get(meta)?.dynListener === true)) return true; + if ((name === "error" || name === ERROR_MONITOR_EVENT) && table.get("error")?.valueEmit === true) + return true; + const sig = streamForced ? undefined : table.get(name); + return sig?.dynListener === true && sig.listenersRead === true; +} + +/** Whether the event's listener array is read as checked-dynamic values: + * its registrations all hold a checked-dynamic function value (flexible + * and computed-name events, or a dyn-flavored listener beside a read of + * the array). */ +function listenerArrayIsDyn(lowerer: Lowerer, name: string, sig: EventSig | undefined): boolean { + return ( + (sig?.dynListener === true && sig.listenersRead === true) || + flexibleJsEvent(name, sig) || + computedPatterns(lowerer).some( + (pattern) => pattern.listener && patternMatchesName(pattern, name), + ) + ); } function flexibleJsEvent(name: string, sig: EventSig | undefined): boolean { @@ -286,12 +335,48 @@ function emitterEvents(lowerer: Lowerer): Map { } }; + // Events whose listener array is read; applied to the table after the + // walk, so a read alone adds no entry. + const listenersRead = new Set(); for (const sf of lowerer.program.getImplementationSourceFiles()) { if (sf.isDeclarationFile) continue; const walk = (node: ts.Node): void => { ts.forEachChild(node, walk); - if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return; - const member = node.expression.name.text; + if (!ts.isCallExpression(node)) return; + const callee = node.expression; + if ( + (ts.isIdentifier(callee) || ts.isPropertyAccessExpression(callee)) && + (ts.isIdentifier(callee) ? callee : callee.name).text === "getEventListeners" && + node.arguments.nodes.length === 2 + ) { + const nameT = (() => { + try { + return lowerer.typeOf(node.arguments.nodes[1]!); + } catch { + return null; + } + })(); + if (nameT?.isStringLiteralType()) listenersRead.add(literalEventKey(nameT.value)); + return; + } + if (!ts.isPropertyAccessExpression(callee)) return; + const member = callee.name.text; + if (member === "listeners" || member === "rawListeners") { + const arg = node.arguments.nodes[0]; + let nameT: ts.Type | null = null; + try { + if ( + !arg || + !scannedEmitterInfo(lowerer, lowerer.mapTypeOf(lowerer.typeOf(callee.expression))) + ) + return; + nameT = lowerer.typeOf(arg); + } catch { + return; + } + if (nameT.isStringLiteralType()) listenersRead.add(literalEventKey(nameT.value)); + return; + } const isEmit = member === "emit"; if (!isEmit && !REGISTER_MEMBERS.has(member)) return; const arg0 = node.arguments.nodes[0]; @@ -299,7 +384,7 @@ function emitterEvents(lowerer: Lowerer): Map { let recvT: IrType | null = null; let nameT: ts.Type | null = null; try { - recvT = lowerer.mapTypeOf(lowerer.typeOf(node.expression.expression)); + recvT = lowerer.mapTypeOf(lowerer.typeOf(callee.expression)); nameT = lowerer.typeOf(arg0); } catch { return; // checker trouble is the lowering's business, not the scan's @@ -323,6 +408,46 @@ function emitterEvents(lowerer: Lowerer): Map { return; } const name = literal; + if (META_EVENTS.has(name) && !isEmit && node.arguments.nodes[1]) { + // A dyn-flavored meta listener receives each added or removed + // listener as a checked-dynamic function value. The scan cannot + // lower the listener as dynListenerFlavor does, so it counts every + // listener that is not statically a typed function: a factory's + // result (a wrapper whose runtime value is checked-dynamic), a + // JavaScript function without parameters, and the dyn-flavored + // forms. Counting one too many only routes more registrations + // through the adapter. + const cbNode = node.arguments.nodes[1]; + let typed = false; + try { + const cbCt = lowerer.typeOf(cbNode); + const cbT = lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, cbNode, cbCt); + typed = + cbT?.kind === "func" && + !ts.isCallExpression(stripTypeCasts(cbNode)) && + !(cbT.params.length === 0 && isJsSourceFile(sf)) && + !cbT.params.some((p) => p.kind === "dyn") && + !readsArgumentsPack(lowerer, cbNode); + } catch { + typed = false; + } + if (!typed) table.get(name)!.dynListener = true; + return; + } + if (isEmit && name === "error") { + try { + const payload = node.arguments.nodes.slice(1); + if ( + payload.length !== 1 || + !rootsAtError(lowerer, lowerer.mapTypeOf(lowerer.typeOf(payload[0]!))) + ) { + table.get("error")!.valueEmit = true; + } + } catch { + table.get("error")!.valueEmit = true; + } + return; + } if (META_EVENTS.has(name) || name === "error" || name === ERROR_MONITOR_EVENT) return; // forced tuples // Stream receivers' runtime-emitted events carry PER-BASE forced // tuples (lower-stream.ts) — their sites never join the program- @@ -377,6 +502,10 @@ function emitterEvents(lowerer: Lowerer): Map { }; walk(sf); } + for (const name of listenersRead) { + const sig = table.get(name); + if (sig) sig.listenersRead = true; + } lowerer.emitterEventTable = table; lowerer.emitterComputedPatterns = computedPatterns; return table; @@ -604,6 +733,7 @@ function lowerFlexListenerCall( once: boolean, prepend: boolean, loc: SrcLoc, + onFn: "emitter.onFlex" | "emitter.onAny" = "emitter.onFlex", ): IrExpr { if (receiver.type.kind !== "object") { lowerer.noLowering( @@ -633,10 +763,10 @@ function lowerFlexListenerCall( lowerer.noLowering(`${member} with a non-function computed-name listener`, cbNode); } const recvT = receiver.type; - const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; + const key = `${registering ? onFn : "emitter.offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); + helper = keyedHelperName(lowerer, `%${registering ? onFn : "emitter.offFlex"}`, key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -671,7 +801,7 @@ function lowerFlexListenerCall( kind: "return", value: { kind: "libCall", - fn: registering ? "emitter.onFlex" : "emitter.offDyn", + fn: registering ? onFn : "emitter.offDyn", args: [ varRef("r.0", recvT, loc), varRef("n.0", STRING, loc), @@ -699,6 +829,59 @@ function lowerFlexListenerCall( loc, }; } +/** A lowered value as a checked-dynamic payload: boxed as any untyped + * value is (records and arrays keep their identity), otherwise a refusal + * at the argument. */ +function boxedPayload(lowerer: Lowerer, value: IrExpr, node: ts.Expression): IrExpr { + if (value.type.kind === "dyn") return value; + if ( + value.kind === "unitLit" || + canConvertToDyn( + value.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return lowerer.coerceToExpected(value, DYN); + } + lowerer.noLowering( + `emit('error') with a '${lowerer.fmt(value.type)}' payload`, + node, + "the payload cannot box into a dynamic value", + ); +} + +/** The listener array of an event whose registrations all hold a checked- + * dynamic function value (listenerArrayIsDyn), each comparing equal to the + * function that was registered. JavaScript sources read the untyped array, + * whose functions take every argument a call passes. TypeScript sources + * read the element type the typed path declares, the event's tuple: a + * listener of exactly that signature unwraps to the original closure. */ +function dynListenerArray( + lowerer: Lowerer, + table: Map, + receiver: IrExpr, + name: string, + call: ts.CallExpression, + loc: SrcLoc, +): IrExpr { + const array: IrExpr = { + kind: "libCall", + fn: "emitter.listenersDyn", + args: [receiver, eventNameExpr(name, loc)], + type: DYN, + loc, + }; + if (isJsSourceFile(ts.getSourceFileOfNode(call))) return array; + const tuple = tupleOf(lowerer, table, name, call); + return { + kind: "dynCheck", + value: array, + type: arrayOf({ kind: "func", params: tuple, ret: { kind: "void" } }), + loc, + }; +} + /** The event's unified tuple, with conflicts reported at this site. */ function tupleOf( lowerer: Lowerer, @@ -820,10 +1003,29 @@ function lowerDynListenerCall( streamData: boolean, ): IrExpr { if (META_EVENTS.has(name)) { - lowerer.noLowering( - `'${member}' of '${name}' with a checked-dynamic listener`, + // The registration installs the registry's dynamic half: the listener + // receives the event name and the added or removed listener's + // checked-dynamic function value, as in Node. Every other listener + // then registers through the adapter (registersAsDyn), which the event + // table's scan arranges. + if (registering && emitterEvents(lowerer).get(name)?.dynListener !== true) { + lowerer.noLowering( + `'${member}' of '${name}' with a checked-dynamic listener the event table did not record`, + cbNode, + "register the listener directly at this call", + ); + } + return lowerFlexListenerCall( + lowerer, + member, + eventNameExpr(name, loc), + receiver, cbNode, - "Node passes the listener FUNCTION as the meta event's second argument, which has no dynamic conversion — annotate the listener's parameter as a string", + registering, + once, + prepend, + loc, + "emitter.onAny", ); } const getRecord = (id: string) => lowerer.shapes.get(id); @@ -1490,7 +1692,9 @@ export function lowerEmitterMethodCall( // to the declared type, or take the value (dyn). const sides = streamSidesOf(lowerer, info); if (name === "data" && (sides === "r" || sides === "rw")) { - const dynFlavor = dynListenerFlavor(lowerer, args[1]!); + const dynFlavor = + dynListenerFlavor(lowerer, args[1]!) ?? + (registersAsDyn(lowerer, name, true) ? "func" : null); if (dynFlavor !== null) { return lowerDynListenerCall( lowerer, @@ -1575,7 +1779,11 @@ export function lowerEmitterMethodCall( const probe = tryLowerExpression(lowerer, args[1]!); if (probe) checkKeypressListener(lowerer, args[1]!, probe); } - const dynFlavor = dynListenerFlavor(lowerer, args[1]!) ?? (keypress ? "func" : null); + const dynFlavor = + dynListenerFlavor(lowerer, args[1]!) ?? + (keypress || registersAsDyn(lowerer, name, streamForcedTuple(lowerer, info, name) !== null) + ? "func" + : null); if (dynFlavor !== null) { return lowerDynListenerCall( lowerer, @@ -1654,29 +1862,36 @@ export function lowerEmitterMethodCall( ); const receiver = lowerReceiver(); if (name === "error") { - // The special event: exactly one %Error-rooted payload; no listener - // means the runtime THROWS it (emitter.emitError, may-throw). - if (args.length !== 2) { - lowerer.noLowering( - `emit('error') with ${args.length - 1} payload arguments`, - call, - "the supported form is emit('error', err) with an Error-hierarchy payload (non-Error payloads would need Node's ERR_UNHANDLED_ERROR wrapping — no lowering yet)", - ); - } - const err = lowerer.lowerExpr(args[1]!); - const rootsAtError = (t: IrType): boolean => { - if (t.kind !== "object") return false; - for (let c: ClassInfo | null = lowerer.classes.get(t.className) ?? null; c; c = c.base) { - if (c.def.name === "%Error") return true; + // The special event: one %Error-rooted payload; no listener means the + // runtime THROWS it (emitter.emitError, may-throw). + const err = args.length === 2 ? lowerer.lowerExpr(args[1]!) : null; + if (err === null || !rootsAtError(lowerer, err.type)) { + // Any other payload, or another argument count: the arguments box + // to dyn. With no 'error' listener the runtime throws Node's + // ERR_UNHANDLED_ERROR (the payload's util.inspect in the message, + // the payload as `context`); listeners receive the arguments as + // given. + if (emitOverrideAtOrAbove(info) || superRecv !== undefined) { + lowerer.noLowering( + `emit('error') with a non-Error payload through an emit override`, + call, + "the override specialization takes the event's one Error payload", + ); } - return false; - }; - if (!rootsAtError(err.type)) { - lowerer.noLowering( - `emit('error') with a '${lowerer.fmt(err.type)}' payload`, - args[1]!, - "the supported payload is an Error-hierarchy instance", - ); + const payload = args + .slice(1) + .map((arg, i) => + i === 0 && err !== null + ? boxedPayload(lowerer, err, arg) + : lowerer.lowerExprExpecting(arg, DYN), + ); + return { + kind: "libCall", + fn: "emitter.emitErrorValue", + args: [receiver, ...payload], + type: BOOL, + loc, + }; } return emitDispatchExpr( lowerer, @@ -1824,13 +2039,10 @@ export function lowerEmitterMethodCall( } const name = eventNameOf(lowerer, member, args[0]!); const sig = table.get(name); - if ( - sig?.dynListener || - flexibleJsEvent(name, sig) || - computedPatterns(lowerer).some( - (pattern) => pattern.listener && patternMatchesName(pattern, name), - ) - ) { + if (listenerArrayIsDyn(lowerer, name, sig)) { + return dynListenerArray(lowerer, table, lowerReceiver(), name, call, loc); + } + if (sig?.dynListener) { // A dyn-adapted registration means the runtime bucket can hold // originals of MIXED signatures — no one honest element type. lowerer.noLowering( @@ -2809,13 +3021,10 @@ export function lowerEventsEmitterHelper( const name = eventNameOf(lowerer, "events.getEventListeners", args[1]!); const table = emitterEvents(lowerer); const sig = table.get(name); - if ( - sig?.dynListener || - flexibleJsEvent(name, sig) || - computedPatterns(lowerer).some( - (pattern) => pattern.listener && patternMatchesName(pattern, name), - ) - ) { + if (listenerArrayIsDyn(lowerer, name, sig)) { + return dynListenerArray(lowerer, table, receiver, name, call, loc); + } + if (sig?.dynListener) { lowerer.noLowering( `events.getEventListeners of the event '${name}'`, call, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index dce26c486..4ee5fc609 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -317,6 +317,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "emitter.emit", "emitter.emitFlex", "emitter.emitAny", + "emitter.emitErrorValue", "emitter.emitMixed", "emitter.listenersDyn", "emitter.emitError", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 844e7ed83..7c9b5a0fa 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4349,6 +4349,9 @@ function buildLibFnSignatures() { "emitter.emit": { argTypes: [null, STRING], result: BOOL }, "emitter.emitFlex": { argTypes: [null, STRING], result: BOOL }, "emitter.emitAny": { argTypes: [null, STRING], result: BOOL }, + /** emit('error', ...payload) with a non-Error payload: (recv, then the + * arguments boxed) — VARIADIC like emitFlex. */ + "emitter.emitErrorValue": { argTypes: [null], result: BOOL }, /** An untyped emit of an event with a fixed tuple: (recv, name, k, then * k checked-dynamic arguments as given, then the event's converted * tuple) — VARIADIC like emitter.emit. hasTyped answers whether a diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index cf237747b..2bff90234 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1121,6 +1121,8 @@ const RUNTIME_FEATURE_RULES = { "webstream.class", "webstream.install", "util.styleText", + // ERR_UNHANDLED_ERROR quotes util.inspect of a non-Error payload. + "emitter.emitErrorValue", "crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 8d3a735ee..0ab7ee196 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4154,6 +4154,7 @@ function validateFunction( e.fn === "emitter.emit" || e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny" || + e.fn === "emitter.emitErrorValue" || e.fn === "emitter.emitMixed" || e.fn === "readable.new" || e.fn === "writable.new" || @@ -6191,6 +6192,12 @@ function validateFunction( err(`libCall ${e.fn} payloads must be checked-dynamic`, e.loc); } } + if ( + e.fn === "emitter.emitErrorValue" && + e.args.slice(1).some((arg) => arg.type.kind !== "dyn") + ) { + err(`libCall emitter.emitErrorValue payloads must be checked-dynamic`, e.loc); + } if (e.fn === "emitter.emitMixed") { const count = e.args[2]; const k = count?.kind === "numLit" ? count.value : -1; @@ -6204,6 +6211,7 @@ function validateFunction( e.fn === "emitter.emit" || e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny" || + e.fn === "emitter.emitErrorValue" || e.fn === "emitter.emitMixed" || e.fn === "emitter.hasTyped" || e.fn === "emitter.listenersDyn" || diff --git a/packages/compiler/test/emitter-flex-events.test.ts b/packages/compiler/test/emitter-flex-events.test.ts index 8677e9ecd..fa21ca349 100644 --- a/packages/compiler/test/emitter-flex-events.test.ts +++ b/packages/compiler/test/emitter-flex-events.test.ts @@ -17,17 +17,6 @@ test.each([ const ee = new Derived(); ee.on('value', (value) => console.log(value)); ee.emit('value'); ee.emit('value', 1);`, "variable-arity emit through an EventEmitter subclass overriding emit", ], - [ - "listener arrays", - `const ee = new EventEmitter(); ee.on('value', () => {}); - ee.emit('value'); ee.emit('value', 1); console.log(ee.listeners('value'));`, - "listeners of the event 'value'", - ], - [ - "special error events", - `const ee = new EventEmitter(); ee.on('error', (value) => console.log(value)); ee.emit('error');`, - "emit('error') with 0 payload arguments", - ], [ "reserved stream events", `const ee = new EventEmitter(); ee.on('data', (value) => console.log(value)); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-computed-name-listeners.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-computed-name-listeners.ts.json new file mode 100644 index 000000000..a507a6479 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-computed-name-listeners.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-computed-name-listeners.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic-typed.ts.json new file mode 100644 index 000000000..e3b8e0a8a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-listeners-dynamic-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic.cjs.json new file mode 100644 index 000000000..08d14e258 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-listeners-dynamic.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-listeners-dynamic.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-dynamic.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-dynamic.cjs.json new file mode 100644 index 000000000..ae062ddc9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-dynamic.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-meta-listeners-dynamic.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads-typed.ts.json new file mode 100644 index 000000000..69794a8b2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-unhandled-error-payloads-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads.cjs.json new file mode 100644 index 000000000..adbfaf1d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-unhandled-error-payloads.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-unhandled-error-payloads.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/ee-computed-listeners.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/ee-computed-listeners.ts.json deleted file mode 100644 index e5e275d35..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/ee-computed-listeners.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/ee-computed-listeners.ts" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 002f16f5a..c72662670 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -1193,13 +1193,29 @@ bool scr_emitter_has_typed(ScrEmitter *em, ScrStr *name) { return false; } -/* Node's ERR_UNHANDLED_ERROR for a non-Error payload: the message quotes - * util.inspect of the payload. This unit renders the primitive payloads - * inspect prints plainly; others report that they are not supported. */ -static void scr_ee_unhandled_error(const ScrDyn *er) { +/* Throws Node's ERR_UNHANDLED_ERROR for a non-Error payload: the message + * quotes `shown` (util.inspect of the payload) and `context` is the + * payload. */ +SCR_COLD static void scr_ee_throw_unhandled(const char *shown, size_t len, const ScrDyn *er) { ScrJsonBuf text; scr_jb_init(&text); scr_jb_puts(&text, "Unhandled error. ("); + scr_jb_write(&text, shown, len); + scr_jb_putc(&text, ')'); + ScrStr *message = scr_jb_finish(&text); + ScrError *err = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(err, "ERR_UNHANDLED_ERROR"); + scr_error_set_prop(err, "context", 7, scr_dyn_retain((ScrDyn *)er)); + scr_throw_obj(err, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* ERR_UNHANDLED_ERROR through an untyped value. This unit renders the + * primitive payloads inspect prints plainly; others report that they are + * not supported. */ +static void scr_ee_unhandled_error(const ScrDyn *er) { + ScrJsonBuf text; + scr_jb_init(&text); bool shown = true; switch (er->kind) { case SCR_DYN_UNDEF: scr_jb_puts(&text, "undefined"); break; @@ -1233,13 +1249,7 @@ static void scr_ee_unhandled_error(const ScrDyn *er) { } ScrStr *message = scr_jb_finish(&text); if (shown) { - ScrJsonBuf full; - scr_jb_init(&full); - scr_jb_write(&full, message->data, message->len); - scr_jb_putc(&full, ')'); - ScrStr *rendered = scr_jb_finish(&full); - scr_throw_error_msg_code(SCR_ERR_ERROR, rendered->data, rendered->len, "ERR_UNHANDLED_ERROR"); - scr_str_release(rendered); + scr_ee_throw_unhandled(message->data, message->len, er); } else { const char *refusal = "emit('error') through an untyped value with this non-Error payload and no 'error' listener is not supported yet"; @@ -1251,16 +1261,35 @@ static void scr_ee_unhandled_error(const ScrDyn *er) { /* emit('error', ...) through an untyped value: errorMonitor first; with no * 'error' listener an Error payload throws itself and anything else throws * ERR_UNHANDLED_ERROR, Node's contract; otherwise listeners receive the - * arguments as given, and typed listeners the payload as an Error. */ -static bool scr_ee_emit_error_dyn(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc) { + * arguments as given, and typed listeners the payload as an Error. `render` + * (util.inspect, when the call site links it) quotes the payload in the + * ERR_UNHANDLED_ERROR message; without it only primitive payloads render. */ +static bool scr_ee_emit_error_dyn(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc, + ScrStr *(*render)(ScrDyn *, double, double)) { const ScrDyn *er = argc ? args[0] : scr_dyn_undefined(); ScrError *err = scr_dyn_err_instanceof(er, SCR_ERR_ERROR) ? scr_errdyn_err_of(er) : NULL; bool had = false; if (!scr_ee_has(em->reg, "error")) { scr_ee_emit_mixed_va(em, SCR_EE_ERROR_MONITOR, args, argc, err != NULL, err); if (!scr_exc_pending()) { - if (err) scr_dyn_throw(scr_dyn_retain((ScrDyn *)er)); - else scr_ee_unhandled_error(er); + if (err) { + scr_dyn_throw(scr_dyn_retain((ScrDyn *)er)); + } else if (render) { + ScrStr *shown = render((ScrDyn *)er, 0, 2); + if (scr_exc_pending()) { + /* A throwing inspect: Node quotes the payload's string conversion + * instead, whose own throw propagates. */ + if (shown) scr_str_release(shown); + scr_exc_clear(); + shown = scr_dyn_string_coerce_js(er); + } + if (shown) { + scr_ee_throw_unhandled(shown->data, shown->len, er); + scr_str_release(shown); + } + } else { + scr_ee_unhandled_error(er); + } } } else { had = scr_ee_emit_mixed_va(em, name, args, argc, err != NULL, err); @@ -1269,6 +1298,17 @@ static bool scr_ee_emit_error_dyn(ScrEmitter *em, ScrStr *name, ScrDyn *const *a return had; } +/* emit('error', ...args) from typed code whose payload is not statically + * an Error (the arguments boxed): scr_ee_emit_error_dyn, quoting the + * payload with `render`. */ +bool scr_emitter_emit_error_value(ScrEmitter *em, ScrDyn *const *args, size_t argc, + ScrStr *(*render)(ScrDyn *, double, double)) { + ScrStr *name = scr_str_new("error", 5); + bool had = scr_ee_emit_error_dyn(em, name, args, argc, render); + scr_str_release(name); + return had; +} + /* An untyped emit of a name typed code never fixes a tuple for (computed * names, runtime-internal registries): the arguments as given. */ bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc) { @@ -1288,7 +1328,7 @@ bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMess * typed listeners with a string name. */ bool scr_emitter_emit_any(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc) { if (!scr_ee_is_error_monitor(name)) { - if (scr_ee_is_name(name, "error")) return scr_ee_emit_error_dyn(em, name, args, argc); + if (scr_ee_is_name(name, "error")) return scr_ee_emit_error_dyn(em, name, args, argc, NULL); if (scr_ee_is_name(name, "newListener") || scr_ee_is_name(name, "removeListener")) { ScrStr *s = argc && args[0]->kind == SCR_DYN_STR ? args[0]->v.str : NULL; return scr_ee_emit_mixed_va(em, name, args, argc, s != NULL, s); diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index 29d4c397f..94578150f 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -109,6 +109,10 @@ ScrEmitter *scr_emitter_on_flex(ScrEmitter *em, ScrStr *name, const ScrDyn *cb, ScrEmitter *scr_emitter_on_any(ScrEmitter *em, ScrStr *name, const ScrDyn *cb, bool once, bool prepend); bool scr_emitter_emit_any(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc); +/* emit('error', ...args) with a non-Error payload from typed code, the + * arguments boxed; `render` is util.inspect's dyn entry (scr_insp_dyn). */ +bool scr_emitter_emit_error_value(ScrEmitter *em, ScrDyn *const *args, size_t argc, + ScrStr *(*render)(ScrDyn *, double, double)); ScrEmitter *scr_emitter_off_dyn(ScrEmitter *em, ScrStr *name, const ScrDyn *cb); /* The registry's checked-dynamic half (meta-event argument vectors and * rebuilt listener values), installed by the first on_any call; NULL in diff --git a/tests/corpus/emitter-computed-name-listeners.ts b/tests/corpus/emitter-computed-name-listeners.ts new file mode 100644 index 000000000..324181e23 --- /dev/null +++ b/tests/corpus/emitter-computed-name-listeners.ts @@ -0,0 +1,14 @@ +// listeners() and rawListeners() from TypeScript of an event registered +// under a computed name: the registered functions in order. +import { EventEmitter } from "node:events"; + +const emitter = new EventEmitter(); +const suffix = process.argv[2] ?? "x"; +const onTopic = (): void => console.log("topic listener"); +emitter.on(`topic:${suffix}`, onTopic); +emitter.on(`topic:${suffix}`, () => console.log("second")); +console.log(emitter.listeners("topic:x").length, emitter.listeners("topic:x")[0] === onTopic); +for (const l of emitter.listeners("topic:x")) l(); +console.log(emitter.listeners("topic:y").length); +emitter.off("topic:x", onTopic); +console.log(emitter.rawListeners("topic:x").length); diff --git a/tests/corpus/emitter-listeners-dynamic-typed.ts b/tests/corpus/emitter-listeners-dynamic-typed.ts new file mode 100644 index 000000000..50b1ef060 --- /dev/null +++ b/tests/corpus/emitter-listeners-dynamic-typed.ts @@ -0,0 +1,15 @@ +// listeners() and rawListeners() from TypeScript of an event with a typed +// listener and a checked-dynamic one: each element is the registered +// function, called with the event's arguments. +import { EventEmitter } from "node:events"; +const e = new EventEmitter(); +const typed = (n: number, s: string): void => console.log("typed", n, s); +const loose = ((...args: unknown[]) => console.log("loose", args.length)) as any; +e.on("tick", typed); +e.on("tick", loose); +e.emit("tick", 1, "one"); +console.log(e.listeners("tick").length, e.listeners("tick")[0] === typed, e.rawListeners("tick")[1] === loose); +for (const l of e.listeners("tick")) (l as (n: number, s: string) => void)(2, "two"); +e.off("tick", typed); +e.emit("tick", 3, "three"); +console.log(e.listenerCount("tick"), e.listeners("tick").length); diff --git a/tests/corpus/emitter-listeners-dynamic.cjs b/tests/corpus/emitter-listeners-dynamic.cjs new file mode 100644 index 000000000..573aaf5f4 --- /dev/null +++ b/tests/corpus/emitter-listeners-dynamic.cjs @@ -0,0 +1,32 @@ +// listeners(), rawListeners() and getEventListeners() of events whose +// listeners include functions with untyped parameters or that read +// arguments: the array holds the registered functions in order. +const { EventEmitter, getEventListeners } = require('events'); +const e = new EventEmitter(); +function typed() { console.log('typed'); } +function packed() { console.log('packed', arguments.length); } +function withParam(a) { console.log('withParam', a); } +e.on('foo', typed); +e.on('foo', packed); +e.once('foo', withParam); +let fl = e.listeners('foo'); +console.log(fl.length, fl[0] === typed, fl[1] === packed, fl[2] === withParam); +const raw = e.rawListeners('foo'); +console.log(raw.length, raw[2] === withParam || typeof raw[2].listener === 'function'); +console.log(getEventListeners(e, 'foo').length); +e.emit('foo', 1, 2); +fl = e.listeners('foo'); +console.log(fl.length); +fl[0](); +fl[1](7, 8, 9); +e.off('foo', typed); +console.log(e.listeners('foo').length, e.listeners('foo')[0] === packed); +e.removeListener('foo', packed); +console.log(e.listeners('foo').length, e.listenerCount('foo')); +const g = new EventEmitter(); +g.on('bar', function (x) { console.log('bar', x); }); +g.on('bar', (x, y) => console.log('bar2', x, y)); +g.emit('bar', 'a'); +g.emit('bar', 'a', 'b', 'c'); +console.log(g.listeners('bar').length, typeof g.listeners('bar')[1]); +console.log(e.listeners('nothing').length); diff --git a/tests/corpus/emitter-meta-listeners-dynamic.cjs b/tests/corpus/emitter-meta-listeners-dynamic.cjs new file mode 100644 index 000000000..d048d33ad --- /dev/null +++ b/tests/corpus/emitter-meta-listeners-dynamic.cjs @@ -0,0 +1,37 @@ +// Checked-dynamic 'newListener' and 'removeListener' listeners receive the +// event name and the added or removed listener function. +const EventEmitter = require('events'); +function listener1() {} +function listener2(a) { console.log('l2', a); } +const ee = new EventEmitter(); +ee.on('newListener', function (name, fn) { + console.log('new', name, fn === listener1, fn === listener2, typeof fn); +}); +ee.on('hello', listener1); +ee.on('hello', listener2); +ee.on('removeListener', (name, cb) => { + console.log('removed', name, cb === listener1, cb === listener2); +}); +ee.removeListener('hello', listener1); +ee.emit('hello', 5); +console.log(ee.listeners('hello').length, ee.listeners('hello')[0] === listener2); +ee.removeAllListeners('hello'); +console.log(ee.listenerCount('hello')); +const other = new EventEmitter(); +other.once('removeListener', (name, cb) => console.log('once-removed', name, typeof cb)); +other.on('x', (n) => console.log('x', n)); +other.emit('x', 1); +other.removeAllListeners(); +console.log(other.eventNames()); + +// A meta listener produced by a wrapper factory, as test helpers do. +function wrap(fn) { + return function () { + return fn.apply(this, arguments); + }; +} +function plain() {} +const wrapped = new EventEmitter(); +wrapped.on('ping', plain); +wrapped.on('removeListener', wrap((name, cb) => console.log('wrapped removed', name, cb === plain))); +wrapped.removeListener('ping', plain); diff --git a/tests/corpus/emitter-unhandled-error-payloads-typed.ts b/tests/corpus/emitter-unhandled-error-payloads-typed.ts new file mode 100644 index 000000000..1bb90e7f7 --- /dev/null +++ b/tests/corpus/emitter-unhandled-error-payloads-typed.ts @@ -0,0 +1,46 @@ +// emit('error', payload) from typed code with a payload that is not an +// Error: Node's ERR_UNHANDLED_ERROR when nothing listens, the payload as +// given otherwise. +import { EventEmitter, errorMonitor } from "node:events"; + +interface UnhandledError extends Error { + code?: string; + context?: unknown; +} + +const e = new EventEmitter(); +const record = { id: 3, tags: ["a", "b"] }; +const attempts: Array<() => boolean> = [ + () => e.emit("error", "plain string"), + () => e.emit("error", 12.5), + () => e.emit("error", record), + () => e.emit("error", [1, 2, 3]), + () => e.emit("error", null), + () => e.emit("error"), + () => e.emit("error", "a", "b"), +]; +for (const attempt of attempts) { + try { + attempt(); + console.log("no throw"); + } catch (caught) { + const err = caught as UnhandledError; + console.log(err.code, err.message, err.context === record, typeof err.context); + } +} + +let monitored = 0; +e.on(errorMonitor, () => { + monitored++; +}); +try { + e.emit("error", "watched"); +} catch (caught) { + console.log((caught as UnhandledError).code, monitored); +} + +const handled = new EventEmitter(); +handled.on("error", (err: unknown) => console.log("got", err instanceof Error ? `Error: ${err.message}` : err)); +console.log(handled.emit("error", 7)); +console.log(handled.emit("error", { reason: "closed" })); +console.log(handled.emit("error", new Error("real"))); diff --git a/tests/corpus/emitter-unhandled-error-payloads.cjs b/tests/corpus/emitter-unhandled-error-payloads.cjs new file mode 100644 index 000000000..01ba59c95 --- /dev/null +++ b/tests/corpus/emitter-unhandled-error-payloads.cjs @@ -0,0 +1,67 @@ +// emit('error', payload) with a payload that is not an Error: with no +// 'error' listener it throws ERR_UNHANDLED_ERROR, whose message quotes +// util.inspect of the payload and whose context is the payload; listeners +// receive the arguments as given. +const { EventEmitter, errorMonitor } = require("events"); + +const e = new EventEmitter(); +const payloads = ["Accepts a string", "it's", { message: "Error!" }, 42, -0, null, undefined, true, [1, "two"], { nested: { deep: [1, 2] } }]; +for (const v of payloads) { + try { + e.emit("error", v); + console.log("no throw"); + } catch (err) { + console.log(err instanceof Error, err.name, err.code, err.message, err.context === v); + } +} + +try { + e.emit("error"); +} catch (err) { + console.log(err.code, err.message, err.context); +} + +try { + e.emit("error", "first", "second"); +} catch (err) { + console.log(err.code, err.message, err.context); +} + +// errorMonitor sees the payload before the throw. +const m = new EventEmitter(); +m.on(errorMonitor, function (v) { + console.log("monitor", v, arguments.length); +}); +try { + m.emit("error", { code: 7 }); +} catch (err) { + console.log("thrown", err.code, err.context.code); +} + +// With an 'error' listener nothing throws. +const l = new EventEmitter(); +l.on("error", function (a, b) { + console.log("listener", a, b, arguments.length); +}); +console.log(l.emit("error", "x", 2)); +console.log(l.emit("error", { k: 1 })); +console.log(l.emit("error")); + +// An Error payload still throws itself. +try { + e.emit("error", new TypeError("typed")); +} catch (err) { + console.log(err.name, err.message, err.code); +} + +// A payload whose util.inspect throws is quoted by its string conversion. +try { + e.emit("error", { + message: "Error!", + [require("util").inspect.custom]() { + throw new Error("no inspect"); + }, + }); +} catch (err) { + console.log(err.code, err.message); +} diff --git a/tests/diagnostics/ee-computed-listeners.ts b/tests/diagnostics/ee-computed-listeners.ts deleted file mode 100644 index babe0f311..000000000 --- a/tests/diagnostics/ee-computed-listeners.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { EventEmitter } from "node:events"; - -const emitter = new EventEmitter(); -const suffix = process.argv[2] ?? "x"; -emitter.on(`topic:${suffix}`, () => {}); -emitter.listeners("topic:x"); diff --git a/tests/harness/__snapshots__/ee-computed-listeners.ts.txt b/tests/harness/__snapshots__/ee-computed-listeners.ts.txt deleted file mode 100644 index 46181c0e8..000000000 --- a/tests/harness/__snapshots__/ee-computed-listeners.ts.txt +++ /dev/null @@ -1,8 +0,0 @@ -ee-computed-listeners.ts:6:1 - error SC2020: 'listeners of the event 'topic:x'' is part of the standard library types but has no scriptc lowering yet - - 5 | emitter.on(`topic:${suffix}`, () => {}); - 6 | emitter.listeners("topic:x"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 7 | - - hint: a listener of this event uses checked-dynamic dispatch, so the listener array has no one static element type — listenerCount(name) counts \ No newline at end of file diff --git a/tests/harness/corpus-costs/emitter-computed-name-listeners.ts.json b/tests/harness/corpus-costs/emitter-computed-name-listeners.ts.json new file mode 100644 index 000000000..736e63575 --- /dev/null +++ b/tests/harness/corpus-costs/emitter-computed-name-listeners.ts.json @@ -0,0 +1 @@ +{ "plain": 4200, "san": 7100 } diff --git a/tests/harness/corpus-costs/emitter-listeners-dynamic-typed.ts.json b/tests/harness/corpus-costs/emitter-listeners-dynamic-typed.ts.json new file mode 100644 index 000000000..ee5a3fc43 --- /dev/null +++ b/tests/harness/corpus-costs/emitter-listeners-dynamic-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 7300 } diff --git a/tests/harness/corpus-costs/emitter-listeners-dynamic.cjs.json b/tests/harness/corpus-costs/emitter-listeners-dynamic.cjs.json new file mode 100644 index 000000000..424aeb55c --- /dev/null +++ b/tests/harness/corpus-costs/emitter-listeners-dynamic.cjs.json @@ -0,0 +1 @@ +{ "plain": 6300, "san": 8700 } diff --git a/tests/harness/corpus-costs/emitter-meta-listeners-dynamic.cjs.json b/tests/harness/corpus-costs/emitter-meta-listeners-dynamic.cjs.json new file mode 100644 index 000000000..62bba9734 --- /dev/null +++ b/tests/harness/corpus-costs/emitter-meta-listeners-dynamic.cjs.json @@ -0,0 +1 @@ +{ "plain": 4800, "san": 7700 } diff --git a/tests/harness/corpus-costs/emitter-unhandled-error-payloads-typed.ts.json b/tests/harness/corpus-costs/emitter-unhandled-error-payloads-typed.ts.json new file mode 100644 index 000000000..fd922e646 --- /dev/null +++ b/tests/harness/corpus-costs/emitter-unhandled-error-payloads-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 8500 } diff --git a/tests/harness/corpus-costs/emitter-unhandled-error-payloads.cjs.json b/tests/harness/corpus-costs/emitter-unhandled-error-payloads.cjs.json new file mode 100644 index 000000000..197f0a911 --- /dev/null +++ b/tests/harness/corpus-costs/emitter-unhandled-error-payloads.cjs.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 8800 }