Skip to content

correctness: a const-bound array literal containing an inline array literal reads undefined, serves stale data, and silently drops compound assignments #10733

Description

@proggeramlug

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 landedJSON.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:751except 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions