[Proxies] Refactoring prototype climbing in the spec
Thanks for all this work!
Le 07/11/2011 16:54, Tom Van Cutsem a écrit :
Hi,
I wrote up an initial (but fairly complete) draft of a proposed refactoring of the ES5 [[Get]], [[Put]] and [[HasProperty]] algorithms to change the way in which these algorithms climb the prototype chain: strawman:refactoring_put
This is mainly beneficial for proxies, as the prototype walking strategy becomes observable to proxies-used-as-prototypes.
IMHO, the refactored algorithms interoperate with proxies in a much saner way, finally restoring the intuitive semantics of the "get" and "set" traps that MarkM and I had in mind from the beginning, but which Sean Eagan pointed out were flawed given the ES5 spec algorithms.
I am a big fan of this refactoring. I like the idea that the algorithm transfers full control to the proxy. Also, accepting this strawman would solve my inherited event properties problem because I would have access to the receiver from the get trap and would be able to do the binding I want.
About [[SetP]]:
- It seems to me that we've traded a duplicated [[GetProperty]] call for a duplicated [[GetOwnProperty]] (Step 1 and 5.a) call on the receiver. This could be avoided by storing the property descriptor at the beginning (when O.[[setP]](Receiver, P, V) is called and O === receiver). Step 5.a could be remove by the change.
- If step 5.a is removed, I think that step 5.b.i is useless, because we would have returned from the set when O was the receiver (by step 2.a of [[setP]])
- If step 5.a is removed, then step 5.c is useless, because if the desc had a [[set]], then we would have already returned from one of the substep of step 3 when O was the receiver
Why not redefining [[Get]] as what you have defined as [[GetP]] and equivalent for [[Set]] and [[SetP]]? Current 8.12.3 [[Get]] (P) would become [[Get]] (Receiver, P). It would be called with O as initial value for Receiver pretty much everywhere in the spec except within [[Get]] recursive calls. Equivalent for [[Set]]. It would prevent the replacement of 2 Object.* methods by 2 others.
With the refactoring, on the direct proxy strawman, I don't think we need the "proxy" argument for the get and set traps anymore. It was here as the receiver, but since we have the receiver itself, the get and set trap can just have the receiver and the target as argument.
I am a big fan of this refactoring. I like the idea that the algorithm transfers full control to the proxy. Also, accepting this strawman would solve my inherited event properties problem because I would have access to the receiver from the get trap and would be able to do the binding I want.
Glad to hear that.
About [[SetP]]:
- It seems to me that we've traded a duplicated [[GetProperty]] call for a duplicated [[GetOwnProperty]] (Step 1 and 5.a) call on the receiver. This could be avoided by storing the property descriptor at the beginning (when O.[[setP]](Receiver, P, V) is called and O === receiver). Step 5.a could be remove by the change.
The double [[GetOwnProperty]] invocation on the initial receiver should not be observable: if the initial Receiver of [[SetP]] is a proxy, the proxy's [[SetP]] algorithm (which triggers the "set" trap) executes instead of this algorithm. So it's only if the initial Receiver is a normal object that this code executes, and then the call to [[GetOwnProperty]] on line 1 will never trigger proxy code.
When [[SetP]] is called by evaluating a property assignment expression, Receiver also always denotes a normal non-proxy object. However, with the proposed Object.getProperty built-in one can write:
Object.getProperty(aProxy, name, aNormalObject)
which would trigger aNormalObject.[[SetP]](aProxy, name), so then Receiver would indeed be a proxy. Line 1 then calls aNormalObject.[[GetOwnProperty]], while line 5.a, if reached, would trigger aProxy.[[GetOwnProperty]].
Long story short: the "O" variable in the [[SetP]] algorithm always denotes a normal non-proxy object. In general, the "Receiver" variable can be any object.
- If step 5.a is removed, I think that step 5.b.i is useless, because we would have returned from the set when O was the receiver (by step 2.a of [[setP]])
- If step 5.a is removed, then step 5.c is useless, because if the desc had a [[set]], then we would have already returned from one of the substep of step 3 when O was the receiver
Why not redefining [[Get]] as what you have defined as [[GetP]] and equivalent for [[Set]] and [[SetP]]? Current 8.12.3 [[Get]] (P) would become [[Get]] (Receiver, P). It would be called with O as initial value for Receiver pretty much everywhere in the spec except within [[Get]] recursive calls. Equivalent for [[Set]]. It would prevent the replacement of 2 Object.* methods by 2 others.
The reason I introduced separate [[GetP]] and [[SetP]] methods is simply so that the "public API" of Objects remained intact. Your suggestion is reasonable: trade proliferation of built-in methods for a small public API change in the spec. I guess this is just a matter of find/replacing all the calls to [[Get]]/[[Put]]. Might even be an opportunity to get rid of "put" and use "set" consistently throughout the spec.
With the refactoring, on the direct proxy strawman, I don't think we need the "proxy" argument for the get and set traps anymore. It was here as the receiver, but since we have the receiver itself, the get and set trap can just have the receiver and the target as argument.
Good point. I'd wager that in most use cases, knowing that |receiver| is either your own proxy or a descendant of that proxy is sufficient. I can't currently think of a use case where the get/set trap would really want to be able to access the proxy itself, not just the proxy or a descendant. That doesn't mean that use cases for it don't exist, however. I guess if access to the proxy is really needed, it's easy enough to accomplish:
var p = Proxy.for(target, handler); handler.proxy = p;
Now the proxy is accessible as |this.proxy| in all traps. This may seem like a hassle, but if the use cases for accessing a proxy are sufficiently minor, getting rid of the proxy argument to the get/set traps seems like a good tradeoff.
On 7 November 2011 16:54, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
I wrote up an initial (but fairly complete) draft of a proposed refactoring of the ES5 [[Get]], [[Put]] and [[HasProperty]] algorithms to change the way in which these algorithms climb the prototype chain: strawman:refactoring_put
Looks good, and as far as I can see from a first read, solves the issues we were discussing so far.
But I have a follow-up request. :) Regarding redundant trap calls with proxies there is another, more pervasive problem with the current spec: in lots of places it first calls [[HasProperty]] and then [[Get]]. With proxies, this always implies two trap calls, which seems wasteful. Would it be possible to refactor that, too?
Seems more difficult, because we would need to enable [[Get]] (and hence the get trap) to signal lookup failure. (Too bad that we cannot reuse `undefined' for it.) But I think the current situation isn't satisfactory.
Le 08/11/2011 11:54, Tom Van Cutsem a écrit :
About [[SetP]]: * It seems to me that we've traded a duplicated [[GetProperty]] call for a duplicated [[GetOwnProperty]] (Step 1 and 5.a) call on the receiver. This could be avoided by storing the property descriptor at the beginning (when O.[[setP]](Receiver, P, V) is called and O === receiver). Step 5.a could be remove by the change.
The double [[GetOwnProperty]] invocation on the initial receiver should not be observable: if the initial Receiver of [[SetP]] is a proxy, the proxy's [[SetP]] algorithm (which triggers the "set" trap) executes instead of this algorithm.
Very true. And even if there is no trap, the default action is to trigger [[Set]] on the target, so no duplicated call.
(...)
Long story short: the "O" variable in the [[SetP]] algorithm always denotes a normal non-proxy object. In general, the "Receiver" variable can be any object.
* If step 5.a is removed, I think that step 5.b.i is useless, because we would have returned from the set when O was the receiver (by step 2.a of [[setP]]) * If step 5.a is removed, then step 5.c is useless, because if the desc had a [[set]], then we would have already returned from one of the substep of step 3 when O was the receiver Why not redefining [[Get]] as what you have defined as [[GetP]] and equivalent for [[Set]] and [[SetP]]? Current 8.12.3 [[Get]] (P) would become [[Get]] (Receiver, P). It would be called with O as initial value for Receiver pretty much everywhere in the spec except within [[Get]] recursive calls. Equivalent for [[Set]]. It would prevent the replacement of 2 Object.* methods by 2 others.
The reason I introduced separate [[GetP]] and [[SetP]] methods is simply so that the "public API" of Objects remained intact. Your suggestion is reasonable: trade proliferation of built-in methods for a small public API change in the spec. I guess this is just a matter of find/replacing all the calls to [[Get]]/[[Put]].
Exactly, that's what I had in mind.
Might even be an opportunity to get rid of "put" and use "set" consistently throughout the spec.
I cannot agree more with this idea. Especially with proxies having a "set" trap and accessor properties having "setters", [[Put]] sounds wrong even though that has been the name in ES5, ES3 (and even before?)
With the refactoring, on the direct proxy strawman, I don't think we need the "proxy" argument for the get and set traps anymore. It was here as the receiver, but since we have the receiver itself, the get and set trap can just have the receiver and the target as argument.
Good point. I'd wager that in most use cases, knowing that |receiver| is either your own proxy or a descendant of that proxy is sufficient. I can't currently think of a use case where the get/set trap would really want to be able to access the proxy itself, not just the proxy or a descendant.That doesn't mean that use cases for it don't exist, however.
Oh true, I thought that proxy and the receiver were the same objects but if the proxy is on the prototype, it is not the case.
I guess if access to the proxy is really needed, it's easy enough to accomplish:
var p = Proxy.for(target, handler); handler.proxy = p;
Indeed.
This is all going in a good direction.
I only have a few minor comments:
I don't think that [[GetP]] and [[PutP]] need to be "internal methods" In spec'ing this I think I would make them be "abstract operations". Internal methods are extensions points that can be over-ridden on a per object basis. That is what [[Get]] and [[Put]] provides. GetP and SetP define fixed operations.
More generally, I think there should be a 1::1 correspondence between the internal methods in listed in ES5 Table 8 and fundamental proxy traps. We either need to eliminate any internal methods that don't trap or add corresponding traps. In this proposal you eliminate the need for [[CanPUt]] and you raise the question of whether that is possible for also get rid of [[GetProperty]]. My analysis (docs.google.com/spreadsheet/ccc?key=0Ak51JfLL8QLYdDFkcy1VUl9OQ3BSc1kxeDI4RkJsc0E ) says that we can, so we should just go ahead and eliminate it.
We might even consider eliminating [[Delete]] by defining [DefneOwnProperty] to mean delete.
Finally, I also have some thoughts about [[HasProperty]] but I will put those in a replay to Andreas message on that touches on that.
On Nov 8, 2011, at 7:33 AM, Andreas Rossberg wrote:
On 7 November 2011 16:54, Tom Van Cutsem <tomvc.be at gmail.com> wrote:
I wrote up an initial (but fairly complete) draft of a proposed refactoring of the ES5 [[Get]], [[Put]] and [[HasProperty]] algorithms to change the way in which these algorithms climb the prototype chain: strawman:refactoring_put
Looks good, and as far as I can see from a first read, solves the issues we were discussing so far.
But I have a follow-up request. :) Regarding redundant trap calls with proxies there is another, more pervasive problem with the current spec: in lots of places it first calls [[HasProperty]] and then [[Get]]. With proxies, this always implies two trap calls, which seems wasteful. Would it be possible to refactor that, too?
Seems more difficult, because we would need to enable [[Get]] (and hence the get trap) to signal lookup failure. (Too bad that we cannot reuse `undefined' for it.) But I think the current situation isn't satisfactory.
I agree, the current specification style is fine when we are dealing with side-effectly free internal operations but as soon as they are reified they become problematic and a performance issue.
I believe that the [[HasProperty]] [[Get]] combination is always used in situations where a "conditional [[Get]]" is desired. I suggest that we redefine [[Get]] to take a second optional argument which is used as a signal value to indicated that the property does not exist. Internally (within the specification) any unique object value can be used as the signal value as the spec. will never allow that value to escape such that it could be stored as a property value.
A similar approach could be taken with the trap and Object.getProperty APIs but there is the hazard that handler code might capture and misuse (store into a property) the signal value. It isn't clear to me whether this would constitute an exploitable hazard or whether it would just be an annoying bug. This capture problem could be avoid if the trap always passed a new object to the handler when the [[Get]] call uses its second argument. I'm reluctant to impose an (even trivial) object allocation on [[Get]] traps but it probably is still less overhead then making two traps. Also, it would not be needed in situations where the [[Get]] only had a single argument.
Using this approach, the get handler signature could be something like: function(receiver, name, target, proxy, missingMarker=undefined) and the signature for Object.getProperty could be: function(receiver, name, missingMarker=undefined, parent=receiver) (for the optional argument ordering, I'm assuming that use of a missingMarker is more common then using an explicit parent)
Given the limitations of ES function protocol the moral equivalent of either reference parameters or multiple value returns also require an object allocation. It isn't clear that we can do much better than this style of API. If we do this we should be able to eliminate both the [[HasProperty]] internal method and the has trap.
2011/11/8 Allen Wirfs-Brock <allen at wirfs-brock.com>
I don't think that [[GetP]] and [[PutP]] need to be "internal methods" In spec'ing this I think I would make them be "abstract operations". Internal methods are extensions points that can be over-ridden on a per object basis. That is what [[Get]] and [[Put]] provides. GetP and SetP define fixed operations.
I proposed [[GetP]] and [[SetP]] as internal methods because Proxies would "inherit" the built-in behavior for [[Get]] and [[Put]], but "override" the [[GetP]] and [[SetP]] operations. If [[GetP]] and [[SetP]] become abstract operations, won't they need to explicitly dispatch? Proxies could override [[Get]] and [[Put]], but that would require them to duplicate the Object [[Get]] and [[Put]] code.
More generally, I think there should be a 1::1 correspondence between the internal methods in listed in ES5 Table 8 and fundamental proxy traps.
I absolutely agree we should strive to achieve this.
2011/11/8 Allen Wirfs-Brock <allen at wirfs-brock.com>
On Nov 8, 2011, at 7:33 AM, Andreas Rossberg wrote:
But I have a follow-up request. :) Regarding redundant trap calls with proxies there is another, more pervasive problem with the current spec: in lots of places it first calls [[HasProperty]] and then [[Get]]. With proxies, this always implies two trap calls, which seems wasteful. Would it be possible to refactor that, too?
Seems more difficult, because we would need to enable [[Get]] (and hence the get trap) to signal lookup failure. (Too bad that we cannot reuse `undefined' for it.) But I think the current situation isn't satisfactory.
I agree, the current specification style is fine when we are dealing with side-effectly free internal operations but as soon as they are reified they become problematic and a performance issue.
Is the situation really that problematic? Skimming the ES5.1 spec for [[HasProperty]], I encounter calls in the following places:
-
ToPropertyDescriptor conversion (only triggers proxy code via Object.defineProperty(obj, name, aProxyAsDescriptor) and Object.defineProperties).
-
lots of methods on Array.prototype (map, forEach, filter, some, reduce, reduceRight, reverse, splice, indexOf, lastIndexOf, every, shift, slice, sort, unshift): the only way in which these trigger proxy code is when |this| is a proxy, i.e. when they are called as Array.prototype.theMethod.call(aProxy). (calling aProxy.theMethod triggers the proxy's "get" trap)
-
Array.prototype.concat: [[HasProperty]] is called on a built-in Array only.
-
10.2.1.2 Object environment records uses [[HasProperty]] in a number of places. Isn't this needed only for |with|, so only triggered by |with (aProxy) { ... }|? Since ES.next builds on ES5/strict, this doesn't seem to carry much weight.
-
The in operator: only calls [[HasProperty]], not followed by a call to [[Get]] (so it's not an instance of the "conditional [[Get]]" pattern).
None of the conditional [[Get]] patterns on proxies seem particularly common operations, even if proxies would be widely used. Are they worth the complexity of changing one of the most critical and common operations in Javascript?
On Nov 9, 2011, at 3:53 AM, Tom Van Cutsem wrote:
2011/11/8 Allen Wirfs-Brock <allen at wirfs-brock.com> On Nov 8, 2011, at 7:33 AM, Andreas Rossberg wrote:
But I have a follow-up request. :) Regarding redundant trap calls with proxies there is another, more pervasive problem with the current spec: in lots of places it first calls [[HasProperty]] and then [[Get]]. With proxies, this always implies two trap calls, which seems wasteful. Would it be possible to refactor that, too?
Seems more difficult, because we would need to enable [[Get]] (and hence the get trap) to signal lookup failure. (Too bad that we cannot reuse `undefined' for it.) But I think the current situation isn't satisfactory.
I agree, the current specification style is fine when we are dealing with side-effectly free internal operations but as soon as they are reified they become problematic and a performance issue.
Is the situation really that problematic? Skimming the ES5.1 spec for [[HasProperty]], I encounter calls in the following places:
ToPropertyDescriptor conversion (only triggers proxy code via Object.defineProperty(obj, name, aProxyAsDescriptor) and Object.defineProperties).
lots of methods on Array.prototype (map, forEach, filter, some, reduce, reduceRight, reverse, splice, indexOf, lastIndexOf, every, shift, slice, sort, unshift): the only way in which these trigger proxy code is when |this| is a proxy, i.e. when they are called as Array.prototype.theMethod.call(aProxy). (calling aProxy.theMethod triggers the proxy's "get" trap)
Array.prototype.concat: [[HasProperty]] is called on a built-in Array only.
the array methods are my primary concern as the HasProperty/Get combination is used to preserve "holes" as the algorithms iterate over "arrays". On large arrays, there can be lots of these calls.
However, I'm not sure I understand your comments about theMethod.call(aProxy) vs. aProxy.theMethod(). In either case when theMethod is actually executed the this value must be aProxy and the internal calls to [[HasProperty]] and [[Get]] need to trap.
- 10.2.1.2 Object environment records uses [[HasProperty]] in a number of places. Isn't this needed only for |with|, so only triggered by |with (aProxy) { ... }|? Since ES.next builds on ES5/strict, this doesn't seem to carry much weight.
And for global object bindings. It still has to work, consider for example, a proxy based DOM object that is accessed from non-Harmony code. The global object, it self, might be a Proxy.
- The in operator: only calls [[HasProperty]], not followed by a call to [[Get]] (so it's not an instance of the "conditional [[Get]]" pattern).
None of the conditional [[Get]] patterns on proxies seem particularly common operations, even if proxies would be widely used. Are they worth the complexity of changing one of the most critical and common operations in Javascript?
The array functions are my primary concern.
On Nov 9, 2011, at 3:17 AM, Tom Van Cutsem wrote:
2011/11/8 Allen Wirfs-Brock <allen at wirfs-brock.com> I don't think that [[GetP]] and [[PutP]] need to be "internal methods" In spec'ing this I think I would make them be "abstract operations". Internal methods are extensions points that can be over-ridden on a per object basis. That is what [[Get]] and [[Put]] provides. GetP and SetP define fixed operations.
I proposed [[GetP]] and [[SetP]] as internal methods because Proxies would "inherit" the built-in behavior for [[Get]] and [[Put]], but "override" the [[GetP]] and [[SetP]] operations. If [[GetP]] and [[SetP]] become abstract operations, won't they need to explicitly dispatch? Proxies could override [[Get]] and [[Put]], but that would require them to duplicate the Object [[Get]] and [[Put]] code.
[[Get]] seems to do nothing but redispatch to [[GetP]] so its definition could be replaced with the body of [[GetP]].
[[Puit] does a redispatch to [[SetP]] followed by a "mode" based conditional throw depending upon the result of the [[SetP]]. I din't think the the throwing behavior needs (or even should) be over-ride able by a proxy (or even by an alternative internal implementation). I would factor the conditional throw code out of [[Put]] and make it the responsibility of the caller. In practice, this just means that I would define a new Abstraction Operation that is used in place of direct calls to [[Put]]. The abstraction operation wold essentially have the definition you provide for [[Put]] except that it would call [[Put]] instead of [[SetP]] and the definition of [[Put]] would be replaced with the body of [[SetP]]
2011/11/9 Allen Wirfs-Brock <allen at wirfs-brock.com>
On Nov 9, 2011, at 3:53 AM, Tom Van Cutsem wrote:
lots of methods on Array.prototype (map, forEach, filter, some, reduce, reduceRight, reverse, splice, indexOf, lastIndexOf, every, shift, slice, sort, unshift): the only way in which these trigger proxy code is when |this| is a proxy, i.e. when they are called as Array.prototype.theMethod.call(aProxy). (calling aProxy.theMethod triggers the proxy's "get" trap)
Array.prototype.concat: [[HasProperty]] is called on a built-in Array only.
the array methods are my primary concern as the HasProperty/Get combination is used to preserve "holes" as the algorithms iterate over "arrays". On large arrays, there can be lots of these calls.
However, I'm not sure I understand your comments about theMethod.call(aProxy) vs. aProxy.theMethod(). In either case when theMethod is actually executed the this value must be aProxy and the internal calls to [[HasProperty]] and [[Get]] need to trap.
You're right, I forgot the "get" trap just traps property access, not the full method invocation.
The "get" trap could return a bound method, binding theMethod's |this| to the wrapped array, but that would break inheritance and would forego intercepting the individual array elements accessed by the bound function.
- 10.2.1.2 Object environment records uses [[HasProperty]] in a number of places. Isn't this needed only for |with|, so only triggered by |with (aProxy) { ... }|? Since ES.next builds on ES5/strict, this doesn't seem to carry much weight.
And for global object bindings. It still has to work, consider for example, a proxy based DOM object that is accessed from non-Harmony code. The global object, it self, might be a Proxy.
I see. So IIUC, in such a runtime environment, dereferencing a global variable name would then first trigger the global object's "has" trap, potentially followed by the "get" trap.
Are you sure you want to kill [[HasProperty]] entirely? If we replace it by a conditional [[Get]], wouldn't that mean that the expression |name in obj| might unnecessarily trigger an accessor in obj?
2011/11/9 Allen Wirfs-Brock <allen at wirfs-brock.com>
[[Get]] seems to do nothing but redispatch to [[GetP]] so its definition could be replaced with the body of [[GetP]].
[[Puit] does a redispatch to [[SetP]] followed by a "mode" based conditional throw depending upon the result of the [[SetP]]. I din't think the the throwing behavior needs (or even should) be over-ride able by a proxy (or even by an alternative internal implementation). I would factor the conditional throw code out of [[Put]] and make it the responsibility of the caller. In practice, this just means that I would define a new Abstraction Operation that is used in place of direct calls to [[Put]]. The abstraction operation wold essentially have the definition you provide for [[Put]] except that it would call [[Put]] instead of [[SetP]] and the definition of [[Put]] would be replaced with the body of [[SetP]]
Agreed. So if I understood all of that correctly, we would have:
- Abstract [[GetProperty]] and [[SetProperty]] operations (the methods I called [[GetP]] and [[SetP]])
- Object.getProperty and Object.setProperty built-ins that call [[GetProperty]] and [[SetProperty]]
- [[Get]] and [[Set]] methods on Objects that call [[GetProperty]] and [[SetProperty]]
- [[Get]] and [[Set]] methods on Proxies that trigger "get" and "set" traps
- An abstract operation [[Put]](O, P, V, Throw) that calls O.[Set] and performs the "strict mode" reject behavior.
Le 09/11/2011 12:17, Tom Van Cutsem a écrit :
2011/11/8 Allen Wirfs-Brock <allen at wirfs-brock.com <mailto:allen at wirfs-brock.com>>
More generally, I think there should be a 1::1 correspondence between the internal methods in listed in ES5 Table 8 and fundamental proxy traps.
I absolutely agree we should strive to achieve this.
I disagree.
First of all, Table 8 is incomplete:
- It lacks a function for property enumeration and proxies need a trap for that (for-in loops, Object.getOwnPropertyNames, Object.keys).
- There is no method for passing [[Extensible]] from true to false, though this behavior is used and needs a ("fundamental") trap as well. The reason for this is certainly that this action can be described in one spec algorithm step, so there was no need from a specification perspective to consider this as an internal method, but it's still a fundamental operation you can perform on an object. (I may be missing others)
A second point I disagree on is the idea of "fundamental trap". This notion existed on the original proxy proposal. I think it was more for the purpose of writing traps more easily. With the direct proxies proposal, the notion of fundamental and derived traps disappears (and hopefully, direct proxies will be promoted during the next TC-39 meeting).
Regardless of Table 8 being incomplete, I think that proxy traps have another factor to take into account which is the relationship with surface syntax. When someone writes "let a = o.a;", the intention is to call the "get" operation regardless of whether the "a" property is on o or somewhere on its prototype chain. This is another thing that I like in the [[Get]] refactoring: when the prototype is the proxy, its get trap gets called and not its getOwnPropertyDescriptor trap. It makes proxy traps more in line with the object clients requests.
We might even consider eliminating [[Delete]] by defining [DefneOwnProperty] to mean delete.
Alongside with the idea of a 1::1 mapping with trap, this would make disappear the "delete" trap and I think it's a bad idea. Currently, I'm pretty happy with the idea that my delete trap is called when "delete myProxy.prop;" is executed.
I agree with the idea that traps should match as much as possible the internal object API, but i think it should be a right balance between the internal and external API.
On Nov 10, 2011, at 1:36 AM, Tom Van Cutsem wrote:
2011/11/9 Allen Wirfs-Brock <allen at wirfs-brock.com> [[Get]] seems to do nothing but redispatch to [[GetP]] so its definition could be replaced with the body of [[GetP]].
[[Puit] does a redispatch to [[SetP]] followed by a "mode" based conditional throw depending upon the result of the [[SetP]]. I din't think the the throwing behavior needs (or even should) be over-ride able by a proxy (or even by an alternative internal implementation). I would factor the conditional throw code out of [[Put]] and make it the responsibility of the caller. In practice, this just means that I would define a new Abstraction Operation that is used in place of direct calls to [[Put]]. The abstraction operation wold essentially have the definition you provide for [[Put]] except that it would call [[Put]] instead of [[SetP]] and the definition of [[Put]] would be replaced with the body of [[SetP]]
Agreed. So if I understood all of that correctly, we would have:
- Abstract [[GetProperty]] and [[SetProperty]] operations (the methods I called [[GetP]] and [[SetP]])
Yes, current spec. conventions would just call these GetProperty and SetProperty abstract operations.
- Object.getProperty and Object.setProperty built-ins that call [[GetProperty]] and [[SetProperty]]
Also, to fully reify property manipulation I think we also need to factor [[Delete]] in a similar manner and provide an Object.deleteProperty built-in that calls the DeleteProperty abstraction operations.
I touch upon this in strawman:object_model_reformation#a_side_note_on_reflective_property_access (note that this stawman is still under construction)
the basic reason is that just like with Get/Put a handler may want to invoke the primitive behavior of Delete upon the target object without going through the delete operator (which could retrigger a trap).
- [[Get]] and [[Set]] methods on Objects that call [[GetProperty]] and [[SetProperty]]
- [[Get]] and [[Set]] methods on Proxies that trigger "get" and "set" traps
Yes, although I prefer to think of [[Get]] and [[Set]] consistently for both Proxy and "normal" objects. They are polymorphic "traps" that dispatch to a handler that is determined by the nature of the object. For native objects [[Get]] and [[Set]] dispatch to handlers that just call GetProperty or SetProperty
- An abstract operation [[Put]](O, P, V, Throw) that calls O.[Set] and performs the "strict mode" reject behavior.
Yes, call it Put.
On Nov 10, 2011, at 1:26 AM, Tom Van Cutsem wrote:
Are you sure you want to kill [[HasProperty]] entirely? If we replace it by a conditional [[Get]], wouldn't that mean that the expression |name in obj| might unnecessarily trigger an accessor in obj?
Good point, I didn't think about the fact that [[Get]] always triggers accessor calls.
We probably do need to continue to have a side-effect free [[HasProperty]]. I still think it is desirable to have a conditional [[Get]] available for use in places like the array algorithms. Whether it is an over-roading of the [[Get]] api or an additional trap is debatable.
2011/11/10 Allen Wirfs-Brock <allen at wirfs-brock.com>
On Nov 10, 2011, at 1:36 AM, Tom Van Cutsem wrote:
- Object.getProperty and Object.setProperty built-ins that call [[GetProperty]] and [[SetProperty]]
Also, to fully reify property manipulation I think we also need to factor [[Delete]] in a similar manner and provide an Object.deleteProperty built-in that calls the DeleteProperty abstraction operations.
I touch upon this in strawman:object_model_reformation#a_side_note_on_reflective_property_access (note that this stawman is still under construction)
the basic reason is that just like with Get/Put a handler may want to invoke the primitive behavior of Delete upon the target object without going through the delete operator (which could retrigger a trap).
Actually I was mistaken: the above quoted line should read:
- Object.getProperty and Object.setProperty built-ins that call the [[Get]] and [[Set]] methods
In other words: it's important that Object.getProperty(aProxy, name) triggers that proxy's "get" trap rather than performing the Object algorithm and calling the getOwnPropertyDescriptor trap. Otherwise, proxies would not compose well (think of a proxy p1 wrapping another proxy p2: if p1 forwards a property access to its target (p2), p2's "get" trap should be triggered). (+ we could end up with duplicated getOwnPropertyDescriptor calls, as pointed out by David earlier)
If the goal is to intentionally avoid proxy traps, one can write up the algorithm in Javascript itself (as I have done on the strawman page). But I think it's important that Object.getProperty does trigger proxy traps (if only for consistency: Object.getOwnPropertyDescriptor et al. also trigger proxy traps).
Regarding property deletion: if an object is implemented as a proxy, and you would want to delete a property from that object, I'm not sure why you would want to circumvent triggering the delete trap?
- [[Get]] and [[Set]] methods on Objects that call [[GetProperty]] and [[SetProperty]]
- [[Get]] and [[Set]] methods on Proxies that trigger "get" and "set" traps
Yes, although I prefer to think of [[Get]] and [[Set]] consistently for both Proxy and "normal" objects. They are polymorphic "traps" that dispatch to a handler that is determined by the nature of the object. For native objects [[Get]] and [[Set]] dispatch to handlers that just call GetProperty or SetProperty
Do you mean that the direction you would like to go is to look at normal ES5 objects as proxies with a special, built-in handler?
On Nov 10, 2011, at 3:28 AM, David Bruant wrote:
Le 09/11/2011 12:17, Tom Van Cutsem a écrit :
2011/11/8 Allen Wirfs-Brock <allen at wirfs-brock.com>
More generally, I think there should be a 1::1 correspondence between the internal methods in listed in ES5 Table 8 and fundamental proxy traps.
I absolutely agree we should strive to achieve this. I disagree.
First of all, Table 8 is incomplete:
- It lacks a function for property enumeration and proxies need a trap for that (for-in loops, Object.getOwnPropertyNames, Object.keys).
- There is no method for passing [[Extensible]] from true to false, though this behavior is used and needs a ("fundamental") trap as well. The reason for this is certainly that this action can be described in one spec algorithm step, so there was no need from a specification perspective to consider this as an internal method, but it's still a fundamental operation you can perform on an object. (I may be missing others)
I agree that Table 8 is incomplete (and has unnecessary items in it). The point is that Table 8 should define the fundamental Object semantic API that is used by all clients, including the specifications of language feature semantics, the specification of built-in functions, and the semantics that are reified via the Proxy and Object reflection API. One consistent set of object semantics operations, used by everybody.
A second point I disagree on is the idea of "fundamental trap". This notion existed on the original proxy proposal. I think it was more for the purpose of writing traps more easily. With the direct proxies proposal, the notion of fundamental and derived traps disappears (and hopefully, direct proxies will be promoted during the next TC-39 meeting).
I was using "fundamental" to mean what i just stated above. I agree that only these fundamental object operations need to be reflected in the Proxy API.
Regardless of Table 8 being incomplete, I think that proxy traps have another factor to take into account which is the relationship with surface syntax. When someone writes "let a = o.a;", the intention is to call the "get" operation regardless of whether the "a" property is on o or somewhere on its prototype chain. This is another thing that I like in the [[Get]] refactoring: when the prototype is the proxy, its get trap gets called and not its getOwnPropertyDescriptor trap. It makes proxy traps more in line with the object clients requests.
The surface syntax perspective is exactly how I conceptualize the "Table 8" level of the specification. It is the interface between the runtime object model and the surface features of the language.
However, there were some practical engineering considerations that went into the ES5 factoring of the internal methods. For example, [[Put]] calls [[DefineOwnProperty]] to set a property's value because it made it easier to specify Array behavior. Creating an array indexed property (possibly via [[Put]]) and letting the value of the "length" property both have side-effects. There are multiple ways that such values can be set, including using Object.defineProperty. By making the default implementation of [[Put]] delegate to [[DefineOwnProperty]] I only needed to over-ride [[DefineOwnProperty]] for array instances to specify their semantics. If I hand't done this, I would have had to consistently over-ridden both [[DefineOwnProperty]] and [[Put]].
As long, as the "internal methods" were just specification devices, this sort of refactoring could be done as a convenience. But as soon as the reify them via traps we have to thing about the broader and permanent impacts of such refactoring.
On Nov 10, 2011, at 9:03 AM, Tom Van Cutsem wrote:
2011/11/10 Allen Wirfs-Brock <allen at wirfs-brock.com>
On Nov 10, 2011, at 1:36 AM, Tom Van Cutsem wrote:
- Object.getProperty and Object.setProperty built-ins that call [[GetProperty]] and [[SetProperty]]
Also, to fully reify property manipulation I think we also need to factor [[Delete]] in a similar manner and provide an Object.deleteProperty built-in that calls the DeleteProperty abstraction operations.
I touch upon this in strawman:object_model_reformation#a_side_note_on_reflective_property_access (note that this stawman is still under construction)
the basic reason is that just like with Get/Put a handler may want to invoke the primitive behavior of Delete upon the target object without going through the delete operator (which could retrigger a trap).
Actually I was mistaken: the above quoted line should read:
- Object.getProperty and Object.setProperty built-ins that call the [[Get]] and [[Set]] methods
In other words: it's important that Object.getProperty(aProxy, name) triggers that proxy's "get" trap rather than performing the Object algorithm and calling the getOwnPropertyDescriptor trap. Otherwise, proxies would not compose well (think of a proxy p1 wrapping another proxy p2: if p1 forwards a property access to its target (p2), p2's "get" trap should be triggered). (+ we could end up with duplicated getOwnPropertyDescriptor calls, as pointed out by David earlier)
If the goal is to intentionally avoid proxy traps, one can write up the algorithm in Javascript itself (as I have done on the strawman page). But I think it's important that Object.getProperty does trigger proxy traps (if only for consistency: Object.getOwnPropertyDescriptor et al. also trigger proxy traps).
Yes, on further thought, I agree that Object getProperty should trigger the trap.
Regarding property deletion: if an object is implemented as a proxy, and you would want to delete a property from that object, I'm not sure why you would want to circumvent triggering the delete trap?
In strawman:object_model_reformation I'm exploring explicitly distinguishing the semants of obj.propName and obj[expression] such that an object can explicitly define its obj[expression] behaviors without using a Proxy (note that currently by the time a Proxy is called, the distinction is already lost so even if you wanted to use a Proxy for this purpose they may not be adequate). In many cases (including the default), the object-specific "handler" for obj[expression] will delegate back to Object.getProperty/Object.setProperty (either directly or via a super call). But note that it can do so via direct [ ] syntax, as that would loop. This can actually all be modeled with a new Reference variant within the spec. that distinguishes . references from [ ] references.
However, I also need to account for the delete operator and distinguish delete obj.propName from delete obj[expression] . This can be taken care of by the same reference extension (essentially References needs a delete "method"). A handler for deleting obj[expression]] may also want to delegate back to normal property deletion and for the same reason as for obj[exper] get/set handlers it can't directly use the syntactic form. Instead it needs to call an Object.deleteProperty function.
BTW, rather than adding these methods to Object, I'm beginning to think it might be better to import them from a built-in reflection module.
- [[Get]] and [[Set]] methods on Objects that call [[GetProperty]] and [[SetProperty]]
- [[Get]] and [[Set]] methods on Proxies that trigger "get" and "set" traps
Yes, although I prefer to think of [[Get]] and [[Set]] consistently for both Proxy and "normal" objects. They are polymorphic "traps" that dispatch to a handler that is determined by the nature of the object. For native objects [[Get]] and [[Set]] dispatch to handlers that just call GetProperty or SetProperty
Do you mean that the direction you would like to go is to look at normal ES5 objects as proxies with a special, built-in handler?
Yes, exactly. BTW, I think this was probably the intent of the originally internal methods and native+host objects design.
2011/11/10 Allen Wirfs-Brock <allen at wirfs-brock.com>
On Nov 10, 2011, at 9:03 AM, Tom Van Cutsem wrote:
Regarding property deletion: if an object is implemented as a proxy, and you would want to delete a property from that object, I'm not sure why you would want to circumvent triggering the delete trap?
In strawman:object_model_reformation I'm exploring explicitly distinguishing the semants of obj.propName and obj[expression] such that an object can explicitly define its obj[expression] behaviors without using a Proxy (note that currently by the time a Proxy is called, the distinction is already lost so even if you wanted to use a Proxy for this purpose they may not be adequate). In many cases (including the default), the object-specific "handler" for obj[expression] will delegate back to Object.getProperty/Object.setProperty (either directly or via a super call). But note that it can do so via direct [ ] syntax, as that would loop. This can actually all be modeled with a new Reference variant within the spec. that distinguishes . references from [ ] references.
However, I also need to account for the delete operator and distinguish delete obj.propName from delete obj[expression] . This can be taken care of by the same reference extension (essentially References needs a delete "method"). A handler for deleting obj[expression]] may also want to delegate back to normal property deletion and for the same reason as for obj[exper] get/set handlers it can't directly use the syntactic form. Instead it needs to call an Object.deleteProperty function.
Understood.
BTW, rather than adding these methods to Object, I'm beginning to think it might be better to import them from a built-in reflection module.
Indeed. Perhaps that built-in reflection module could be the same module that defines the default forwarding handler. Your note that we need an Object.deleteProperty method apart from the built-in syntax reminded me that the default forwarding handler for proxies is really all about providing method-based alternatives to things that can currently only be achieved through built-in syntax.
In the direct proxies proposal, I proposed defining a Proxy.forward object that enables forwarding all operations interceptable by proxies to another object. A prototype implementation of it exists here: < code.google.com/p/es-lab/source/browse/trunk/src/proxies/DirectProxies.js#1250
If you observe closely, you'll note that the default forwarding API is really the "dual" of the Proxy API. Proxies uniformly turn all sorts of operations on objects (whether triggered through syntax or built-in methods) into trap invocations, for instance: Object.getOwnPropertyDescriptor(proxy, name) =>
handler.getOwnPropertyDescriptor(name, target) name in proxy => handler.has(name, target)
etc.
The default forwarding API turns these trap invocations back into the original operations: Proxy.forward.getOwnPropertyDescriptor(name, target) =>
Object.getOwnPropertyDescriptor(target, name); Proxy.forward.has(name, target) => name in target
etc.
That's why the forwarding API is crucial for double lifting to work: if a handler is itself a proxy, then to intercept handler[trapName], we need to be able to generically forward the operation, which is made possible by writing Proxy.forward[trapName].
I proposed the name "Proxy.forward" since that looks very natural when writing handler code that simply wants to "augment" the default behavior:
funtion makeChangeLogger(target) { return Proxy.for(target, { set: function(receiver, name, value, target) { log('property '+name+' on '+target+' set to '+value); return Proxy.forward.set(receiver, name, value, target); } });}
However, it now seems to me that the methods encapsulated by Proxy.forward can be more generally useful for arbitrary meta-programming, not just for forwarding in the case of proxies.
I see an opportunity here to kill two birds with one stone: we can introduce a reflection module (assume it's called "Reflect" for now) that contains the Proxy.forward methods. That would: a) obviate the need for Object.deleteProperty (now named Reflect.delete), Object.getProperty (Reflect.get) and Object.setProperty (Reflect.set). b) obviate the need for Proxy.forward (the above code snippet would call Reflect.set instead, which is even shorter)
The design guideline for this Reflect module would be that the methods it exposes have exactly the same name + parameter list as the Proxy API trap names. Not only does this retain consistency, it also enables easy double lifting (or perhaps other design patterns that want to treat operations on objects / Proxy traps generically).
I wrote up an initial (but fairly complete) draft of a proposed refactoring of the ES5 [[Get]], [[Put]] and [[HasProperty]] algorithms to change the way in which these algorithms climb the prototype chain: < strawman:refactoring_put>
This is mainly beneficial for proxies, as the prototype walking strategy becomes observable to proxies-used-as-prototypes.
IMHO, the refactored algorithms interoperate with proxies in a much saner way, finally restoring the intuitive semantics of the "get" and "set" traps that MarkM and I had in mind from the beginning, but which Sean Eagan pointed out were flawed given the ES5 spec algorithms.
The biggest change is in the [[Put]] algorithm. For those not into ES spec language, I wrote up the behavior for ES5 [[Put]] and my proposed ES.next [[Put]] in JS itself: ES5 [[Put]]: < code.google.com/p/es-lab/source/browse/trunk/src/es5adapt/setProperty.js#115
Proposed ES.next [[Put]]: < code.google.com/p/es-lab/source/browse/trunk/src/es5adapt/setProperty.js#68
The refactored version also fixes the anomalies resulting from the ES5 [[CanPut]] vs. [[Put]] split that Andreas Rossberg pointed out earlier on this list.
When I say "refactoring" here, I really do intend for these new algorithms to be equivalent to the ES5 algorithms for non-proxy objects. To test whether these algorithms are indeed equivalent, I wrote up a little test-suite that runs in the browser: < es-lab.googlecode.com/svn/trunk/src/es5adapt/testSetProperty.html>
The results look promising (success on Firefox7, 1 failure on Chrome/Safari because these allow overriding of non-writable inherited data props, haven't tested other browsers yet). Still, the more es-discuss eye-balls that can scrutinize these algorithms, the better.