Skip to content

fix: native-compile winston (ES5 .call(this) super-chain → 59/59 corpus)#5368

Merged
proggeramlug merged 3 commits into
mainfrom
fix/winston-es5-super-call-chain
Jun 18, 2026
Merged

fix: native-compile winston (ES5 .call(this) super-chain → 59/59 corpus)#5368
proggeramlug merged 3 commits into
mainfrom
fix/winston-es5-super-call-chain

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Gets winston to native-compile + run (winston object, exit 0), the last of 59 corpus packages still failing. Corpus is now 59/59 (100%), 0 regressions.

winston is multi-wall (class Logger extends Transform from readable-stream, an ES5-constructor stack). The OUTER class extends Transform routing was already fixed (#5357); this PR clears three further walls in the INNER ES5/readable-stream machinery.

Root causes & walls cleared (one commit each)

Wall 1 — util.inherits value-read never dispatched (the ES5 super-chain root)

require('inherits')(Transform, Duplex) is an indirect util.inherits value-call (the inherits package exports util.inherits). Reading a native-module callable export as a value mints a BOUND_METHOD closure that dispatches through the per-module NM_DISPATCH_REGISTRY, populated by js_nm_install_<module>(). But the PropertyGet on a NativeModuleRef (util.inherits value) lowered to js_native_module_property_by_name without emitting the install. The direct call form (util.inherits(a,b)) is statically lowered straight to the runtime extern and never needs the registry — so a module reached ONLY via the value-read path left the registry empty and the indirect call resolved to undefined.

Result: the ES5 parent edges (Mid→Base) were never registered, so the nested Readable.call(this) guard if (!(this instanceof Readable)) saw false, returned a discarded new Readable(), and this._readableState.needReadable = true threw on null (_stream_transform.js:106).

Fix: emit js_nm_install_<module>() before js_native_module_property_by_name in the NativeModuleRef PropertyGet branch (property_get.rs).

Wall 2 — Stream.prototype.on.call(this, …) borrow threw

readable-stream's Readable.prototype.on = function (ev, fn) { var res = Stream.prototype.on.call(this, ev, fn); … } (with Stream = require('stream')). In Node require('stream') IS the legacy Stream constructor, so Stream.prototype.on is EventEmitter.prototype.on. Perry models the module as a namespace object, so both require('stream').prototype and require('stream').Stream.prototype lacked the EventEmitter methods — Stream.prototype.on was undefined and the .call threw "Function.prototype.call was called on a value that is not a function" (_stream_readable.js:745).

Fix: install_event_emitter_prototype_methods() puts on/once/emit/removeListener/removeAllListeners/… on the legacy Stream.prototype AND the Readable/Writable/Duplex/Transform/PassThrough prototypes as receiver-from-this values (slot-0 capture = TAG_UNDEFINED sentinel → this_value reads IMPLICIT_THIS, set by Function.prototype.call/apply). get_native_module_constant resolves require('stream').prototype to that same legacy prototype.

Wall 3 — defineProperty(Class.prototype, …) getter bound this to the prototype

winston's Object.defineProperty(Logger.prototype, 'transports', { get() { const { pipes } = this._readableState; … } }), read as this.transports inside the constructor, threw "Cannot convert undefined or null to object" (logger.js:695). The getter is an ordinary method closure whose body reads this from its captured receiver slot, not IMPLICIT_THIS; the inherited prototype-accessor walk merely set IMPLICIT_THIS and called the closure, so the getter observed the prototype (whose _readableState is undefined) instead of the instance.

Fix: route inherited_proto_accessor_value through invoke_accessor_getter, which clones the getter closure with this rebound to the real receiver (matching the own-accessor read path).

Files changed

  • crates/perry-codegen/src/expr/property_get.rs (Wall 1)
  • crates/perry-runtime/src/object/native_module_stream.rs, crates/perry-runtime/src/node_stream.rs, crates/perry-runtime/src/node_stream_dispatch.rs, crates/perry-runtime/src/object/native_module.rs (Wall 2)
  • crates/perry-runtime/src/object/class_registry.rs (Wall 3)

Tests

  • tests/test_native_module_value_read_install_codegen.sh (Wall 1, runtime + IR assertion)
  • tests/test_stream_prototype_eventemitter_borrow.sh (Wall 2)
  • tests/test_defineproperty_prototype_getter_this.sh (Wall 3)

Validation

  • winston: --no-cache runs clean (winston object, exit 0) — GREEN.
  • Full corpus on the coherent binary (PERRY_NO_AUTO_OPTIMIZE=1, --no-cache, returncode+binary): 59/59 (100%), 0 failing. No regressions in ws/rxjs/commander/pino/bluebird/execa (the other stream/ES5-inheritance packages).
  • perry-runtime unit tests: 1061/1061 pass single-threaded (the 3 parallel failures are pre-existing shared-state flakes).
  • All 3 new regression tests pass. cargo fmt --all --check clean; check_file_size.sh, gc_store_site_inventory.py, addr_class_inventory.py all pass.

Version / CHANGELOG

Left to the maintainer to fold at merge (per CLAUDE.md / task constraints).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed native module property reads to properly initialize modules before access.
    • Corrected this binding in stream method calls and prototype getters.
    • Fixed require('stream').prototype to return the correct prototype object.
  • New Features

    • EventEmitter prototype methods now available on stream prototypes.
  • Tests

    • Added regression tests for prototype getter this binding.
    • Added regression tests for native module value-read code generation.
    • Added regression tests for stream prototype EventEmitter behavior.

Ralph Küpper added 3 commits June 18, 2026 01:24
…perty path

Reading a native-module callable export AS A VALUE (`const f = util.inherits`)
and invoking it indirectly dispatches through the per-module NM_DISPATCH_REGISTRY,
populated by `js_nm_install_<module>()`. The PropertyGet on a NativeModuleRef
(`util.inherits` value) lowered to `js_native_module_property_by_name` without
ever emitting the install. The *direct* call form (`util.inherits(a, b)`) is
statically lowered straight to the runtime extern and never touches the registry,
so a module reached ONLY through the value-read path left the registry empty and
the indirect call resolved to `undefined`.

This broke winston's `class Logger extends Transform` (readable-stream):
`require('inherits')(Transform, Duplex)` is an indirect `util.inherits` value-call.
With it skipped, the ES5 parent edges were never registered, so the nested
`Readable.call(this)` guard `if (!(this instanceof Readable))` saw false, returned
a discarded `new Readable()`, and `this._readableState.needReadable = true` threw
on null.

Fix: emit `js_nm_install_<module>()` before the `js_native_module_property_by_name`
call in the NativeModuleRef PropertyGet branch (property_get.rs).

Adds tests/test_native_module_value_read_install_codegen.sh (runtime + IR assertion).
…or .call borrows

readable-stream's `Readable.prototype.on = function (ev, fn) { var res =
Stream.prototype.on.call(this, ev, fn); … }` (with `Stream = require('stream')`)
needs `Stream.prototype.on` to be EventEmitter.prototype.on. In Node
`require('stream')` IS the legacy Stream constructor; Perry models it as a
namespace object, so both `require('stream').prototype` and
`require('stream').Stream.prototype` lacked the EventEmitter methods —
`Stream.prototype.on` was undefined and the `.call` threw
"Function.prototype.call was called on a value that is not a function".

Two parts:
- install_event_emitter_prototype_methods() installs on/once/emit/
  removeListener/removeAllListeners/addListener/etc. on the legacy
  Stream.prototype AND the Readable/Writable/Duplex/Transform/PassThrough
  prototypes, as receiver-from-`this` values (slot-0 capture = TAG_UNDEFINED
  sentinel → this_value reads IMPLICIT_THIS, set by Function.prototype.call/
  apply). Existing instance-bound closures always capture a real object so are
  unaffected.
- get_native_module_constant resolves `require('stream').prototype` to the same
  legacy Stream.prototype the `.Stream` export carries.

Adds tests/test_stream_prototype_eventemitter_borrow.sh.
…) getters

A getter installed via Object.defineProperty(Class.prototype, name, { get }) is
an ordinary method closure whose body reads this from its captured receiver
slot, not IMPLICIT_THIS. The inherited prototype-accessor walk
(inherited_proto_accessor_value) merely set IMPLICIT_THIS and called the closure
with js_closure_call0, so the getter observed the PROTOTYPE it lives on instead
of the instance — this.<ownField> came back undefined.

winston's Object.defineProperty(Logger.prototype, 'transports', { get() {
const { pipes } = this._readableState; … } }), read as this.transports inside
the Logger constructor, then threw 'Cannot convert undefined or null to object'
(this._readableState was undefined on the prototype).

Fix: route inherited_proto_accessor_value through invoke_accessor_getter, which
clones the getter closure with this rebound to the real receiver (and applies
strict/sloppy coercion), matching the own-accessor read path.

Adds tests/test_defineproperty_prototype_getter_this.sh.
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Three runtime correctness fixes: (1) codegen now emits a per-module install call before native-module property reads; (2) stream prototype objects receive EventEmitter method closures that resolve this dynamically at call-site, and require("stream").prototype now returns the real prototype; (3) inherited accessor getters dispatch through invoke_accessor_getter with the real receiver. Three regression test scripts are added.

Changes

Native module install, stream EventEmitter prototype, and accessor this-binding

Layer / File(s) Summary
Inherited accessor getter receiver fix
crates/perry-runtime/src/object/class_registry.rs
inherited_proto_accessor_value now dispatches through invoke_accessor_getter(acc.get, receiver) instead of mutating IMPLICIT_THIS and calling the getter closure manually, binding the real instance as the getter's this.
Native module install emission in PropertyGet codegen
crates/perry-codegen/src/expr/property_get.rs
When lowering a PropertyGet on a NativeModuleRef, an upfront call void @js_nm_install_<module>() is emitted (if available) before js_native_module_property_by_name, ensuring the dispatch registry is populated for indirectly invoked module exports.
Stream prototype EventEmitter method installation
crates/perry-runtime/src/node_stream_dispatch.rs, crates/perry-runtime/src/node_stream.rs, crates/perry-runtime/src/object/native_module.rs, crates/perry-runtime/src/object/native_module_stream.rs
install_event_emitter_prototype_methods allocates per-method closures capturing TAG_UNDEFINED and installs them on a given prototype object (addListener deduplicates to the on closure). this_value returns js_implicit_this_get() when slot-0 is TAG_UNDEFINED. require("stream").prototype is special-cased to return the legacy stream constructor's dynamic prototype. Both attach_stream_legacy_prototype and attach_stream_constructor_prototype call the installer.
Regression test scripts
tests/test_defineproperty_prototype_getter_this.sh, tests/test_native_module_value_read_install_codegen.sh, tests/test_stream_prototype_eventemitter_borrow.sh
Three new Bash tests covering: prototype getter this bound to instance, util.inherits value-read codegen emitting js_nm_install_util(), and stream prototype EventEmitter borrowing (on/emit/removeListener) for both require("stream").prototype and require("stream").Stream.prototype.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PerryTS/perry#5112: Directly related — the native_module_stream.rs and native_module.rs changes in this PR complete the require("stream").prototype / util.inherits(X, require("stream")) prototype chain support that PR #5112 partially addressed.
  • PerryTS/perry#5220: Directly related — that PR introduces NativeModuleRef lowering for require("<native>"), and this PR extends the same PropertyGet codegen path to emit the per-module install call needed to make the value-read use-case functional.
  • PerryTS/perry#5348: Directly related — both modify class_registry.rs's inherited_proto_accessor_value to ensure prototype-installed getters correctly bind the instance receiver.

Poem

🐇 Hop, hop, the prototype chain was frayed,
this went missing, the getter misbehaved!
Now closures capture TAG_UNDEFINED neat,
The install call fires before the stream heartbeat.
EventEmitter borrows land soft and true —
The registry's stocked and the tests pass too! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly references the main achievement (fixing winston to 59/59 corpus), clearly summarizing the primary change from the changeset.
Description check ✅ Passed The PR description is comprehensive, covering summary, root causes, wall-by-wall fixes, files changed, tests added, and validation results. All required template sections are addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/winston-es5-super-call-chain

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-runtime/src/node_stream.rs`:
- Around line 151-162: The comparison in the if statement uses the fully
qualified constant `crate::value::TAG_UNDEFINED` instead of the local
`TAG_UNDEFINED` constant that is defined and already used consistently
throughout this file in multiple other locations. Replace
`crate::value::TAG_UNDEFINED` with the local `TAG_UNDEFINED` constant to
maintain consistency with the rest of the codebase in this file.
🪄 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: da51e427-217c-445f-bdb5-2344b169a119

