Skip to content

runtime: complete Array.prototype.concat variadic and non-mutating semantics #2805

Description

@andrewtdiz

Summary

Array.prototype.concat should accept any number of arguments, return a new array, leave the receiver unchanged, and follow Symbol.isConcatSpreadable / array-like spreadability rules. Perry currently handles only the single-argument static path, returns the receiver unchanged for multi-arg static calls, and dynamic dispatch calls the internal mutating concat helper.

Node parity probe

Run with Node v25.9.0:

for (const [label, fn] of [
  ["concat one array", () => [1].concat([2, 3])],
  ["concat multiple", () => [1].concat([2], [3, 4], 5)],
  ["concat preserves original", () => { const a = [1]; const out = a.concat([2]); return [out, a, out === a]; }],
  ["concat spreadable false", () => { const x = [2, 3]; x[Symbol.isConcatSpreadable] = false; return [1].concat(x); }],
  ["concat array-like spreadable true", () => { const x = {0: "a", 1: "b", length: 2, [Symbol.isConcatSpreadable]: true}; return [1].concat(x); }],
]) {
  try {
    console.log(label + ": " + JSON.stringify(fn()));
  } catch (e) {
    console.log(label + ": throws " + e.name + ": " + e.message);
  }
}

Output:

concat one array: [1,2,3]
concat multiple: [1,2,3,4,5]
concat preserves original: [[1,2],[1],false]
concat spreadable false: [1,[2,3]]
concat array-like spreadable true: [1,"a","b"]

Current Perry behavior in source

The current static path is intentionally single-argument only:

  • crates/perry-codegen/src/lower_array_method.rs:106-132 routes one-arg concat to js_array_concat_new, but the comment says "For simplicity we only handle single-argument concat." and args.len() != 1 returns recv_box unchanged.
  • crates/perry-runtime/src/array/concat_reverse.rs:94-124 implements js_array_concat_new(arr, other) for exactly one other array.

Dynamic dispatch uses the mutating helper:

  • crates/perry-runtime/src/object/native_call_method.rs:1255-1260 handles dynamic "concat" by reading only the first argument and calling crate::array::js_array_concat(arr, other_ptr).
  • crates/perry-runtime/src/array/concat_reverse.rs:5-92 documents js_array_concat as an append-into-destination helper used by spread/desugaring, while js_array_concat_new is the JS-semantic non-mutating helper.

The fixture comments still call out multi-arg concat as a known divergence:

  • test-files/test_compat_arrays_iterators.ts:5-8 excludes multi-arg concat from parity coverage, while line 40 covers only the single-argument case.

Expected fix shape

Implement full JS concat behavior for static and dynamic paths:

  • Accept zero or more concat arguments.
  • Always return a new array and leave the receiver unchanged.
  • Append each argument in order.
  • Spread arrays by default.
  • Respect Symbol.isConcatSpreadable === false for arrays and === true for array-like objects.
  • Preserve non-array/non-spreadable values as single elements.

Please add regression coverage for zero args, one arg, multiple args, primitive/non-array arguments, receiver immutability, Symbol.isConcatSpreadable false on arrays, and spreadable array-like objects.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions