From 8cb4e49f1b0c9c0fe7b7c4a76525254e233f855e Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Sat, 1 Aug 2026 21:12:11 +0000 Subject: [PATCH 1/2] Add reproduction for Array issue --- packages/effect/test/ArrayNaNIndex.test.ts | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/effect/test/ArrayNaNIndex.test.ts diff --git a/packages/effect/test/ArrayNaNIndex.test.ts b/packages/effect/test/ArrayNaNIndex.test.ts new file mode 100644 index 00000000000..a7abedcffc0 --- /dev/null +++ b/packages/effect/test/ArrayNaNIndex.test.ts @@ -0,0 +1,30 @@ +import { assert, describe, it } from "@effect/vitest" +import { Array as Arr, Option } from "effect" + +describe("Array NaN indexes", () => { + const input = [1, 2, 3] + + it("rejects NaN in get", () => { + assert.deepStrictEqual(Arr.get(input, Number.NaN), Option.none()) + }) + + it("rejects NaN in getUnsafe", () => { + assert.throws(() => Arr.getUnsafe(input, Number.NaN), /Index out of bounds/) + }) + + it("rejects NaN in insertAt", () => { + assert.deepStrictEqual(Arr.insertAt(input, Number.NaN, 4), Option.none()) + }) + + it("rejects NaN in replace", () => { + assert.deepStrictEqual(Arr.replace(input, Number.NaN, 4), Option.none()) + }) + + it("rejects NaN in modify", () => { + assert.deepStrictEqual(Arr.modify(input, Number.NaN, (n) => n * 2), Option.none()) + }) + + it("treats removing NaN as an out-of-bounds no-op", () => { + assert.deepStrictEqual(Arr.remove(input, Number.NaN), input) + }) +}) From 878bc0e7a17a8e9b125c1f40c62c78f9b7b65d55 Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Sun, 2 Aug 2026 09:40:03 +0200 Subject: [PATCH 2/2] Fix Array index handling --- .changeset/fix-array-non-finite-indexes.md | 5 +++++ packages/effect/src/Array.ts | 19 +++++++++++-------- packages/effect/test/ArrayNaNIndex.test.ts | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 .changeset/fix-array-non-finite-indexes.md diff --git a/.changeset/fix-array-non-finite-indexes.md b/.changeset/fix-array-non-finite-indexes.md new file mode 100644 index 00000000000..dcfd9db0fd6 --- /dev/null +++ b/.changeset/fix-array-non-finite-indexes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Array index operations handling `NaN` and fractional indexes. diff --git a/packages/effect/src/Array.ts b/packages/effect/src/Array.ts index 35eb7002231..c35cf612a90 100644 --- a/packages/effect/src/Array.ts +++ b/packages/effect/src/Array.ts @@ -909,7 +909,7 @@ export const length = (self: ReadonlyArray): number => self.length /** @internal */ export function isOutOfBounds(i: number, as: ReadonlyArray): boolean { - return i < 0 || i >= as.length + return !Number.isFinite(i) || i < 0 || i >= as.length } const clamp = (i: number, as: ReadonlyArray): number => Math.floor(Math.min(Math.max(0, i), as.length)) @@ -1875,10 +1875,11 @@ export const insertAt: { (self: Iterable, i: number, b: B): Option.Option> } = dual(3, (self: Iterable, i: number, b: B): Option.Option> => { const out: Array = Array.from(self) // copy because `splice` mutates the array - if (i < 0 || i > out.length) { + const index = Math.floor(i) + if (index !== out.length && isOutOfBounds(index, out)) { return Option.none() } - out.splice(i, 0, b) + out.splice(index, 0, b) return Option.some(out as any) }) @@ -1966,12 +1967,13 @@ export const modify: { ): Option.Option | B>> } = dual(3, (self: Iterable, i: number, f: (a: A) => B): Option.Option> => { const arr = Array.from(self) - if (isOutOfBounds(i, arr)) { + const index = Math.floor(i) + if (isOutOfBounds(index, arr)) { return Option.none() } const out: Array = arr - const b = f(arr[i]) - out[i] = b + const b = f(arr[index]) + out[index] = b return Option.some(out) }) @@ -2004,10 +2006,11 @@ export const remove: { (self: Iterable, i: number): Array } = dual(2, (self: Iterable, i: number): Array => { const out = Array.from(self) - if (isOutOfBounds(i, out)) { + const index = Math.floor(i) + if (isOutOfBounds(index, out)) { return out } - out.splice(i, 1) + out.splice(index, 1) return out }) diff --git a/packages/effect/test/ArrayNaNIndex.test.ts b/packages/effect/test/ArrayNaNIndex.test.ts index a7abedcffc0..9c5f258c338 100644 --- a/packages/effect/test/ArrayNaNIndex.test.ts +++ b/packages/effect/test/ArrayNaNIndex.test.ts @@ -1,9 +1,9 @@ import { assert, describe, it } from "@effect/vitest" import { Array as Arr, Option } from "effect" -describe("Array NaN indexes", () => { - const input = [1, 2, 3] +const input = [1, 2, 3] +describe("Array NaN indexes", () => { it("rejects NaN in get", () => { assert.deepStrictEqual(Arr.get(input, Number.NaN), Option.none()) }) @@ -28,3 +28,13 @@ describe("Array NaN indexes", () => { assert.deepStrictEqual(Arr.remove(input, Number.NaN), input) }) }) + +describe("Array fractional indexes", () => { + it("floors the index in replace", () => { + assert.deepStrictEqual(Arr.replace(input, 1.5, 4), Option.some([1, 4, 3])) + }) + + it("floors the index in modify", () => { + assert.deepStrictEqual(Arr.modify(input, 1.5, (n) => n * 2), Option.some([1, 4, 3])) + }) +})