Proxies: wrong "receiver" used in default "set" trap

# Tom Van Cutsem (12 years ago)

Someone recently reported an issue [1] while using my harmony-reflect shim for direct proxies. The fix probably requires a change in the proxy specification. I'm unsure how to resolve it, so I thought I'd bring it to the list.

The issue is as follows:

consider a proxy with an empty handler: var proxy = Proxy(target, {});

now consider assigning a property to the proxy: proxy.foo = 42

This triggers the proxy's "set" trap, but since the handler does not define this trap, the default behavior is to forward the assignment to the target.

As currently specified, the intercepted assignment is forwarded as:

Reflect.set(target, 'foo', 42, proxy)

where 'proxy' is used as the "initial receiver" of the property assignment.

This fourth "receiver" argument matters for two reasons:

  1. if 'target' defines or inherits 'foo' as an accessor property, inside that accessor, |this| will point to that receiver argument.
  2. if receiver !== target, then Reflect.set will add a new data property to receiver, rather than update an existing data property on the target.

Currently, for an assignment as shown above, the proxy itself is passed as the fourth 'receiver' argument. Thus:

  1. inside triggered accessors, |this| will refer to the proxy, not to the target. This is acceptable.
  2. since proxy !== target, Reflect.set will try to add data properties to 'proxy', rather than update an existing data property on the target. This is not acceptable and is what causes the issue [1].

I see two solutions, but can't decide on which is better. There may be better solutions altogether.

Option A: change the default forwarding behavior of the "set" trap to:

if the proxy is the initial receiver of the property assignment, then return Reflect.set(target, name, val, target) else return Reflect.set(target, name, val, receiver)

If the initial receiver is an object that delegates to a proxy, the proxy won't change the receiver upon forwarding, in order to not interfere with prototype inheritance.

This solution fixes point 2) since Reflect.set is no longer confused about the proxy. Re. point 1), the |this|-binding inside forwarded accessors will now refer to the target object itself, which I find equally acceptable.

For reasons of symmetry, if we go this route, we probably need to change the default forwarding behavior of "get" in a similar way. Point 2) does not come up in this case, but Point 1) does. We probably want the rules for |this|-binding in forwarded getters to be consistent with setters.

Option B: Address point 2) directly by changing the test that determines property addition versus property update inside Reflect.set (i.e. the [[SetP]] internal method of objects) so that the algorithm no longer tests whether target === receiver, but rather whether target === receiver || receiver is a proxy for target.

This solves the issue at hand, although it feels like a more ad hoc solution.

Another way of looking at things is that the Reflect.set (or [[SetP]]) algorithm currently assumes that the "receiver" argument is either the target object itself, or otherwise an object that directly or indirectly inherits from the target object. If this assumption is violated, strange behavior can ensue. In the above example, the proxy passed in as the "receiver" argument is neither the target object nor an object that inherits from it, hence the strange behavior.

Cheers, Tom

[1] tvcutsem/harmony-reflect#11

# Allen Wirfs-Brock (12 years ago)

I think it has to be A, for consistency with [[Call]]. Note that when [[Call]] is directly forwarded to the target, the this value is set to target. It wouldn't be self-consistent if directly forwarded foo.access and foo.method() invocations used different this values.

I assume that by "proxy is the initial receiver", you mean that for a [[SetP]]/[[GetP]] invocation that SameValue(O,Receiver) is true. If so, that also seems right to me. In writjing various [[SetP]]/[[GetP]]/equivalent proxy traps, this test is one I've found that I routine have to make. It basically distinguished between the initial application of the operation and a proto climbing (or other similar delegating) application.

I'm actually surprised that I didn't notice this wrong receiver issue when I incorporated Proxy exotics into the ES6 spec. as I spend a lot of time thinking about what was the correct this value to forward in various situations. I'm not blissfully happy with the current forwarding model, but I think it is ok as long as we make sure it is completely self consistent. The situations I was concerned with are similar for [[Call]] when it is directly forward. If prox.foo() is forwarded so that the foo method is invoked with the proxy's target as the this value then any this.bar() calls within the foo method dispatches through the target rather than prox. From an OO delegation perspective this feels wrong. However, it feels ok if you think of direct forward not as a form of object delegation but rather redirection to a completely oindependent object(the target). In that case, it is very important that all forwarding consistently use the target as the receiver.

If you really want to [[Call]] a target method with the proxy as the this value or do the equivalent for [[SetP]]//[[GetP]] you can do it by providing a hander that does what you want rather than depending upon direct forwarding.

# Andreas Rossberg (12 years ago)

On 18 December 2012 22:56, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

Option B: Address point 2) directly by changing the test that determines property addition versus property update inside Reflect.set (i.e. the [[SetP]] internal method of objects) so that the algorithm no longer tests whether target === receiver, but rather whether target === receiver || receiver is a proxy for target.

This solves the issue at hand, although it feels like a more ad hoc solution.

Indeed, especially since the length of the proxy chain may be >1.

So it has to be A. (Or the definition of Reflect.set has to change. I don't have much love for the case distinction in there anyway. But it's probably a necessary consequence of the somewhat incoherent property assignment model we are stuck with.)

# Tom Van Cutsem (12 years ago)

2012/12/19 Allen Wirfs-Brock <allen at wirfs-brock.com>

I think it has to be A, for consistency with [[Call]]. Note that when [[Call]] is directly forwarded to the target, the this value is set to target. It wouldn't be self-consistent if directly forwarded foo.access and foo.method() invocations used different this values.

Hmm, in the current proxies draft spec, the default [[Call]] behavior does not modify the thisBinding upon forwarding, so:

var that = {} var proxy = Proxy(function() { return this === that; }, {}) // default "apply" trap proxy.call(that) // true

It would be very strange for |this| to be bound to the function object itself. That typically does not make sense.

I assume that by "proxy is the initial receiver", you mean that for a

[[SetP]]/[[GetP]] invocation that SameValue(O,Receiver) is true. If so, that also seems right to me. In writjing various [[SetP]]/[[GetP]]/equivalent proxy traps, this test is one I've found that I routine have to make. It basically distinguished between the initial application of the operation and a proto climbing (or other similar delegating) application.

Yes, that is what I meant.

I'm actually surprised that I didn't notice this wrong receiver issue when

I incorporated Proxy exotics into the ES6 spec. as I spend a lot of time thinking about what was the correct this value to forward in various situations. I'm not blissfully happy with the current forwarding model, but I think it is ok as long as we make sure it is completely self consistent. The situations I was concerned with are similar for [[Call]] when it is directly forward. If prox.foo() is forwarded so that the foo method is invoked with the proxy's target as the this value then any this.bar() calls within the foo method dispatches through the target rather than prox. From an OO delegation perspective this feels wrong. However, it feels ok if you think of direct forward not as a form of object delegation but rather redirection to a completely oindependent object(the target). In that case, it is very important that all forwarding consistently use the target as the receiver.

Yes, the behavior here is odd: for "direct" method invocations such as proxy.foo(), we want pure forwarding behavior, with |this| bound to the target in the forwarded call. for "delegated" method invocations such as Object.create(proxy).foo(), prototype-based inheritance dictates that inside the forwarded foo(), |this| should be bound to the child object.

It's because we are mixing Lieberman-style delegation with plain forwarding. The above behavior is the best compromise I can come up with.

If you really want to [[Call]] a target method with the proxy as the this

value or do the equivalent for [[SetP]]//[[GetP]] you can do it by providing a hander that does what you want rather than depending upon direct forwarding.

Yes. It's important to point out that we're only talking about the default behavior here. There are many policy decisions a proxy author can make. We have to choose one as the default. I think we're in agreement that for property access/assignment, the default should be to rebind |this| to the target for "direct" accesses, and to leave |this| unmodified for "delegated" accesses.

# Tom Van Cutsem (12 years ago)

2012/12/19 Andreas Rossberg <rossberg at google.com>

So it has to be A. (Or the definition of Reflect.set has to change. I don't have much love for the case distinction in there anyway. But it's probably a necessary consequence of the somewhat incoherent property assignment model we are stuck with.)

Indeed. To clarify: the case-distinction is made in ES5 as well, although it is implicit there because of the use of [[GetOwnProperty]] versus [[GetProperty]] (see ES5 8.12.5 [[Put]]). The test is needed to distinguish updating of "own" data properties (8.12.5 step 3.b) from adding a new own data property (8.12.5 step 6.b). As [[GetProperty]] is removed from ES6, we need to test the Receiver to figure out what case we're in.

# Allen Wirfs-Brock (12 years ago)

On Dec 19, 2012, at 2:01 AM, Tom Van Cutsem wrote:

2012/12/19 Allen Wirfs-Brock <allen at wirfs-brock.com> I think it has to be A, for consistency with [[Call]]. Note that when [[Call]] is directly forwarded to the target, the this value is set to target. It wouldn't be self-consistent if directly forwarded foo.access and foo.method() invocations used different this values.

Hmm, in the current proxies draft spec, the default [[Call]] behavior does not modify the thisBinding upon forwarding, so:

var that = {} var proxy = Proxy(function() { return this === that; }, {}) // default "apply" trap proxy.call(that) // true

It would be very strange for |this| to be bound to the function object itself. That typically does not make sense.

Yes, sorry about that. I was think about the contrasting the this value that gets past on a method call on a proxy and I mind farted and looked at [[Call]] when that isn't the issue.

I assume that by "proxy is the initial receiver", you mean that for a [[SetP]]/[[GetP]] invocation that SameValue(O,Receiver) is true. If so, that also seems right to me. In writjing various [[SetP]]/[[GetP]]/equivalent proxy traps, this test is one I've found that I routine have to make. It basically distinguished between the initial application of the operation and a proto climbing (or other similar delegating) application.

Yes, that is what I meant.

I'm actually surprised that I didn't notice this wrong receiver issue when I incorporated Proxy exotics into the ES6 spec. as I spend a lot of time thinking about what was the correct this value to forward in various situations. I'm not blissfully happy with the current forwarding model, but I think it is ok as long as we make sure it is completely self consistent. The situations I was concerned with are similar for [[Call]] when it is directly forward. If prox.foo() is forwarded so that the foo method is invoked with the proxy's target as the this value then any this.bar() calls within the foo method dispatches through the target rather than prox. From an OO delegation perspective this feels wrong. However, it feels ok if you think of direct forward not as a form of object delegation but rather redirection to a completely oindependent object(the target). In that case, it is very important that all forwarding consistently use the target as the receiver.

Yes, the behavior here is odd: for "direct" method invocations such as proxy.foo(), we want pure forwarding behavior, with |this| bound to the target in the forwarded call. for "delegated" method invocations such as Object.create(proxy).foo(), prototype-based inheritance dictates that inside the forwarded foo(), |this| should be bound to the child object.

Yes, this is exactly the issue that caused me to wander down the [[Call]] trail above.

Consider:

let target = {foo() {return this}}; let proxy = Proxy(target, {});

As currently spec'ed. proxy.foo() is going to return proxy rather than target.

That's because, proxy.foo evaluates to a Reference whose base is proxy. Getting the value of the reference does a [[GetP]] that has proxy as both its this value and receiver value. That is directly forwarded to target (as currently spec'ed: still with proxy as the receiver value), with which returns the function. We then do a [[Call]] of the function using the base of the Reference (ie, proxy) as the this value. So, for proxy.foo() we are not getting pure forwarding.

If we make the Option A change that seems right for [[GetP]]/[[SetP]] then we will have an inconsistency between the this value used for a method invoked as proxy.foo() and a accessor invoked as proxy.bar

This issue arises because method lookup and method invocation is done in two separate steps. I'm not sure there is an easy fix that doesn't involve introduce a [[CallProperty]] internal method.

# Brandon Benvie (12 years ago)

Andrea Giammarchi has pointed out in the past that it's not possible to fully shim noSuchMethod because of this same issue. Perhaps introducing a [[CallProperty]] is not such a bad idea.

# Tom Van Cutsem (12 years ago)

[+jwalden]

2012/12/19 Allen Wirfs-Brock <allen at wirfs-brock.com>

Consider:

let target = {foo() {return this}}; let proxy = Proxy(target, {});

As currently spec'ed. proxy.foo() is going to return proxy rather than target.

That's because, proxy.foo evaluates to a Reference whose base is proxy. Getting the value of the reference does a [[GetP]] that has proxy as both its this value and receiver value. That is directly forwarded to target (as currently spec'ed: still with proxy as the receiver value), with which returns the function. We then do a [[Call]] of the function using the base of the Reference (ie, proxy) as the this value. So, for proxy.foo() we are not getting pure forwarding.

If we make the Option A change that seems right for [[GetP]]/[[SetP]] then we will have an inconsistency between the this value used for a method invoked as proxy.foo() and a accessor invoked as proxy.bar

This issue arises because method lookup and method invocation is done in two separate steps. I'm not sure there is an easy fix that doesn't involve introduce a [[CallProperty]] internal method.

Very good point!

Since adding a [[CallProperty]] (or [[Invoke]]) internal method is a big change, I reconsidered how we could stick with the current semantics (where |this| always remains bound to the proxy in all cases), yet avoid the [[SetP]] anomaly.

The fix was obvious once I saw it, and involves only a minor change in [[SetP]], and no changes at all in the forwarding behavior of proxies.

Consider the current [[SetP]] algorithm (I will use the version on p.70 of your ES6 draft rev. 12 for reference)

The [[SetP]] algorithm first walks the proto chain in search for either a) an inherited setter, or b) an inherited non-writable data property. The moment we hit a writable data property, we can stop looking, and we are allowed to add or update the data property to the initial receiver object. The algorithm then just has to decide whether to update an existing data property, or add a new one.

Currently, that test is performed in step 5.b. by testing whether the "current" object we are visiting in the proto chain O is the Receiver object. At first sight, this is a rather weird way of deciding between update vs. addition. We can get away with this test because we've just queried the O object for an own property (ownDesc). Hence, if O === Receiver, then we know Receiver already has the same property and we must update, rather than add.

In the presence of proxies, this test is no longer valid, as Receiver may be a proxy for O.

The right fix (at least, it feels like the obvious right fix to me), is not to test whether O === Receiver, but rather just let the algorithm explicitly test whether the Receiver object already defines an own property with key P, to decide between update vs. addition:

replace step 5.b with: 5.b Let existingDesc be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. 5.c If existingDesc is not undefined, then ... // same steps as before

Now, for normal objects, this test is redundant since existingDesc and ownDesc will denote the same property. But that redundancy is harmless, just as in ES5 [[Put]] it was redundant to call both [[GetProperty]] and [[GetOwnProperty]] on the same object. In the fast path, engines will not follow this algorithm anyway. If Receiver is a proxy, then the difference matters and the proxy will be able to intercept the call to [[GetOwnProperty]] (which is what makes this revised algorithm work in the presence of proxies).

Jeff, I'd appreciate your input as you previously helped revise the [[SetP]] algorithm [1]. Actually, looking back at your message, it seems we may have originally performed the redundant check, but removed it because we couldn't see a reason not to.

In any case, I updated my little test-harness where I check conformance between ES5 [[Put]] and ES6 [[SetP]], and with this change all tests continue to pass (meaning that both algorithms yield the same result on normal objects).

For those who rather read Javascript than spec language, there's a diff [2] showing the changes in my harmony-reflect shim needed to fix the issue I reported on earlier.

[1] esdiscuss/2012-April/022561 [2] git.io/p3jV0w

# Tom Van Cutsem (12 years ago)

2012/12/19 Allen Wirfs-Brock <allen at wirfs-brock.com>

If we make the Option A change that seems right for [[GetP]]/[[SetP]] then we will have an inconsistency between the this value used for a method invoked as proxy.foo() and a accessor invoked as proxy.bar

To clarify, if we apply the fix to [[SetP]] described in my previous message, the following remains the default forwarding behavior of proxies:

If target.foo is a method: proxy.foo() will call target.foo() with |this| bound to the proxy (i.e. Lieberman-style delegation)

If target.bar is an accessor: proxy.foo will call the target.foo getter with |this| bound to the proxy (delegation) proxy.foo = 42 will call the target.foo setter with |this| bound to the proxy (delegation)

If target.baz is a writable data property proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', {value:42}) (and not Object.defineProperty(proxy, 'baz', {value:42,enumerable:true,writable:true,configurable:true}) as it did previously) This behavior is consistent with the method and accessor case: in all cases, the target delegates back to the proxy.

This bears repeating: the above are only defaults, and proxy authors are free to change the policy by actually implementing traps and taking control.

One thing that I learned from all this is that it's simpler to think of the proxy as delegating (as opposed to forwarding) to its target by default. And under the semantics of invoke = get + apply, that is actually the simplest option.

# Andreas Rossberg (12 years ago)

On 20 December 2012 11:09, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

Currently, that test is performed in step 5.b. by testing whether the "current" object we are visiting in the proto chain O is the Receiver object. At first sight, this is a rather weird way of deciding between update vs. addition. We can get away with this test because we've just queried the O object for an own property (ownDesc). Hence, if O === Receiver, then we know Receiver already has the same property and we must update, rather than add.

In the presence of proxies, this test is no longer valid, as Receiver may be a proxy for O.

The right fix (at least, it feels like the obvious right fix to me), is not to test whether O === Receiver, but rather just let the algorithm explicitly test whether the Receiver object already defines an own property with key P, to decide between update vs. addition:

replace step 5.b with: 5.b Let existingDesc be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. 5.c If existingDesc is not undefined, then ... // same steps as before

Now, for normal objects, this test is redundant since existingDesc and ownDesc will denote the same property.

...or existingDesc is already known to be undefined, in the case where Receiver !== O, right?

But that redundancy is harmless, just as in ES5 [[Put]] it was redundant to call both [[GetProperty]] and [[GetOwnProperty]] on the same object. In the fast path, engines will not follow this algorithm anyway. If Receiver is a proxy, then the difference matters and the proxy will be able to intercept the call to [[GetOwnProperty]] (which is what makes this revised algorithm work in the presence of proxies).

Sounds good to me.

# David Bruant (12 years ago)

Le 20/12/2012 11:09, Tom Van Cutsem a écrit :

(...) The right fix (at least, it feels like the obvious right fix to me), is not to test whether O === Receiver, but rather just let the algorithm explicitly test whether the Receiver object already defines an own property with key P, to decide between update vs. addition:

replace step 5.b with: 5.b Let existingDesc be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. 5.c If existingDesc is not undefined, then ... // same steps as before

Now, for normal objects, this test is redundant since existingDesc and ownDesc will denote the same property. But that redundancy is harmless, just as in ES5 [[Put]] it was redundant to call both [[GetProperty]] and [[GetOwnProperty]] on the same object. In the fast path, engines will not follow this algorithm anyway. If Receiver is a proxy, then the difference matters and the proxy will be able to intercept the call to [[GetOwnProperty]] (which is what makes this revised algorithm work in the presence of proxies).

A note in the spec to explain this would probably be a good idea, because it's a very subtle difference.

# Tom Van Cutsem (12 years ago)

2012/12/20 Andreas Rossberg <rossberg at google.com>

On 20 December 2012 11:09, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

Currently, that test is performed in step 5.b. by testing whether the "current" object we are visiting in the proto chain O is the Receiver object. At first sight, this is a rather weird way of deciding between update vs. addition. We can get away with this test because we've just queried the O object for an own property (ownDesc). Hence, if O === Receiver, then we know Receiver already has the same property and we must update, rather than add.

In the presence of proxies, this test is no longer valid, as Receiver may be a proxy for O.

The right fix (at least, it feels like the obvious right fix to me), is not to test whether O === Receiver, but rather just let the algorithm explicitly test whether the Receiver object already defines an own property with key P, to decide between update vs. addition:

replace step 5.b with: 5.b Let existingDesc be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. 5.c If existingDesc is not undefined, then ... // same steps as before

Now, for normal objects, this test is redundant since existingDesc and ownDesc will denote the same property.

