Found while investigating the process-global array-index latch (which turned out to be a non-issue for the workload — see the perf note at the bottom). This is a silent wrong-value bug, not a crash.
Repro
const a = [1, 2, 3];
Object.setPrototypeOf(a, { 7: "inherited", foo: "bar" });
a[7] // node: "inherited" perry: undefined
a.foo // node: "bar" perry: undefined
7 in a // node: true perry: false
The classic ES5 array-subclass idiom is broken the same way:
function MyList() {}
MyList.prototype = Object.create(Array.prototype);
MyList.prototype.tag = "mylist";
MyList.prototype.first = function () { return this[0]; };
const arr = [10, 20];
Object.setPrototypeOf(arr, MyList.prototype);
arr.tag // node: "mylist" perry: undefined
arr.first() // node: 10 perry: TypeError / missing
Root cause
array_custom_array_prototype (crates/perry-runtime/src/array/indexing.rs:237-268) accepts the recorded [[Prototype]] only when that prototype is itself GC_TYPE_ARRAY. Retargeting an array's prototype to an ordinary object records the change (the process-wide deopt latch is set, so perry pays the cost) but the lookup path then declines to consult it — so the array inherits nothing.
perry therefore pays the deoptimisation for a case it does not implement.
Why it was never caught
The only fixture covering array prototype retargeting, test-files/test_gap_typed_arrays.ts:38, uses an array as the new prototype — the one shape that works. The object-prototype case has no coverage.
Suggested fixture
A test that sets an array's prototype to (a) a plain object with an indexed key and a named key, (b) an Object.create(Array.prototype)-derived prototype carrying methods, and (c) null, checking element reads, named reads, in, and method calls against node in each case.
Perf context, for completeness: the same investigation established that class X extends Array does not latch PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED on current main (subclass instances have been elements-backed GC_TYPE_OBJECT since #8974), that the claude-code and pi bundles never latch it, and that forcing it on for cc --help changes instructions by 0.001%. The latch is a genuine latent risk when it does fire (measured up to 14× on push, process-wide and permanent), but it is not a workload problem today.
Found while investigating the process-global array-index latch (which turned out to be a non-issue for the workload — see the perf note at the bottom). This is a silent wrong-value bug, not a crash.
Repro
The classic ES5 array-subclass idiom is broken the same way:
Root cause
array_custom_array_prototype(crates/perry-runtime/src/array/indexing.rs:237-268) accepts the recorded[[Prototype]]only when that prototype is itselfGC_TYPE_ARRAY. Retargeting an array's prototype to an ordinary object records the change (the process-wide deopt latch is set, so perry pays the cost) but the lookup path then declines to consult it — so the array inherits nothing.perry therefore pays the deoptimisation for a case it does not implement.
Why it was never caught
The only fixture covering array prototype retargeting,
test-files/test_gap_typed_arrays.ts:38, uses an array as the new prototype — the one shape that works. The object-prototype case has no coverage.Suggested fixture
A test that sets an array's prototype to (a) a plain object with an indexed key and a named key, (b) an
Object.create(Array.prototype)-derived prototype carrying methods, and (c)null, checking element reads, named reads,in, and method calls against node in each case.Perf context, for completeness: the same investigation established that
class X extends Arraydoes not latchPERRY_ARRAY_INDEX_FAST_PATH_INVALIDATEDon current main (subclass instances have been elements-backedGC_TYPE_OBJECTsince #8974), that the claude-code and pi bundles never latch it, and that forcing it on forcc --helpchanges instructions by 0.001%. The latch is a genuine latent risk when it does fire (measured up to 14× onpush, process-wide and permanent), but it is not a workload problem today.