Skip to content

fix(runtime): built-ins/Array sort + mutation + species test262 parity#4994

Merged
proggeramlug merged 1 commit into
mainfrom
array-sort-mutation-parity
Jun 11, 2026
Merged

fix(runtime): built-ins/Array sort + mutation + species test262 parity#4994
proggeramlug merged 1 commit into
mainfrom
array-sort-mutation-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Closes the built-ins/Array mutation-method tail: 729 → 821 of 823 (+92) across the 12 swept dirs (sort splice concat filter some map flat flatMap copyWithin fill toSorted toSpliced), judged against the node v26 oracle. The two remaining failures are out-of-scope tails (bare Array[1] global-ident lowering gap; sort.call(Symbol()) needs Symbol/BigInt wrapper objects).

Zero-regression validation

  • Shard 0/12 of built-ins + language (2645 judged) vs an origin/main baseline build: 0 new failures, +252 fixed as collateral (85.2% → 94.7% on the shard) — the ===-correctness, in-operator, and delete fixes help well beyond Array.
  • cargo test -p perry-runtime (RUST_TEST_THREADS=1): 1022 pass.
  • Failure-set diffs (not aggregates) at every iteration; sweeps run with a cleaned compile cache (the runner's TMPDIR object cache survives Rust rebuilds and was masking codegen fixes).

Root causes (before → after)

Root cause Effect before
HIR array-literal element type = first element's type [1,true,"x"] claimed Array(Number); === between two such loads compiled to raw fcmp — NaN-boxed bool/string/undefined never equal
sort had no spec-ops path accessors/sparse/inherited Array.prototype/Object.prototype elements mis-sorted; holes & undefined stringified instead of partitioned to the end; sort/precise-* all red
Array.prototype.{sort,concat,forEach,map,filter,some,every,find*,reduce*,indexOf,lastIndexOf,includes,at,join} were noop thunks every reflective borrow (obj.sort = Array.prototype.sort) or prototype-chain hit silently returned garbage
Dense entries trusted statically-Array receivers var x=[]; … x={0:0}; x.sort()/x.splice()/x.length read an ObjectHeader as ArrayHeader (or clean_arr_ptr NULLed it → silent no-op)
concat had no ArraySpeciesCreate; splice never validated length writability poisoned-ctor/species TypeErrors and 2^32 RangeErrors missing
Inline index/length fast paths ignored descriptors & prototype pollution reads bypassed index accessors, Array.prototype[i]/Object.prototype[i], custom array [[Prototype]] — fixed via cheap runtime guard flags (relaxed atomics) so unpolluted programs keep the fast path
Proxy/handle id bands (0xF0000+) dereferenced as GcHeaders Array.prototype.indexOf.call(proxy, …) SIGSEGV; generic engine now routes Proxy traps (incl. revoked-throw)
delete of an accessor-only object property succeeded vacuously ghost descriptors kept HasProperty true mid-iteration
copyWithin memmove'd stale storage after side-effecting coercions; generic form clamped ToLength at u32::MAX spec per-index loop on mutation; 2^53-1 lengths; Proxy has/get/set/delete traps

Also: own setter-only accessors now shadow inherited props in the generic engine; fill end-coercions (explicit undefined → len, −∞ → 0); foo.prototype = someArray is stored and linked to instances; sort comparator results go through ToNumber (NaN → +0); callbacks bind this = undefined when no thisArg (explicit thisArg routes the generic engine).

Code-only PR per contributor guidelines — no version bump / changelog (maintainer folds at merge).

…est262 parity

built-ins/Array mutation dirs (sort/splice/concat/filter/some/map/flat/
flatMap/copyWithin/fill/toSorted/toSpliced): 729 -> 821 of 823 (+92).
Broad shard 0/12 of built-ins+language vs origin/main: 0 new failures,
+252 fixed (85.2% -> 94.7%).

Root causes:
- HIR array-literal element inference took the FIRST element's type, so a
  mixed literal claimed Array(Number) and === between two such loads
  compiled to a raw fcmp (NaN-boxed bool/string/undefined never equal).
  Mixed literals now infer Array(Any).
- Array.prototype.sort gained the spec-ops SortIndexedProperties path for
  exotic receivers (index accessors, sparse storage, inherited
  Array.prototype / Object.prototype elements, custom array prototypes):
  HasProperty/Get collection (undefined partitioned, never compared),
  GC-rooted temp arrays across comparator calls, Set write-back that fires
  inherited Object.prototype setters, trailing DeletePropertyOrThrow.
  Default sort partitions holes/undefined; comparator results go through
  ToNumber (NaN -> +0).
- Array.prototype noop thunks (sort, concat, forEach/map/filter/some/every/
  find*/reduce*/indexOf/lastIndexOf/includes/at/join) replaced with real
  thunks routing the generic js_arraylike_* engine, so reflective borrows
  (obj.sort = Array.prototype.sort) and prototype-chain hits run the real
  algorithm. The generic engine gained sort/splice/concat entries, Proxy
  receivers (traps incl. revoked-throw), Date/RegExp/Error expando
  receivers, own-accessor shadowing, and canonical Object.prototype
  fallbacks for inherited length/indices.
- Type-confused receivers (a statically-Array variable reassigned to a
  plain object) reaching dense ArrayHeader entry points (sort/splice/
  concat/length) are detected on the RAW pointer before clean_arr_ptr and
  routed to the generic engine.
- concat/splice run ArraySpeciesCreate (poisoned constructor accessors,
  non-constructor species TypeError, ArrayCreate RangeError for len >=
  2^32); splice throws on non-writable/getter-only length; spliced/concat
  holes read through the prototype chain.
- copyWithin: side-effecting index coercions that mutate the array divert
  to a per-index spec loop; generic form ToLength clamps at 2^53-1 and
  routes Proxy has/get/set/delete traps. fill: explicit-undefined end means
  length, -Infinity end means 0, accessor-aware length read.
- Inline index/length fast paths stand down via runtime guard flags when
  index accessors, Array.prototype / Object.prototype indexed pollution, or
  a custom array [[Prototype]] exist; hole and OOB reads then walk the real
  chain. delete of an accessor-only object property now clears the
  descriptor side table. foo.prototype = someArray is stored and linked to
  instances. Proxy/handle id bands are never dereferenced as GcHeaders.

Remaining 2: filter/15.4.4.20-9-b-6 (bare Array[1] global lowering gap)
and sort/call-with-primitive (needs Symbol/BigInt wrapper objects).

Verified: 12-dir sweep 821/823 with clean compile cache, runtime unit
tests 1022 pass, shard 0/12 built-ins+language zero-regression vs
origin/main.
@proggeramlug proggeramlug merged commit da4f8be into main Jun 11, 2026
12 of 13 checks passed
@proggeramlug proggeramlug deleted the array-sort-mutation-parity branch June 11, 2026 10:45
proggeramlug pushed a commit that referenced this pull request Jun 11, 2026
da4f8be (#4994) landed with a red lint job (admin merge), so every
PR's merge-ref now fails it. Two findings, both from that commit:

- array/sort.rs:126 — the second tail-copy loop's store sits one line
  outside the ±6-line window of the existing GC_STORE_AUDIT(STACK)
  marker; add the same marker next to that loop (same caller-rooted
  scratch-buffer justification).
- array/from_concat.rs:174 + object/prototype_chain.rs:58 — new
  GcHeader probes outside addr_class.rs (from_concat is the #4994 split
  of the already-allowlisted concat_reverse probe; prototype_chain's is
  guarded by is_above_handle_band + is_valid_obj_ptr). Allowlist both
  with justifications.
proggeramlug added a commit that referenced this pull request Jun 11, 2026
…ed adapter options and no-ops (#4996)

* fix(stdlib): real semantics or loud warn for silently-ignored adapter options and no-ops (#4917)

zlib: deflate-family stream factories + deflateRawSync honor options.level
(threaded through make_codec_state; .reset() rebuilds at the same level);
a supplied dictionary warns once via the shared runtime validator (covers
ext-zlib too); Brotli/zstd factories warn once when an options object is
passed.

exponential-backoff: real npm semantics — numOfAttempts/startingDelay/
timeMultiple/maxDelay/delayFirstAttempt/jitter/retry parsed and honored,
and Promise-returning tasks now retry on REJECTION (previously the first
promise was passed through and no retry ever happened) via a GC-rooted
state machine chaining js_promise_then + timer-queue delays instead of
blocking thread::sleep.

mongodb: findOne resolves a parsed document object (the JSON.parse
property-access bug that blocked this is fixed); BSON types surface in
relaxed extended-JSON shape.

mysql2/pg: FieldPacket.type/columnType carry the numeric MySQL wire type
ID (name->ID map; sqlx 0.8 keeps the raw byte pub(crate)); pg fields get
numeric dataTypeID (type OID), tableID/columnID from the RowDescription
via sqlx relation_id()/relation_attribute_no(), dataTypeSize/-Modifier
sentinel -1, format "text". Both stdlib and ext twins updated.

http.Agent: keepSocketAlive/reuseSocket warn once (reqwest owns the
pool); destroy() un-flagged — it really drops the per-agent client on
the ext path. fastify: storing an 'upgrade' handler warns at
registration (#1113 tracks real dispatch). worker.ref/unref: already
real (event-loop refcount verified both directions); stale stub notes
dropped — the lines #4917 cited are MessagePort no-ops, not Worker.

Manifest: #4917 stub inventory 18 -> 9 with narrowed notes; keystone
test updated; docs regenerated. New parity test
test_gap_zlib_4917_level.ts matches node v26 byte-for-byte.

* lint: unbreak main's audit gates inherited from #4994

da4f8be (#4994) landed with a red lint job (admin merge), so every
PR's merge-ref now fails it. Two findings, both from that commit:

- array/sort.rs:126 — the second tail-copy loop's store sits one line
  outside the ±6-line window of the existing GC_STORE_AUDIT(STACK)
  marker; add the same marker next to that loop (same caller-rooted
  scratch-buffer justification).
- array/from_concat.rs:174 + object/prototype_chain.rs:58 — new
  GcHeader probes outside addr_class.rs (from_concat is the #4994 split
  of the already-allowlisted concat_reverse probe; prototype_chain's is
  guarded by is_above_handle_band + is_valid_obj_ptr). Allowlist both
  with justifications.

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant