Summary
EventEmitter listeners that capture an outer-scope mutable variable and write the listener's argument into it (a normal closure pattern) write `NaN` (for numeric args) or `undefined` (for object property reads) instead of the actual argument. Booleans assigned from the closure are unaffected.
Repro 1 — `test_express_mount`
```ts
const ee = new EventEmitter();
let hitCount = 0;
ee.on("hit", (n: number) => { hitCount = n; });
ee.emit("hit", 7);
console.log("hitCount:", hitCount);
// Node: hitCount: 7
// Perry: hitCount: NaN
let mounted = false;
let parentName = "";
child.on("mount", function (this: any, parent: { name: string }) {
mounted = true; // <-- boolean: works
parentName = parent.name; // <-- property read: undefined
});
child.emit("mount", { name: "parent-app" });
// Node: parentName: parent-app
// Perry: parentName: undefined
```
Repro 2 — `test_issue_850_eventemitter`
```ts
const onceVals: number[] = [];
em.once("solo", (n: number) => { onceVals.push(n); });
em.emit("solo", 7);
// Node: once values: [ 7 ]
// Perry: once values: [ NaN ]
const addSeen: number[] = [];
const handler = (n: number) => { addSeen.push(n); };
em.addListener("alias", handler);
em.emit("alias", 1);
// Node: addListener/removeListener: [ 1 ]
// Perry: addListener/removeListener: [ NaN ]
```
Pattern
| Listener body |
Result |
| `x = true` |
✅ works |
| `x = numericArg` |
❌ writes NaN |
| `x = objArg.field` |
❌ writes undefined |
| `arr.push(numericArg)` |
❌ pushes NaN |
The bool case writing successfully suggests it's not a closure-capture-back-into-outer-scope bug per se — the closure is invoked and does write to the captured slot. The numeric argument arriving as NaN points at the EventEmitter→listener argument-passing path, which is dropping or mis-tagging the f64 payload on dispatch.
Suspected root cause
The EventEmitter shim (`__perry_ee_init`-style lazy init) was rewritten in #986. Likely the listener-call dispatch in the shim doesn't NaN-box numeric args correctly when forwarding from `emit(name, ...args)` to the stored callback.
Surfaced by
CI parity gate on tag `v0.5.1019` (run 26236443275). Both `test_express_mount` and `test_issue_850_eventemitter` fail with this same shape.
Summary
EventEmitter listeners that capture an outer-scope mutable variable and write the listener's argument into it (a normal closure pattern) write `NaN` (for numeric args) or `undefined` (for object property reads) instead of the actual argument. Booleans assigned from the closure are unaffected.
Repro 1 — `test_express_mount`
```ts
const ee = new EventEmitter();
let hitCount = 0;
ee.on("hit", (n: number) => { hitCount = n; });
ee.emit("hit", 7);
console.log("hitCount:", hitCount);
// Node: hitCount: 7
// Perry: hitCount: NaN
let mounted = false;
let parentName = "";
child.on("mount", function (this: any, parent: { name: string }) {
mounted = true; // <-- boolean: works
parentName = parent.name; // <-- property read: undefined
});
child.emit("mount", { name: "parent-app" });
// Node: parentName: parent-app
// Perry: parentName: undefined
```
Repro 2 — `test_issue_850_eventemitter`
```ts
const onceVals: number[] = [];
em.once("solo", (n: number) => { onceVals.push(n); });
em.emit("solo", 7);
// Node: once values: [ 7 ]
// Perry: once values: [ NaN ]
const addSeen: number[] = [];
const handler = (n: number) => { addSeen.push(n); };
em.addListener("alias", handler);
em.emit("alias", 1);
// Node: addListener/removeListener: [ 1 ]
// Perry: addListener/removeListener: [ NaN ]
```
Pattern
The bool case writing successfully suggests it's not a closure-capture-back-into-outer-scope bug per se — the closure is invoked and does write to the captured slot. The numeric argument arriving as NaN points at the EventEmitter→listener argument-passing path, which is dropping or mis-tagging the f64 payload on dispatch.
Suspected root cause
The EventEmitter shim (`__perry_ee_init`-style lazy init) was rewritten in #986. Likely the listener-call dispatch in the shim doesn't NaN-box numeric args correctly when forwarding from `emit(name, ...args)` to the stored callback.
Surfaced by
CI parity gate on tag `v0.5.1019` (run 26236443275). Both `test_express_mount` and `test_issue_850_eventemitter` fail with this same shape.