...or existingDesc is already known to be undefined, in the case where Receiver !== O, right?

Yes. If ownDesc is undefined, we never even reach step 5.b.

If Receiver !== O we previously added a new data property. With the new test, if existingDesc is undefined, we also add a new data property.

So the behavior is consistent.

# Allen Wirfs-Brock (12 years ago)

On Dec 20, 2012, at 2:21 AM, Tom Van Cutsem wrote:

2012/12/19 Allen Wirfs-Brock <allen at wirfs-brock.com> If we make the Option A change that seems right for [[GetP]]/[[SetP]] then we will have an inconsistency between the this value used for a method invoked as proxy.foo() and a accessor invoked as proxy.bar

To clarify, if we apply the fix to [[SetP]] described in my previous message, the following remains the default forwarding behavior of proxies:

If target.foo is a method: proxy.foo() will call target.foo() with |this| bound to the proxy (i.e. Lieberman-style delegation)

If target.bar is an accessor: proxy.foo will call the target.foo getter with |this| bound to the proxy (delegation) proxy.foo = 42 will call the target.foo setter with |this| bound to the proxy (delegation)

If target.baz is a writable data property proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', {value:42}) (and not Object.defineProperty(proxy, 'baz', {value:42,enumerable:true,writable:true,configurable:true}) as it did previously) This behavior is consistent with the method and accessor case: in all cases, the target delegates back to the proxy.

This is actually essential to maintaining ES5 default semantics because {value:42} and {value: 42, enumerable:true,writable:true,configurable:true} have quite different meanings to the ordinary [[DefineOwnProperty]].

It is equally important that if target.baz does not exist that [[DefineOwnProperty]] is called on proxy with with the full {value: 42, enumerable:true,writable:true,configurable:true} descriptor to ensure that the new property gets created using the "crated by assignment" attributes rather than the [[DefineOwnProperty]] defaults.

This bears repeating: the above are only defaults, and proxy authors are free to change the policy by actually implementing traps and taking control.

One thing that I learned from all this is that it's simpler to think of the proxy as delegating (as opposed to forwarding) to its target by default. And under the semantics of invoke = get + apply, that is actually the simplest option.

Yes, this is the best way I have found to think about them and I've been patiently waiting for you to see the light :-) However, a corollary is that most internal method calls within derived internal methods/traps need to delegate back to the original "receiver". In some cases, we don't provide the original receiver as an extra argument, so it isn't available to do this.

A quick scan of the ordinary internal method suggests that we may have this problem for [[Delete]], [[Enumerate]], [[Keys]], and [[GetOwnProertyKeys]].

More generally, I would argue that all Proxy traps (and the corresponding Reflect functions) potentially need access to the original "receiver". We don't know what somebody is going to do in such traps and they well need to call back to the original receiver for the same sort of consistency issues we have encountered with [[SetP]]. this is particularly apparent if you think multiple levels of proxies chained through their target slots.

This is subtle and maybe we can slip by without addressing it. But a consistent application of delegation semantics would seem to require it.

Delete

# Allen Wirfs-Brock (12 years ago)

below On Dec 20, 2012, at 2:09 AM, Tom Van Cutsem wrote:

[+jwalden]

2012/12/19 Allen Wirfs-Brock <allen at wirfs-brock.com> ...

Currently, that test is performed in step 5.b. by testing whether the "current" object we are visiting in the proto chain O is the Receiver object. At first sight, this is a rather weird way of deciding between update vs. addition. We can get away with this test because we've just queried the O object for an own property (ownDesc). Hence, if O === Receiver, then we know Receiver already has the same property and we must update, rather than add.

In the presence of proxies, this test is no longer valid, as Receiver may be a proxy for O.

I don't think this really makes any difference. A an object and a Proxy for the object are two distinct object and they need to always be treated as such. The real problem with the SameAs test is that when Receiver and O are different you can't really assume anything about how they relate to each other as Reflect.set can be called with arbitrary values provided for its target and receiver parameters.

The right fix (at least, it feels like the obvious right fix to me), is not to test whether O === Receiver, but rather just let the algorithm explicitly test whether the Receiver object already defines an own property with key P, to decide between update vs. addition:

replace step 5.b with: 5.b Let existingDesc be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. 5.c If existingDesc is not undefined, then ... // same steps as before

