Summary
A const binding whose initialiser is an array literal containing an inline array literal produces a scalar-replaced copy that diverges from the heap object. Reads of the inner array in a value position return undefined, chained reads return stale data while the heap holds the updated value, and a compound assignment through the chain is a silent no-op.
No error in any case.
const arr = [[1, 2, 3]];
const t = arr[0];
console.log(JSON.stringify(t), t === arr[0], Array.isArray(t));
// node : [1,2,3] true true
// perry: undefined false false
Three symptoms, one cause
(a) value-position read is undefined — shown above.
(b) a chained read is served from the stale copy while the heap is correct:
const arr = [[1, 2, 3]];
function b() { return arr[0]; }
b()[1] = 99;
console.log(arr[0][1], JSON.stringify(arr));
// node : 99 [[1,99,3]]
// perry: 2 [[1,99,3]]
The write landed — JSON.stringify(arr) sees [[1,99,3]] in both runtimes — but the read returns 2. The program's own data is inconsistent with itself, which is the most confusing possible form of this bug.
(c) a compound assignment through the chain is a complete no-op:
const arr = [[1, 2, 3]];
arr[0][1] += 10;
console.log(arr[0][1], JSON.stringify(arr));
// node : 12 [[1,12,3]]
// perry: 2 [[1,2,3]]
What does not trigger it
All of these are correct in both runtimes, which narrows the trigger sharply:
let or var for the outer binding
- binding the inner array first —
const inner = [1,2,3]; const arr = [inner];
- building the outer array with
push
- reassigning the outer element before use
for (const x of arr)
console.log(arr[0]) directly — served from the scalar slot, so it happens to work
arr[0].length
- three-deep nesting,
[[[1,2,3]]]
- an object outer,
{ a: [1,2,3] }
- a plain
arr[0][1] = 99 with no compound operator
So it needs: const outer binding, an array literal initialiser, an inline array literal element, and a read of that element into a value position.
Relationship to existing work
This is the scalar_replaced_arrays family already documented at crates/perry-codegen/src/expr/index_get.rs:751 — except that here the outer array also exists on the heap. Every mutation is visible through JSON.stringify(arr), so the scalar copy and the heap object diverge rather than one simply being absent. That is what makes (b) possible, and it is the part the existing note does not cover.
It is also the array analogue of #10689 (fixed in #10705), where a read on a scalar-replaced object folded to the constant undefined because the escape check did not consult the class chain. Same shape of failure — silent, no error, and repaired by anything that makes the receiver escape — on a different container.
Provenance
Found while working on #10718. It is pre-existing and unrelated to #10731 — verified on base 4715bc2fa and on that PR's head 1246fd9df, byte-identical output on all 19 probes. Compared against node v26.8.1 on Linux x86-64.
An earlier description of this defect as "b()[k()] += 10 silently drops the store" was wrong and has been retracted: the call and the compound assignment are both incidental, and a flat array behaves correctly. The trigger is the nested literal.
Summary
A
constbinding whose initialiser is an array literal containing an inline array literal produces a scalar-replaced copy that diverges from the heap object. Reads of the inner array in a value position returnundefined, chained reads return stale data while the heap holds the updated value, and a compound assignment through the chain is a silent no-op.No error in any case.
Three symptoms, one cause
(a) value-position read is
undefined— shown above.(b) a chained read is served from the stale copy while the heap is correct:
The write landed —
JSON.stringify(arr)sees[[1,99,3]]in both runtimes — but the read returns2. The program's own data is inconsistent with itself, which is the most confusing possible form of this bug.(c) a compound assignment through the chain is a complete no-op:
What does not trigger it
All of these are correct in both runtimes, which narrows the trigger sharply:
letorvarfor the outer bindingconst inner = [1,2,3]; const arr = [inner];pushfor (const x of arr)console.log(arr[0])directly — served from the scalar slot, so it happens to workarr[0].length[[[1,2,3]]]{ a: [1,2,3] }arr[0][1] = 99with no compound operatorSo it needs:
constouter binding, an array literal initialiser, an inline array literal element, and a read of that element into a value position.Relationship to existing work
This is the
scalar_replaced_arraysfamily already documented atcrates/perry-codegen/src/expr/index_get.rs:751— except that here the outer array also exists on the heap. Every mutation is visible throughJSON.stringify(arr), so the scalar copy and the heap object diverge rather than one simply being absent. That is what makes (b) possible, and it is the part the existing note does not cover.It is also the array analogue of #10689 (fixed in #10705), where a read on a scalar-replaced object folded to the constant
undefinedbecause the escape check did not consult the class chain. Same shape of failure — silent, no error, and repaired by anything that makes the receiver escape — on a different container.Provenance
Found while working on #10718. It is pre-existing and unrelated to #10731 — verified on base
4715bc2faand on that PR's head1246fd9df, byte-identical output on all 19 probes. Compared against node v26.8.1 on Linux x86-64.An earlier description of this defect as "
b()[k()] += 10silently drops the store" was wrong and has been retracted: the call and the compound assignment are both incidental, and a flat array behaves correctly. The trigger is the nested literal.