Skip to content

fix(runtime): implement Proxy apply/construct trap dispatch (#3656)#3875

Merged
proggeramlug merged 3 commits into
mainfrom
worktree-fix-3656-proxy
Jun 1, 2026
Merged

fix(runtime): implement Proxy apply/construct trap dispatch (#3656)#3875
proggeramlug merged 3 commits into
mainfrom
worktree-fix-3656-proxy

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Implements Proxy [[Call]] (apply trap) and [[Construct]] (construct trap) exotic behavior per spec — closes the core of #3656.

Before this PR Perry only had partial proxy-call support: direct calls of a statically-known proxy worked, but p.call(...)/p.apply(...), indirect proxy callees, missing/null/not-callable traps, the trap's this binding, and the construct return-value check were all wrong or crashed.

What changed

js_proxy_apply / js_proxy_construct (runtime, proxy.rs)

  • Trap absent / undefined / null → forward [[Call]]/[[Construct]] to the target, recursing through proxy targets, threading thisArg and newTarget.
  • Trap present but not callable (e.g. apply: {}) → TypeError, instead of silently no-op'ing.
  • Trap presentCall(trap, handler, «target, thisArg, argArray») (apply) / «target, argArray, newTarget» (construct), with the handler bound as the trap's this (via clone_closure_rebind_this + IMPLICIT_THIS).
  • Construct trap result must be an Object → else TypeError (symbols rejected).
  • Removed the old "trap returned undefined → call the target anyway" fallback, which broke traps that legitimately return undefined/a custom value.

Dispatch routing (so the new behavior is actually reached)

  • p.call(thisArg, …) / p.apply(thisArg, args) lower to Call(ProxyGet(p, "call"|"apply"), …). A new codegen intercept (try_lower_proxy_fn_call_apply) routes these through the proxy's [[Call]] with thisArg bound, instead of reading .call/.apply off the forwarded target and invoking the target directly. The runtime Function.prototype.call/apply arms get the same proxy guard.
  • A proxy value reaching the generic value-call (js_native_call_value) or dynamic-new (js_new_function_construct) paths — e.g. record.proxy() off Proxy.revocable — now dispatches to js_proxy_apply/js_proxy_construct instead of reading the encoded proxy id as a closure pointer and segfaulting.

Reflect.construct newTarget — threaded the 3rd argument through a new new_target field on Expr::ReflectConstruct (HIR ir + walkers + stable-hash + both codegen backends) so forwarded construct traps observe the correct NewTarget.

Validation (test262 built-ins/Proxy, Node differential via scripts/test262_subset.py)

dir main this PR
Proxy/apply 9% (1/11) 91% (10/11)
Proxy/construct 27% (3/11) 100% (11/11)

All of #3656's explicitly-listed evidence cases now pass. No regressions: built-ins/Function (57.1%) and language/statements/class (15.7%) parity unchanged, and normal fn.call/fn.apply/indirect-call/class-construction all match Node exactly. cargo test -p perry-hir -p perry-codegen green; cargo fmt --check clean.

Out of scope (follow-ups filed)

The 4 remaining *-target-is-proxy cases need deeper, orthogonal features:

  • generator this-binding through a forwarded [[Call]];
  • newTarget-driven prototype installation for subclassing builtins/classes (Array, extends, bound ctors) via a forwarded [[Construct]].

Note

This branch also resolves a pre-existing unresolved merge-conflict marker that had landed on main in native_module.rs (union of the punycode + timers namespace-key arms) — main does not currently compile without it.

Ralph Küpper added 2 commits May 31, 2026 22:37
Implements the Proxy [[Call]] and [[Construct]] exotic behaviors per spec:

- Trap absent / undefined / null → forward the operation to the target
  (recursing through proxy targets), threading thisArg for [[Call]] and
  newTarget for [[Construct]].
- Trap present but not callable → TypeError (a present-but-non-function
  trap like `apply: {}` no longer falls through as a silent no-op).
- Trap present → Call(trap, handler, «target, thisArg, argArray») for apply
  and «target, argArray, newTarget» for construct, with the handler bound as
  the trap's `this`. The construct trap's result must be an Object, else
  TypeError (symbols are primitives and rejected).

Removes the old pragmatic "trap returned undefined → call the target anyway"
fallback in js_proxy_apply, which broke traps that legitimately return
undefined or a custom value without calling the target.

Routing fixes so the new dispatch is actually reached:
- p.call(thisArg, ...) / p.apply(thisArg, args) on a proxy now route through
  the proxy's [[Call]] (apply trap) with thisArg bound, instead of reading
  `.call`/`.apply` off the forwarded target and invoking the target directly.
  Handled both at codegen (ProxyGet("call"|"apply") callee) and in the runtime
  Function.prototype.call/apply arms.
- A proxy value invoked as a function or constructor through the generic
  value-call / dynamic-new paths (e.g. `record.proxy()` off Proxy.revocable)
  now dispatches to js_proxy_apply / js_proxy_construct instead of reading the
  encoded proxy id as a closure pointer and segfaulting.

Threads newTarget through Reflect.construct's 3rd argument (new field on
Expr::ReflectConstruct) so forwarded construct traps observe the right
NewTarget.

test262 built-ins/Proxy: apply 9%→91%, construct 27%→100% (Node differential).
Remaining failures are deeper, orthogonal features tracked separately:
generator `this`-binding through a forwarded [[Call]], and newTarget-driven
prototype installation for subclassing builtins/classes via forwarded
[[Construct]].
Implements the Proxy [[Call]] and [[Construct]] exotic behaviors per spec:

- Trap absent / undefined / null → forward the operation to the target
  (recursing through proxy targets), threading thisArg for [[Call]] and
  newTarget for [[Construct]].
- Trap present but not callable → TypeError (a present-but-non-function
  trap like `apply: {}` no longer falls through as a silent no-op).
- Trap present → Call(trap, handler, «target, thisArg, argArray») for apply
  and «target, argArray, newTarget» for construct, with the handler bound as
  the trap's `this`. The construct trap's result must be an Object, else
  TypeError (symbols are primitives and rejected).

Removes the old pragmatic "trap returned undefined → call the target anyway"
fallback in js_proxy_apply, which broke traps that legitimately return
undefined or a custom value without calling the target.

Routing fixes so the new dispatch is actually reached:
- p.call(thisArg, ...) / p.apply(thisArg, args) on a proxy now route through
  the proxy's [[Call]] (apply trap) with thisArg bound, instead of reading
  `.call`/`.apply` off the forwarded target and invoking the target directly.
  Handled both at codegen (ProxyGet("call"|"apply") callee) and in the runtime
  Function.prototype.call/apply arms.
- A proxy value invoked as a function or constructor through the generic
  value-call / dynamic-new paths (e.g. `record.proxy()` off Proxy.revocable)
  now dispatches to js_proxy_apply / js_proxy_construct instead of reading the
  encoded proxy id as a closure pointer and segfaulting.

Threads newTarget through Reflect.construct's 3rd argument (new field on
Expr::ReflectConstruct) so forwarded construct traps observe the right
NewTarget.

test262 built-ins/Proxy: apply 9%→91%, construct 27%→100% (Node differential).
Remaining failures are deeper, orthogonal features tracked separately:
generator `this`-binding through a forwarded [[Call]], and newTarget-driven
prototype installation for subclassing builtins/classes via forwarded
[[Construct]].

Also resolves a pre-existing unresolved merge-conflict marker that had landed
on main in native_module.rs (union of the `punycode` and `timers` namespace-key
arms) so the tree compiles.
@proggeramlug proggeramlug merged commit a4beedf into main Jun 1, 2026
11 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-3656-proxy branch June 1, 2026 06:57
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