Yes, here is the new step 5 as I have just updated it in my spec. draft (ignore the 1, it's really 5) If IsDataDescriptor(ownDesc) is true, then If ownDesc.[[Writable]] is false, return false. If Type(Receiver) is not Object, return false. Let existingDescriptor be be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. ReturnIfAbrupt(existingDescriptor). If existingDescriptor is not undefined, then Let valueDesc be the Property Descriptor {[[Value]]: V}. Return the result of calling the [[DefineOwnProperty]] internal method of Receiver with arguments P and valueDesc. Else Receiver does not currently have a property P, Return the result of performing CreateOwnDataProperty(Receiver, P, V).

# Tom Van Cutsem (12 years ago)

2012/12/20 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 20, 2012, at 2:21 AM, Tom Van Cutsem wrote:

If target.baz is a writable data property proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', {value:42}) (and not Object.defineProperty(proxy, 'baz', {value:42,enumerable:true,writable:true,configurable:true}) as it did previously) This behavior is consistent with the method and accessor case: in all cases, the target delegates back to the proxy.

This is actually essential to maintaining ES5 default semantics because {value:42} and {value: 42, enumerable:true,writable:true,configurable:true} have quite different meanings to the ordinary [[DefineOwnProperty]].

Indeed. And this is what caused the issue I reported in the OP.

It is equally important that if target.baz does not exist that [[DefineOwnProperty]] is called on proxy with with the full {value: 42, enumerable:true,writable:true,configurable:true} descriptor to ensure that the new property gets created using the "crated by assignment" attributes rather than the [[DefineOwnProperty]] defaults.

Yes, this is the case.

One thing that I learned from all this is that it's simpler to think of the proxy as delegating (as opposed to forwarding) to its target by default. And under the semantics of invoke = get + apply, that is actually the simplest option.

Yes, this is the best way I have found to think about them and I've been patiently waiting for you to see the light :-) However, a corollary is that most internal method calls within derived internal methods/traps need to delegate back to the original "receiver". In some cases, we don't provide the original receiver as an extra argument, so it isn't available to do this.

A quick scan of the ordinary internal method suggests that we may have this problem for [[Delete]], [[Enumerate]], [[Keys]], and [[GetOwnProertyKeys]].

More generally, I would argue that all Proxy traps (and the corresponding Reflect functions) potentially need access to the original "receiver". We don't know what somebody is going to do in such traps and they well need to call back to the original receiver for the same sort of consistency issues we have encountered with [[SetP]]. this is particularly apparent if you think multiple levels of proxies chained through their target slots.

I'm not sure I follow. In my understanding, the original Receiver is only needed for traps that involve prototype-chain walking and are thus |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] since they also walk the proto-chain, although it's not strictly necessary as the language currently does not make these operations |this|-sensitive.

# Tom Van Cutsem (12 years ago)

2012/12/20 Allen Wirfs-Brock <allen at wirfs-brock.com>

Yes, here is the new step 5 as I have just updated it in my spec. draft (ignore the 1, it's really 5)

  1. If IsDataDescriptor(ownDesc) is true, then
    1. If ownDesc.[[Writable]] is false, return false.
    2. If Type(Receiver) is not Object, return false.
    3. Let existingDescriptor be be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P.
    4. ReturnIfAbrupt(existingDescriptor).
    5. If existingDescriptor is not undefined, then
      1. Let valueDesc be the Property Descriptor {[[Value]]: V}.
      2. Return the result of calling the [[DefineOwnProperty]] internal method of Receiver with arguments P and valueDesc.
    6. Else *Receiver *does not currently have a property P,
      1. Return the result of performing CreateOwnDataProperty(* Receiver*, P, V).

Allen

Looks good!

As you note, with the introduction of a user-facing Reflect.set function, Receiver can be an arbitrary object. Thus, it may be that existingDescriptor denotes an accessor property, in which case the [[DefineOwnProperty]] call will fail, but this is perfectly acceptable.

# Allen Wirfs-Brock (12 years ago)

On Dec 20, 2012, at 12:12 PM, Tom Van Cutsem wrote:

2012/12/20 Allen Wirfs-Brock <allen at wirfs-brock.com> Yes, here is the new step 5 as I have just updated it in my spec. draft (ignore the 1, it's really 5) If IsDataDescriptor(ownDesc) is true, then If ownDesc.[[Writable]] is false, return false. If Type(Receiver) is not Object, return false. Let existingDescriptor be be the result of calling the [[GetOwnProperty]] internal method of Receiver with argument P. ReturnIfAbrupt(existingDescriptor). If existingDescriptor is not undefined, then Let valueDesc be the Property Descriptor {[[Value]]: V}. Return the result of calling the [[DefineOwnProperty]] internal method of Receiver with arguments P and valueDesc. Else Receiver does not currently have a property P, Return the result of performing CreateOwnDataProperty(Receiver, P, V).

Allen

Looks good!

As you note, with the introduction of a user-facing Reflect.set function, Receiver can be an arbitrary object. Thus, it may be that existingDescriptor denotes an accessor property, in which case the [[DefineOwnProperty]] call will fail, but this is perfectly acceptable.

Maybe not fail. If existingDescriptor is an accessor descriptor with [[Configurable]]: true, then the [[DefineOwnProperty]] call will convert the property into a data property. But maybe that is also ok as this situation could only occur via direct use of Reflect.put or perhaps a strangely defined proxy handler. Both of those are new situations where we are free to define the semantics.

# Allen Wirfs-Brock (12 years ago)

On Dec 20, 2012, at 12:07 PM, Tom Van Cutsem wrote:

2012/12/20 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 20, 2012, at 2:21 AM, Tom Van Cutsem wrote:

If target.baz is a writable data property proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', {value:42}) (and not Object.defineProperty(proxy, 'baz', {value:42,enumerable:true,writable:true,configurable:true}) as it did previously) This behavior is consistent with the method and accessor case: in all cases, the target delegates back to the proxy.

This is actually essential to maintaining ES5 default semantics because {value:42} and {value: 42, enumerable:true,writable:true,configurable:true} have quite different meanings to the ordinary [[DefineOwnProperty]].

Indeed. And this is what caused the issue I reported in the OP.

It is equally important that if target.baz does not exist that [[DefineOwnProperty]] is called on proxy with with the full {value: 42, enumerable:true,writable:true,configurable:true} descriptor to ensure that the new property gets created using the "crated by assignment" attributes rather than the [[DefineOwnProperty]] defaults.

Yes, this is the case.

One thing that I learned from all this is that it's simpler to think of the proxy as delegating (as opposed to forwarding) to its target by default. And under the semantics of invoke = get + apply, that is actually the simplest option.

Yes, this is the best way I have found to think about them and I've been patiently waiting for you to see the light :-) However, a corollary is that most internal method calls within derived internal methods/traps need to delegate back to the original "receiver". In some cases, we don't provide the original receiver as an extra argument, so it isn't available to do this.

A quick scan of the ordinary internal method suggests that we may have this problem for [[Delete]], [[Enumerate]], [[Keys]], and [[GetOwnProertyKeys]].

More generally, I would argue that all Proxy traps (and the corresponding Reflect functions) potentially need access to the original "receiver". We don't know what somebody is going to do in such traps and they well need to call back to the original receiver for the same sort of consistency issues we have encountered with [[SetP]]. this is particularly apparent if you think multiple levels of proxies chained through their target slots.

I'm not sure I follow. In my understanding, the original Receiver is only needed for traps that involve prototype-chain walking and are thus |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] since they also walk the proto-chain, although it's not strictly necessary as the language currently does not make these operations |this|-sensitive.

The proxy target delegation chain is also this-sensitive when it invokes internal methods. For example, in the revised [[SetP]] step 5 it is important that the [[DefineOwnProperty]] calls (in 5.e..ii and indirectly in 5.f.i are made on Receiver and not O.

Let's take a simple case, the [[Keys]] internal method. It needs to do a [[GetOwnProperty]] call for each property to determine whether or not it is enumerable. If [[Keys]] is invoked on a Proxy and automatically delegated to the target, should the [[GetOwnProperty]] call be made to the target or to the original receiver (the proxy)? If it is invoked on the target, we will get a list of what the target thinks are its enumerable properties. The proxy, itself, might have a different idea of which of its properties are enumerable. Since the original operation was invoked on the proxy, we presumably want [[Keys]] to tells us what that object (the proxy) thinks are its enumerable own properties not what the target actually has as enumerable own properties. The latter could actually be leaking a secret that the proxy is trying to keep.

Here is another way to think about it. Both the ES prototype chain and the ES proxy target chain can be view as examples of Lieberman style delegation, but at different abstraction levels. Prototype delegation is about delegation of ES functions. The self-calls take place at the level of ES functions and the implementation layer is careful to pass the correct this values to inherited (ie delegated) functions. The mechanisms for describing the semantics (and perhaps implementing it) uses the ES internal methods. But, for prototype delegation purposes, the internal methods do not make true self-calls. Historically, ES internal methods are not inherited/delegated along the prototype chain. This is why the Receiver needs to be passed as an explicit parameter to the internal methods that are responsible for implementing ES function level self-call semantics.

For proxies, things are flipped. The delegation isn't at the level of ES functions, instead it is at the level of internal methods. The self-calls that are of interest are not to ES level methods but to internal methods. That's why [[Keys]] really should be doing a self-call of [[GetOwnProperty]] through the original receiver rather than to the same target object that fielded the [[Keys]] call. Similar situations exist within [[Delete]].

If you step back a bit and just think about the concepts of Lieberman delegation and self-calls without worry about the specific of the proxies or the ES MOP I think you will come to see that delegated target calls naturally should self-call back to the original object. That's what Lieberman style delegation is all about.

Or, here's another way to look at it. You need real self-calls that go back to the original receiver whenever you have an interface with interdependent operations (some of which may be "derived" and other may be "fundamental") and where you permit individual operations to be selectively delegated. Otherwise, leaf objects won't present a "self"-consistent set of operations. This problem goes away, if you do not allow delegation at the individual operation level, but instead only permit either all operations or no operations of the interface to be over-ridden/delegated. This "no individual over-rides" solution is essentially what the ES spec. historically applied to internal methods. But direct proxies now allow over-rides at the individual internal method granularity. This exposes the sorts of inconsistencies I'm describing.

# Andreas Rossberg (12 years ago)

On 21 December 2012 03:00, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On Dec 20, 2012, at 12:07 PM, Tom Van Cutsem wrote:

I'm not sure I follow. In my understanding, the original Receiver is only needed for traps that involve prototype-chain walking and are thus |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] since they also walk the proto-chain, although it's not strictly necessary as the language currently does not make these operations |this|-sensitive.

The proxy target delegation chain is also this-sensitive when it invokes internal methods. For example, in the revised [[SetP]] step 5 it is important that the [[DefineOwnProperty]] calls (in 5.e..ii and indirectly in 5.f.i are made on Receiver and not O.

[...] If you step back a bit and just think about the concepts of Lieberman delegation and self-calls without worry about the specific of the proxies or the ES MOP I think you will come to see that delegated target calls naturally should self-call back to the original object. That's what Lieberman style delegation is all about.

While I agree with your line of reasoning in principle, it seems that your proposed change imposes substantial complications on implementations. While simple forwarding of missing traps allows reusing existing code for performing the respective operations (including all sorts of optimisations and special-casing), it seems to me that a delegation semantics requires duplicating much of the core functionality of objects to correctly deal with the rare case where the object is a proxy target.

So far, proxies where mainly a special case implementations could distinguish early on, and not care about them in the rest of the logic for a given operation (except where you had to do proto climbing). With delegation semantics everywhere, that is no longer the case, and everything becomes intertwined.

If a VM is no longer able to reuse existing optimisations easily for the proxy case, my guess is that such a semantics would make direct proxies significantly slower in practice. I, for one, would not look forward to implementing the change, let alone optimising it. :)

That said, I normally stand on the side of a better semantics. But we should be aware of the likely implications in this case.

# Tom Van Cutsem (12 years ago)

2012/12/21 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 20, 2012, at 12:07 PM, Tom Van Cutsem wrote:

2012/12/20 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 20, 2012, at 2:21 AM, Tom Van Cutsem wrote:

If target.baz is a writable data property proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', {value:42}) (and not Object.defineProperty(proxy, 'baz', {value:42,enumerable:true,writable:true,configurable:true}) as it did previously) This behavior is consistent with the method and accessor case: in all cases, the target delegates back to the proxy.

This is actually essential to maintaining ES5 default semantics because {value:42} and {value: 42, enumerable:true,writable:true,configurable:true} have quite different meanings to the ordinary [[DefineOwnProperty]].

Indeed. And this is what caused the issue I reported in the OP.

It is equally important that if target.baz does not exist that [[DefineOwnProperty]] is called on proxy with with the full {value: 42, enumerable:true,writable:true,configurable:true} descriptor to ensure that the new property gets created using the "crated by assignment" attributes rather than the [[DefineOwnProperty]] defaults.

Yes, this is the case.

One thing that I learned from all this is that it's simpler to think of the proxy as delegating (as opposed to forwarding) to its target by default. And under the semantics of invoke = get + apply, that is actually the simplest option.

Yes, this is the best way I have found to think about them and I've been patiently waiting for you to see the light :-) However, a corollary is that most internal method calls within derived internal methods/traps need to delegate back to the original "receiver". In some cases, we don't provide the original receiver as an extra argument, so it isn't available to do this.

A quick scan of the ordinary internal method suggests that we may have this problem for [[Delete]], [[Enumerate]], [[Keys]], and [[GetOwnProertyKeys]].

More generally, I would argue that all Proxy traps (and the corresponding Reflect functions) potentially need access to the original "receiver". We don't know what somebody is going to do in such traps and they well need to call back to the original receiver for the same sort of consistency issues we have encountered with [[SetP]]. this is particularly apparent if you think multiple levels of proxies chained through their target slots.

I'm not sure I follow. In my understanding, the original Receiver is only needed for traps that involve prototype-chain walking and are thus |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] since they also walk the proto-chain, although it's not strictly necessary as the language currently does not make these operations |this|-sensitive.

The proxy target delegation chain is also this-sensitive when it invokes internal methods. For example, in the revised [[SetP]] step 5 it is important that the [[DefineOwnProperty]] calls (in 5.e..ii and indirectly in 5.f.i are made on Receiver and not O.

Let's take a simple case, the [[Keys]] internal method. It needs to do a [[GetOwnProperty]] call for each property to determine whether or not it is enumerable. If [[Keys]] is invoked on a Proxy and automatically delegated to the target, should the [[GetOwnProperty]] call be made to the target or to the original receiver (the proxy)? If it is invoked on the target, we will get a list of what the target thinks are its enumerable properties. The proxy, itself, might have a different idea of which of its properties are enumerable. Since the original operation was invoked on the proxy, we presumably want [[Keys]] to tells us what that object (the proxy) thinks are its enumerable own properties not what the target actually has as enumerable own properties. The latter could actually be leaking a secret that the proxy is trying to keep.

If we're talking secret-keeping-proxies, these probably should just override and implement all traps, and not default to forwarding to the original target. That feels too brittle for an abstraction that's trying to keep a secret.

Here is another way to think about it. Both the ES prototype chain and the ES proxy target chain can be view as examples of Lieberman style delegation, but at different abstraction levels. Prototype delegation is about delegation of ES functions. The self-calls take place at the level of ES functions and the implementation layer is careful to pass the correct this values to inherited (ie delegated) functions. The mechanisms for describing the semantics (and perhaps implementing it) uses the ES internal methods. But, for prototype delegation purposes, the internal methods do not make true self-calls. Historically, ES internal methods are not inherited/delegated along the prototype chain. This is why the Receiver needs to be passed as an explicit parameter to the internal methods that are responsible for implementing ES function level self-call semantics.

For proxies, things are flipped. The delegation isn't at the level of ES functions, instead it is at the level of internal methods. The self-calls that are of interest are not to ES level methods but to internal methods. That's why [[Keys]] really should be doing a self-call of [[GetOwnProperty]] through the original receiver rather than to the same target object that fielded the [[Keys]] call. Similar situations exist within [[Delete]].

Thanks. That's a nice explanation of the two levels of delegation.

If you step back a bit and just think about the concepts of Lieberman delegation and self-calls without worry about the specific of the proxies or the ES MOP I think you will come to see that delegated target calls naturally should self-call back to the original object. That's what Lieberman style delegation is all about.

Or, here's another way to look at it. You need real self-calls that go back to the original receiver whenever you have an interface with interdependent operations (some of which may be "derived" and other may be "fundamental") and where you permit individual operations to be selectively delegated. Otherwise, leaf objects won't present a "self"-consistent set of operations. This problem goes away, if you do not allow delegation at the individual operation level, but instead only permit either all operations or no operations of the interface to be over-ridden/delegated. This "no individual over-rides" solution is essentially what the ES spec. historically applied to internal methods. But direct proxies now allow over-rides at the individual internal method granularity. This exposes the sorts of inconsistencies I'm describing.

Your references to "derived" vs "fundamental" and self-consistency make me think about the Handler API again:

If a proxy handler simply subclasses Handler, does not override any derived trap, and overrides all fundamental traps to simply forward to the target, then I think you basically achieve what you would have achieved if the ES6 spec used delegation internally.

If a derived trap is called on such a handler, it inherits the default implementation from Handler which basically mimics all the ES6 built-in algorithms, but with explicit self-sends to call the fundamental traps. Granted, the fundamental traps themselves are still forwarded (not delegated), but for fundamental traps, there is no other derived operation to call back on anyway (they are not Receiver-sensitive, so to speak), so that's OK.

As Andreas points out, modifying the built-in semantics of normal objects to support delegation is a non-trivial change that may impact the performance of normal, non-proxy objects. At the very least implementations will want to branch on whether the target and the receiver object are the same (then they can fall back on the current implementation), otherwise they must explicitly execute the ES6 spec algorithm.

I think that with the provision of the Handler API, we provide Proxy authors with the same benefits than if we would add delegation to the ES6 built-ins, without any required changes to existing internal methods for normal Objects.

# Allen Wirfs-Brock (12 years ago)

On Dec 21, 2012, at 5:15 AM, Andreas Rossberg wrote:

On 21 December 2012 03:00, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On Dec 20, 2012, at 12:07 PM, Tom Van Cutsem wrote:

I'm not sure I follow. In my understanding, the original Receiver is only needed for traps that involve prototype-chain walking and are thus |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] since they also walk the proto-chain, although it's not strictly necessary as the language currently does not make these operations |this|-sensitive.

The proxy target delegation chain is also this-sensitive when it invokes internal methods. For example, in the revised [[SetP]] step 5 it is important that the [[DefineOwnProperty]] calls (in 5.e..ii and indirectly in 5.f.i are made on Receiver and not O.

[...] If you step back a bit and just think about the concepts of Lieberman delegation and self-calls without worry about the specific of the proxies or the ES MOP I think you will come to see that delegated target calls naturally should self-call back to the original object. That's what Lieberman style delegation is all about.

While I agree with your line of reasoning in principle, it seems that your proposed change imposes substantial complications on implementations. While simple forwarding of missing traps allows reusing existing code for performing the respective operations (including all sorts of optimisations and special-casing), it seems to me that a delegation semantics requires duplicating much of the core functionality of objects to correctly deal with the rare case where the object is a proxy target.

So far, proxies where mainly a special case implementations could distinguish early on, and not care about them in the rest of the logic for a given operation (except where you had to do proto climbing). With delegation semantics everywhere, that is no longer the case, and everything becomes intertwined.

If a VM is no longer able to reuse existing optimisations easily for the proxy case, my guess is that such a semantics would make direct proxies significantly slower in practice. I, for one, would not look forward to implementing the change, let alone optimising it. :)

That said, I normally stand on the side of a better semantics. But we should be aware of the likely implications in this case.

/Andreas

yes, I understand and share your concerns. I don't yet have any actual proposed changes that I want to put on the table. For now, I'm only trying to understand the conceptual issues and why they seem to cause problems in certain cases.

But here is a sketch, of a possible approach that might work:

We've already parameterized the most important internal methods with a Receiver parameter. Their only appear to be two or three others (for example, [[Delete]]) that, conceptually, should be making self-calls back to a Receiver and they don't seem to be commonly used in perf critical situations, so we probably could add that parameter without a great impact.

However, any user written "internal method" (in other words, any trap) might want to make self-calls. But maybe that's not so bad because the only way (other than extending the implementation or via a host object API) to introduce such arbitrary, self-call dependent behaviors is via a Proxy. Also, all delegation from a trap handler to the target object should be done via a Reflect.* call. So, all Proxy trap signatures and all Reflect.* functions would need to include a Receiver parameter.

The Proxy internal methods would then be responsible for passing the Receiver parameter (the Proxy) to the traps and the Reflect.* functions would be responsible for propagating the Receiver parameter its target is a also a Proxy.

This would see to pretty much isolate the special case delegation semantics to situations that actually use a handler and the Reflect functions.

# Allen Wirfs-Brock (12 years ago)

On Dec 21, 2012, at 7:01 AM, Tom Van Cutsem wrote:

2012/12/21 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 20, 2012, at 12:07 PM, Tom Van Cutsem wrote:

2012/12/20 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 20, 2012, at 2:21 AM, Tom Van Cutsem wrote:

If target.baz is a writable data property proxy.baz = 42 will eventually call Object.defineProperty(proxy, 'baz', {value:42}) (and not Object.defineProperty(proxy, 'baz', {value:42,enumerable:true,writable:true,configurable:true}) as it did previously) This behavior is consistent with the method and accessor case: in all cases, the target delegates back to the proxy.

This is actually essential to maintaining ES5 default semantics because {value:42} and {value: 42, enumerable:true,writable:true,configurable:true} have quite different meanings to the ordinary [[DefineOwnProperty]].

Indeed. And this is what caused the issue I reported in the OP.

It is equally important that if target.baz does not exist that [[DefineOwnProperty]] is called on proxy with with the full {value: 42, enumerable:true,writable:true,configurable:true} descriptor to ensure that the new property gets created using the "crated by assignment" attributes rather than the [[DefineOwnProperty]] defaults.

Yes, this is the case.

One thing that I learned from all this is that it's simpler to think of the proxy as delegating (as opposed to forwarding) to its target by default. And under the semantics of invoke = get + apply, that is actually the simplest option.

Yes, this is the best way I have found to think about them and I've been patiently waiting for you to see the light :-) However, a corollary is that most internal method calls within derived internal methods/traps need to delegate back to the original "receiver". In some cases, we don't provide the original receiver as an extra argument, so it isn't available to do this.

A quick scan of the ordinary internal method suggests that we may have this problem for [[Delete]], [[Enumerate]], [[Keys]], and [[GetOwnProertyKeys]].

More generally, I would argue that all Proxy traps (and the corresponding Reflect functions) potentially need access to the original "receiver". We don't know what somebody is going to do in such traps and they well need to call back to the original receiver for the same sort of consistency issues we have encountered with [[SetP]]. this is particularly apparent if you think multiple levels of proxies chained through their target slots.

I'm not sure I follow. In my understanding, the original Receiver is only needed for traps that involve prototype-chain walking and are thus |this|-sensitive. That would be just [[GetP]] and [[SetP]]. One can make the case (David has done so in the past) for [[HasProperty]] and [[Enumerate]] since they also walk the proto-chain, although it's not strictly necessary as the language currently does not make these operations |this|-sensitive.

The proxy target delegation chain is also this-sensitive when it invokes internal methods. For example, in the revised [[SetP]] step 5 it is important that the [[DefineOwnProperty]] calls (in 5.e..ii and indirectly in 5.f.i are made on Receiver and not O.

Let's take a simple case, the [[Keys]] internal method. It needs to do a [[GetOwnProperty]] call for each property to determine whether or not it is enumerable. If [[Keys]] is invoked on a Proxy and automatically delegated to the target, should the [[GetOwnProperty]] call be made to the target or to the original receiver (the proxy)? If it is invoked on the target, we will get a list of what the target thinks are its enumerable properties. The proxy, itself, might have a different idea of which of its properties are enumerable. Since the original operation was invoked on the proxy, we presumably want [[Keys]] to tells us what that object (the proxy) thinks are its enumerable own properties not what the target actually has as enumerable own properties. The latter could actually be leaking a secret that the proxy is trying to keep.

If we're talking secret-keeping-proxies, these probably should just override and implement all traps, and not default to forwarding to the original target. That feels too brittle for an abstraction that's trying to keep a secret.

It's just an example of a very simply use of Proxy's where a ES programmer might naively think they only need to over-ride a single fundamental trap in order to get their desired effect. For example, they might want a property named "length" to always be treated as non-enumerable. They might reasonably think that all they need to do is a handler that looks like:

   {getOwnPropertyDescrptor(target,key) {
        let desc = Reflect.getOwnPropertyDescriptor(target,key);
        if (desc && name==="length) desc.enumerable=false;
        return desc;
   }}

Here is another way to think about it. Both the ES prototype chain and the ES proxy target chain can be view as examples of Lieberman style delegation, but at different abstraction levels. Prototype delegation is about delegation of ES functions. The self-calls take place at the level of ES functions and the implementation layer is careful to pass the correct this values to inherited (ie delegated) functions. The mechanisms for describing the semantics (and perhaps implementing it) uses the ES internal methods. But, for prototype delegation purposes, the internal methods do not make true self-calls. Historically, ES internal methods are not inherited/delegated along the prototype chain. This is why the Receiver needs to be passed as an explicit parameter to the internal methods that are responsible for implementing ES function level self-call semantics.

For proxies, things are flipped. The delegation isn't at the level of ES functions, instead it is at the level of internal methods. The self-calls that are of interest are not to ES level methods but to internal methods. That's why [[Keys]] really should be doing a self-call of [[GetOwnProperty]] through the original receiver rather than to the same target object that fielded the [[Keys]] call. Similar situations exist within [[Delete]].

Thanks. That's a nice explanation of the two levels of delegation.

If you step back a bit and just think about the concepts of Lieberman delegation and self-calls without worry about the specific of the proxies or the ES MOP I think you will come to see that delegated target calls naturally should self-call back to the original object. That's what Lieberman style delegation is all about.

Or, here's another way to look at it. You need real self-calls that go back to the original receiver whenever you have an interface with interdependent operations (some of which may be "derived" and other may be "fundamental") and where you permit individual operations to be selectively delegated. Otherwise, leaf objects won't present a "self"-consistent set of operations. This problem goes away, if you do not allow delegation at the individual operation level, but instead only permit either all operations or no operations of the interface to be over-ridden/delegated. This "no individual over-rides" solution is essentially what the ES spec. historically applied to internal methods. But direct proxies now allow over-rides at the individual internal method granularity. This exposes the sorts of inconsistencies I'm describing.

Your references to "derived" vs "fundamental" and self-consistency make me think about the Handler API again:

(yes, that was intentional)

If a proxy handler simply subclasses Handler, does not override any derived trap, and overrides all fundamental traps to simply forward to the target, then I think you basically achieve what you would have achieved if the ES6 spec used delegation internally.

If a derived trap is called on such a handler, it inherits the default implementation from Handler which basically mimics all the ES6 built-in algorithms, but with explicit self-sends to call the fundamental traps. Granted, the fundamental traps themselves are still forwarded (not delegated), but for fundamental traps, there is no other derived operation to call back on anyway (they are not Receiver-sensitive, so to speak), so that's OK.

As Andreas points out, modifying the built-in semantics of normal objects to support delegation is a non-trivial change that may impact the performance of normal, non-proxy objects. At the very least implementations will want to branch on whether the target and the receiver object are the same (then they can fall back on the current implementation), otherwise they must explicitly execute the ES6 spec algorithm.

I think that with the provision of the Handler API, we provide Proxy authors with the same benefits than if we would add delegation to the ES6 built-ins, without any required changes to existing internal methods for normal Objects.

Yes, I generally agree that Handler is a way out of this. But if that is our solution to the problem then I still think we have a usability issue with the current Proxy API design.

# Tom Van Cutsem (12 years ago)

2012/12/21 Allen Wirfs-Brock <allen at wirfs-brock.com>

On Dec 21, 2012, at 7:01 AM, Tom Van Cutsem wrote:

I think that with the provision of the Handler API, we provide Proxy authors with the same benefits than if we would add delegation to the ES6 built-ins, without any required changes to existing internal methods for normal Objects.

Yes, I generally agree that Handler is a way out of this. But if that is our solution to the problem then I still think we have a usability issue with the current Proxy API design.

The usability cuts both ways: in the old Proxy API the fundamental vs. derived trap distinction was baked-in, but you had to explicitly implement all fundamental traps before you had a complete object abstraction. Direct proxies are generally simpler to get started with, but there are pitfalls when implementing only some of the traps.

We previously agreed that this is a known problem in OO frameworks. I'm not sure whether there's a silver bullet here. I think the best we can do is to provide adequate documentation of the dependencies, and point out the added value of subclassing Handler.