fix(runtime): Array.prototype read-only methods accept array-like receivers#4231
Merged
Conversation
Array.prototype.<method>.call(arrayLikeObject, ...) was broken across the
whole read-only Array method surface (map/filter/reduce/reduceRight/some/
every/forEach/find/findIndex/findLast/findLastIndex/at/indexOf/lastIndexOf/
includes/slice/join/flatMap). A generic array-like receiver — a plain object
with a `length` property and indexed keys, e.g. {length:3, 0:"a", 1:"b"} —
was treated as a raw ArrayHeader: methods read garbage (8.48e-314) or threw
"value is not a function" / TypeError.
Two orthogonal root causes, both fixed:
1. HIR (primary): `try_builtin_prototype_method_apply_call` rewrote
`Array.prototype.map.call(like, fn)` into `like.map(fn)`, which the normal
member-call fast path only routes to `js_array_*` when the receiver is
statically array-typed; for an Any/object receiver it lowered to a dynamic
method lookup that found no `map` field and threw. Now, when the resolved
method is a known read-only Array method, the receiver is materialized into
a REAL array via `Expr::ArrayFrom` (→ js_array_from_value → js_array_clone,
which already detects `{length, 0:…}` array-likes) before the dedicated
`Expr::Array*` variant is built. This also fixes the methods whose codegen
is INLINED (e.g. forEach reads `(*arr).length`/`arr[i]` directly rather than
calling js_array_forEach) — the inlined loop now sees a genuine ArrayHeader.
2. Runtime (defense-in-depth): add `normalize_array_receiver`, used at the
entry of the read-only `js_array_*` helpers in place of `clean_arr_ptr`. It
reads the GC-header obj_type; for GC_TYPE_OBJECT (a plain array-like) it
materializes via `js_array_from_arraylike`, otherwise it falls straight
through to `clean_arr_ptr`. Real arrays (GC_TYPE_ARRAY) pay only one
already-warm header-byte read + an integer compare — the registry lookups
and materialization are reached only for plain-object receivers, so the
`[1,2,3].map(...)` hot path is unchanged. This catches array-like receivers
that reach the runtime through indirection the HIR fold can't see statically.
Mutators (push/pop/shift/unshift/splice/sort/reverse/fill/copyWithin) and
concat are intentionally left as-is: mutators need spec write-back of indexed
props + length to the receiver (out of scope for this pass), and concat's
array-like receiver has non-spreading single-element semantics that differ
from the array algorithm.
Verified byte-identical to `node --experimental-strip-types` across all
read-only methods on a {length:3,0,1,2} array-like, plus regression cases on
real arrays (map/filter/reduce/for-of/spread/slice/indexOf) and a TypedArray
(both PERRY_NO_AUTO_OPTIMIZE and default auto-optimize builds). 974/974
perry-runtime lib tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Array.prototype.<method>.call(arrayLikeObject, ...)now works for the read-only/ returning Array methods (map, filter, forEach, find, findIndex, findLast,
findLastIndex, some, every, flatMap, reduce, reduceRight, indexOf, lastIndexOf,
includes, slice, at, join). Previously these treated a generic array-like object
receiver as a raw ArrayHeader and read garbage or threw TypeError.
Why
The single largest test262 cluster: ~1,618
built-ins/Arrayfailures, spreadevenly across every prototype method — one shared root cause: array-like object
receivers weren't materialized into real arrays.
How
Two orthogonal fixes:
try_builtin_prototype_method_apply_callrewroteArray.prototype.map.call(like, fn)→like.map(fn), which for anAny/object receiver lowered to a dynamic lookup that found nomapand threw"value is not a function". Now, for a known read-only method, the receiver is
materialized via
Expr::ArrayFrom(→js_array_from_value/js_array_clone,which already detect
{length, 0:…}array-likes) before building the dedicatedarray-method node. This also fixes inlined-codegen methods (e.g.
forEach).normalize_array_receiverreplacesclean_arr_ptrat the read-only method entries. It reads the GC-headerobj_type; ONLYGC_TYPE_OBJECT(a plain array-like) is materialized viajs_array_from_arraylike. Real arrays (GC_TYPE_ARRAY) fall straight through— one warm header read + an int compare, no materialization — so
[1,2,3].map(...)is not regressed.Verification
built-ins/Array: 1618 → 1161 (503 fixed; net −457).node --experimental-strip-typesunder BOTHPERRY_NO_AUTO_OPTIMIZE=1and default auto-optimize: array-like map/indexOf/reduce/slice/join/includes, real-array map/filter/reduce/slice/indexOf, for-of,
spread, TypedArray
.map, chained filter→map→reduce.perry-runtimesuite: 974/974.Known: 46 newly-surfaced (pre-existing) failures → #4230
The correct materialization exposes ~46 pre-existing latent bugs the old
garbage-read path masked. Confirmed identical on clean
main(notregressions): Array iteration doesn't skip holes (
[1,,3].mapcalls cb 3× vsNode's 2×) and
[].find()returnsNaNvsundefined. Tracked in #4230 for adedicated hole-skipping pass (which would recover these 46 + fix real sparse
arrays). Net effect of this PR remains strongly positive (−457).
Scope
Mutators (push/pop/shift/unshift/splice/sort/reverse/fill/copyWithin) and
concatare intentionally NOT changed — they need spec write-back / havedifferent array-like semantics; left for a follow-up.