Skip to content

Array methods absent from %TypedArray%.prototype answer instead of throwing (flat, flatMap, push, pop, shift, unshift, splice, toSpliced) #8138

Closed

Description

@proggeramlug

Summary

Nine Array.prototype methods that do not exist on %TypedArray%.prototype
answer with a value on a typed-array receiver instead of throwing. Node throws
TypeError: … is not a function; perry returns [], undefined, or garbage.

function opaque(x: any): any { return x; }

opaque(new Int32Array([1, 2, 3])).flatMap((x: any) => [x])
// node:  TypeError: opaque(...).flatMap is not a function
// perry: [5.08e-321, 2.1219957924e-314, 1e-323]

Measured against node v26.5.1 on main @ 0a1e78e5f.

Full list

The methods absent from %TypedArray%.prototype are exactly:
concat, flat, flatMap, pop, push, shift, splice, toSpliced,
unshift.

expression perry node
i32.concat([9]) [9] TypeError
i32.flat() [] TypeError
i32.flatMap(f) [5.08e-321,2.12e-314,1e-323] — garbage TypeError
i32.push(9) [] TypeError
i32.pop() undefined TypeError
i32.shift() undefined TypeError
i32.unshift(9) [] TypeError
i32.splice(1,1) {obj}, receiver untouched TypeError
i32.toSpliced(1,1) [] TypeError
u8.flat() [] TypeError
u8.toSpliced(1,1) [] TypeError
u8.push(9) [] TypeError

u8.splice, u8.concat and u8.flatMap DO throw — but with
TypeError: (Buffer).splice is not a function, i.e. right answer, wrong
message and wrong reason (dispatch_buffer_method's name list simply has no
entry, rather than the property genuinely being absent).

Why this is NOT the #8041 receiver-reroute family

These helpers are dead below clean_arr_ptr in the same textual sense as the
ones #8090/#8109/#8119/#8120/#8130 fixed, and it is tempting to file them
together. They are a different bug, and "fixing the reroute" would make them
worse:

js_array_concat is the worked example, already noted in #8130's caveats. Node
gives [{}] for a typed-array argument; perry gives [] today; and the
pre-#8041 live reroute gave [1,2,3]. It was wrong before #8041 and wrong
after, in different ways. Waking the reroute back up would restore an answer
node never produces.

The actual defect is one level up: the property should not resolve at all.
Perry's method dispatch answers flatMap on a typed array because the fused
Expr::ArrayFlatMap fold and is_array_expr both treat Int32Array as an
array, and nothing consults %TypedArray%.prototype's membership. So the fix
is a receiver-kind check at method resolution, producing the spec
TypeError, not a reroute inside the helper.

js_array_concat specifically is being handled by #8124
(concat-spreadable); this issue covers the other eight.

Suggested shape

A single table of the nine names, consulted where the typed-array/Buffer
dispatch decision is already made — dispatch_typed_array_method
(object/native_call_method/typed_array.rs) and dispatch_buffer_method
(object/buffer_dispatch.rs) already return None / "is not a function" for
unrecognised names, so the missing piece is preventing the static HIR folds
from claiming these nine for a receiver whose type is a typed array. That is
the same gate is_array_expr already applies to exclude Uint8Array from
copyWithin's fold (local_array_methods.rs:800-839), generalised.

Low urgency: every row here is a program that is already broken and would
throw under node, so no working code depends on the current answers. The
flatMap row is the one worth prioritising, because it returns garbage
values
rather than an empty result, which a caller can propagate silently.

Context

Found by the exhaustive Array.prototype receiver sweep that produced #8135.
The full 33-row table, with the funnel verdict and measured node diff for every
Array.prototype-reachable runtime entry point, is in the #8135 PR description.

