Skip to content

fix(hir,runtime,stdlib): bare events specifier resolves to full EventEmitter (#4995)#5002

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4995-bare-events
Jun 11, 2026
Merged

fix(hir,runtime,stdlib): bare events specifier resolves to full EventEmitter (#4995)#5002
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4995-bare-events

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #4995. The bare core specifier events (default import, namespace import, or require('events')) produced an EventEmitter whose instances had an empty prototype.on/.emit/.setMaxListeners all undefined — while node:events worked. This blocked signal-exit (cli-cursorrestore-cursor), the next ink (#348) wall after #4993.

Root cause — two layers, both real

1. HIR lowering. new EE() over an events default import / namespace import / CJS alias lowered to the empty-object placeholder instead of the real EventEmitter. Separately, emitter method-value reads (typeof emitter.on) were gated on the canonical class name — but var EE = require('events') registers the native instance under class name "EE", so an aliased read fell through to a zero-arg events.on() native call that threw ERR_INVALID_ARG_TYPE.

2. A latent, broader bug. Even node:events dynamic dispatch (const x: any = e; x.on(...)) was fully broken in flip binaries. perry-stdlib's handle-dispatch arms called the in-crate crate::events::* registry, but the well-known flip links perry-ext-events, whose handles that registry never sees — so the probe returned false and every dynamic .on/.emit/.setMaxListeners silently no-op'd or read undefined.

Fix

  • HIR (expr_new.rs, expr_member.rs): route events default/namespace/alias new to Expr::New { class_name: "EventEmitter" }; gate emitter method-value reads on the module name, not the canonical class name.
  • Runtime (tags.rs, handle.rs, class_registry.rs): new js_set_native_events_construct dispatcher so js_new_function_construct builds a real emitter when new lands on a bound events.EventEmitter export value.
  • stdlib (common/dispatch.rs, events/domain.rs): handle-dispatch arms (probe / on / constructors) now call the linker-resolved extern "C" events symbols instead of in-crate crate::events::*, so dynamic dispatch consults the same registry the constructors used — mirroring the sqlite duplicate-symbol contract (drizzle: SQLiteInsertBase methods (_prepare, prepare, execute, returning, etc) return undefined when accessed via Any-typed receiver #643). New external-events-construct feature registers the constructor when the well-known flip strips bundled-events.

Validation

  • 6-variant repro matrix (named/default/namespace × bare/node:) + the exact signal-exit require('events') pattern all pass.
  • New gap test test_gap_events_import_4995.tsbyte-identical vs node --experimental-strip-types.
  • Zero gap-suite regressions: my 31 failures are a strict subset of the baseline's 32 (baseline-only test_gap_node_fs is flaky and unrelated). Runtime tests 1022/1023 (the one failure is the pre-existing macOS date test).

Code-only per the maintainer-folds-metadata-at-merge convention (no version bump / changelog).

…ntEmitter (#4995)

The bare core specifier `events` (default import, namespace import, or
`require('events')`) produced an EventEmitter whose instances had an empty
prototype — `.on`/`.emit`/`.setMaxListeners` were all undefined — while
`node:events` worked. This blocked signal-exit (cli-cursor → restore-cursor),
the next ink wall after #4993.

Two layers, both fixed:

- HIR (expr_new.rs, expr_member.rs): `new EE()` over an events default
  import / namespace import / CJS alias now lowers to the real
  `EventEmitter` constructor (Expr::New { class_name: "EventEmitter" })
  instead of the empty-object placeholder. Emitter method-value reads on a
  native `events` instance are gated on module name (not the canonical class
  name) so an aliased constructor binding's reads stay PropertyGet rather
  than calling `events.on()` with no receiver.

- Runtime + stdlib: a new `js_set_native_events_construct` dispatcher lets
  `js_new_function_construct` build a real emitter when `new` lands on a
  bound `events.EventEmitter` export value. The handle-dispatch arms in
  perry-stdlib now call the linker-resolved `extern "C"` events symbols
  (probe / on / constructors) instead of in-crate `crate::events::*`, so when
  the well-known flip links perry-ext-events, dynamic dispatch consults the
  same handle registry the constructors used — previously every dynamic
  `.on`/`.emit` on an emitter silently no-op'd. New `external-events-construct`
  feature wires the constructor registration when the flip strips
  `bundled-events`. Mirrors the sqlite duplicate-symbol contract (#643).

New gap test test_gap_events_import_4995.ts — byte-identical vs
node --experimental-strip-types. Zero gap-suite regressions vs baseline.
@proggeramlug proggeramlug merged commit a04fe49 into main Jun 11, 2026
12 of 13 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-4995-bare-events branch June 11, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant