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
Storing to a Buffer/Uint8Array through a computed key that is a string at runtime but any at compile time silently writes a byte instead of an own property, and the value reads back undefined.
Repro
constbuf=Buffer.alloc(4);constkeys: any[]=["dyn"];constk: any=keys[0];// a string at runtime, `any` statically(bufasany)[k]="D";console.log((bufasany)[k]);// node: "D" perry: undefined
Static string keys are fine (buf["lit"], or a local declared : string) — #6406 fixed those. This is the dynamic half, and it's the common shape: a key off an untyped array, out of Object.keys(), or returned from a call.
Root cause
The byte-path guard (expr_assign.rs, and mirrored in expr_member/member_tail.rs) folds to Uint8ArraySet / Uint8ArrayGet whenever the key is not provably a string:
let key_is_string = matches!(index.as_ref(),Expr::String(_))
|| matches!(index.as_ref(),Expr::LocalGet(kid)if/* declared Type::String */);if !key_is_string && matches!(ty,Type::Named(n)if n == "Uint8Array" || n == "Buffer"){/* fold to bytes */}
An any-typed key is not provably a string, so it folds — and the byte path coerces it to an index, dropping the property store.
Why the obvious fix doesn't work
Inverting the guard to require a provably numeric key is unsound-free but destroys the buffer fast path, because a for loop counter is not typed Number:
A loop counter and a string-holding any key are the same type to this guard. Requiring Type::Number therefore un-folds every buf[i] in a loop. Measured on a plain for (let i…) buf[i] = i & 0xff kernel:
guard
Uint8Array folds
!key_is_string (today)
1
key_is_numeric
0
The native-region-proof compiler-output gate catches this immediately (its fixtures — h1_buffer_alias_negative, image_convolution — are exactly this shape). I tried it on #6406 and reverted.
Discriminate at runtime. Always fold, and have the byte-path helper route a non-index key to the property path. Costs a check on the hot path but is robust to any key shape.
(1) is the cleaner win if the induction-variable analysis is cheap, since it also improves typing generally.
Storing to a
Buffer/Uint8Arraythrough a computed key that is a string at runtime butanyat compile time silently writes a byte instead of an own property, and the value reads backundefined.Repro
Static string keys are fine (
buf["lit"], or a local declared: string) — #6406 fixed those. This is the dynamic half, and it's the common shape: a key off an untyped array, out ofObject.keys(), or returned from a call.Root cause
The byte-path guard (
expr_assign.rs, and mirrored inexpr_member/member_tail.rs) folds toUint8ArraySet/Uint8ArrayGetwhenever the key is not provably a string:An
any-typed key is not provably a string, so it folds — and the byte path coerces it to an index, dropping the property store.Why the obvious fix doesn't work
Inverting the guard to require a provably numeric key is unsound-free but destroys the buffer fast path, because a
forloop counter is not typedNumber:A loop counter and a string-holding
anykey are the same type to this guard. RequiringType::Numbertherefore un-folds everybuf[i]in a loop. Measured on a plainfor (let i…) buf[i] = i & 0xffkernel:Uint8Arrayfolds!key_is_string(today)key_is_numericThe
native-region-proofcompiler-output gate catches this immediately (its fixtures —h1_buffer_alias_negative,image_convolution— are exactly this shape). I tried it on #6406 and reverted.Two real fixes
Numberfor numeric induction variables.for (let i = 0; …; i++)with no non-numeric assignment in the body is a number. Then a soundkey_is_numericguard keeps the fast path and fixes this bug. Worth doing carefully — widening type visibility can wake latent fast paths that were only "correct" because unreachable (that's how perf(codegen): keep the numeric-array specialization when the array is captured (#6369) #6377 revived runtime:await Promise.all([...])never resumes when inputs settle via executor-captured resolvers — program silently exits 0 #6328).(1) is the cleaner win if the induction-variable analysis is cheap, since it also improves typing generally.