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

Tom Van Cutsem tomvc.be at gmail.com
Fri Dec 21 07:01:29 PST 2012


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.

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


More information about the es-discuss mailing list