diff --git a/crates/perry-codegen/src/expr/proxy_reflect.rs b/crates/perry-codegen/src/expr/proxy_reflect.rs index 35ec0fe83..124a9642c 100644 --- a/crates/perry-codegen/src/expr/proxy_reflect.rs +++ b/crates/perry-codegen/src/expr/proxy_reflect.rs @@ -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> { + 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 = 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, diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index 4d7fbefd8..d1253004c 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -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)? { diff --git a/crates/perry-hir/src/lower/expr_call/local_array_methods.rs b/crates/perry-hir/src/lower/expr_call/local_array_methods.rs index a8d5776fe..40e574e37 100644 --- a/crates/perry-hir/src/lower/expr_call/local_array_methods.rs +++ b/crates/perry-hir/src/lower/expr_call/local_array_methods.rs @@ -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, diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 8adbbd8a2..7e521cd34 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -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 { let a = |i: usize| arg_at(args_ptr, args_len, i); let has = |i: usize| (args_len > i) as i32; Some(match method { diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 62d4cabf6..289e348e9 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -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, diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index c09591b76..5ed9434d6 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -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,