diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index bbb720cc7..740697323 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -236,7 +236,12 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ "scr_util_callable.c", ), unit( - (u) => u.opts.dynBuiltinClass && u.opts.inspect && u.opts.dynInvoke && u.opts.symbol, + (u) => + u.opts.dynBuiltinClass && + u.opts.inspect && + u.opts.dynInvoke && + u.opts.symbol && + (u.opts.emitter || u.opts.workers), "scr_dyn_builtin_class.c", ), unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"), diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 162bf55d3..02a4dd52a 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -234,10 +234,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([ /** libCall name prefixes that link the child-process unit. */ const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."]; +/** The entry points scr_dyn_builtin_class.c defines. The unit calls + * listeners through the dyn-invoke unit, renders unhandled 'error' + * payloads through the inspect unit, creates EventEmitter's private + * symbols through the symbol unit and shares defaultMaxListeners with the + * emitter unit, so each of them links all five. */ +const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [ + "dyn.builtinClass", + "dyn.builtinInstanceOf", + "dyn.functionInstanceOf", +]; + /** Entry points that link the symbol unit, besides the sym. and disposable. families. */ const SYMBOL_LIB_FNS: ReadonlySet = new Set([ // EventEmitter's kShapeMode and kCapture properties - "dyn.builtinClass", + ...BUILTIN_CLASS_LIB_FNS, "dyn.usingDispose", "dyn.usingMethod", "util.isDeepStrictEqual", @@ -511,11 +522,11 @@ const RUNTIME_FEATURE_RULES = { * unit, render unhandled 'error' payloads through the inspect unit and * create EventEmitter's private symbols through the symbol unit. */ dynBuiltinClass: rule({ - fns: ["dyn.builtinClass", "dyn.builtinInstanceOf", "dyn.functionInstanceOf"], + fns: [...BUILTIN_CLASS_LIB_FNS], }), dynInvoke: rule({ initial: (mod) => mod.workers === true, - fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, "dyn.builtinClass"], + fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS], // Constant-key reads of Object.prototype member names answer the // inherited member through scr_dyn_invoke.c. expr: (node, _tables) => @@ -532,7 +543,7 @@ const RUNTIME_FEATURE_RULES = { emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), - fns: [...STREAM_LIB_FNS], + fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS], prefixes: ["emitter.", ...STREAM_LIB_PREFIXES], }), /** Subclasses of the runtime EventTarget/Event bases tear down and box @@ -567,7 +578,7 @@ const RUNTIME_FEATURE_RULES = { "global.native", "util.styleText", "crypto.cipherNew", - "dyn.builtinClass", + ...BUILTIN_CLASS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ], prefixes: ["insp.", "console.", "test."], diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json new file mode 100644 index 000000000..d9ad97beb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5817-constructor-function-instanceof.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json new file mode 100644 index 000000000..f22e1aaeb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5818-constructor-function-emitter-defaults.cjs" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index a6b11f53e..8f9a0bc69 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -91,7 +91,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // Untyped EventEmitter and stream constructor values call listeners // through the dyn-invoke unit, inspect unhandled 'error' payloads and // create EventEmitter's private symbols. - ["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol")], + ["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")], ["scr_stream.c", "stream"], // The zlib stream classes reference both the zlib engine and the stream unit. ["scr_zlib_stream.c", all("zlib", "stream")], diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 76a7ab15e..6d894615b 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -263,9 +263,7 @@ static double bc_max_listeners(ScrDyn *self) { if (!max) return -1; double value; if (bc_undefined(max)) { - ScrDyn *fallback = bc_get(bc_class(BC_EMITTER), "defaultMaxListeners"); - value = fallback ? bc_number(fallback) : 10; - if (fallback) scr_dyn_release(fallback); + value = scr_emitter_get_default_max(); } else { value = bc_number(max); } @@ -1017,6 +1015,42 @@ static ScrDyn *bc_set_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) return self; } +/* EventEmitter.defaultMaxListeners, Node's accessor over the one default + * the typed EventEmitter reads too (scr_events_emitter.c). The setter + * validates as Node's validateNumber(arg, 'defaultMaxListeners', 0). */ +static ScrDyn *bc_default_max_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + return scr_dyn_new_num(scr_emitter_get_default_max()); +} + +static ScrDyn *bc_default_max_set(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + static const char name[] = "defaultMaxListeners"; + ScrStr *key = scr_str_new(name, sizeof name - 1); + scr_emitter_set_default_max_chk(argc ? args[0] : scr_dyn_undefined(), key); + scr_str_release(key); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(scr_dyn_undefined()); +} + +static void bc_define_default_max(ScrDyn *ctor) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "get", 3, + scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_get, 0, "native:events", "get")); + scr_dyn_obj_set(descriptor, "set", 3, + scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_set, 1, "native:events", "set")); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true)); + ScrStr *name = scr_str_new("defaultMaxListeners", 19); + ScrDyn *key = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *defined = scr_dyn_define_property(ctor, key, descriptor); + if (defined) scr_dyn_release(defined); + scr_dyn_release(key); + scr_dyn_release(descriptor); +} + static ScrDyn *bc_get_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; (void)args; @@ -1070,6 +1104,13 @@ static bool bc_pipe_on(ScrDyn *emitter, const char *event, ScrClosure *state, in } static bool bc_pipe_cleanup(ScrClosure *state) { + /* A second run (dest emitting 'close' from end(), with cleanup still + * queued on the source's 'end') finds the listeners removed, as Node's + * repeated removeListener calls do. */ + ScrDyn *handlers = bc_cap(state, BC_PIPE_HANDLERS); + bool cleaned = handlers->kind == SCR_DYN_ARR && scr_dyn_arr_len(handlers) == 0; + scr_dyn_release(handlers); + if (cleaned) return true; ScrDyn *source = bc_cap(state, BC_PIPE_SOURCE); ScrDyn *dest = bc_cap(state, BC_PIPE_DEST); bool ok = bc_pipe_remove(source, "data", state, BC_ONDATA) && @@ -1367,7 +1408,7 @@ static ScrDyn *bc_new_class(int kind, ScrDyn *parent) { bc_method(prototype, "listenerCount", bc_listener_count, 2); bc_method(prototype, "eventNames", bc_event_names, 0); bc_set(ctor, "EventEmitter", scr_dyn_retain(ctor)); - bc_set(ctor, "defaultMaxListeners", scr_dyn_new_num(10)); + bc_define_default_max(ctor); ScrDyn *count = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_static_listener_count, 2, "native:events", "listenerCount"); bc_set(ctor, "listenerCount", count); } else if (kind == BC_STREAM) { diff --git a/tests/corpus/5817-constructor-function-instanceof.cjs b/tests/corpus/5817-constructor-function-instanceof.cjs new file mode 100644 index 000000000..355f53a64 --- /dev/null +++ b/tests/corpus/5817-constructor-function-instanceof.cjs @@ -0,0 +1,15 @@ +// `this instanceof F` in a constructor function, in a program that uses no +// EventEmitter or stream constructor value: the call-without-new idiom +// returns a new instance, and instanceof follows the function's prototype. +"use strict"; +function Glob(pattern) { + if (!(this instanceof Glob)) return new Glob(pattern); + this.pattern = pattern; +} +Glob.prototype.describe = function () { + return `glob ${this.pattern}`; +}; +const called = Glob("*.ts"); +const constructed = new Glob("*.js"); +console.log(called.describe(), constructed.describe()); +console.log(called instanceof Glob, {} instanceof Glob, Object.create(Glob.prototype) instanceof Glob); diff --git a/tests/corpus/5818-constructor-function-emitter-defaults.cjs b/tests/corpus/5818-constructor-function-emitter-defaults.cjs new file mode 100644 index 000000000..d89383b3a --- /dev/null +++ b/tests/corpus/5818-constructor-function-emitter-defaults.cjs @@ -0,0 +1,53 @@ +// EventEmitter.defaultMaxListeners read and written through the untyped +// EventEmitter constructor (Node's accessor and its validation), and a +// legacy pipe whose destination emits 'close' synchronously from end(). +"use strict"; +const util = require("util"); +const EventEmitter = require("events"); +const Stream = require("stream"); + +function Bus() { + EventEmitter.call(this); +} +util.inherits(Bus, EventEmitter); + +console.log(EventEmitter.defaultMaxListeners); +console.log(new Bus().getMaxListeners()); +EventEmitter.defaultMaxListeners = 3; +console.log(EventEmitter.defaultMaxListeners, new Bus().getMaxListeners()); +for (const value of [-1, "4", Number.NaN]) { + try { + EventEmitter.defaultMaxListeners = value; + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +console.log(EventEmitter.defaultMaxListeners); +EventEmitter.defaultMaxListeners = 10; + +function Source() { + Stream.call(this); + this.readable = true; +} +util.inherits(Source, Stream); + +function Sink() { + Stream.call(this); + this.writable = true; +} +util.inherits(Sink, Stream); +Sink.prototype.write = function (chunk) { + console.log("write", chunk); + return true; +}; +Sink.prototype.end = function () { + console.log("sink end"); + this.emit("close"); +}; + +const source = new Source(); +const sink = new Sink(); +source.pipe(sink); +source.emit("data", "a"); +console.log("emit end", source.emit("end")); +console.log("listeners", source.listenerCount("end"), source.listenerCount("data"), sink.listenerCount("close")); diff --git a/tests/harness/corpus-costs/5817-constructor-function-instanceof.cjs.json b/tests/harness/corpus-costs/5817-constructor-function-instanceof.cjs.json new file mode 100644 index 000000000..15eedbeaa --- /dev/null +++ b/tests/harness/corpus-costs/5817-constructor-function-instanceof.cjs.json @@ -0,0 +1 @@ +{ "plain": 2655, "san": 7944 } diff --git a/tests/harness/corpus-costs/5818-constructor-function-emitter-defaults.cjs.json b/tests/harness/corpus-costs/5818-constructor-function-emitter-defaults.cjs.json new file mode 100644 index 000000000..0f4eb2e02 --- /dev/null +++ b/tests/harness/corpus-costs/5818-constructor-function-emitter-defaults.cjs.json @@ -0,0 +1 @@ +{ "plain": 2746, "san": 51663 }