Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion crates/perry-codegen/src/lower_call/console_promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,13 +756,30 @@ pub fn try_lower_native_method_str_dispatch(
.and_then(|n| class_builtin_collection_kind(ctx, n))
.filter(|kind| matches!(*kind, "Map" | "Set"))
.is_some_and(|kind| is_collection_method_for_kind(kind, property.as_str()));
// A `class X extends Array` instance's inherited Array methods
// (`push`/`map`/`join`/…) are NOT class methods — they live on the
// runtime's spec-generic array-like engine over the plain `ObjectHeader`
// instance. The static class-dispatch tower above finds no method entry
// (Array is not a user class) and returns None, so without this carve-out
// the call falls into the closure-call fallthrough, which reads the
// method as a non-callable property and throws "value is not a function".
// Route it through `js_native_call_method` (whose relaxed plain-object
// mutator guard + Array-subclass read arm handle it). A user-defined
// override of the same name resolves earlier via the static tower, so it
// never reaches here. Mirrors the `is_collection_subclass_method`
// carve-out for Map/Set subclasses.
let is_array_subclass_method = class_name_opt
.as_deref()
.is_some_and(|n| class_extends_builtin_array(ctx, n))
&& is_array_like_method(property.as_str());
let skip_native = matches!(object.as_ref(), Expr::GlobalGet(_))
|| matches!(object.as_ref(), Expr::NativeModuleRef(_))
|| (class_name_opt.is_some()
&& !is_buffer_class
&& !class_unknown_to_codegen
&& !is_well_known_proto_method
&& !is_collection_subclass_method);
&& !is_collection_subclass_method
&& !is_array_subclass_method);
if !skip_native {
// Issue #92 fast path: intrinsify Buffer numeric reads
// (`buf.readInt32BE(off)` etc.) when the receiver is a tracked
Expand Down Expand Up @@ -951,6 +968,81 @@ fn is_collection_method_for_kind(kind: &str, method: &str) -> bool {
}
}

/// True when `cls_name` is a user class that (transitively) extends the builtin
/// `Array` — `class Stack extends Array`, `class B extends Stack`, … The chain
/// walk mirrors [`class_builtin_collection_kind`]: any ancestor whose
/// `extends_name` is `Array` (or `ReadonlyArray`) marks the class as an Array
/// subclass. `Array` itself is not a user class (never in `ctx.classes`), so the
/// heritage is detected by name at the end of the chain.
pub fn class_extends_builtin_array(ctx: &FnCtx<'_>, cls_name: &str) -> bool {
fn is_array_name(name: &str) -> bool {
matches!(name, "Array" | "ReadonlyArray")
}
// A receiver whose static type IS `Array` is a real array, not a subclass —
// it has its own array-method lowering and must not be treated here.
if is_array_name(cls_name) {
return false;
}
let mut cur = Some(cls_name.to_string());
let mut depth = 0usize;
while let Some(c) = cur {
if depth > 32 {
break;
}
let Some(ci) = ctx.classes.get(&c) else {
// Reached a name codegen doesn't track — it may be the builtin
// `Array` heritage itself.
return is_array_name(c.as_str());
};
if let Some(parent) = ci.extends_name.as_deref() {
if is_array_name(parent) {
return true;
}
}
cur = ci.extends_name.clone();
depth += 1;
}
false
}

/// The inherited `Array.prototype` methods the runtime's spec-generic array-like
/// engine handles for a `class X extends Array` instance (union of the mutator
/// and read dispatch families in `perry-runtime`'s `array::generic`). A method
/// name outside this set is left on the normal class-dispatch path (a genuine
/// user method already resolved earlier; anything else keeps its prior behavior).
fn is_array_like_method(method: &str) -> bool {
matches!(
method,
// mutators (try_object_arraylike_mutator / run_object_mutator)
"push"
| "pop"
| "shift"
| "unshift"
| "reverse"
| "splice"
| "sort"
| "concat"
// reads (dispatch_arraylike_read_method)
| "forEach"
| "map"
| "filter"
| "some"
| "every"
| "find"
| "findIndex"
| "findLast"
| "findLastIndex"
| "reduce"
| "reduceRight"
| "indexOf"
| "lastIndexOf"
| "includes"
| "at"
| "join"
| "slice"
)
}

pub fn try_lower_class_static_accessor_call(
ctx: &mut FnCtx<'_>,
cls_name: &str,
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-runtime/src/array/from_concat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,19 @@ pub(crate) fn append_concat_arg(result: *mut ArrayHeader, value: f64) -> *mut Ar
// Is the spreadable flag explicitly set?
let spreadable = read_concat_spreadable(value);

// `class X extends Array` — the instance is object-backed, so the dense
// `ArrayHeader` spread below would misread it. `js_array_is_array` reports
// true for it, so intercept before that branch and spread a dense snapshot
// of its elements (honoring an explicit `@@isConcatSpreadable === false`).
if crate::array::is_array_subclass_instance(value) {
if spreadable == Some(false) {
return js_array_push_f64(result, value);
}
let snap = crate::array::array_subclass_dense_snapshot(value);
let snap_ptr = (snap.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const ArrayHeader;
return append_spread_array(result, snap_ptr);
}

Comment on lines +263 to +275

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what al_get returns for absent indices on object-backed array subclass instances.
rg -n -A 20 'pub.*fn al_get\b' crates/perry-runtime/src/array/generic.rs

Repository: PerryTS/perry

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant symbols and files.
rg -n "array_subclass_dense_snapshot|append_spread_array|al_get\\b|TAG_HOLE|is_array_subclass_instance|subclass_backing_for_default_iteration" crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 14478


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the array module files and sizes before reading targeted sections.
git ls-files 'crates/perry-runtime/src/array/**' | sort

Repository: PerryTS/perry

Length of output: 1114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the source files that define the relevant helpers.
fd -a -t f 'generic.rs|from_concat.rs|*.rs' crates/perry-runtime/src/array

Repository: PerryTS/perry

Length of output: 487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '412,470p;1939,1968p' crates/perry-runtime/src/array/generic.rs
printf '\n----\n'
sed -n '724,770p' crates/perry-runtime/src/array/from_concat.rs

Repository: PerryTS/perry

Length of output: 5844


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the array-subclass snapshot path and the concat spreader.
sed -n '412,470p;1939,1968p' crates/perry-runtime/src/array/generic.rs
printf '\n----\n'
sed -n '724,770p' crates/perry-runtime/src/array/from_concat.rs

Repository: PerryTS/perry

Length of output: 5844


Preserve holes in array-subclass concat snapshots. crates/perry-runtime/src/array/generic.rs::array_subclass_dense_snapshot writes al_get results directly, so absent indices become undefined instead of TAG_HOLE. append_spread_array only replays HasProperty/Get for TAG_HOLE, so sparse class X extends Array instances will spread with explicit undefined elements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/array/from_concat.rs` around lines 263 - 275,
Preserve sparse holes when creating subclass snapshots: update
array_subclass_dense_snapshot in generic.rs to detect absent indices and store
TAG_HOLE instead of writing al_get’s undefined result. Ensure
append_spread_array can replay HasProperty/Get behavior for those holes so
sparse Array subclass concat results retain their original holes.

// Arrays (and set/map/typed-array/buffer that concat treats array-like via
// js_array_concat) spread by default, unless @@isConcatSpreadable === false.
let is_array = js_array_is_array(value).to_bits() == 0x7FFC_0000_0000_0004;
Expand Down
35 changes: 30 additions & 5 deletions crates/perry-runtime/src/array/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn boxed_bool(b: bool) -> f64 {
}

#[inline(always)]
fn nanbox_arr(arr: *mut ArrayHeader) -> f64 {
pub(super) fn nanbox_arr(arr: *mut ArrayHeader) -> f64 {
f64::from_bits(JSValue::pointer(arr as *const u8).bits())
}

Expand Down Expand Up @@ -409,7 +409,7 @@ fn object_get_named_property_chain(obj_ptr: usize, name: &str) -> f64 {
}

/// `Get(ToObject(recv), k)` (returns `undefined` for absent/out-of-range).
fn al_get(recv: f64, k: i64) -> f64 {
pub(super) fn al_get(recv: f64, k: i64) -> f64 {
let arr = as_real_array(recv);
if !arr.is_null() {
if k < 0 {
Expand Down Expand Up @@ -1338,7 +1338,10 @@ pub fn plain_object_value(arr: *const ArrayHeader) -> Option<f64> {
return None;
}
let class_id = crate::object::js_object_get_class_id(arr as *const crate::object::ObjectHeader);
if class_id != 0 && !crate::object::is_anon_shape_class_id(class_id) {
if class_id != 0
&& !crate::object::is_anon_shape_class_id(class_id)
&& !super::subclass::is_array_subclass_class_id(class_id)
{
return None;
}
Some(recv)
Expand Down Expand Up @@ -1894,6 +1897,19 @@ fn classify_own_slot(v: f64) -> OwnSlot {
}
}

/// True when `object` owns a user method (an own callable field, not a borrowed
/// builtin) named `method`, so an array-like read/iterate fast path must defer
/// to it rather than hijack the name. Mirrors the own-slot gate that
/// [`try_object_arraylike_mutator`] applies to the mutating family, for callers
/// (the Array-subclass read arm) that reach `dispatch_arraylike_read_method`
/// directly.
pub(crate) fn object_owns_user_method(object: f64, method: &str) -> bool {
let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::object::ObjectHeader;
let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32);
let own = crate::object::js_object_get_field_by_name_f64(raw, key);
matches!(classify_own_slot(own), OwnSlot::UserMethod)
}

/// Dispatch a generic `Array.prototype` mutator over an array-like receiver.
///
/// Returns `Some(result)` only when `object` is a plain heap object / closure
Expand All @@ -1902,6 +1918,9 @@ fn classify_own_slot(v: f64) -> OwnSlot {
/// `js_native_call_method` keep their existing behavior. The caller routes
/// `pop` / `shift` / `push` / `unshift` / `reverse` / `splice` here before the
/// dense array arms that would otherwise read the object as an `ArrayHeader`.
///
/// A `class X extends Array` instance (see [`super::subclass`]) is also admitted
/// by the relaxed guard below, so its inherited mutators run on the object.
pub fn try_object_arraylike_mutator(
object: f64,
method: &str,
Expand All @@ -1917,7 +1936,10 @@ pub fn try_object_arraylike_mutator(
// regression, so leave those to the normal vtable dispatch.
let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::object::ObjectHeader;
let class_id = crate::object::js_object_get_class_id(raw);
if class_id != 0 && !crate::object::is_anon_shape_class_id(class_id) {
if class_id != 0
&& !crate::object::is_anon_shape_class_id(class_id)
&& !super::subclass::is_array_subclass_class_id(class_id)
{
return None;
}
// Fire the generic engine when the own `method_name` slot is absent (the
Expand Down Expand Up @@ -1951,7 +1973,10 @@ pub fn run_object_mutator(
}
let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::object::ObjectHeader;
let class_id = crate::object::js_object_get_class_id(raw);
if class_id != 0 && !crate::object::is_anon_shape_class_id(class_id) {
if class_id != 0
&& !crate::object::is_anon_shape_class_id(class_id)
&& !super::subclass::is_array_subclass_class_id(class_id)
{
return None;
}
let result = match method {
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/array/is_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ pub extern "C" fn js_array_is_array(value: f64) -> f64 {
let obj_type = (*gc_header).obj_type;
if obj_type == GC_TYPE_ARRAY || obj_type == crate::gc::GC_TYPE_LAZY_ARRAY {
true_val
} else if obj_type == crate::gc::GC_TYPE_OBJECT {
// `class X extends Array` — the instance is a plain ObjectHeader, but
// ECMA-262 IsArray must still report true for an Array subclass.
let class_id = crate::object::js_object_get_class_id(
raw_ptr as *const crate::object::ObjectHeader,
);
if super::subclass::is_array_subclass_class_id(class_id) {
true_val
} else {
false_val
}
} else {
false_val
}
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-runtime/src/array/iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,19 @@ pub(crate) fn array_from_spread_value(value: f64) -> *mut ArrayHeader {
}
None => {}
}
// `class X extends Array` instance — object-backed; spread (`[...sub]`,
// `fn(...sub)`) over a dense snapshot of its indexed elements. The generic
// `[Symbol.iterator]` lookup below would resolve the inherited array
// iterator, which misreads the plain object as a dense `ArrayHeader`.
// Matches the Map/Set-subclass branch above. Skipped when the subclass
// declared its own `[Symbol.iterator]`, so the override drives the spread
// via the generic symbol lookup below.
if crate::array::is_array_subclass_instance(value)
&& !crate::array::array_subclass_has_iterator_override(value)
{
let snap = crate::array::array_subclass_dense_snapshot(value);
return crate::value::js_nanbox_get_pointer(snap) as *mut ArrayHeader;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if crate::typedarray::lookup_typed_array_kind(raw_ptr).is_some() {
return crate::typedarray::typed_array_to_array(
raw_ptr as *const crate::typedarray::TypedArrayHeader,
Expand Down
6 changes: 5 additions & 1 deletion crates/perry-runtime/src/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod search;
mod sort;
mod species;
mod splice_slice;
mod subclass;

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -51,7 +52,7 @@ pub use self::generic::{
js_arraylike_splice, try_array_proto_chain_method, try_object_arraylike_mutator,
};
pub(crate) use self::generic::{
non_array_object_receiver, object_pop as generic_object_pop,
non_array_object_receiver, object_owns_user_method, object_pop as generic_object_pop,
object_shift as generic_object_shift, object_sort, object_splice, plain_object_value,
};
pub use self::generic_mutators::{
Expand Down Expand Up @@ -107,6 +108,9 @@ pub use self::iterator::{
};
pub(crate) use self::sort::object_prototype_has_index_prop;
pub(crate) use self::sort::object_prototype_index_get as sort_object_prototype_index_get;
pub use self::subclass::{
array_subclass_dense_snapshot, array_subclass_has_iterator_override, is_array_subclass_instance,
};
// Issue #1572 — flatten helpers reused by `node_stream::ns_iter_flat_map`
// so an `async function*` mapper return is driven through the iterator
// protocol instead of being appended as a single chunk.
Expand Down
Loading
Loading