diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index 8f3d7827d..27c86de91 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -4294,6 +4294,12 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { return finish(`scr_buffer_new_string_fail(${arg(0)})`); case "fs.toUnixTimestamp": return finish(`scr_fs_to_unix_timestamp(${arg(0)})`); + case "error.argTypeThrow": + // Always throws with the runtime-rendered Received tail (the + // error.nodeThrow dummy pattern). Borrows all three. + return finish( + `(scr_throw_arg_type(${arg(0)}, ${arg(1)}, ${arg(2)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`, + ); // The fs Buffer forms (scr_bytes_io.c): the sync pair throws // like the utf8 forms (may-throw seed set); the promise form // rejects instead. @@ -4860,10 +4866,17 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { return finish( `(${cType(e.type).trim()})scr_emitter_set_max((ScrEmitter *)${arg(0)}, ${arg(1)})`, ); + case "emitter.setMaxChk": + // The checked-dynamic ladder (may throw); +1 receiver back. + return finish( + `(${cType(e.type).trim()})scr_emitter_set_max_chk((ScrEmitter *)${arg(0)}, ${arg(1)})`, + ); case "emitter.getMax": return finish(`scr_emitter_get_max((ScrEmitter *)${arg(0)})`); case "emitter.setDefaultMax": return finish(`scr_emitter_set_default_max(${arg(0)})`); + case "emitter.setDefaultMaxChk": + return finish(`scr_emitter_set_default_max_chk(${arg(0)}, ${arg(1)})`); case "emitter.getDefaultMax": return finish(`scr_emitter_get_default_max()`); // node:stream (scr_stream.c — linked exactly when these appear, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0cbed3d1b..8f997f894 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -509,6 +509,8 @@ const LIB_FN_SYMS: Record = { "emitter.names": "scr_emitter_event_names", "emitter.listeners": "scr_emitter_listeners", "emitter.setMax": "scr_emitter_set_max", + "emitter.setMaxChk": "scr_emitter_set_max_chk", + "emitter.setDefaultMaxChk": "scr_emitter_set_default_max_chk", "emitter.getMax": "scr_emitter_get_max", "emitter.setDefaultMax": "scr_emitter_set_default_max", "emitter.getDefaultMax": "scr_emitter_get_default_max", @@ -9289,6 +9291,24 @@ class LlEmitter { // Loop liveness first (one table for generic and special shapes). if (USES_TIMERS_LIB_FNS.has(e.fn)) this.usesTimers = true; // The handful with non-generic shapes first. + if (e.fn === "error.argTypeThrow") { + // Always throws with the runtime-rendered Received tail (the + // error.nodeThrow dummy pattern). Borrows all three. + const an = this.emitExpr(e.args[0]!); + const ex = this.emitExpr(e.args[1]!); + const got = this.emitExpr(e.args[2]!); + this.declare(`declare void @scr_throw_arg_type(ptr, ptr, ptr)`); + B.line(`call void @scr_throw_arg_type(ptr ${an.name}, ptr ${ex.name}, ptr ${got.name})`); + const ty = this.llType(e.type); + if (ty === "void") { + this.emitPendingCheck(); + return { name: "", type: e.type }; + } + const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null"; + const out = this.own({ name: dummy, type: e.type }); + this.emitPendingCheck(); + return out; + } if (e.fn === "error.nodeThrow") { // The compiler-resolved Node-parity throw (always throws — the // typed dummy is abandoned by the pending check's unwind). diff --git a/packages/compiler/src/frontend/lowering/lower-emitter.ts b/packages/compiler/src/frontend/lowering/lower-emitter.ts index 7b89063e6..91f491000 100644 --- a/packages/compiler/src/frontend/lowering/lower-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-emitter.ts @@ -507,6 +507,21 @@ export function lowerEmitterMethodCall(L: Lowerer, call: ts.CallExpression, // everything else fences by its static name. if (L.checker.getConstructSignatures(L.typeOf(access.expression)).length > 0) { if (member === "setMaxListeners" && args.length === 1) { + if (L.mapTypeOf(L.typeOf(args[0]!))?.kind !== "f64") { + // Node's runtime ladder over the DOM value ("setMaxListeners" is + // the message's slot for the static form). + const raw = L.lowerExpr(args[0]!); + if (raw.type.kind === "dyn" || raw.kind === "unitLit" || L.dynConvertible(raw.type)) { + const n: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc }; + return { + kind: "libCall", + fn: "emitter.setDefaultMaxChk", + args: [n, { kind: "strLit", value: "setMaxListeners", type: STRING, loc }], + type: VOID, + loc, + }; + } + } const n = L.lowerExprExpecting(args[0]!, F64); if (n.type.kind !== "f64") { L.noLowering( @@ -518,6 +533,36 @@ export function lowerEmitterMethodCall(L: Lowerer, call: ts.CallExpression, return { kind: "libCall", fn: "emitter.setDefaultMax", args: [n], type: VOID, loc }; } if (member === "setMaxListeners") { + // The per-target form with a target that is provably NOT an + // emitter/EventTarget (the invalid-input probes): Node validates n + // first, then throws ERR_INVALID_ARG_TYPE on the target. Claimed + // when n is a pure read (identifier/literal — nothing to evaluate) + // and the target crosses into the DOM for the Received tail. + if (args.length === 2 && !args.some(ts.isSpreadElement)) { + const nT = L.mapTypeOf(L.typeOf(args[0]!)); + const tT = L.mapTypeOf(L.typeOf(args[1]!)); + const nPure = ts.isIdentifier(args[0]!) || ts.isLiteralExpression(args[0]!); + const targetNotEmitter = + tT !== null && tT.kind !== "dyn" && tT.kind !== "jsval" && + !(tT.kind === "object"); + if (nT?.kind === "f64" && nPure && targetNotEmitter) { + const raw = L.lowerExpr(args[1]!); + if (raw.type.kind === "dyn" || raw.kind === "unitLit" || L.dynConvertible(raw.type)) { + const got: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc }; + return { + kind: "libCall", + fn: "error.argTypeThrow", + args: [ + { kind: "strLit", value: "eventTargets", type: STRING, loc }, + { kind: "strLit", value: "an instance of EventEmitter or EventTarget", type: STRING, loc }, + got, + ], + type: VOID, + loc, + }; + } + } + } L.noLowering( `EventEmitter.setMaxListeners with ${args.length} arguments`, call, @@ -774,6 +819,16 @@ export function lowerEmitterMethodCall(L: Lowerer, call: ts.CallExpression, if (member === "setMaxListeners") { if (args.length !== 1) L.noLowering(`setMaxListeners with ${args.length} arguments`, call); const receiver = L.lowerExpr(access.expression); + if (L.mapTypeOf(L.typeOf(args[0]!))?.kind !== "f64") { + // The invalid-input probes (string n, dyn helpers): Node's ladder + // runs at runtime — ERR_INVALID_ARG_TYPE for non-numbers, + // ERR_OUT_OF_RANGE below zero, and a well-typed dyn still applies. + const raw = L.lowerExpr(args[0]!); + if (raw.type.kind === "dyn" || raw.kind === "unitLit" || L.dynConvertible(raw.type)) { + const n: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc }; + return { kind: "libCall", fn: "emitter.setMaxChk", args: [receiver, n], type: receiver.type, loc }; + } + } const n = L.lowerExprExpecting(args[0]!, F64); return { kind: "libCall", fn: "emitter.setMax", args: [receiver, n], type: receiver.type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index a31548339..cf9fe3e36 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6618,6 +6618,49 @@ export function lowerBinary(L: Lowerer, expr: ts.BinaryExpression): IrExpr { const value = L.lowerExprExpecting(expr.right, target.type); return { kind: "assignExpr", localId: target.id, value, type: target.type, loc }; } + // `events.defaultMaxListeners = v` — the module-property write + // Node validates (validateNumber(n, 'defaultMaxListeners', 0)): + // the value crosses into the DOM and the runtime ladder throws + // ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE with Node's exact slot + // name; valid numbers apply. The expression's value is the RHS + // (JS's assignment yield). + if ( + op === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(expr.left) && + !expr.left.questionDotToken && + expr.left.name.text === "defaultMaxListeners" + ) { + const bi = L.builtinMemberOf(expr.left); + if (bi && bi.module === "events" && bi.member === "defaultMaxListeners") { + const rhs = L.lowerExpr(expr.right); + if (rhs.type.kind === "dyn" || rhs.kind === "unitLit" || L.dynConvertible(rhs.type)) { + const valTmp = L.declareHiddenLocal("%setVal", rhs.type); + const valRef = (): IrExpr => ({ kind: "varRef", localId: valTmp.id, type: rhs.type, loc }); + const dynVal: IrExpr = + rhs.type.kind === "dyn" ? valRef() : { kind: "dynFrom", value: valRef(), type: DYN, loc }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: valTmp.id, init: rhs, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "emitter.setDefaultMaxChk", + args: [dynVal, { kind: "strLit", value: "defaultMaxListeners", type: STRING, loc }], + type: VOID, + loc, + }, + loc, + }, + ], + result: valRef(), + type: rhs.type, + loc, + }; + } + } + } // Member targets whose storage IS a module global — namespace // members (`N.x`) and expando function members (`Foo.baz`): the // same assignExpr, since the "member" is a variable. diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 9ab27e461..53fd14cbb 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3578,6 +3578,32 @@ function isEsModuleStamp(expr: ts.Expression): boolean { }; } } + // `events.defaultMaxListeners = v` in STATEMENT position — the + // module-property write Node validates (the lowerBinary + // expression twin): the value crosses into the DOM, the runtime + // ladder throws Node's exact errors, valid numbers apply. + if (!expr.left.questionDotToken && expr.left.name.text === "defaultMaxListeners") { + const bi = L.builtinMemberOf(expr.left); + if (bi && bi.module === "events" && bi.member === "defaultMaxListeners") { + const loc = locOf(expr); + const rhs = L.lowerExpr(expr.right); + if (rhs.type.kind === "dyn" || rhs.kind === "unitLit" || L.dynConvertible(rhs.type)) { + const dynVal: IrExpr = + rhs.type.kind === "dyn" ? rhs : { kind: "dynFrom", value: rhs, type: DYN, loc }; + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "emitter.setDefaultMaxChk", + args: [dynVal, { kind: "strLit", value: "defaultMaxListeners", type: STRING, loc }], + type: VOID, + loc, + }, + loc, + }; + } + } + } // `r._read = fn` / `w._write = fn` (and the other underscore // methods) on a runtime-stream-rooted receiver: Node's // own-property shadow of the prototype method — the runtime diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 137440579..cb7874db9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -7083,6 +7083,11 @@ export class Lowerer { } const bi = this.builtinMemberOf(expr); if (!bi) return null; + // events.defaultMaxListeners READS the process-wide default (its + // write twin routes through emitter.setDefaultMaxChk). + if (bi.module === "events" && bi.member === "defaultMaxListeners") { + return { kind: "libCall", fn: "emitter.getDefaultMax", args: [], type: F64, loc }; + } const c = builtinModuleConstOf(this, bi.module, bi.member); if (c !== undefined) return builtinConstLit(c, loc); if (bi.member === "constants" && bi.module === "fs") { diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index bf33e889f..c14c32029 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -2521,6 +2521,19 @@ export type IrLibFn = * finite numbers coerce (negatives answer now/1000, Node's shape); * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ | "fs.toUnixTimestamp" + /** The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME- + * rendered Received tail: args [argname, "of type ..." clause, the + * offending DOM value]. ALWAYS THROWS; polymorphic result (the + * error.nodeThrow pattern). May-throw seed. */ + | "error.argTypeThrow" + /** The checked-dynamic max-listeners ladders: setMaxChk is the + * instance form over a DOM n (non-numbers ERR_INVALID_ARG_TYPE, + * negatives/NaN ERR_OUT_OF_RANGE; +1 receiver back — chaining); + * setDefaultMaxChk is the static/property form, its second argument + * naming the message slot ("setMaxListeners" for the static call, + * "defaultMaxListeners" for the module-property assignment). */ + | "emitter.setMaxChk" + | "emitter.setDefaultMaxChk" /** The Buffer forms of the fs quartet: readFileSync(path) with NO * encoding → bytes (+1), writeFileSync(path, bytes), and the * fs/promises readFile(path) no-encoding form (an already-settled @@ -6204,6 +6217,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "bytes.compareChk", "buffer.newStringFail", "fs.toUnixTimestamp", + "error.argTypeThrow", + "emitter.setMaxChk", + "emitter.setDefaultMaxChk", "fs.readFileSyncBytes", "fs.writeFileSyncBytes", "zlib.inflateSync", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index c526eb455..917083d6b 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -672,6 +672,9 @@ export const LIB_FN_SIGS: Recordkind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("setMaxListeners", "of type number", n); + return scr_emitter_retain(em); + } + return scr_emitter_set_max(em, n->v.num); +} + ScrEmitter *scr_emitter_set_max(ScrEmitter *em, double n) { if (!(n >= 0)) { /* negatives and NaN — Node's validateNumber */ char num[32]; @@ -682,6 +694,28 @@ double scr_emitter_get_max(ScrEmitter *em) { return scr_emitter_default_max; } +/* The checked-dynamic defaultMaxListeners ladder — `name` picks the + * message's slot ("setMaxListeners" for the static call, + * "defaultMaxListeners" for the module-property assignment, Node's own + * split). Borrowed. */ +void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name) { + if (n->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail(name->data, "of type number", n); + return; + } + if (!(n->v.num >= 0)) { + char num[32]; + size_t nlen = scr_f64_to_str(n->v.num, num); + char msg[128]; + int len = snprintf(msg, sizeof msg, + "The value of \"%s\" is out of range. It must be >= 0. Received %.*s", + name->data, (int)nlen, num); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return; + } + scr_emitter_set_default_max(n->v.num); +} + void scr_emitter_set_default_max(double n) { if (!(n >= 0)) { /* negatives and NaN — Node's validateNumber(n, "setMaxListeners", 0) */ char num[32]; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8d3220db..b40f65dad 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -635,6 +635,14 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) { * number (5)") — the handle dispatchers' and argument validators' * per-arg gates. `expected` is the full "of type ..."/"an instance of * ..." clause. */ +/* The compiler-resolved ERR_INVALID_ARG_TYPE throw with a RUNTIME- + * rendered Received tail (error.argTypeThrow — the always-throwing + * lowered arms whose offending value is not a literal). Borrows all + * three; always throws catchably. */ +void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got) { + scr_dyn_arg_type_fail(argname->data, expected->data, got); +} + void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got) { char detail[64]; const char *d = scr_dyn_specific_type(got, detail, sizeof detail); diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 6e744f806..859d8df02 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -2968,6 +2968,9 @@ void scr_dyn_check_listener(const ScrDyn *cb, const char *argname); * generic ERR_INVALID_ARG_TYPE thrower over it (`expected` is the whole * "of type ..." clause). The handle dispatchers' per-arg gates. */ const char *scr_dyn_specific_type(const ScrDyn *v, char *buf, size_t cap); +/* ERR_INVALID_ARG_TYPE with the runtime-rendered Received tail (the + * error.argTypeThrow libCall). Borrows all three; always throws. */ +void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got); void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got); /* Listener-closure builders for the handle dispatchers' .on(...) paths: * a runtime-built ScrClosure whose capture is the boxed dyn listener and @@ -4181,6 +4184,9 @@ ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got); * numbers coerce (negatives answer now/1000), the rest throw Node's * ERR_INVALID_ARG_TYPE. Borrowed. */ double scr_fs_to_unix_timestamp(const ScrDyn *t); +/* The checked-dynamic max-listeners ladders (scr_events_emitter.c). */ +ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n); +void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name); double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd); double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd); ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end); diff --git a/tests/corpus/2560-emitter-max-listeners-ladders.cjs b/tests/corpus/2560-emitter-max-listeners-ladders.cjs new file mode 100644 index 000000000..c2ad2d3c0 --- /dev/null +++ b/tests/corpus/2560-emitter-max-listeners-ladders.cjs @@ -0,0 +1,33 @@ +// The max-listeners argument ladders, Node-exact: setMaxListeners over a +// non-number throws ERR_INVALID_ARG_TYPE with the rendered Received +// tail, negatives/NaN throw validateNumber's ERR_OUT_OF_RANGE, the +// module-property assignment names its own slot (defaultMaxListeners), +// the qualified static names setMaxListeners, and valid values apply. +'use strict'; +const events = require('events'); +const e = new events.EventEmitter(); +const show = (fn) => { + try { + console.log('ret', fn()); + } catch (err) { + console.log(`${err.name}|${err.code}|${err.message}`); + } +}; + +show(() => { e.setMaxListeners(NaN); }); +show(() => { e.setMaxListeners(-1); }); +show(() => { e.setMaxListeners('and even this'); }); +show(() => { e.setMaxListeners(null); }); +show(() => { e.setMaxListeners(42); }); +console.log(e.getMaxListeners()); + +show(() => { events.defaultMaxListeners = NaN; }); +show(() => { events.defaultMaxListeners = -1; }); +show(() => { events.defaultMaxListeners = 'and even this'; }); +show(() => { events.defaultMaxListeners = 20; }); +console.log(events.defaultMaxListeners); + +show(() => { events.EventEmitter.setMaxListeners(NaN); }); +show(() => { events.EventEmitter.setMaxListeners('nope'); }); +show(() => { events.EventEmitter.setMaxListeners(11); }); +console.log(events.defaultMaxListeners);