fix(runtime): <index> in <Uint8Array> / Buffer resolves numeric indices (#6148) - #6163
Conversation
…ices (#6148) `0 in new Uint8Array([1,2,3])` returned false (and `"length" in u8` too). `Uint8Array`/`Buffer` are backed by a header-less registered *buffer*, not `TYPED_ARRAY_REGISTRY`, so the `in` operator's typed-array arm (which gates on `lookup_typed_array_kind`) never saw them — every other typed-array kind (Int16Array, Uint32Array, Float64Array, …) already worked at any size, so this was Uint8Array/Buffer-specific. Add a buffer branch to `js_object_has_property` mirroring the property-get path: detect via `is_registered_buffer`, then a numeric index is present iff it is in `[0, js_buffer_length)`, and the own view slots (`length`/`byteLength`/`byteOffset`/`BYTES_PER_ELEMENT`/`buffer`) are present. Inherited prototype *members* via `in` on a buffer (`"subarray" in u8`, `"toString" in u8`) are left as a follow-up — the same string-key gap the registered typed-array arm has (tied to lazy `%TypedArray%.prototype` population), tracked separately. Validated vs Node: index `in` for Uint8Array (small + large), `Buffer.from`, Float64Array, Int16Array; negative/out-of-bounds indices; the view slots; plus an `in` regression sweep (plain object / array / registered typed array / Object.create / primitive-throw).
📝 WalkthroughWalkthroughAdds a fast-path branch in ChangesBuffer in-operator fast path
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-runtime/src/object/field_get_set/has_property.rs`:
- Around line 325-342: The buffer-branch handling in has_property is only
checking the named view slots, so string-encoded indices like "0" are
incorrectly falling through to false; update this branch to also recognize
canonical array-index string keys the same way the typed-array path delegates to
typed_array_has_property. Use the existing key parsing in has_property (the
key_val.is_any_string() block) and add an index check before returning false so
in-bounds numeric strings on buffer-backed views are reported as present, while
still preserving the current named-slot behavior and the intended exclusion for
inherited prototype members.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b09541f1-d033-468d-bca1-a0672bb7de85
📒 Files selected for processing (1)
crates/perry-runtime/src/object/field_get_set/has_property.rs
| if key_val.is_any_string() { | ||
| // Own view slots, always present. Inherited prototype MEMBERS | ||
| // (`subarray`, `map`, `toString`, …) via `in` on a buffer are a | ||
| // separate follow-up — the same string-key gap the registered | ||
| // typed-array arm above has, tied to lazy `%TypedArray%` | ||
| // prototype population. | ||
| let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; | ||
| if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) } | ||
| .and_then(|b| std::str::from_utf8(b).ok()) | ||
| { | ||
| if matches!( | ||
| name, | ||
| "length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer" | ||
| ) { | ||
| return nanbox_true; | ||
| } | ||
| } | ||
| return nanbox_false; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
String-encoded numeric indices ("0" in buf) incorrectly return false.
The buffer branch only matches string keys against the five named view slots and returns false for everything else. This means "0" in new Uint8Array([1,2,3]) returns false, when per spec it should return true — ToPropertyKey("0") resolves to an in-bounds index.
The typed-array branch above (lines 272–281) delegates all string keys to typed_array_has_property, which handles string-encoded indices. The buffer branch lacks equivalent handling. The comment at lines 326–330 claims this is "the same string-key gap the registered typed-array arm above has," but the typed-array arm handles string keys comprehensively — the string-encoded index gap is unique to this buffer branch.
This is not covered by the PR's stated scope limitation (which only excludes inherited prototype members like "subarray" in u8). String-encoded numeric indices are own properties, not inherited members.
🐛 Proposed fix: add `canonical_array_index` check for string-encoded indices
if key_val.is_any_string() {
// Own view slots, always present. Inherited prototype MEMBERS
// (`subarray`, `map`, `toString`, …) via `in` on a buffer are a
// separate follow-up — the same string-key gap the registered
// typed-array arm above has, tied to lazy `%TypedArray%`
// prototype population.
let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN];
if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) }
.and_then(|b| std::str::from_utf8(b).ok())
{
if matches!(
name,
"length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer"
) {
return nanbox_true;
}
+ // String-encoded numeric indices (e.g. `"0" in buf`).
+ if let Some(idx) = super::super::canonical_array_index(name) {
+ if (idx as i32) < len {
+ return nanbox_true;
+ }
+ }
}
return nanbox_false;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if key_val.is_any_string() { | |
| // Own view slots, always present. Inherited prototype MEMBERS | |
| // (`subarray`, `map`, `toString`, …) via `in` on a buffer are a | |
| // separate follow-up — the same string-key gap the registered | |
| // typed-array arm above has, tied to lazy `%TypedArray%` | |
| // prototype population. | |
| let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; | |
| if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) } | |
| .and_then(|b| std::str::from_utf8(b).ok()) | |
| { | |
| if matches!( | |
| name, | |
| "length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer" | |
| ) { | |
| return nanbox_true; | |
| } | |
| } | |
| return nanbox_false; | |
| if key_val.is_any_string() { | |
| // Own view slots, always present. Inherited prototype MEMBERS | |
| // (`subarray`, `map`, `toString`, …) via `in` on a buffer are a | |
| // separate follow-up — the same string-key gap the registered | |
| // typed-array arm above has, tied to lazy `%TypedArray%` | |
| // prototype population. | |
| let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; | |
| if let Some(name) = unsafe { crate::string::js_string_key_bytes(key_val, &mut sso) } | |
| .and_then(|b| std::str::from_utf8(b).ok()) | |
| { | |
| if matches!( | |
| name, | |
| "length" | "byteLength" | "byteOffset" | "BYTES_PER_ELEMENT" | "buffer" | |
| ) { | |
| return nanbox_true; | |
| } | |
| // String-encoded numeric indices (e.g. `"0" in buf`). | |
| if let Some(idx) = super::super::canonical_array_index(name) { | |
| if (idx as i32) < len { | |
| return nanbox_true; | |
| } | |
| } | |
| } | |
| return nanbox_false; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/perry-runtime/src/object/field_get_set/has_property.rs` around lines
325 - 342, The buffer-branch handling in has_property is only checking the named
view slots, so string-encoded indices like "0" are incorrectly falling through
to false; update this branch to also recognize canonical array-index string keys
the same way the typed-array path delegates to typed_array_has_property. Use the
existing key parsing in has_property (the key_val.is_any_string() block) and add
an index check before returning false so in-bounds numeric strings on
buffer-backed views are reported as present, while still preserving the current
named-slot behavior and the intended exclusion for inherited prototype members.
…ers, order-independent (#6164) `"subarray" in ta`, `"map" in ta`, `"toString" in ta` and other inherited `%TypedArray%.prototype` / `Object.prototype` members returned false or gave creation-order-dependent results, because the string-key membership walk consulted the shared `%TypedArray%.prototype` intrinsic only if it happened to have been built already (first registered typed-array creation) — and the buffer-backed `Uint8Array` branch never populated it and only matched the five named view slots. - `typed_array_prototype_chain_has` now builds the intrinsic on demand (`ensure_typed_array_intrinsic`, idempotent) before consulting it, so the result no longer depends on creation order. - The buffer branch of `js_object_has_property` routes non-slot string keys through the same walk (`Uint8Array`/`Buffer` is a `%TypedArray%`), so inherited prototype members resolve there too — and a canonical numeric-index string (`"0" in u8`) is reported as an own index (folds in #6163's review follow-up, superseding #6168). Buffer-specific `Buffer.prototype` methods (`readUInt8`, …) are not covered here. Validated vs Node: `subarray`/`map`/`filter`/`slice`/`join`/`indexOf`, `toString`/`valueOf`/`hasOwnProperty`/`constructor`, `Symbol.iterator`, numeric + canonical-index strings, and view slots — for registered typed arrays AND buffer-backed `Uint8Array`, buffer-first and typed-array-first; plus an `in` regression sweep (plain object / array / Object.create / class-ref).
…order-independent (#6164) (#6170) * fix(runtime): `<key> in` typed array / Buffer resolves prototype members, order-independent (#6164) `"subarray" in ta`, `"map" in ta`, `"toString" in ta` and other inherited `%TypedArray%.prototype` / `Object.prototype` members returned false or gave creation-order-dependent results, because the string-key membership walk consulted the shared `%TypedArray%.prototype` intrinsic only if it happened to have been built already (first registered typed-array creation) — and the buffer-backed `Uint8Array` branch never populated it and only matched the five named view slots. - `typed_array_prototype_chain_has` now builds the intrinsic on demand (`ensure_typed_array_intrinsic`, idempotent) before consulting it, so the result no longer depends on creation order. - The buffer branch of `js_object_has_property` routes non-slot string keys through the same walk (`Uint8Array`/`Buffer` is a `%TypedArray%`), so inherited prototype members resolve there too — and a canonical numeric-index string (`"0" in u8`) is reported as an own index (folds in #6163's review follow-up, superseding #6168). Buffer-specific `Buffer.prototype` methods (`readUInt8`, …) are not covered here. Validated vs Node: `subarray`/`map`/`filter`/`slice`/`join`/`indexOf`, `toString`/`valueOf`/`hasOwnProperty`/`constructor`, `Symbol.iterator`, numeric + canonical-index strings, and view slots — for registered typed arrays AND buffer-backed `Uint8Array`, buffer-first and typed-array-first; plus an `in` regression sweep (plain object / array / Object.create / class-ref). * fix(runtime): canonical numeric index on typed array/Buffer `in` never inherits (#6164) Address CodeRabbit review: per the IntegerIndexedExotic [[HasProperty]] (ECMA-262 §10.4.9.2), a CanonicalNumericIndexString is present iff it is a valid in-bounds integer index and otherwise absent — it must NOT fall through to the prototype chain. The prior code only short-circuited the in-bounds case; an out-of-bounds (`"100"` on a length-5 view), negative (`"-1"`), `"-0"`, or fractional (`"1.5"`) canonical index leaked into `typed_array_prototype_chain_has` (harmless today only because the intrinsic has no numeric-named members). Add `is_canonical_numeric_index_string` (round-trips `ToString(ToNumber(s))`, plus the `"-0"` special case) and short-circuit any canonical numeric index to the in-bounds result, skipping the prototype scan. Non-canonical numeric-looking forms (`"00"`, `"1e3"`, `"0x1"`) remain ordinary string keys and still consult the prototype. Verified byte-for-byte vs Node. --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Fixes #6148 (the index case).
Summary
0 in new Uint8Array([1,2,3])returnedfalse(as did"length" in u8),while every other typed-array kind worked:
Uint8Array/Bufferare backed by a header-less registered buffer (notTYPED_ARRAY_REGISTRY), so theinoperator's typed-array arm — which gates onlookup_typed_array_kind— never saw them. It was Uint8Array/Buffer-specific,independent of size.
Fix
Add a buffer branch to
js_object_has_property, mirroring the property-get path(
get_field_by_name_tail.rs): detect viais_registered_buffer, then[0, js_buffer_length)length/byteLength/byteOffset/BYTES_PER_ELEMENT/buffer) are presentScope / follow-up
Inherited prototype members via
inon a buffer ("subarray" in u8,"toString" in u8) are left as a follow-up: it's the same string-key gap theregistered typed-array arm already has, tied to lazy
%TypedArray%.prototypepopulation (order-dependent), so it deserves its own fix covering both.
Validation
Byte-for-byte vs
node --experimental-strip-types: indexinfor Uint8Array(small + large),
Buffer.from, Float64Array, Int16Array; negative andout-of-bounds indices; the five view slots; plus an
inregression sweep (plainobject, array, registered typed array,
Object.create, primitive-throw).Summary by CodeRabbit
inoperator now returns the expected results for numeric indices and supported buffer-related properties.falseresults.