diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index aff98d9b49..9962302c3c 100644 --- a/packages/codemode/interpreter-support.md +++ b/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 diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts index 365a077837..5c3574088b 100644 --- a/packages/codemode/src/interpreter/methods.ts +++ b/packages/codemode/src/interpreter/methods.ts @@ -727,16 +727,16 @@ const invokeArrayMethod = ( } 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 = ( 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 = ( } 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": diff --git a/packages/codemode/test/array-callbacks-test262.test.ts b/packages/codemode/test/array-callbacks-test262.test.ts index 3de3a94a4c..c34137abd0 100644 --- a/packages/codemode/test/array-callbacks-test262.test.ts +++ b/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) + }) +})