Repository navigation
Description
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
const buf = Buffer.alloc(4);
const keys: any[] = ["dyn"];
const k: any = keys[0]; // a string at runtime, `any` statically
(buf as any)[k] = "D";
console.log((buf as any)[k]); // node: "D" perry: undefinedStatic 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:
Let { id: 2, name: "i", ty: Any, mutable: true, init: Some(Integer(0)) }
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.
Two real fixes
- Infer
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). - 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.
Activity
fix(runtime,codegen): address review — canonical index strings + comp…
fix: Buffer own-property reads via dynamic key (#6412) and shadowed f…