Skip to content

[JSC] TypedArray indexOf/lastIndexOf/includes: an int32 needle that a Float16Array or Float32Array cannot hold exactly matches nothing - #604

Closed
robobun wants to merge 1 commit into
mainfrom
robobun/b68345e0/typed-array-indexof-int32-needle
Closed

robobun wants to merge 1 commit into
mainfrom
robobun/b68345e0/typed-array-indexof-int32-needle

Conversation

@robobun

@robobun robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

indexOf, lastIndexOf and includes on a Float32Array or Float16Array match a neighbouring element when the needle is an integer-valued Number that the element type cannot hold exactly:

const f32 = new Float32Array([16777216, 2147483648]);
f32.indexOf(16777217)          // 0, expected -1 (rounds to 2^24)
f32.includes(2147483647)       // true, expected false (rounds to 2^31)
const f16 = new Float16Array([Infinity, 2048, 65504]);
f16.indexOf(65520)             // 0, expected -1 (rounds to Infinity)
f16.indexOf(2049)              // 1, expected -1 (rounds to 2048)
f16.includes(65505)            // true, expected false (rounds to 65504)

The spec compares with IsStrictlyEqual (indexOf, lastIndexOf) and SameValueZero (includes) on Number values, and 16777217 !== 16777216. V8 returns -1 / false for all of the above.

The result also depends on how JSC happens to encode the needle. The same value read back from a Float64Array is a double-encoded JSValue and already returns -1, as does any fractional needle. Only int32-encoded needles hit: integer literals, parseInt results, integer arithmetic.

The cause is in TypedArrayAdaptors.h. toNativeFromValueWithoutCoercion converts the needle to the element type and rejects it when the conversion is not exact. toNativeFromDoubleWithoutCoercion does the round-trip check. FloatTypedArrayAdaptor::toNativeFromInt32WithoutCoercion and Float16Adaptor::toNativeFromInt32WithoutCoercion are a plain static_cast, so an int32 above 2^24 (float) or 2^11 (half) is rounded to the nearest representable value, and a half at or above 65520 becomes an infinity. The search then finds that rounded value. Upstream WebKit has the same code.

Fix

Check that the int32 round-trips through the element type and return std::nullopt when it does not, the same idiom IntegralTypedArrayAdaptor and the double path use. Float64Array is unaffected because every int32 is a double, and the three callers (genericTypedArrayViewProtoFuncIncludes, IndexOf, LastIndexOf) already treat nullopt as "not found".

Verification

  • JSTests/stress/typed-array-index-of-int32-needle-not-representable.js: Float32 needles around ±2^24 and ±2^31, Float16 needles around ±2048, 65504..65536 and ±2^31, each passed both as an int32 and as the same value read from a Float64Array, through all three methods, plus Float64Array and Int32Array controls. Fails on the current jsc at the first case (Float32Array 16777217 indexOf: got 0, expected -1), passes with this change.
  • node v26.3.0 agrees with every expectation in the file except new Float16Array([..., -2048]).indexOf(-2048), where V8 returns -1 for any negative finite half (new Float16Array([-1]).indexOf(-1) is -1 there). That is a V8 bug, JSC returns the index before and after this change.

… Float16Array or Float32Array cannot hold exactly matches nothing

%TypedArray%.prototype.indexOf and lastIndexOf compare with IsStrictlyEqual,
and includes with SameValueZero, on Number values. 16777217 is not equal to
any element of a Float32Array, because no float has that value. The search
converts the needle to the element type first and rejects it when the
conversion is not exact. The double path did that. The int32 path did a
plain static_cast, so an int32 needle was rounded to the nearest float or
half and then matched that neighbour:

  new Float32Array([16777216]).indexOf(16777217)   // 0, expected -1
  new Float32Array([2 ** 31]).includes(2 ** 31 - 1) // true, expected false
  new Float16Array([2048]).indexOf(2049)           // 0, expected -1
  new Float16Array([Infinity]).indexOf(65520)      // 0, expected -1

The same needle read back from a Float64Array (a double-encoded JSValue)
already returned -1, so the result depended on the value's internal
representation.

Make FloatTypedArrayAdaptor and Float16Adaptor check that the int32
round-trips through the element type, like IntegralTypedArrayAdaptor and
the double path already do. Float64Array is unaffected: every int32 is a
double.

* JSTests/stress/typed-array-index-of-int32-needle-not-representable.js: Added.
* Source/JavaScriptCore/runtime/TypedArrayAdaptors.h:
(JSC::FloatTypedArrayAdaptor::toNativeFromInt32WithoutCoercion):
(JSC::Float16Adaptor::toNativeFromInt32WithoutCoercion):
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 12 days. After that, they cost $0.25 per reviewed file.

Or wait 3 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 51d59b84-4bba-4d13-a027-84c556bf891a

📥 Commits

Reviewing files that changed from the base of the PR and between 10350dd and a9c8309.

📒 Files selected for processing (2)
  • JSTests/stress/typed-array-index-of-int32-needle-not-representable.js
  • Source/JavaScriptCore/runtime/TypedArrayAdaptors.h

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

@claude claude 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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

}
}

for (let i = 0; i < 1e3; ++i)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) New stress test hardcodes 1e3 iterations instead of using testLoopCount, violating JSTests/README.md rule 2 (imported by JSTests/CLAUDE.md): tests must use testLoopCount so the harness can tune iteration count per configuration for tier-up and early exit. Fix: replace for (let i = 0; i < 1e3; ++i) with for (let i = 0; i < testLoopCount; ++i).

Extended reasoning...

JSTests/CLAUDE.md @-imports JSTests/README.md, whose "Adding Tests" section states new tests are required to use testLoopCount to control iteration count because the jsc CLI sets it based on the test configuration (so tests tier up where that matters and exit early where it doesn't). Line 89 of the new test uses a literal 1e3. In eager-tier configurations this may over-iterate (risking the 200ms budget in rule 1), and in no-JIT/interpreter-only configurations it wastes time; more importantly it breaks the mandated convention that lets the harness control tier coverage. Existing stress tests (e.g. JSTests/stress/zero-to-string.js) follow the testLoopCount pattern.

Verification: nit — /home/claude/webkit/JSTests/stress/typed-array-index-of-int32-needle-not-representable.js:89 reads for (let i = 0; i < 1e3; ++i), a hardcoded iteration count. /home/claude/webkit/JSTests/CLAUDE.md line 1 @-imports README.md, and /home/claude/webkit/JSTests/README.md:17-20 states new tests are "required" to "Use testLoopCount … to control how many iterations a test runs. The jsc…

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
a9c83095 autobuild-preview-pr-604-a9c83095 2026-09-08 22:34:21 UTC

@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Cross-reference: #609 fixes the same toNativeFromValueWithoutCoercion helper for all three needle representations (this path, plus the Uint8Clamped double path from the original report). The two PRs touch the same lines, so only one shape should merge. See the note on #609.

@robobun

robobun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #609. That change routes FloatTypedArrayAdaptor and Float16Adaptor toNativeFromInt32WithoutCoercion through the checked toNativeFromDoubleWithoutCoercion (range check, then round-trip check), which rejects the same int32 needles this PR does, and also fixes the BigInt64 / BigUint64 and Uint8Clamped paths of the same helper. Its stress test JSTests/stress/typed-array-index-of-unrepresentable-needle.js covers the Float32 (2^24 + 1, INT32_MAX) and Float16 (2049, 65520, 65536, INT32_MAX, INT32_MIN) rows. The Bun-side bump is oven-sh/bun#42093.

@robobun robobun closed this Sep 9, 2026
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