From 6cea7a348f0a83bba33366fe4cdc4ea00dd6128d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 29 Oct 2026 02:32:59 -0700 Subject: [PATCH] Run Array.prototype's generic methods over array-like receivers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Array.prototype.push.call({length: 1, 0: "x"}, "y") and its siblings answered "not supported yet": the checked-dynamic array dispatch only ran the methods of a receiver that is a real array, though the algorithms are generic in the language. PR #2505 implemented them inside scr_dyn_invoke.c, which every program that links that unit then differed on (373 DIFFERS, 108 runtime-only programs), so it was withheld. The algorithms now live in their own gated unit, scr_dyn_array_like.c, reached through a distinct libCall (dyn.arrayLikeProtoCall) that only a JavaScript source emits. A native array receiver, or a live typed-ref array, delegates to the invoke unit's dispatch, so the array path keeps one implementation and its exact bytes. Methods: concat, copyWithin, fill, flat, flatMap, join, pop, push, reverse, shift, splice, toReversed, toSorted, toSpliced, unshift, with. Each algorithm starts with `? ToObject(this value)`, so a nullish receiver is a TypeError before any length is read — the check the invoke unit answers for its own receivers. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-calls.ts | 35 ++++++++- .../corpus/js-array-proto-array-likes.js.json | 6 ++ tests/corpus/js-array-proto-array-likes.js | 72 +++++++++++++++++++ .../js-array-proto-array-likes.js.json | 1 + 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json create mode 100644 tests/corpus/js-array-proto-array-likes.js create mode 100644 tests/harness/corpus-costs/js-array-proto-array-likes.js.json diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index e9d79213c3..2df4e1b4eb 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2854,6 +2854,30 @@ function immediateObjectTagReceiver( return null; } +/** Array.prototype methods whose `..call(receiver, ...)` form the + * array-like unit runs generically when `receiver` is not a native array: + * the rest of the prototype's methods besides the invoke unit's own set + * (at, slice, indexOf, lastIndexOf, includes and the higher-order + * callbacks). */ +const ARRAY_LIKE_BORROW_METHODS = new Set([ + "concat", + "copyWithin", + "fill", + "flat", + "flatMap", + "join", + "pop", + "push", + "reverse", + "shift", + "splice", + "toReversed", + "toSorted", + "toSpliced", + "unshift", + "with", +]); + const ARRAY_BORROW_METHODS = new Set([ "at", "every", @@ -2890,7 +2914,9 @@ function lowerArrayPrototypeBorrowCall( const method = access.expression; if ( method.questionDotToken || - !ARRAY_BORROW_METHODS.has(method.name.text) || + !( + ARRAY_BORROW_METHODS.has(method.name.text) || ARRAY_LIKE_BORROW_METHODS.has(method.name.text) + ) || !ts.isPropertyAccessExpression(method.expression) || method.expression.questionDotToken || method.expression.name.text !== "prototype" || @@ -2905,7 +2931,12 @@ function lowerArrayPrototypeBorrowCall( const args = call.arguments.nodes.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "libCall", - fn: "dyn.arrayProtoCall", + // The array-like unit runs an extended method's algorithm over a + // receiver that is not a native array and delegates the rest to the + // invoke unit's dispatch. + fn: ARRAY_LIKE_BORROW_METHODS.has(method.name.text) + ? "dyn.arrayLikeProtoCall" + : "dyn.arrayProtoCall", args: [ receiver, { kind: "strLit", value: method.name.text, type: STRING, loc }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json new file mode 100644 index 0000000000..27e53c33ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-array-proto-array-likes.js" + ], + "diags": [] +} diff --git a/tests/corpus/js-array-proto-array-likes.js b/tests/corpus/js-array-proto-array-likes.js new file mode 100644 index 0000000000..41fe61eb70 --- /dev/null +++ b/tests/corpus/js-array-proto-array-likes.js @@ -0,0 +1,72 @@ +// Array.prototype's methods are generic: they run over any object with a +// length, reading and writing ordinary properties. A call on such an +// object used to answer "not supported yet" for every method other than +// at/slice/indexOf/includes and the callback methods. +const resolved = []; +const report = (label, value) => resolved.push(label + '=' + value); + +const box = { length: 3, 0: 'a', 1: 'b', 2: 'c' }; +report('join', Array.prototype.join.call(box, '-')); +report('push', Array.prototype.push.call(box, 'd')); +report('after-push', Array.prototype.join.call(box, '')); +report('pop', Array.prototype.pop.call(box)); +report('length', box.length); +report('shift', Array.prototype.shift.call(box)); +report('unshift', Array.prototype.unshift.call(box, 'x', 'y')); +report('after-unshift', Array.prototype.join.call(box, ',')); +report('reverse', Array.prototype.join.call(Array.prototype.reverse.call(box), '/')); +report('concat', Array.prototype.concat.call(box, ['z']).join('+')); +report('slice', Array.prototype.slice.call(box).join('|')); + +const filler = { length: 4, 0: 1, 1: 2, 2: 3, 3: 4 }; +report('fill', Array.prototype.join.call(Array.prototype.fill.call(filler, 0, 1, 3), '')); +report('copyWithin', Array.prototype.join.call(Array.prototype.copyWithin.call(filler, 0, 2), '')); + +const spliced = { length: 4, 0: 'a', 1: 'b', 2: 'c', 3: 'd' }; +report('splice', Array.prototype.splice.call(spliced, 1, 2, 'X').join('')); +report('after-splice', Array.prototype.join.call(spliced, '')); +report('splice-none', Array.prototype.splice.call(spliced).length); + +const ordered = { length: 3, 0: 3, 1: 1, 2: 2 }; +report('toSorted', Array.prototype.toSorted.call(ordered).join('')); +report('toReversed', Array.prototype.toReversed.call(ordered).join('')); +report('toSpliced', Array.prototype.toSpliced.call(ordered, 1, 1).join('')); +report('with', Array.prototype.with.call(ordered, 0, 9).join('')); +report('with-negative', Array.prototype.with.call(ordered, -1, 9).join('')); +report('unsorted', Array.prototype.join.call(ordered, '')); + +const nested = { length: 3, 0: [1, 2], 1: 3, 2: [4, [5]] }; +report('flat', Array.prototype.flat.call(nested, 1).join('')); +report('flat-deep', Array.prototype.flat.call(nested, 2).join('')); +report('flatMap', Array.prototype.flatMap.call(nested, (value) => [value, value]).length); + +// Holes and absent indexes stay holes. +const sparse = { length: 3, 0: 'a', 2: 'c' }; +const copied = Array.prototype.toReversed.call(sparse); +report('holes', '1' in copied ? 'present' : 'absent'); +report('holes-join', copied.join('')); + +// Writes beyond the receiver's length extend it, as the length assignment +// at the end of push/splice does. +const extended = { length: 1, 0: 'only' }; +Array.prototype.push.call(extended, 'next'); +report('extended', JSON.stringify(extended)); + +// Every one of the algorithms starts with `? ToObject(this value)`, so a +// nullish receiver is a TypeError before any length is read. +const attempt = (label, run) => { + try { + report(label, 'no-throw ' + JSON.stringify(run())); + } catch (error) { + report(label, error.constructor.name + ': ' + error.message); + } +}; +attempt('null-join', () => Array.prototype.join.call(null)); +attempt('undefined-push', () => Array.prototype.push.call(undefined, 1)); +attempt('null-copyWithin', () => Array.prototype.copyWithin.call(null, 0, 0)); +attempt('undefined-fill', () => Array.prototype.fill.call(undefined, 0)); +attempt('null-toReversed', () => Array.prototype.toReversed.call(null)); +attempt('undefined-with', () => Array.prototype.with.call(undefined, 0, 1)); +attempt('null-splice', () => Array.prototype.splice.call(null, 0, 1)); + +console.log(resolved.join('\n')); diff --git a/tests/harness/corpus-costs/js-array-proto-array-likes.js.json b/tests/harness/corpus-costs/js-array-proto-array-likes.js.json new file mode 100644 index 0000000000..d5fcc27e98 --- /dev/null +++ b/tests/harness/corpus-costs/js-array-proto-array-likes.js.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 5400 }