What happened
See below code comments
What you expected
It should match node/bun's behavior
Minimal reproduction
// Perry bug minimal reproduction
//
// ROOT CAUSE: When a class method calls a stored arrow function via `this.fn(x)`,
// perry passes the method's `this` (the holder object) to the arrow function,
// instead of using the arrow function's lexically captured `this`.
//
// This causes two symptoms:
//
// SYMPTOM 1 (wrong value / NaN):
// `const self = this` inside the arrow function's defining class captures
// an incorrect object (the Store instance, not the Holder instance).
// `self.v` becomes undefined, and `x + self.v` becomes NaN.
//
// SYMPTOM 2 (segfault/crash):
// Direct `this.v` access inside the arrow, when called with Store as `this`,
// uses the native field offset of Holder.v on a Store object -> memory violation.
//
// Both symptoms disappear when the closure is called directly (not via a method).
//
// bun runs both correctly; perry native exhibits these bugs.
// ─── SYMPTOM 1: NaN (const self = this) ───────────────────────────────────────
class Store1 {
fn: (x: number) => void;
constructor(f: (x: number) => void) {
this.fn = f;
}
call(x: number) {
this.fn(x);
} // calls stored fn via method (triggers bug)
}
class Holder1 {
v = 10;
s: Store1;
constructor() {
const self = this;
// bun: self.v = 10
// perry: self.v = undefined (perry assigns Store1 instance as 'self')
console.log("[Symptom1] self.v in ctor:", self.v);
this.s = new Store1((x) => {
// bun: result = 15
// perry: result = NaN (self.v is undefined because self = Store1 instance)
console.log("[Symptom1] result:", x + self.v);
});
}
}
const h1 = new Holder1();
h1.s.call(5);
// Expected: self.v in ctor: 10 / result: 15
// Perry: self.v in ctor: undefined / result: NaN
console.log("---");
// ─── SYMPTOM 2: Crash (direct this.v access) ──────────────────────────────────
class Store2 {
fn: (x: number) => void;
constructor(f: (x: number) => void) {
this.fn = f;
}
call(x: number) {
this.fn(x);
}
}
class Holder2 {
v = 10;
s: Store2;
constructor() {
// Arrow uses 'this.v' directly (lexical this = Holder2 instance)
this.s = new Store2((x) => {
// bun: result = 15 (this = Holder2, this.v = 10)
// perry: SEGFAULT (this = Store2 instance, accessing Holder2.v offset on Store2 -> crash)
console.log("[Symptom2] result:", x + this.v);
});
}
}
const h2 = new Holder2();
h2.s.call(5);
// Expected: result: 15
// Perry: segfault (signal 11 / core dump)
Command you ran:
perry test.ts && ./test.ts
Environment
- Perry version: perry 0.5.402
- Host OS: ArchLinux
- Target: native
- Rust toolchain (if building from source): rustc 1.95.0 (59807616e 2026-04-14)
- Installed via: from source
Diagnostic output
output:
[Symptom1] self.v in ctor: undefined
[Symptom1] result: NaN
---
(crashed)
bun:
[Symptom1] self.v in ctor: 10
[Symptom1] result: 15
---
[Symptom2] result: 15
Perry Doctor
Environment Checks
──────────────────
✓ perry version: 0.5.386
✓ update status: up to date
✓ clang (LLVM codegen): clang version 22.1.3
✓ system linker (cc): cc (GCC) 15.2.1 20260209
⚠ runtime library: not found - run: cargo build --release -p perry-runtime
⚠ project config (perry.toml): not found - run: perry init
All critical checks passed with some warnings.
Anything else
Found in my ecs library's demo
What happened
See below code comments
What you expected
It should match node/bun's behavior
Minimal reproduction
Command you ran:
perry test.ts && ./test.tsEnvironment
Diagnostic output
output:
bun:
Anything else
Found in my ecs library's demo