You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Reading a literal-key property before its first write makes later reads return undefined #9024
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
constc: any={};constb=c["k"];// read before the key existsc["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:
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:
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.
No longer reproduces on current main: the exact reproducer and the full 7-case probe (bracket/dot syntax, non-empty receiver, double read, the if (!cache[k]) cache[k] = ... pattern) now print node-identical output — verified on the tip with a clean perry-base build on the Linux box AND a local build, both flag states.
The fix came in with #9028 (escape a scalar-replaced object when an undeclared field is written), which matches the mechanism this issue's characterization table pointed at: the literal-key read-before-write established a compile-time absence proof on a scalar-replacement candidate, and the later undeclared-field write went to storage the proven-absent read path never consulted. A variable key defeated the proof, which is why that variant always worked.
Bisect-grade confirmation: the issue's SHA 40f63c96b3 (= #9020) still shows undefined; the current tip (with #9028) prints 7. Closing.
Activity
fix(codegen): escape a scalar-replaced object when an undeclared fiel…
proggeramlug commentedon Aug 29, 2026
No longer reproduces on current
main: the exact reproducer and the full 7-case probe (bracket/dot syntax, non-empty receiver, double read, theif (!cache[k]) cache[k] = ...pattern) now print node-identical output — verified on the tip with a cleanperry-basebuild on the Linux box AND a local build, both flag states.The fix came in with #9028 (escape a scalar-replaced object when an undeclared field is written), which matches the mechanism this issue's characterization table pointed at: the literal-key read-before-write established a compile-time absence proof on a scalar-replacement candidate, and the later undeclared-field write went to storage the proven-absent read path never consulted. A variable key defeated the proof, which is why that variant always worked.
Bisect-grade confirmation: the issue's SHA
40f63c96b3(= #9020) still showsundefined; the current tip (with #9028) prints7. Closing.fix(codegen): PropertySet rejections honour sloppy mode (#9459); a wr…
fix(codegen): an inherited property read no longer folds to undefined…
fix(#10822): delete on a scalar-replaced receiver escapes it