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

Allen Wirfs-Brock allen at wirfs-brock.com
Fri Dec 21 09:14:28 PST 2012


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.

Allen

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20121221/c6dfa5f5/attachment.html>


More information about the es-discuss mailing list