Sfoglia il codice sorgente

fix(codemode): align array callback behavior (#36584)

Aiden Cline 1 mese fa
parent
commit
828a4b3f15

+ 0 - 1
packages/codemode/interpreter-support.md

@@ -171,7 +171,6 @@ ultimate source of truth.
 - [ ] `Array.prototype.toSpliced`.
 - [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
 - [ ] Complete sparse-array parity. Promise combinators do consume holes as `undefined` members, as in JS.
-- [ ] Correct `findLast` return behavior when its predicate mutates the examined element.
 
 ## Strings
 

+ 22 - 21
packages/codemode/src/interpreter/methods.ts

@@ -727,16 +727,16 @@ const invokeArrayMethod = <R>(
         }
         return undefined
       case "reduce": {
-        let accumulator: unknown
-        let start: number
-        if (args.length >= 2) {
-          accumulator = args[1]
-          start = 0
-        } else {
-          if (length === 0)
-            throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
-          accumulator = target[0]
-          start = 1
+        let start = 0
+        let accumulator = args[1]
+        if (args.length < 2) {
+          while (start < length && !(start in target)) start += 1
+          if (start === length)
+            throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node).as(
+              "TypeError",
+            )
+          accumulator = target[start]
+          start += 1
         }
         for (let index = start; index < length; index += 1) {
           if (!(index in target)) continue
@@ -745,16 +745,16 @@ const invokeArrayMethod = <R>(
         return accumulator
       }
       case "reduceRight": {
-        let accumulator: unknown
-        let start: number
-        if (args.length >= 2) {
-          accumulator = args[1]
-          start = length - 1
-        } else {
-          if (length === 0)
-            throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
-          accumulator = target[length - 1]
-          start = length - 2
+        let start = length - 1
+        let accumulator = args[1]
+        if (args.length < 2) {
+          while (start >= 0 && !(start in target)) start -= 1
+          if (start < 0)
+            throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node).as(
+              "TypeError",
+            )
+          accumulator = target[start]
+          start -= 1
         }
         for (let index = start; index >= 0; index -= 1) {
           if (!(index in target)) continue
@@ -764,7 +764,8 @@ const invokeArrayMethod = <R>(
       }
       case "findLast":
         for (let index = length - 1; index >= 0; index -= 1) {
-          if (yield* apply([target[index], index, target])) return target[index]
+          const item = target[index]
+          if (yield* apply([item, index, target])) return item
         }
         return undefined
       case "findLastIndex":

+ 55 - 0
packages/codemode/test/array-callbacks-test262.test.ts

@@ -26,9 +26,11 @@
  * - test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js
  * - test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js
  * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js
+ * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-c-ii-20.js
  * - test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js
  * - test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js
  * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js
+ * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-ii-20.js
  * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js
  * - test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js
  * - test/built-ins/Array/prototype/flatMap/depth-always-one.js
@@ -210,6 +212,11 @@ const cases = [
     code: `let calls = 0; const result = [1].reduce(() => { calls += 1; return 2 }); return [result, calls]`,
     expected: [1, 0],
   },
+  {
+    path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-c-ii-20.js",
+    code: `let accessed = false; const result = [11].reduce((previous) => { accessed = true; return previous === undefined }, undefined); return [result, accessed]`,
+    expected: [true, true],
+  },
   {
     path: "test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js",
     code: `const input = [1, 2, 3, 4, 5]; input.reduce(() => 1); return input`,
@@ -225,6 +232,11 @@ const cases = [
     code: `let calls = 0; const result = [1].reduceRight(() => { calls += 1; return 2 }); return [result, calls]`,
     expected: [1, 0],
   },
+  {
+    path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-ii-20.js",
+    code: `let accessed = false; const result = [11].reduceRight((previous) => { accessed = true; return previous === undefined }, undefined); return [result, accessed]`,
+    expected: [true, true],
+  },
   {
     path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js",
     code: `const input = [1, 2, 3, 4, 5]; input.reduceRight(() => 1); return input`,
@@ -323,3 +335,46 @@ describe("Test262 Array callback adaptations", () => {
     })
   }
 })
+
+describe("Array callback regressions", () => {
+  test("reduce and reduceRight find the first present element", async () => {
+    expect(
+      await value(`
+        const left = []
+        left[2] = 3
+        const right = []
+        right[0] = 4
+        right[3] = 1
+        right.pop()
+        return [left.reduce((a, b) => a + b), right.reduceRight((a, b) => a + b)]
+      `),
+    ).toEqual([3, 4])
+  })
+
+  test("reduce and reduceRight reject arrays containing only holes", async () => {
+    expect(
+      await value(`
+        const values = []
+        values[2] = 1
+        values.pop()
+        let left
+        let right
+        try { values.reduce((a, b) => a + b) } catch (error) { left = error.name }
+        try { values.reduceRight((a, b) => a + b) } catch (error) { right = error.name }
+        return [left, right]
+      `),
+    ).toEqual(["TypeError", "TypeError"])
+  })
+
+  test("findLast returns the value observed before predicate mutation", async () => {
+    expect(
+      await value(`
+        const values = [1]
+        return values.findLast((item, index, array) => {
+          array[index] = 2
+          return true
+        })
+      `),
+    ).toBe(1)
+  })
+})