fix(runtime): implement Proxy apply/construct trap dispatch (#3656)#3875
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sthisbinding, and the construct return-value check were all wrong or crashed.What changed
js_proxy_apply/js_proxy_construct(runtime,proxy.rs)undefined/null→ forward[[Call]]/[[Construct]]to the target, recursing through proxy targets, threadingthisArgandnewTarget.apply: {}) →TypeError, instead of silently no-op'ing.Call(trap, handler, «target, thisArg, argArray»)(apply) /«target, argArray, newTarget»(construct), with the handler bound as the trap'sthis(viaclone_closure_rebind_this+IMPLICIT_THIS).TypeError(symbols rejected).undefined→ call the target anyway" fallback, which broke traps that legitimately returnundefined/a custom value.Dispatch routing (so the new behavior is actually reached)
p.call(thisArg, …)/p.apply(thisArg, args)lower toCall(ProxyGet(p, "call"|"apply"), …). A new codegen intercept (try_lower_proxy_fn_call_apply) routes these through the proxy's[[Call]]withthisArgbound, instead of reading.call/.applyoff the forwarded target and invoking the target directly. The runtimeFunction.prototype.call/applyarms get the same proxy guard.js_native_call_value) or dynamic-new(js_new_function_construct) paths — e.g.record.proxy()offProxy.revocable— now dispatches tojs_proxy_apply/js_proxy_constructinstead of reading the encoded proxy id as a closure pointer and segfaulting.Reflect.constructnewTarget — threaded the 3rd argument through a newnew_targetfield onExpr::ReflectConstruct(HIR ir + walkers + stable-hash + both codegen backends) so forwarded construct traps observe the correctNewTarget.Validation (test262
built-ins/Proxy, Node differential viascripts/test262_subset.py)Proxy/applyProxy/constructAll of #3656's explicitly-listed evidence cases now pass. No regressions:
built-ins/Function(57.1%) andlanguage/statements/class(15.7%) parity unchanged, and normalfn.call/fn.apply/indirect-call/class-construction all match Node exactly.cargo test -p perry-hir -p perry-codegengreen;cargo fmt --checkclean.Out of scope (follow-ups filed)
The 4 remaining
*-target-is-proxycases need deeper, orthogonal features: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
maininnative_module.rs(union of thepunycode+timersnamespace-key arms) —maindoes not currently compile without it.