Activity

  1. proggeramlug commented on Aug 15, 2026

    @proggeramlug
    ContributorAuthor

    The "Suggested shape" premise is false — measured

    This issue proposes:

    dispatch_typed_array_method … and dispatch_buffer_method … already return
    None / "is not a function" for unrecognised names, so the missing piece is
    preventing the static HIR folds from claiming these nine

    The first half is only true for dispatch_buffer_method. Measured on main
    @ a3118cfea, oracle node 26.5.1, --profile perry-dev,
    PERRY_NO_AUTO_OPTIMIZE=1:

    function opaque(x: any): any { return x; }
    const i32 = new Int32Array([1, 2, 3]);
    const arr = [1, 2, 3];
    const o: any = {};
    opaque(i32).nosuch()   // perry ""   node TypeError: opaque(...).nosuch is not a function
    opaque(arr).nosuch()   // perry ""   node TypeError: opaque(...).nosuch is not a function
    o.nosuch()             // perry TypeError: nosuch is not a function      (right shape)
    opaque(u8).nosuch()    // perry TypeError: (Buffer).nosuch is not a function

    dispatch_typed_array_method does return None for an unrecognised name, but
    the dispatch tower's tail then answers the empty string — and it does so
    for a plain array receiver too, not just a typed array. So "decline the
    fold" produces "", not a TypeError: the nine names would move from a wrong
    value to a different wrong value.

    Making them throw therefore means introducing an unknown-method throw for
    array-ish receivers in general — a much broader behavioural change than the
    nine names, with its own blast radius (every is_array_expr-classified
    receiver reaching the tower's tail). And the static-receiver half
    (i32.flat(), 11 of the 29 measured rows) has to decline in is_array_expr /
    the HIR folds, which puts perry-codegen in the diff and brings the
    perry-codegen integration suite (1476 passed / 9 failed baseline, not run
    per-PR) into scope.

    Full measured table on a3118cfea

    u8.* rows already throw with the right shape but the wrong reason
    (dispatch_buffer_method's name list has no entry). i32.* rows do not throw
    at all:

    expression perry node
    opaque(i32).flat() [] TypeError
    opaque(i32).flatMap(f) [5.08e-321,4.24e-314,1.5e-323] — garbage TypeError
    opaque(i32).push(9) 0 TypeError
    opaque(i32).pop() undefined TypeError
    opaque(i32).shift() undefined TypeError
    opaque(i32).unshift(9) 0 TypeError
    opaque(i32).splice(1,1) [] TypeError
    opaque(i32).toSpliced(1,1) [] TypeError
    opaque(i32).concat([9]) [9] TypeError
    opaque(u8).flat() [] TypeError
    opaque(u8).toSpliced(1,1) [] TypeError
    i32.flat() (static) [] TypeError
    (i32 as any).push(9) (static) 1, and the binding is corrupted TypeError
    (i32 as any).pop() (static) 9 TypeError
    (i32 as any).toSpliced(1,1) (static) [9] TypeError
    (u8 as any).flat() (static) [] TypeError
    (u8 as any).toSpliced(1,1) (static) [] TypeError

    The static rows are worse than the issue records. After
    (i32 as any).push(9); (i32 as any).pop() the receiver itself is destroyed:
    Array.from(i32) reads [9] where node has [1,2,3] — the ArrayHeader
    mutators rewrote the TypedArrayHeader's {length, capacity} prefix. Any fix
    needs a control asserting the receiver's contents are UNCHANGED after each
    attempted call, not only that the call threw.

    Suggested re-scoping

    1. Decide the general question first: should an unknown method on an array /
      typed-array receiver throw? That is the actual defect and it subsumes all
      nine names. opaque(arr).nosuch() returning "" is its own bug.
    2. Only then gate the nine names, and only if the general throw does not
      already cover them.
    3. The message shape reachable today is TypeError: <name> is not a function
      (the object path) or TypeError: (Buffer).<name> is not a function (the
      buffer path); node's opaque(...).<name> is not a function needs the
      call-site expression text, which the runtime does not have. A test should
      assert the shape, not byte-equality with node.

    Measured while fixing #8149 / #8139 (PR #8186), which were filed from the same
    sweep. Those two do not share a cause with this one: #8149 is the
    receiver-routing family, #8139 is a missing arm in a value-kind dispatcher, and
    this issue is about method existence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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