Skip to content

Reading a literal-key property before its first write makes later reads return undefined #9024

Description

@proggeramlug

Reading a property with a literal key before its first write, then writing it, makes every later read return undefined — a silent wrong value, no error, no crash.

Reproducer

const c: any = {};
const b = c["k"];   // read before the key exists
c["k"] = 7;
console.log(c["k"]);   // node: 7      perry: undefined

Object.keys(c) and JSON.stringify(c) both show the property IS there (neverWritten, {"neverWritten":7}), so the value is stored correctly and only the read is wrong.

Characterization

variant node perry
c["k"] read-before-write 7 undefined
c.k read-before-write (static syntax) 7 undefined
read twice after the write 7 7 undefined undefined
non-empty receiver ({ z: 0 }) first 7 undefined
no read before the write 7 7
variable key (const n = "k"; c[n]) 7 7

The discriminator is the literal key plus a preceding absent-read. A variable key is fine, which points at a compile-time absence proof being established by the first read and reused after the write that adds the property — rather than a runtime cache.

Reproduces identically with PERRY_NO_AUTO_OPTIMIZE=1 and with the default auto-optimize build.

Pre-existing, on main

Found while auditing #9021 (which reworks computed reads), so I A/B'd before attributing it. Both builds are --profile perry-dev of -p perry -p perry-runtime-static -p perry-stdlib-static:

build result
#9021 branch undefined
main @ 40f63c96b3 undefined

main and the #9021 branch produce byte-identical output across my whole 7-case probe, so #9021 neither causes nor worsens this. Recording that explicitly because the bug sits squarely in the area that PR touches and would be easy to pin on it.

Why it matters

This is an ordinary shape — probe a field, then populate it:

if (!cache["key"]) { cache["key"] = compute(); }
return cache["key"];   // undefined

Unlike #9019 (a segfault on a patched .next()), this fails silently and returns a wrong value, which makes it the more dangerous of the two. It is not caught by the gap suite today.

Node used is the pinned oracle, v26.5.1.

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