From a538ff42f0ab99581f2e65a00a03510781090b19 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:19:16 -0500 Subject: [PATCH] Read only value slots in lastIndexOf with an element-typed needle lastIndexOf with a needle of the element type read every non-hole index as the element type. An undefined or foreign element written through a dynamic view (or a hole that toSorted turned into undefined) has no element-typed payload, so the read aborted with "array index N is undefined". The search now reads an index only when it holds a value, and skips undefined and foreign elements, as Node's strict equality does for a string or number needle. --- .../lowering/containers/array-methods.ts | 32 ++++++++++++------- .../6068-foreign-elements-through-views.ts | 23 +++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 4ef203edd..0e8b4fd84 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1788,17 +1788,27 @@ function arraySearchHelper( }, loc, }; - const visitPresent: IrStmt[] = [ - { kind: "varDecl", localId: "value.0", init: value, loc }, - { - kind: "if", - cond: match, - then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], - else_: null, - loc, - }, - advance, - ]; + const found: IrStmt = { + kind: "if", + cond: directNeedle ? equal : match, + then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], + else_: null, + loc, + }; + // A direct needle reads only value slots: an undefined or foreign + // element written through a dynamic view has no element-typed payload. + const visitPresent: IrStmt[] = directNeedle + ? [ + { + kind: "if", + cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc }, + then: [{ kind: "varDecl", localId: "value.0", init: value, loc }, found], + else_: null, + loc, + }, + advance, + ] + : [{ kind: "varDecl", localId: "value.0", init: value, loc }, found, advance]; const skipHole: IrStmt[] = method === "lastIndexOf" ? [advance] diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index 5181d7fe5..1b23f6fa7 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -177,3 +177,26 @@ holderView.push({ }, }); console.log(JSON.stringify(holder), JSON.stringify(holder)); + +// toSorted keeps the full length and reads holes as undefined; indexOf, +// lastIndexOf and includes skip the undefined and foreign elements. +const searched: string[] = ["b", "a"]; +const searchedView: unknown[] = searched; +searchedView[9] = 4; +searchedView[5] = undefined; +console.log(searched.indexOf("b"), searched.lastIndexOf("b"), searched.lastIndexOf("a", 8)); +console.log(searched.includes("a"), searched.lastIndexOf("z")); +const searchedSorted = searched.toSorted(); +console.log(searchedSorted.length, searchedSorted.slice(0, 4), searchedSorted.lastIndexOf("b")); +console.log(searchedSorted.indexOf("a"), searchedSorted.includes("b")); +const counts: number[] = [3, 1]; +const countsView: unknown[] = counts; +countsView[6] = "x"; +countsView[4] = 2; +const countsSorted = counts.toSorted(); +console.log(countsSorted.length, countsSorted, countsSorted.lastIndexOf(3), counts.lastIndexOf(2)); +const far: number[] = [5]; +const farView: unknown[] = far; +farView[70000] = "z"; +const farSorted = far.toSorted(); +console.log(farSorted.length, farSorted.slice(0, 3), farSorted.slice(69999), farSorted.lastIndexOf(5));