Skip to content

fix(compile): compile commander's real source (#5137)#5212

Merged
proggeramlug merged 3 commits into
mainfrom
worktree-fix-5137-commander
Jun 15, 2026
Merged

fix(compile): compile commander's real source (#5137)#5212
proggeramlug merged 3 commits into
mainfrom
worktree-fix-5137-commander

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #5137 (reopened) — force-compiling commander's real npm source still crashed even though the native shim path (#5183) works. This matters for the "natively compile the whole dependency tree" goal where the shim isn't the intended end state.

Repro

import { Command } from 'commander';
const program = new Command();
program.name('demo').option('-v, --verbose').argument('<file>');
program.parse(['node', 'x', 'in.txt', '-v']);
console.log(program.args[0], JSON.stringify(program.opts()));

With perry.compilePackages: ["commander"] + allow.compilePackages: ["*"] this went: unresolved js_commander_* link error → TypeError: undefined is not a constructorvalue is not a function. Now prints in.txt {"verbose":true}, matching Node — same as the shim path.

Root causes & fixes

1. Native-instance registration ignored compilePackages.
new Command() was registered as a native commander instance via the hardcoded class-name fallback even when commander's real source is compiled, re-routing its fluent methods to the js_commander_* shim (which #5183 deliberately keeps off the import path). Fixed at the chokepoint — register_native_instance backs off when the module's package is in the compile-packages override, mirroring the import-side back-off is_native_module already does (#665).

2. class X extends EventEmitter had no super-init.
Stream/Error/Event/Request parents install their surface onto this at super(); EventEmitter had no arm, so a source-compiled Command extends EventEmitter had undefined .on/.emit/…. Added js_event_emitter_subclass_init (reuses the generic ns_* emitter closures the stream subclasses already use, via a new emitter_methods() table) and wired it into both super paths: the explicit-super() arm and the no-own-constructor implicit path. The implicit path is gated on !has_imported_ctor so an imported class whose real ctor lives in another module (commander's Command) still runs its real constructor instead of being pre-empted.

3. Pre-existing manifest drift from #5183.
commander::args was a dispatch row with has_receiver: true but a manifest property(...), failing the every_dispatch_entry_has_manifest_counterpart guard and emitting a spurious export const args. Modeled as a receiver method; docs/ regenerated.

Verification

  • commander source-compile and shim paths → in.txt {"verbose":true} (matches Node), including subcommands/actions/version
  • class X extends EventEmitter with explicit ctor, implicit ctor, and instance methods calling this.emit all work; on/once/emit/removeAllListeners/listenerCount verified
  • node:stream subclassing (shared machinery) unaffected
  • cargo test -p perry-codegen -p perry-hir -p perry-runtime green; manifest_consistency 5/5; cargo fmt --check clean; API docs regenerated (api-docs-drift clean)

No version bump / changelog (per maintainer-folds-metadata-at-merge convention).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added package compilation override support
  • Bug Fixes

    • Improved initialization of classes extending EventEmitter to ensure proper method installation and subclass setup

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The head commit changed during the review from fe33a8b to a050be3.

📝 Walkthrough

Walkthrough

Adds a js_event_emitter_subclass_init runtime FFI function and emitter_methods table to install EventEmitter listener/emit methods on existing objects. The codegen layer wires this into two paths: the super() call site for EventEmitter subclasses and the implicit-constructor fallback in lower_new. A new is_compile_package_override query prevents register_native_instance from registering native shims for source-compiled packages.

Changes

EventEmitter subclass initialization and compilePackages guard

Layer / File(s) Summary
Runtime EventEmitter method table and FFI entry point
crates/perry-runtime/src/node_stream_readwrite.rs, crates/perry-runtime/src/node_stream_constructors.rs
emitter_methods() defines 15 listener/emit stub entries; js_event_emitter_subclass_init validates the this handle and installs that table via install_methods_on_existing_object, returning this.
Codegen helper, FFI declaration, and re-exports
crates/perry-codegen/src/expr/write_barrier.rs, crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs, crates/perry-codegen/src/expr/mod.rs
lower_event_emitter_subclass_init emits the runtime call; the js_event_emitter_subclass_init LLVM symbol is declared with DOUBLE (DOUBLE) signature; the helper is re-exported from the expr crate root.
EventEmitter super() branch and implicit-constructor fallback
crates/perry-codegen/src/expr/this_super_call.rs, crates/perry-codegen/src/lower_call/new.rs
Expr::SuperCall gains an EventEmitter-specific branch that evaluates args for side effects, calls the new init helper, and applies FieldInitMode::SelfOnly. lower_new adds a matching fallback for classes that extend EventEmitter with no own constructor.
compilePackages override query and register_native_instance guard
crates/perry-hir/src/ir/constants.rs, crates/perry-hir/src/ir/mod.rs, crates/perry-hir/src/lower/context.rs
is_compile_package_override queries the COMPILE_PACKAGES_OVERRIDE thread-local; register_native_instance uses it to skip native shim registration for source-compiled packages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PerryTS/perry#5185: Updates the FFI provenance registry to route js_event_emitter_* symbols to perry-ext-events, directly adjacent to the new js_event_emitter_subclass_init symbol added here.

Poem

🐇 Hop, hop, EventEmitter's here,
A subclass init beyond all fear!
super() calls now land just right,
Methods installed with fluffy might.
No more undefined constructor woe —
The rabbit's codegen steals the show! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(compile): compile commander's real source (#5137)' clearly identifies the main bug fix (compiling commander's real source) and references the issue number.
Description check ✅ Passed The PR description comprehensively documents the repro case, root causes (native-instance registration, EventEmitter initialization, manifest drift), and verification steps, following the template structure.
Linked Issues check ✅ Passed All code changes directly address issue #5137: native-instance registration now respects compilePackages, EventEmitter subclass initialization is implemented, and manifest drift is corrected, enabling commander to compile and run correctly.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #5137: EventEmitter subclass support, compilePackages override checks, and manifest corrections for commander's real source compilation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-5137-commander

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/expr/this_super_call.rs`:
- Around line 458-479: The EventEmitter initialization in the super-call handler
is only triggered when the direct parent_class is "EventEmitter" (i.e.,
parent_class == None), which misses inheritance chains like B extends A extends
EventEmitter where A has no own constructor. Modify the condition around the
parent_name == "EventEmitter" check in the this_super_call.rs file to walk up
the class hierarchy and detect if EventEmitter is an ancestor anywhere in the
chain, not just as the direct parent. Then apply the same ancestor-chain
detection logic to the no-own-ctor fallback path in
crates/perry-codegen/src/lower_call/new.rs to ensure EventEmitter initialization
runs consistently across both code paths when EventEmitter appears anywhere in
the inheritance hierarchy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad3d8f8f-563b-444c-b841-f2c21efe179c

📥 Commits

Reviewing files that changed from the base of the PR and between a1d9ce6 and b779122.

📒 Files selected for processing (11)
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/lower_call/new.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
  • crates/perry-hir/src/ir/constants.rs
  • crates/perry-hir/src/ir/mod.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-runtime/src/node_stream_constructors.rs
  • crates/perry-runtime/src/node_stream_readwrite.rs
  • docs/api/perry.d.ts
  • docs/src/api/reference.md

Comment on lines +458 to +479
if parent_name.as_str() == "EventEmitter" {
for a in super_args {
let _ = lower_expr(ctx, a)?;
}
let this_box = match ctx.this_stack.last().cloned() {
Some(slot) => ctx.block().load(DOUBLE, &slot),
None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)),
};
ctx.block().call(
DOUBLE,
"js_event_emitter_subclass_init",
&[(DOUBLE, &this_box)],
);
let current_class_name =
ctx.class_stack.last().cloned().unwrap_or_default();
crate::lower_call::apply_field_initializers_recursive(
ctx,
&current_class_name,
crate::lower_call::FieldInitMode::SelfOnly,
)?;
return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle EventEmitter init for inherited chains, not only direct parents.

At Line 458, the init is gated to parent_name == "EventEmitter" only in the parent_class == None path. This misses cases like class B extends A { constructor(){ super(); } } where A extends EventEmitter and A has no own constructor: parent_class is present (A), so no EventEmitter init runs and .on/.emit stay absent on this.

Please add an ancestor-chain termination check for EventEmitter in this super-call flow, and mirror the same logic in crates/perry-codegen/src/lower_call/new.rs (the no-own-ctor fallback path).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/this_super_call.rs` around lines 458 - 479, The
EventEmitter initialization in the super-call handler is only triggered when the
direct parent_class is "EventEmitter" (i.e., parent_class == None), which misses
inheritance chains like B extends A extends EventEmitter where A has no own
constructor. Modify the condition around the parent_name == "EventEmitter" check
in the this_super_call.rs file to walk up the class hierarchy and detect if
EventEmitter is an ancestor anywhere in the chain, not just as the direct
parent. Then apply the same ancestor-chain detection logic to the no-own-ctor
fallback path in crates/perry-codegen/src/lower_call/new.rs to ensure
EventEmitter initialization runs consistently across both code paths when
EventEmitter appears anywhere in the inheritance hierarchy.

Ralph Küpper and others added 3 commits June 15, 2026 14:14
… init + compile-package native-instance back-off (#5137)

Force-compiling commander's npm source (`perry.compilePackages: ["commander"]`
+ `allow.compilePackages: ["*"]`) crashed: first an unresolved
`js_commander_*` link error, then `TypeError: undefined is not a constructor`
/ `value is not a function`. The shim path (#5183) already works; this fixes
the source-compile path that the "compile the whole dependency tree" goal
needs.

Three root causes:

1. Native-instance registration ignored compilePackages. `new Command()`
   was registered as a native `commander` instance off the hardcoded class-name
   fallback even when commander's real source is being compiled, re-routing its
   fluent methods to the `js_commander_*` shim (kept off the import path by
   #5183). Fixed at the chokepoint: `register_native_instance` backs off when
   the module's package is in the compile-packages override — mirroring the
   import-side back-off `is_native_module` already does (#665).

2. `class X extends EventEmitter` (commander's `Command`) had no super-init.
   The stream/Error/Event/Request parents all install their surface onto `this`
   at `super()`; EventEmitter had no arm, so `.on`/`.emit`/… were undefined.
   Added `js_event_emitter_subclass_init` (reuses the generic `ns_*` emitter
   closures the stream subclasses already use, via a new `emitter_methods()`
   table) and wired it into both super-call paths: the explicit-`super()` arm
   (this_super_call.rs) and the no-own-constructor implicit path (new.rs). The
   implicit path is gated on `!has_imported_ctor` so an imported class whose
   real ctor lives in another module (commander's `Command`) still runs its
   real constructor instead of being pre-empted.

3. Pre-existing manifest drift from #5183: `commander::args` was a dispatch
   row with `has_receiver: true` but a manifest `property(...)`, which failed
   the `every_dispatch_entry_has_manifest_counterpart` guard and emitted a
   spurious `export const args`. Modeled as a receiver method; docs regenerated.

Source-compile and shim paths now both print `in.txt {"verbose":true}`,
matching Node, including subcommands/actions/version.
The EventEmitter-subclass-init additions pushed new.rs (1988→2015) and
stdlib_ffi.rs (1999→2003) over the file-size gate. Extract the emitter-init
call into a shared lower_event_emitter_subclass_init helper in write_barrier.rs
(used by both the explicit-super and no-own-ctor paths), carrying the doc
comment once, and trim the FFI-decl comment. Both files back to 2000.
@proggeramlug proggeramlug force-pushed the worktree-fix-5137-commander branch 3 times, most recently from fe33a8b to a050be3 Compare June 15, 2026 21:19
@proggeramlug proggeramlug merged commit bbf3b8b into main Jun 15, 2026
26 of 29 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-5137-commander branch June 15, 2026 22:40
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.

compilePackages: commander throws TypeError: undefined is not a constructor

1 participant