📥 Commits

Reviewing files that changed from the base of the PR and between c9fad1e and 90f89f5.

📒 Files selected for processing (9)
  • crates/perry-codegen/src/expr/property_get.rs
  • crates/perry-runtime/src/node_stream.rs
  • crates/perry-runtime/src/node_stream_dispatch.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/object/native_module_stream.rs
  • tests/test_defineproperty_prototype_getter_this.sh
  • tests/test_native_module_value_read_install_codegen.sh
  • tests/test_stream_prototype_eventemitter_borrow.sh

Comment on lines +151 to +162
// A `TAG_UNDEFINED` slot-0 marks a *prototype-method value* (e.g.
// `Stream.prototype.on`, installed by `attach_event_emitter_prototype_methods`)
// rather than an instance-bound method: it has no fixed receiver and must
// read the call-site `this` (set by `Function.prototype.call`/`apply` into
// IMPLICIT_THIS), so `Stream.prototype.on.call(streamInstance, ev, fn)`
// — readable-stream's `Readable.prototype.on` wrapper — registers the
// listener on the instance, not on the prototype. `build_object` always
// captures a real object pointer, so existing instance-bound closures
// never hit this branch.
if bits != crate::value::TAG_UNDEFINED {
return f64::from_bits(bits);
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that crate::value::TAG_UNDEFINED matches the local constant

# Find the definition in crate::value (likely value.rs or similar)
echo "=== Searching for TAG_UNDEFINED in value module ==="
rg -n 'pub\s+const\s+TAG_UNDEFINED' --type rust crates/perry-runtime/src/

echo ""
echo "=== Local definition in node_stream.rs ==="
rg -n '^const TAG_UNDEFINED' crates/perry-runtime/src/node_stream.rs

echo ""
echo "=== Usage of crate::value::TAG_UNDEFINED in node_stream.rs ==="
rg -n 'crate::value::TAG_UNDEFINED' crates/perry-runtime/src/node_stream.rs

Repository: PerryTS/perry

Length of output: 323


🏁 Script executed:

#!/bin/bash
# Search for all TAG_UNDEFINED definitions across the entire codebase
echo "=== All TAG_UNDEFINED definitions ==="
rg -n 'TAG_UNDEFINED' --type rust crates/perry-runtime/src/ | head -20

echo ""
echo "=== Check if value.rs exists and list its contents ==="
find crates/perry-runtime/src -name "value.rs" -o -name "value" -type d

echo ""
echo "=== Check value module structure ==="
rg -n 'pub\s+(const|static).*TAG' --type rust crates/perry-runtime/src/value.rs 2>/dev/null || echo "value.rs not found or no matches"

echo ""
echo "=== Check what crate::value module exports ==="
rg -n '(pub\s+use|pub\s+const|pub\s+mod)' --type rust crates/perry-runtime/src/value.rs 2>/dev/null | head -30

Repository: PerryTS/perry

Length of output: 2099


🏁 Script executed:

#!/bin/bash
# Check the value module structure
echo "=== Contents of value module directory ==="
ls -la crates/perry-runtime/src/value/

echo ""
echo "=== Check value module's mod.rs for exports ==="
rg -n '(pub\s+(use|const)|TAG)' --type rust crates/perry-runtime/src/value/mod.rs | head -40

echo ""
echo "=== Check for TAG_UNDEFINED export specifically ==="
rg -n 'pub.*TAG' --type rust crates/perry-runtime/src/value/

Repository: PerryTS/perry

Length of output: 3198


Use the local TAG_UNDEFINED constant on line 160 for consistency.

Line 160 uses crate::value::TAG_UNDEFINED while the rest of this file uses the local TAG_UNDEFINED constant defined at line 60. Both constants have the identical value (0x7FFC_0000_0000_0001), but the inconsistency should be resolved. Since this file already defines and uses its own TAG_UNDEFINED throughout (e.g., lines 216, 418, 456), line 160 should use that same local constant for consistency.

🤖 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-runtime/src/node_stream.rs` around lines 151 - 162, The
comparison in the if statement uses the fully qualified constant
`crate::value::TAG_UNDEFINED` instead of the local `TAG_UNDEFINED` constant that
is defined and already used consistently throughout this file in multiple other
locations. Replace `crate::value::TAG_UNDEFINED` with the local `TAG_UNDEFINED`
constant to maintain consistency with the rest of the codebase in this file.

@proggeramlug proggeramlug merged commit 8beff4f into main Jun 18, 2026
14 of 15 checks passed
@proggeramlug proggeramlug deleted the fix/winston-es5-super-call-chain branch June 18, 2026 00:26
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