Skip to content

fix(runtime): typed-array integer-indexed [[Set]] spec rejection + coercion#6059

Merged
proggeramlug merged 1 commit into
mainfrom
fix/t262-mech-ta-set
Jul 6, 2026
Merged

fix(runtime): typed-array integer-indexed [[Set]] spec rejection + coercion#6059
proggeramlug merged 1 commit into
mainfrom
fix/t262-mech-ta-set

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

A typed array is an Integer-Indexed exotic object. Its [[Set]] (ECMA-262 §10.4.5.5) and the underlying TypedArraySetElement (§10.4.5.16) have two observable behaviours Perry was swallowing:

1. Ordinary (non-index) property [[Set]] must report the OrdinarySet boolean.
A non-writable own data expando, a setter-less accessor, or a new key on a non-extensible view must be rejected — a strict ta.k = v should throw and Reflect.set(ta, k, v) should return false. Previously every typed-array set path returned true, so the rejection was silent. typed_array_set_property_by_name now returns the correct spec boolean, and own_set_descriptor consults the typed-array side tables directly (they're invisible to the generic address-keyed descriptor lookups, which are deliberately gated off for typed arrays) so the receiver-threading OrdinarySet reports the right result.

2. TypedArraySetElement coerces the value BEFORE the bounds check.
ToNumber / ToBigInt runs on the value before the IsValidIntegerIndex check, then the store is dropped for an invalid index. Perry short-circuited on an out-of-bounds index and never coerced, so ta[100] = { valueOf() { … } } never fired the hook and a throwing coercion never threw. The invalid-index paths (js_typed_array_set, typed_array_set_numeric_index, and the IntegerIndex arm) now run the per-content-type coercion for side effects first. The hot in-bounds / plain-number path is untouched — the out-of-bounds coercion is gated on a non-plain-number value.

Validation

Built and tested on an internal Linux sweep host (--release).

test262 built-ins/TypedArray + TypedArrayConstructors (2184 cases): +7 pass, 0 regressions (baseline vs. patched failure-set diff was removal-only).

test262 built-ins/Reflect + Proxy + Object/{defineProperty,freeze,preventExtensions} (1688 cases): byte-identical pass/fail between baseline and patched — 0 regressions (as expected; the new own_set_descriptor branch only activates for a typed-array target).

Newly passing:

  • Set/key-is-not-numeric-index.js
  • Set/key-is-not-canonical-index.js
  • Set/tonumber-value-throws.js
  • Set/key-is-out-of-bounds-receiver-is-proto.js
  • (+ the BigInt64/BigUint64 variants of the above)

cargo fmt --all -- --check clean; address-classification audit passes; all four touched files stay under 2000 lines; perry-runtime typedarray unit tests pass.

Scope notes

The remaining receiver-threading cases in this internals slice were deliberately left out of this PR: key-is-symbol.js depends on a separate, non-typed-array-specific bug (a non-writable symbol data property whose value is undefined isn't detected on plain objects either), resized-out-of-bounds-to-in-bounds-index.js needs resizable ArrayBuffers, and the Object.create(ta) + Proxy-receiver prototype-chain cases touch several subsystems (one currently SIGSEGVs) — all bigger, riskier mechanisms best handled separately.

Summary by CodeRabbit

  • Bug Fixes
    • Improved typed array write behavior to better match expected JavaScript semantics.
    • Indexed writes now preserve side effects from value conversion when needed, even when the write is out of bounds or otherwise ignored.
    • Ordinary property writes on typed arrays now more accurately reject unsupported changes, such as non-writable or non-extensible cases.

…jection + coercion rules

A typed array is an Integer-Indexed exotic object; its [[Set]] (ECMA-262
§10.4.5.5) and the underlying TypedArraySetElement (§10.4.5.16) have two
observable behaviours Perry was swallowing:

1. Ordinary (non-index) property [[Set]] must report the OrdinarySet
   boolean. A non-writable own data expando, a setter-less accessor, or a
   new key on a non-extensible view must be REJECTED (return false → a
   strict `ta.k = v` throws, `Reflect.set(ta, k, v)` returns false).
   Previously every path returned `true`, so the rejection was silent.
   `typed_array_set_property_by_name` now returns the correct boolean, and
   `own_set_descriptor` consults the typed-array side tables directly
   (they are invisible to the generic address-keyed descriptor lookups,
   which are deliberately gated off for typed arrays) so the
   receiver-threading OrdinarySet reports the right result.

2. TypedArraySetElement runs ToNumber / ToBigInt on the value BEFORE the
   IsValidIntegerIndex bounds check, then drops the store for an invalid
   index. Perry short-circuited on an out-of-bounds index and never
   coerced, so `ta[100] = { valueOf() { … } }` never fired the hook and a
   throwing coercion never threw. The invalid-index paths
   (`js_typed_array_set`, `typed_array_set_numeric_index`, the
   IntegerIndex arm) now coerce for side effects first. The hot
   in-bounds / plain-number path is untouched — the out-of-bounds
   coercion is gated on a non-plain-number value.

Verified on an internal Linux sweep host against test262
built-ins/TypedArray + TypedArrayConstructors (2184 cases): +7 pass,
0 regressions. built-ins/Reflect + Proxy + Object/{defineProperty,freeze,
preventExtensions} (1688 cases): byte-identical, 0 regressions. Newly
passing: Set/key-is-not-numeric-index, key-is-not-canonical-index,
tonumber-value-throws, key-is-out-of-bounds-receiver-is-proto (+ BigInt
variants).
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Modifies TypedArray [[Set]] behavior to align with ECMAScript spec semantics: out-of-bounds and canonical-invalid index writes now perform coercion for observable side effects before dropping the store; ordinary key sets add explicit rejection paths; and typed arrays gain a dedicated own set-descriptor probe used by the proxy layer instead of the generic descriptor path.

Changes

TypedArray Set Semantics

Layer / File(s) Summary
Coercion side-effect detection and visibility
crates/perry-runtime/src/typedarray/access.rs, crates/perry-runtime/src/typedarray/bigint.rs
Adds value_needs_coercion_side_effect to detect NaN-boxed tags requiring observable coercion; widens coerce_for_kind visibility to pub(crate).
Out-of-bounds write coercion
crates/perry-runtime/src/typedarray/access.rs
js_typed_array_set now conditionally coerces the value via BigInt or Number conversion for out-of-bounds indices before returning, instead of returning immediately.
TypedArraySet rejection and index coercion
crates/perry-runtime/src/typedarray_props.rs
Adds explicit false returns for setter-less accessors, non-writable data properties, and non-extensible new keys; adds typed_array_coerce_element_for_side_effects used for canonical-invalid and out-of-bounds numeric index and integer-index string-key writes.
Own set-descriptor probing and proxy integration
crates/perry-runtime/src/typedarray_props.rs, crates/perry-runtime/src/proxy.rs
Introduces TypedArrayOwnSetDescriptor enum and typed_array_own_set_descriptor function; own_set_descriptor in proxy.rs uses it to bypass the generic descriptor path for typed arrays.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant js_typed_array_set
  participant CoercionCheck as value_needs_coercion_side_effect
  participant BigIntCoerce as bigint::to_bigint_for_store
  participant NumberCoerce as jsvalue_to_f64

  Caller->>js_typed_array_set: set(index, value)
  js_typed_array_set->>js_typed_array_set: validate index bounds
  alt index out of bounds
    js_typed_array_set->>CoercionCheck: check value tag
    CoercionCheck-->>js_typed_array_set: needs_coercion?
    alt needs coercion, BigInt view
      js_typed_array_set->>BigIntCoerce: coerce value
    else needs coercion, Number view
      js_typed_array_set->>NumberCoerce: coerce value
    end
    js_typed_array_set-->>Caller: return without store
  else index in bounds
    js_typed_array_set-->>Caller: perform store
  end
Loading
sequenceDiagram
  participant Proxy as own_set_descriptor
  participant Probe as typed_array_own_set_descriptor
  participant SideTables

  Proxy->>Proxy: target is typed array?
  alt is typed array
    Proxy->>Probe: query(owner, name)
    Probe->>SideTables: read accessor/data state
    SideTables-->>Probe: descriptor state
    Probe-->>Proxy: TypedArrayOwnSetDescriptor or None
    Proxy-->>Proxy: map to OwnSetDescriptor
  else not typed array
    Proxy-->>Proxy: fall back to generic descriptor lookup
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main typed-array [[Set]] rejection and coercion fixes.
Description check ✅ Passed The description is detailed and covers summary and validation, though several template sections are folded into prose or omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/t262-mech-ta-set

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/typedarray_props.rs`:
- Around line 572-576: The numeric-index guard in typed_array_set_numeric_index
is missing the canonical-invalid -0 case, so -0.0 currently falls through and
writes to index 0. Update the validation in typed_array_set_numeric_index to
treat negative zero as invalid alongside the existing finite/integer/range
checks, so the path used by js_typed_array_index_set_dynamic and
js_object_set_index_polymorphic still coerces via
typed_array_coerce_element_for_side_effects and then drops the write.

In `@crates/perry-runtime/src/typedarray/access.rs`:
- Around line 137-156: The out-of-bounds branch in typedarray/access.rs within
the typed array set path should not skip coercion for BigInt views when the
value is a plain string or other primitive that still requires ToBigInt. Update
js_typed_array_set so the invalid-index handling always runs
bigint::to_bigint_for_store for KIND_BIGINT64/KIND_BIGUINT64 before returning,
while keeping jsvalue_to_f64 for numeric views; use
value_needs_coercion_side_effect only for the non-BigInt fast-path if needed.
🪄 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: ac23cddf-6d06-4096-a76d-5361daa23149

📥 Commits

Reviewing files that changed from the base of the PR and between c5246ec and 2f0b2c7.

📒 Files selected for processing (4)
  • crates/perry-runtime/src/proxy.rs
  • crates/perry-runtime/src/typedarray/access.rs
  • crates/perry-runtime/src/typedarray/bigint.rs
  • crates/perry-runtime/src/typedarray_props.rs

Comment on lines 572 to 576
if !index.is_finite() || index.fract() != 0.0 || index < 0.0 || index > u32::MAX as f64 {
// Canonical-invalid index: coerce the value for side effects, then drop.
typed_array_coerce_element_for_side_effects(owner, value);
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=rust -C4 '\btyped_array_set_numeric_index\s*\(' crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 2545


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '200,260p' crates/perry-runtime/src/object/polymorphic_index.rs
printf '\n----\n'
sed -n '650,730p' crates/perry-runtime/src/typedarray_props.rs
printf '\n----\n'
rg -n "numeric_key_u32_index|IsValidIntegerIndex|-0|negative zero|canonical-invalid" crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 28775


Reject -0 on the numeric-index path typed_array_set_numeric_index receives raw f64 values from both js_typed_array_index_set_dynamic and js_object_set_index_polymorphic, so -0.0 reaches this guard unchanged and currently stores at index 0. IsValidIntegerIndex treats -0 as canonical-invalid, so this path should coerce for side effects and drop the write.

🤖 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/typedarray_props.rs` around lines 572 - 576, The
numeric-index guard in typed_array_set_numeric_index is missing the
canonical-invalid -0 case, so -0.0 currently falls through and writes to index
0. Update the validation in typed_array_set_numeric_index to treat negative zero
as invalid alongside the existing finite/integer/range checks, so the path used
by js_typed_array_index_set_dynamic and js_object_set_index_polymorphic still
coerces via typed_array_coerce_element_for_side_effects and then drops the
write.

Comment on lines +137 to 156
let kind = (*ta).kind;
if index < 0 || index as u32 >= (*ta).length {
// TypedArraySetElement (§10.4.5.16) runs `ToNumber`/`ToBigInt` on
// the value BEFORE the IsValidIntegerIndex bounds check, then drops
// the store for an invalid index. The coercion is only observable
// for a value whose conversion has a side effect or throws — an
// object (`valueOf`/`Symbol.toPrimitive` hook), a Symbol / BigInt
// into a numeric view, or a Number into a BigInt view. Gate the
// out-of-bounds coercion on a non-plain-number value so the hot
// in-bounds and primitive-value paths are untouched (test262
// Set/tonumber-value-throws, key-is-out-of-bounds-*).
if value_needs_coercion_side_effect(value) {
if kind == KIND_BIGINT64 || kind == KIND_BIGUINT64 {
let _ = bigint::to_bigint_for_store(value);
} else {
let _ = jsvalue_to_f64(value);
}
}
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== access.rs outline ==\n'
ast-grep outline crates/perry-runtime/src/typedarray/access.rs --view expanded || true

printf '\n== typedarray_props.rs outline ==\n'
ast-grep outline crates/perry-runtime/src/typedarray/typedarray_props.rs --view expanded || true

printf '\n== Relevant search terms ==\n'
rg -n "value_needs_coercion_side_effect|typed_array_coerce_element_for_side_effects|KIND_BIGINT64|KIND_BIGUINT64|key-is-out-of-bounds|ToBigInt|ToNumber" crates/perry-runtime/src crates -S

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '90,180p' crates/perry-runtime/src/typedarray/access.rs
printf '\n---\n'
sed -n '1,220p' crates/perry-runtime/src/typedarray/typedarray_props.rs
printf '\n---\n'
rg -n "key-is-out-of-bounds|Set/tonumber-value-throws|tonumber-value-throws|BigInt64|BigUint64" . -S

Repository: PerryTS/perry

Length of output: 4535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '90,180p' crates/perry-runtime/src/typedarray/access.rs
printf '\n---\n'
sed -n '1,260p' crates/perry-runtime/src/typedarray/typedarray_props.rs
printf '\n---\n'
rg -n "key-is-out-of-bounds|Set/tonumber-value-throws|tonumber-value-throws|BigInt64|BigUint64" . -S

Repository: PerryTS/perry

Length of output: 4535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== crates/perry-runtime/src/typedarray/access.rs ==='
nl -ba crates/perry-runtime/src/typedarray/access.rs | sed -n '90,180p'

echo
echo '=== crates/perry-runtime/src/typedarray/typedarray_props.rs ==='
nl -ba crates/perry-runtime/src/typedarray/typedarray_props.rs | sed -n '1,260p'

echo
echo '=== test references ==='
rg -n "key-is-out-of-bounds|Set/tonumber-value-throws|tonumber-value-throws|BigInt64|BigUint64|TypedArrayConstructors/.*/Set" . -S

Repository: PerryTS/perry

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== typedarray_props.rs relevant slice ===\n'
sed -n '480,590p' crates/perry-runtime/src/typedarray_props.rs

printf '\n=== set-related references ===\n'
rg -n "key-is-out-of-bounds|Set/tonumber-value-throws|typed_array_coerce_element_for_side_effects|coerce_element_for_side_effects|ToBigInt" crates/perry-runtime/src/typedarray_props.rs crates/perry-runtime/src/typedarray/access.rs -S

printf '\n=== test262 or test references ===\n'
rg -n "key-is-out-of-bounds|tonumber-value-throws|BigInt64Array|BigUint64Array" . -g '!target' -g '!dist' -g '!build' -S

Repository: PerryTS/perry

Length of output: 31271


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== crates/perry-runtime/src/typedarray/access.rs (100-170) ==='
sed -n '100,170p' crates/perry-runtime/src/typedarray/access.rs

echo
echo '=== crates/perry-runtime/src/typedarray/bigint.rs (1-180) ==='
sed -n '1,180p' crates/perry-runtime/src/typedarray/bigint.rs

echo
echo '=== crates/perry-runtime/src/typedarray_props.rs (540-570) ==='
sed -n '540,570p' crates/perry-runtime/src/typedarray_props.rs

Repository: PerryTS/perry

Length of output: 11763


Coerce out-of-bounds writes on BigInt views before returning value_needs_coercion_side_effect skips plain strings, so js_typed_array_set can turn ta[100] = 5 or ta[100] = "bad" on a BigInt64Array/BigUint64Array into a silent no-op instead of running ToBigInt and propagating the expected TypeError/SyntaxError. The invalid-index path should coerce unconditionally for BigInt views, matching the property-set path.

🤖 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/typedarray/access.rs` around lines 137 - 156, The
out-of-bounds branch in typedarray/access.rs within the typed array set path
should not skip coercion for BigInt views when the value is a plain string or
other primitive that still requires ToBigInt. Update js_typed_array_set so the
invalid-index handling always runs bigint::to_bigint_for_store for
KIND_BIGINT64/KIND_BIGUINT64 before returning, while keeping jsvalue_to_f64 for
numeric views; use value_needs_coercion_side_effect only for the non-BigInt
fast-path if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant