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
62 changes: 62 additions & 0 deletions crates/perry-codegen/src/expr/proxy_reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,68 @@ pub(crate) fn try_lower_proxy_fn_call_apply(
)))
}

/// `proxy.method(args)` for a method name other than `call`/`apply` — the
/// *fused* member-call form whose callee the HIR lowered to
/// `ProxyGet(p, "method")` (#5196). Reading `.method` off the proxy and then
/// invoking it must bind `this` to the proxy itself, so `Array.prototype.map`
/// & friends iterate the proxy through its `get` trap. The plain closure-call
/// fallthrough loses that receiver (the method runs with `this = undefined`,
/// throwing `Cannot convert undefined or null to object`). Route the call
/// through `js_native_call_method`, whose Proxy arm performs the spec
/// `Get(proxy, "method")` then `Call(method, proxy, args)`. Returns `None`
/// when the callee isn't a proxy member-call so normal dispatch proceeds.
pub(crate) fn try_lower_proxy_method_call(
ctx: &mut FnCtx<'_>,
callee: &Expr,
args: &[Expr],
) -> Result<Option<String>> {
let Expr::ProxyGet { proxy, key } = callee else {
return Ok(None);
};
let Expr::String(method_name) = key.as_ref() else {
return Ok(None);
};
// `.call`/`.apply` route through the proxy's [[Call]] (apply trap) and are
// handled by `try_lower_proxy_fn_call_apply`, which runs first.
if method_name == "call" || method_name == "apply" {
return Ok(None);
}
let recv_box = lower_expr(ctx, proxy)?;
let mut lowered_args: Vec<String> = Vec::with_capacity(args.len());
for a in args {
lowered_args.push(lower_expr(ctx, a)?);
}
let (args_ptr, args_len) = if lowered_args.is_empty() {
("null".to_string(), "0".to_string())
} else {
let n = lowered_args.len();
let buf = ctx.func.alloca_entry_array(DOUBLE, n);
{
let blk = ctx.block();
for (i, value) in lowered_args.iter().enumerate() {
let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]);
blk.store(DOUBLE, value, &slot);
}
}
(buf, n.to_string())
};
let method_idx = ctx.strings.intern(method_name);
let entry = ctx.strings.entry(method_idx);
let bytes_global = format!("@{}", entry.bytes_global);
let name_len = entry.byte_len.to_string();
Ok(Some(ctx.block().call(
DOUBLE,
"js_native_call_method",
&[
(DOUBLE, &recv_box),
(PTR, &bytes_global),
(I64, &name_len),
(PTR, &args_ptr),
(I64, &args_len),
],
)))
}

fn put_value_static_property_fast_path(
ctx: &FnCtx<'_>,
target: &Expr,
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-codegen/src/lower_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R
return Ok(v);
}

// #5196: `proxy.method(args)` (e.g. `proxyArray.map(fn)`) — the fused
// member-call form. Route through `js_native_call_method` so `this` binds
// to the proxy and array methods iterate it through its `get` trap.
if let Some(v) = crate::expr::proxy_reflect::try_lower_proxy_method_call(ctx, callee, args)? {
return Ok(v);
}

// Early-firing branches (#1113 chained native method call, computed
// `obj[str](...)`, CurrentStepClosure, closure-typed local).
if let Some(v) = early_branches::try_lower_native_chain_method_call(ctx, callee, args)? {
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-hir/src/lower/expr_call/local_array_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ pub(super) fn try_local_array_methods(
let method_name = method_ident.sym.as_ref();
if let ast::Expr::Ident(arr_ident) = member.obj.as_ref() {
let arr_name = arr_ident.sym.to_string();
// #5196: a Proxy-wrapped array routes ALL its method calls
// through the proxy member-call path (`ProxyGet` +
// `js_native_call_method`), so the method's `this` binds to the
// proxy and element reads fire its `get` trap. Folding to the
// dense `Expr::Array*` fast paths below would dereference the
// proxy id as a real `ArrayHeader` and SIGSEGV. `arr.map`/
// `.filter`/`.find` already escaped via the `is_class_instance`
// gate (a proxy local is typed `Named("Proxy")`), but
// `reduce`/`forEach`/`join`/`sort`/`splice`/… did not — guard
// them all here, uniformly, by falling through.
if ctx.proxy_locals.contains(&arr_name) {
return Ok(Err(args));
}
// Check that this is NOT a String type (Array, Set, Map are all OK)
// When type is unknown, only enter array block for array-only methods
// (push, pop, etc.), NOT for methods shared with strings (indexOf,
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-runtime/src/array/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,21 @@ pub fn try_array_proto_chain_method(
if !proto_chain_contains_real_array(raw as usize) {
return None;
}
dispatch_arraylike_read_method(object, method, args_ptr, args_len)
}

/// Dispatch a non-mutating generic `Array.prototype` method (the spec-generic
/// engine over `[[Get]]`/`length`) on an arbitrary array-like receiver.
/// Returns `None` for an unsupported method name. The receiver may be a plain
/// object, a Proxy (#5196), etc. — `al_get`/`al_length` route element reads
/// through the receiver's `[[Get]]` (so proxy `get` traps fire). Callers are
/// responsible for any receiver/own-property gating they need.
pub fn dispatch_arraylike_read_method(
object: f64,
method: &str,
args_ptr: *const f64,
args_len: usize,
) -> Option<f64> {
let a = |i: usize| arg_at(args_ptr, args_len, i);
let has = |i: usize| (args_len > i) as i32;
Some(match method {
Expand Down
12 changes: 6 additions & 6 deletions crates/perry-runtime/src/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ pub use self::from_concat::{
};
pub use self::generic::array_proto_mutator;
pub use self::generic::{
js_arraylike_at, js_arraylike_concat, js_arraylike_every, js_arraylike_filter,
js_arraylike_find, js_arraylike_findIndex, js_arraylike_findLast, js_arraylike_findLastIndex,
js_arraylike_forEach, js_arraylike_includes, js_arraylike_indexOf, js_arraylike_join,
js_arraylike_lastIndexOf, js_arraylike_map, js_arraylike_reduce, js_arraylike_reduceRight,
js_arraylike_slice, js_arraylike_some, js_arraylike_sort, js_arraylike_splice,
try_array_proto_chain_method, try_object_arraylike_mutator,
dispatch_arraylike_read_method, js_arraylike_at, js_arraylike_concat, js_arraylike_every,
js_arraylike_filter, js_arraylike_find, js_arraylike_findIndex, js_arraylike_findLast,
js_arraylike_findLastIndex, js_arraylike_forEach, js_arraylike_includes, js_arraylike_indexOf,
js_arraylike_join, js_arraylike_lastIndexOf, js_arraylike_map, js_arraylike_reduce,
js_arraylike_reduceRight, js_arraylike_slice, js_arraylike_some, js_arraylike_sort,
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,
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,24 @@ pub unsafe extern "C" fn js_native_call_method(
// through the target's prototype chain) then `Call(method, proxy, args)`
// with `this` bound to the proxy itself.
if crate::proxy::js_proxy_is_proxy(object) == 1 {
// #5196: a generic, non-mutating `Array.prototype` method on a Proxy
// (`proxyArray.map(fn)`). `Array.prototype.map` etc. iterate `this`
// through `[[Get]]`/`length`; routing the spec-generic engine over the
// proxy fires its `get` trap for `length` and each index. The fused
// path below (Get(proxy,"method") → Call) instead resolves the built-in
// method value and re-enters this dispatcher by name — recursing until
// the depth guard and surfacing the original `Cannot convert undefined
// or null to object`. The generic engine is the same one used for
// plain array-like objects whose prototype chain holds a real array.
let args = refreshed_args();
if let Some(result) = crate::array::dispatch_arraylike_read_method(
object,
method_name,
args.as_ptr(),
args.len(),
) {
return result;
}
let key = crate::string::js_string_from_bytes(
method_name_ptr as *const u8,
method_name_len as u32,
Expand Down