Object.defineProperty(Array.prototype, 7, { set(v) { hits++; }, get() { return "P"; }, configurable: true });
const nums = [1, 2, 3];
nums[7] = 42;
console.log(hits, nums.length, nums[7]);
|
hits |
length |
nums[7] |
| node |
1 |
3 |
"P" |
| perry |
0 |
8 |
42 |
The setter never runs, the array is extended, and the own slot is written. Same for a boolean[] receiver (Object.defineProperty(Array.prototype, 9, …)), and same for Object.defineProperty(arr, i, {set})-free plain stores generally — the receiver's own descriptors are checked, the prototype's are not.
Pre-existing on origin/main (b3f14e9cd), for numeric and tagged receivers alike. I found it while widening the inline store tier in a separate PR and confirmed both tiers behave identically here, so it is not caused by that work.
Why both tiers miss it
The out-of-line guard consults array_prototype_has_index_flag(), object_prototype_has_index_flag() and array_static_proto_recorded(); the inline guard consults the PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED summary byte that those setters maintain. The flags are only ever raised from note_array_index_write / note_object_prototype_index_write, which fire on an index write to the prototype:
pub(crate) fn note_array_index_write(arr: usize) {
if !ARRAY_PROTO_HAS_INDEX.load(Relaxed) && arr != 0 && arr == array_prototype_addr() {
ARRAY_PROTO_HAS_INDEX.store(true, Relaxed);
invalidate_array_index_fast_path();
}
}
Object.defineProperty(Array.prototype, N, …) is not an index write, so neither flag is ever raised and every guard downstream of them is satisfied. Array.prototype[7] = … presumably does raise it — the hole is specific to the defineProperty route.
Fix shape
Raise the same flags from the defineProperty / defineProperties path when the target is Array.prototype or Object.prototype and the key is an array index. That is one call at the definition site and leaves both guards and the summary byte correct without touching either fast path.
Repro is 6 lines; happy to attach the fuller differential (frozen / sealed / preventExtensions / own-element accessor / extension / mixed types) I wrote alongside it — the own-element accessor case passes today, which is what makes the prototype gap stand out.
https://claude.ai/code/session_012Ys25ni6VwDKE71o1NTYAT
nums[7]"P"42The setter never runs, the array is extended, and the own slot is written. Same for a
boolean[]receiver (Object.defineProperty(Array.prototype, 9, …)), and same forObject.defineProperty(arr, i, {set})-free plain stores generally — the receiver's own descriptors are checked, the prototype's are not.Pre-existing on
origin/main(b3f14e9cd), for numeric and tagged receivers alike. I found it while widening the inline store tier in a separate PR and confirmed both tiers behave identically here, so it is not caused by that work.Why both tiers miss it
The out-of-line guard consults
array_prototype_has_index_flag(),object_prototype_has_index_flag()andarray_static_proto_recorded(); the inline guard consults thePERRY_ARRAY_INDEX_FAST_PATH_INVALIDATEDsummary byte that those setters maintain. The flags are only ever raised fromnote_array_index_write/note_object_prototype_index_write, which fire on an index write to the prototype:Object.defineProperty(Array.prototype, N, …)is not an index write, so neither flag is ever raised and every guard downstream of them is satisfied.Array.prototype[7] = …presumably does raise it — the hole is specific to thedefinePropertyroute.Fix shape
Raise the same flags from the
defineProperty/definePropertiespath when the target isArray.prototypeorObject.prototypeand the key is an array index. That is one call at the definition site and leaves both guards and the summary byte correct without touching either fast path.Repro is 6 lines; happy to attach the fuller differential (frozen / sealed / preventExtensions / own-element accessor / extension / mixed types) I wrote alongside it — the own-element accessor case passes today, which is what makes the prototype gap stand out.
https://claude.ai/code/session_012Ys25ni6VwDKE71o1NTYAT