Skip to content

Use the events module and EventEmitter as untyped constructor values - #1544

Open
cramforce wants to merge 1 commit into
mainfrom
opt/ee-class-value-crash
Open

cramforce wants to merge 1 commit into
mainfrom
opt/ee-class-value-crash

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Stacked on #1537 (opt/require-decl-storage); review that first.

Summary

In JavaScript, require("events") is the EventEmitter class, and bundled npm code uses it as a value. A small semaphore does this in its bundled chunk:

var EventEmitter = require("events"), ReleaseEmitter;
ReleaseEmitter = EventEmitter;
// later, in the constructor:
this.releaseEmitter = new ReleaseEmitter();
this.releaseEmitter.on("release", (token) => { /* ... */ });

The assignment was refused with SC1090 ("a runtime-provided class value ('EventEmitter') as an untyped value"), and class Channel extends require("events") {} failed on the require call as a value.

#1512 gave JavaScript an untyped EventEmitter constructor (scr_dyn_builtin_class.c) for constructor functions, and already used it for a destructured EventEmitter binding or events.EventEmitter. This change uses the same constructor wherever the class turns untyped:

  • Class values. The events module binding (const EventEmitter = require("events")) and a direct reference to EventEmitter or a stream class become that constructor when they flow into an untyped slot. new through it builds an emitter object with Node's listener API, as new on a constructor function inheriting from it does. Only the class itself converts: a slot of the class's type can hold a program subclass, so such slots keep the refusal.
  • Identity and instanceof. untyped === EventEmitter compares identity with that constructor. untyped instanceof EventEmitter now also recognizes a boxed compiled emitter, an instance of a program class extending EventEmitter; the prototype test alone answered false for those. The same applies through an untyped binding the checker narrows to the class.
  • extends require("events"). The base is the runtime emitter, as through a require binding.
  • Typed arrays of the class. [EventEmitter] stored as a typed array of the class is refused with SC1090. Before, the destructured binding already produced the untyped constructor there, and the array then failed its class check at run time with a TypeError.

The stream classes follow the same path where they turn untyped; constructing over them keeps #1512's run-time SC2020 until #1533 lands. require("stream") itself is unchanged.

Evidence

  • 6571-events-module-class-value.cjs: the events module binding stored, aliased, constructed through and compared; SC1090 on main.
  • 6572-extends-require-events.cjs: class extends require("events") used as a value; failed on main.
  • 6573-semaphore-release-emitter.cjs: a semaphore whose constructor builds its release emitter through an aliased require binding and listens and emits on it; SC1090 on main.
  • npm-static case 2595-bundled-chunk-emitter: a minified esbuild __commonJS chunk with the mixed var statement, a sibling chunk require, a class extending the binding and an alias; fails on main.
  • Node and native agree in both lanes (plain and sanitized, including the RC audit and LeakSanitizer). The broad emitter/class/inherit/cjs/stream/require lane shows only failures that also fail on unchanged main in the Sandbox. Diagnostics, npm and npm-static suites pass apart from a known environment case.
  • Zero-cost gate (both commits against main): 3429 programs identical, 2 intended differences: 6453 and 6454 test untyped instanceof EventEmitter, which now also checks for a boxed compiled emitter. Bench geomean +0.13%, self-hosted compiler −0.26%, both within tolerance.
  • Lint, format and test:ts7 --baselines-only are clean; the self-hosting IR build has no diagnostics.

Not covered

  • A typed on followed by an untyped emit on the same native emitter aborts at run time (pre-existing, also in TypeScript); it is being fixed separately.
  • Untyped listenerCount, eventNames and similar on a boxed compiled emitter are not routed yet.

🤖 Generated with Claude Code

In JavaScript, `ReleaseEmitter = EventEmitter` over `const EventEmitter =
require("events")` was refused with SC1090 ("a runtime-provided class value
as an untyped value"), and `class Channel extends require("events")` failed
on the require call as a value. Bundled npm code does both, for example a
small semaphore that aliases EventEmitter and constructs its release channel
through the alias.

- The events module binding and the EventEmitter or stream class itself
  turn untyped as the runtime's untyped constructor that constructor
  functions already use (scr_dyn_builtin_class.c). `new` through it builds
  an emitter object with Node's listener API, and `===` against the class
  compares identity.
- `x instanceof EventEmitter` on an untyped value also recognizes a boxed
  compiled emitter (an instance of a program class extending EventEmitter),
  which the prototype test alone answered false for. The same holds through
  an untyped binding narrowed to the class.
- `class X extends require("events")` extends the runtime emitter, as
  through a require binding.
- A typed array of the class built from such values (`[EventEmitter]`) is
  refused with SC1090 rather than failing its class check at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 7:42pm UTC

Comment on lines +176 to +184
const once = (expr: IrExpr, name: string): IrExpr => {
if (isSafeToRepeat(expr)) return expr;
const local = lowerer.declareHiddenLocal(name, DYN);
stmts.push({ kind: "varDecl", localId: local.id, init: expr, loc });
return { kind: "varRef", localId: local.id, type: DYN, loc };
};
const tested = once(value, "%instanceValue");
const ctor = callee ? once(callee, "%instanceCallee") : null;
const compiled = classInstanceOf(lowerer, tested, info, loc);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an EventEmitter instanceof right-hand expression mutates the left-hand binding, the new helper reads the binding after that mutation and returns the wrong result.

Fix on Vercel

This branch was successfully deployed

1 active deployment
Preview — fec779ab Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant