[Harmony Proxies] get/set trap receiver argument unnecessary

# Sean Eagan (13 years ago)

The "get" and "set" traps currently have both "receiver" and "proxy" arguments. This is because it has been suggested that they are not the same in the case of property access on a receiver causing a [[Get]] / [[Put]] call to propogate up the prototype chain to a proxy. From what I can tell though, this never actually occurs.

ES5 section 8.12.3 step 8 (the steps start at 8 instead of 1) calls [[GetProperty]] on the receiver, causing a [[GetProperty]] (not [[Get]]) call to propogate up the prototype chain, meaning the proxy's "getPropertyDescriptor" (not "get") trap is called.

ES5 section 8.12.5 step 4 also calls [[GetProperty]] on the receiver, causing a [[GetProperty]] (not [[Put]]) call to propogate up the prototype chain, meaning the proxy's "getPropertyDescriptor" (not "set") trap is called.

Thanks, Sean Eagan

# David Bruant (13 years ago)

Le 27/04/2011 19:38, Sean Eagan a écrit :

Hi,

The "get" and "set" traps currently have both "receiver" and "proxy" arguments. This is because it has been suggested that they are not the same in the case of property access on a receiver causing a [[Get]] / [[Put]] call to propogate up the prototype chain to a proxy.

The case when they are not the same is when the proxy is in the prototype chain (regardless of what kind of object is "underneath"). See the third example at: developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Proxy#Common_mistakes_and_misunderstanding The proxy object is on the prototype chain and in the "get" trap, "rcvr" is the "base object" while the third optional proxy argument is the proxy prototype object.

# Sean Eagan (13 years ago)

On Wed, Apr 27, 2011 at 1:17 PM, David Bruant <david.bruant at labri.fr> wrote:

The case when they are not the same is when the proxy is in the prototype chain (regardless of what kind of object is "underneath"). See the third example at: developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Proxy#Common_mistakes_and_misunderstanding

I have seen this example. I was trying to indicate that it appears to exhibit incorrect behavior. The proxy's "get" trap should not be called, but rather its "getPropertyDescriptor" trap. And similarly when a proxy is encountered in a prototype chain, its "set" trap should not be called, but again its "getPropertyDescriptor" trap.

Thanks, Sean Eagan

# David Bruant (13 years ago)

Le 27/04/2011 20:37, Sean Eagan a écrit :

Hi David,

On Wed, Apr 27, 2011 at 1:17 PM, David Bruant <david.bruant at labri.fr> wrote:

The case when they are not the same is when the proxy is in the prototype chain (regardless of what kind of object is "underneath"). See the third example at: developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Proxy#Common_mistakes_and_misunderstanding I have seen this example. I was trying to indicate that it appears to exhibit incorrect behavior. The proxy's "get" trap should not be called, but rather its "getPropertyDescriptor" trap. And similarly when a proxy is encountered in a prototype chain, its "set" trap should not be called, but again its "getPropertyDescriptor" trap.

Oh ok, sorry. So in that case, this is something that has been discussed already a couple of times. The internal object API is a bit inconsistent with proxy traps intentions, especially when it comes to recursive calls to traps on the prototype. If I recall correctly, Allen Wirfs-Brock is working on rewriting object internal methods so that they are both semantically equivalent to what we know and coherent with what we expect from proxies. Maybe we should start filing bugs on the topic at bugs.ecmascript.org ?

Tom, are their tests for such prototype-climbing issue?

# Sean Eagan (13 years ago)

This can also be seen by realizing that if proxyA's handler uses the default "get" and "set" trap implementations, and proxyB is proxyA's [[Prototype]], then proxyB's "get" and "set" traps will not be called due to property access on proxyA, but rather its "getPropertyDescriptor" trap.

Thanks, Sean Eagan

On Wed, Apr 27, 2011 at 1:55 PM, David Bruant <david.bruant at labri.fr> wrote:

Oh ok, sorry. So in that case, this is something that has been discussed already a couple of times. The internal object API is a bit inconsistent with proxy traps intentions, especially when it comes to recursive calls to traps on the prototype. If I recall correctly, Allen Wirfs-Brock is working on rewriting object internal methods so that they are both semantically equivalent to what we know and coherent with what we expect from proxies. Maybe we should start filing bugs on the topic at bugs.ecmascript.org ?

I believe in this particular case that ES5 already has the correct / coherent / expected behavior, the bug happens to be with the proxy proposal passing the "receiver" argument. When an author creates an object (or proxy) x, and sets its [[Prototype]] to an object (or proxy) y, they are not granting access to x to y. Since proxies are supposed to be as transparent as possible, if y happens to be a proxy, the author may not even know this, and so would be surprised to know that it now has leaked a reference to x.

Also, the behavior I have stated as correct matches the default "get" and "get" trap implementations. If proxyA's handler uses the default "get" and "set" trap implementations, and proxyB is proxyA's [[Prototype]], then proxyB's "get" and "set" traps will not be called on property access to proxyA, but rather its "getPropertyDescriptor" trap.

Thanks, Sean Eagan

# Sean Eagan (13 years ago)

Sorry, ignore the first paragraph in my last post, I included it at the end instead. Thanks.

# David Bruant (13 years ago)

Le 27/04/2011 21:15, Sean Eagan a écrit :

This can also be seen by realizing that if proxyA's handler uses the default "get" and "set" trap implementations, and proxyB is proxyA's [[Prototype]], then proxyB's "get" and "set" traps will not be called due to property access on proxyA, but rather its "getPropertyDescriptor" trap.

You're right. "get" and "set" default traps were chosen to follow closely internal methods (see [1]) and at the time when "proxy" wasn't an argument of all traps (so a proxy's prototype wasn't reachable from the handler). It may be a good idea to rethink default trap implementations by imposing the recursivity and start from there to rewrite internal methods. I think that pretty much all proto-climbing traps could be rewritten as:

trap: function(/arguments/, proxy){ var res = Object.ownLayerTrap(proxy); return satisfying(res)? res: Object.trap(Object.getPrototypeOf(proxy)); }

The only exception I can think of is "set" which may me slightly more complicated.

On Wed, Apr 27, 2011 at 1:55 PM, David Bruant <david.bruant at labri.fr> wrote:

Oh ok, sorry. So in that case, this is something that has been discussed already a couple of times. The internal object API is a bit inconsistent with proxy traps intentions, especially when it comes to recursive calls to traps on the prototype. If I recall correctly, Allen Wirfs-Brock is working on rewriting object internal methods so that they are both semantically equivalent to what we know and coherent with what we expect from proxies. Maybe we should start filing bugs on the topic at bugs.ecmascript.org ? I believe in this particular case that ES5 already has the correct / coherent / expected behavior, the bug happens to be with the proxy proposal passing the "receiver" argument. When an author creates an object (or proxy) x, and sets its [[Prototype]] to an object (or proxy) y, they are not granting access to x to y. Since proxies are supposed to be as transparent as possible, if y happens to be a proxy, the author may not even know this, and so would be surprised to know that it now has leaked a reference to x.

Actually, the leak already exists. If y has a "p" property which is an accessor, then when performing x.p, the |this| binding of "get" and "set" accessors is x (not y). ES5 8.12.3 [[Get]]: The descriptor /desc/ has been found somewhere in the prototype chain, /getter/ is extracted from it and O is bound as the |this| value (step 13) Same deal with [[Put]]. So, proxies do not leak more than ES5 getters/setters do. Actually, the only place where "receiver" is used in the default get and set trap is as an argument for desc.get.call or desc.set.call.

David

[1] strawman:proxy_set_trap

# Sean Eagan (13 years ago)

On Wed, Apr 27, 2011 at 2:46 PM, David Bruant <david.bruant at labri.fr> wrote:

You're right. "get" and "set" default traps were chosen to follow closely internal methods (see [1]) and at the time when "proxy" wasn't an argument of all traps (so a proxy's prototype wasn't reachable from the handler). It may be a good idea to rethink default trap implementations by imposing the recursivity and start from there to rewrite internal methods. I think that pretty much all proto-climbing traps could be rewritten as:

trap: function(/arguments/, proxy){        var res = Object.ownLayerTrap(proxy);        return satisfying(res)?          res:          Object.trap(Object.getPrototypeOf(proxy));      }

The only exception I can think of is "set" which may me slightly more complicated.

When listing property names (i.e. "getPropertyNames", "enumerate", "iterate") it is actually a concatenation of both own and prototype values, not just one vs. the other. Also with "getPropertyDescriptor", there needs to be logic to return |undefined| when the prototype is null.

Actually, the leak already exists. If y has a "p" property which is an accessor, then when performing x.p, the |this| binding of "get" and "set" accessors is x (not y). ES5 8.12.3 [[Get]]: The descriptor /desc/ has been found somewhere in the prototype chain, /getter/ is extracted from it and O is bound as the |this| value (step 13) Same deal with [[Put]]. So, proxies do not leak more than ES5 getters/setters do. Actually, the only place where "receiver" is used in the default get and set trap is as an argument for desc.get.call or desc.set.call.

Excellent point, the leak occurs either way. However, the leak does occur differently (i.e. via a "receiver" argument instead of a |this| binding), which does not have to be the case. As explained before, the existing ES5 semantics would cause the proxy's "getPropertyDescriptor" trap to be called thus obtaining any "getter" / "setter" that the proxy wants. The |this| binding of this "getter" / "setter" will then be set to the "receiver" by ES5 8.12.3 step 13 for a "getter" or ES5 section 8.12.5 step 5.b for a "setter". The proxy's "get" / "set" trap would not get called, and thus would not need the "receiver" arguments.

Thanks, Sean Eagan

# Brendan Eich (13 years ago)

On Apr 27, 2011, at 2:09 PM, Sean Eagan wrote:

As explained before, the existing ES5 semantics would cause the proxy's "getPropertyDescriptor" trap to be called thus obtaining any "getter" / "setter" that the proxy wants. The |this| binding of this "getter" / "setter" will then be set to the "receiver" by ES5 8.12.3 step 13 for a "getter" or ES5 section 8.12.5 step 5.b for a "setter". The proxy's "get" / "set" trap would not get called, and thus would not need the "receiver" arguments.

I think you're right. Cc'ing folks who may be behind on es-discuss.

# David Bruant (13 years ago)

Le 27/04/2011 23:09, Sean Eagan a écrit :

As explained before, the existing ES5 semantics would cause the proxy's "getPropertyDescriptor" trap to be called thus obtaining any "getter" / "setter" that the proxy wants. The |this| binding of this "getter" / "setter" will then be set to the "receiver" by ES5 8.12.3 step 13 for a "getter" or ES5 section 8.12.5 step 5.b for a "setter". The proxy's "get" / "set" trap would not get called, and thus would not need the "receiver" arguments.

Interesting. What you're saying is that if a proxy in on the prototype chain, then its "set" and "get" traps are never called by a get or set on the base object.

In any case, in any example we could write, the |this| binding is correct not because of the get/set trap on the prototype chain, but because of the |this| binding that is performed at the own layer level.

I think you're right on removing the receiver argument. It should be noted that on that page [1] there is a consensus that a receiver argument should be added to all proto-climbing traps. I don't think we've had the explanation of this point yet. If I recall correctly, this necessity was raised by Andreas Gal (CC'ed). SpiderMonkey-related bug [2]

David

[1] strawman:handler_access_to_proxy [2] bugzilla.mozilla.org/show_bug.cgi?id=643100

# Andreas Gal (13 years ago)

On Apr 28, 2011, at 1:26 AM, David Bruant wrote:

Le 27/04/2011 23:09, Sean Eagan a écrit :

As explained before, the existing ES5 semantics would cause the proxy's "getPropertyDescriptor" trap to be called thus obtaining any "getter" / "setter" that the proxy wants. The |this| binding of this "getter" / "setter" will then be set to the "receiver" by ES5 8.12.3 step 13 for a "getter" or ES5 section 8.12.5 step 5.b for a "setter". The proxy's "get" / "set" trap would not get called, and thus would not need the "receiver" arguments. Interesting. What you're saying is that if a proxy in on the prototype chain, then its "set" and "get" traps are never called by a get or set on the base object.

In any case, in any example we could write, the |this| binding is correct not because of the get/set trap on the prototype chain, but because of the |this| binding that is performed at the own layer level.

I think you're right on removing the receiver argument. It should be noted that on that page [1] there is a consensus that a receiver argument should be added to all proto-climbing traps. I don't think we've had the explanation of this point yet. If I recall correctly, this necessity was raised by Andreas Gal (CC'ed). SpiderMonkey-related bug [2]

Actually we are still going back and forth on this. Proto-climbing might be the wrong selector here. "Can call a getter or setter" seems to be the better category, and that would be only get and set. get and set definitely do need the receiver though. If you have a proxy on the proto chain and you don't find what you are looking for in the direct object, the next step is invoking get on the prototype (proxy), and you need the proper receiver if you have to call a getter (the direct object, not the proxy).

I initially assumed that you would need receiver everywhere where you delegate along the proto chain, but it became clear to me that you actually don't call a getter/setter in most cases (except get/set), so the proper receive can be supplied later when the getter/setter is actually called. I will revisit this with Tom and Mark and dherman before the next TC39 meeting to come up with a more formal analysis of what we need.

# David Bruant (13 years ago)

Le 28/04/2011 10:32, Andreas Gal a écrit :

On Apr 28, 2011, at 1:26 AM, David Bruant wrote:

Le 27/04/2011 23:09, Sean Eagan a écrit :

As explained before, the existing ES5 semantics would cause the proxy's "getPropertyDescriptor" trap to be called thus obtaining any "getter" / "setter" that the proxy wants. The |this| binding of this "getter" / "setter" will then be set to the "receiver" by ES5 8.12.3 step 13 for a "getter" or ES5 section 8.12.5 step 5.b for a "setter". The proxy's "get" / "set" trap would not get called, and thus would not need the "receiver" arguments. Interesting. What you're saying is that if a proxy in on the prototype chain, then its "set" and "get" traps are never called by a get or set on the base object.

In any case, in any example we could write, the |this| binding is correct not because of the get/set trap on the prototype chain, but because of the |this| binding that is performed at the own layer level.

I think you're right on removing the receiver argument. It should be noted that on that page [1] there is a consensus that a receiver argument should be added to all proto-climbing traps. I don't think we've had the explanation of this point yet. If I recall correctly, this necessity was raised by Andreas Gal (CC'ed). SpiderMonkey-related bug [2] Actually we are still going back and forth on this. Proto-climbing might be the wrong selector here. "Can call a getter or setter" seems to be the better category, and that would be only get and set. get and set definitely do need the receiver though. If you have a proxy on the proto chain and you don't find what you are looking for in the direct object, the next step is invoking get on the prototype (proxy), and you need the proper receiver if you have to call a getter (the direct object, not the proxy).

Sean's point is that, as per ES5, in the case you are describing the prototype climbing doesn't occur with the get/set trap on the prototype object, but rather with getPropertyDescriptor (ES5 - 8.12.3 step 8 (which is the first step of the algorithm. Wrong numbering)). The |this| binding then occurs during the get/set call of the base object.

According to ES5, the following example should throw an error:

var handler = { has: function (name) { return name == 'foo'; }, get: function (rcvr, name) { if (name != 'foo') return undefined; print(proxy !== rcvr); return "bye"; }, };

var proxy = Proxy.create(handler); var c = Object.create(proxy); print(c.foo);

As per ES5, "c.foo" should call c.[[Get]] (1) which should call c.[[GetProperty]] (2). This call should fail at finding "foo" at the own layer and should recursively call proxy.[[GetProperty]] (3) (not proxy.[[Get]] ! The example should throw an error, because handler doesn't have a getPropertyDescriptor trap to reify proxy.[[GetProperty]]). When the property descriptor is found and returned from (3), it is returned to (2) then to the c.[[Get]] call (1). If the property descriptor happened to be an accessor descriptor, then the |this| binding should be perfomed at the end of (1) where c.[[Get]] is called and where there is no need to leak the c object (misnumbered 11-13 steps of ES5 - 8.12.3).

As a consequence of the call sequence I have described, Sean's point is that there is actually no need for a receiver argument even in get and set traps.

# Tom Van Cutsem (13 years ago)

I think Sean is correct: under the ES5 semantics, the "receiver" argument to "get" and "set" is not required, because "get" and "set" are not supposed to be invoked on a proxy acting as a prototype. In both cases, property lookup is indeed via [[GetProperty]]. That means "get" will only ever be invoked with |receiver === proxy|.

Let me try to reconstruct how we ended up with the current API.

When Mark and I sketched out the initial API, we reasoned that the [[GetProperty]] internal method for Objects would never be called on a Proxy. All the algorithms where this method was invoked ([[Get]], [[Has]], [[Put]]) would all be replaced by calling a trap instead. Also, this was before Object.getPropertyDescriptor was proposed, and before missing traps (such as 'has') would fall back on derived traps. I even think there was no 'getPropertyDescriptor' trap at that point in the API.

So, with [[GetProperty]] bypassed, the only sensible traps the engine could invoke during property lookup when encountering a proxy in a prototype chain were [[Has]] to query the proxy, and then [[Get]] or [[Set]] to perform the access/assignment.

I just noticed, a remaining artifact of that design can still be seen in the description of [[GetProperty]] for proxies: < harmony:proxies_semantics#detailed_semantics_of_behavior_for_object_and_function_proxies>.

I think [[GetProperty]] for a Proxy should now simply invoke the "getPropertyDescriptor" trap.

That said, I also had a look at my test suite for proxies and studied the behavior on the latest tracemonkey version.

First, there was already a test for delegated property access, but only for function properties, not for accessor properties: < hg.ecmascript.org/tests/harmony/file/b99e5976db56/proxies/TestCases/invokeDelegator.js

I just extended this test case to also check whether, in the function returned by "get", |receiver === this|. This is indeed the case, so proxy writers could use the nested |this| instead of |receiver| to access the delegating object. So even without a "receiver" argument, proxies would be able to transparently forward "method invocations" (function property access) without affecting the |this|-binding.

Triggered by this discussion, I added a test for delegated accessor property access: < hg.ecmascript.org/tests/harmony/file/b99e5976db56/proxies/TestCases/delegatedAccessor.js

With the getPropertyDescriptor trap now in place, it turns out this trap is indeed all one needs for correct transparent forwarding of access/assignment to accessor properties.

This test also revealed an interesting asymmetry in the current tracemonkey behavior: |delegator.foo| will call the 'get' trap of the proxy if it's implemented. This behavior does require 'get' to take the 'receiver' as argument, otherwise it can't transparently forward the access. However, |delegator.foo = 24| does not appear to call the 'set' trap, even when provided, but instead always calls 'getPropertyDescriptor'.

Long story short:

  • with the 'getPropertyDescriptor' trap in place, this seems to be the preferred trap to be invoked on a proxy on the prototype chain during property lookup/assignment (following ES5 semantics)
  • Hence, 'get' and 'set' will never be called in a situation where |receiver !== proxy|
  • Hence, "receiver" is, strictly speaking, unnecessary.

Cheers, Tom

2011/4/28 David Bruant <david.bruant at labri.fr>

# Tom Van Cutsem (13 years ago)

I wrote up a draft strawman for making this change: < strawman:proxy_drop_receiver>.

2011/4/28 Tom Van Cutsem <tomvc.be at gmail.com>