Run Array.prototype's generic methods over array-like receivers

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 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-29 05:38:56 -07:00
co-authored by Claude Opus 5.5
parent e19fa7a6b1
commit 6cea7a348f
4 changed files with 112 additions and 2 deletions
@@ -2854,6 +2854,30 @@ function immediateObjectTagReceiver(
return null;
}
/** Array.prototype methods whose `.<name>.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 },
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/js-array-proto-array-likes.js"
],
"diags": []
}
@@ -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'));
@@ -0,0 +1 @@
{ "plain": 3300, "san": 5400 }