Property Descriptors as script compatible representation (was: Property descriptors as ES6 Maps)
On Nov 1, 2012, at 6:03 AM, David Bruant wrote:
Le 31/10/2012 19:03, Allen Wirfs-Brock a écrit :
Let me summarize what I think is your concern.
In ES5, property descriptor records are a specification device that is used to transport information about object properties between factored components of the ES specification. The same information can be expressed as an ES object that is produced/consumed by Object.getOwnPropertyDescriptor and Object.defineProperty. These are raising/lowering operations that move information from the specification/implication level to the reflective ES language level. One advantage of this layering is that the conversion to/from an actual object takes place once, at a well defined point in the execution sequence and any side-effects of object access occur only at that point. Once the information is represented as an internal "record" we know that all internal consistency preconditions are satisfied and that no side-effects can be associated with access to such records. The part about precondition is particularly interesting and I agree it should be kept. However, it's possible to define ECMAScript contructs that enforce such pre-conditions. My point is that even for the spec, it's possible to define PropertyDescriptors by using ECMAScript constructs, without necessarily creating a new spec-only type.
It's possible, but not necessarily wise. We current have a fairly complete stratification of the implementation/core semantics layer of ES from the reflection layer. This is a good thing, and something that we should be trying to preserve as we introduce proxies. Layer crossings should be minimized. Using higher level level entities to represent lower layer abstractions would be going in the opposite direction.
You concern seems to be that a proxy traps that deal with descriptor objects have no such guarantees and in particular strange things might happen if a descriptor object is itself a proxy. My original concern was that the defineProperty and getOwnPropertyDescriptor traps I/O were interacting directly with PropertyDescriptor algorithms, but after re-reading the proxy spec page more carefully, I realized it is not the case. However, not being the case has a cost:
The proxies_spec page [1] redefines some ES5 15.2.3.* built-ins. Core of the current Object.getOwnPropertyDescriptor is : 3) Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name. 4) Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
Core of the proposed Object.getOwnPropertyDescriptor is : 3) If O is a proxy Return the result of calling TrapGetOwnProperty(O, name) 4) Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name. 5) Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
The above isn't how this will be expressed in the final spec. Instead we will have
- Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name.
- Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
And all Proxy objects will have a [[GetOwnProperty]] internal methods that looks something like:
- Let O be the object upon which this internal method was invoked.
- Let desc be the result of calling TrapGetOwnProperty(O,name)
- Return ToPropertyDescriptor(desc)
(I left out details of exception handling)
If [[GetOwnProperty]] returned the same type of thing than TrapGetOwnProperty (which returns an object), the spec 15.2.3.3 Object.getOwnPropertyDescriptor could remain the same.
But introduces a mutable object (regular a regular object or map doesn't matter) into the lower layer.
Also, I have the feeling that there is duplicated logic between TrapGetOwnProperty and FromPropertyDescriptor or unnecessary back and forth conversions between internal PropertyDescriptor and objects.
Unnecessary duplication will be minimized in the final spec. As will unnecessary conversions. However some of these conversions are necessary to stratify the reflection layer. The are also necessary to prevent property descriptor objects from being unintentionally shared or used as a transport mechanism through the implementation layer.
Currently, TrapGetOwnProperty returns an object, but when there is an actual trap, it goes through:
- NormalizeAndCompletePropertyDescriptor(Object) -> Object (step 6)
- ToCompletePropertyDescriptor(Object) -> PropertyDescriptor (step 3 of NormalizeAndCompletePropertyDescriptor)
- FromPropertyDescriptor(PropertyDescriptor) -> Object (step 4 of NormalizeAndCompletePropertyDescriptor)
Possibly details to cleanup, I need to look deeper. The bottom line, all specification algorithms such as Object.getOwnPropertyDescriptor must be expressed in terms of essential, trapable internal methods and should not be directly aware of Proxies. All such internal methods are polymorphic across ordinary objects, proxy objects, and possibly other exotic objects.
We might as well get rid of the spec-only PropertyDescriptor, define an equivalently pre-condition/invariant enforcing ES5 construct and manipulate that both internally and in trap boundaries.
No we want stratification and and uniform polymorphic interface to the essential internal methods.
The getOwnPropertyDescriptor trap is a lowering operation. (...)
The defineProperty trap is a raising operation. (...)
The other possible concern would be that a trap might directly use Object.getOwnPropertyDescriptor on a proxy object and that this might provide a bogus descriptor. But it can't. (...) I agree with what you wrote here. Basically, the thing that prevent potentially harmful descriptors-as-proxies to being harmful is that from getOwnPropertyDescriptor and to defineProperty traps, the "essence" of the descriptor is copied to a fresh object created internally.
so, I just don't see any basis for your concern. Perhaps, you could elaborate on the nature of the problem as you perceive it. Let's try to ASCII-art it. Here are the paths that property descriptors take. Arrows represent a internal type conversion and/or descriptor completion/normalization
Object.defineProperty on regular object:
ES Object (in user script) --> PropertyDescriptor (and stored as such)
Object.defineProperty on proxies (as I understand its current
specification): ES Object --> PropertyDescriptor --> ES Object (for trap argument) --> PropertyDescriptor The last arrow is not compulsory, but we can decently assume that for most cases, the defineProperty trap will forward the operation to the target
What I'm suggesting is to do the following in both cases:
ES Object --> ESUsablePropDesc (used for proxy trap arguments)
If the above conversions is actually creating a new ES object (and it must to prevent the target from modifying the original descriptor object), it amounts to the same thing.
Note that an implementation of Object.defineProperty may recognize proxies and take such short cuts as long as it preserves the observable semantics. But, at the specification level we are trying to be explicit about those required semantics. Optimization is the job for implementors.
2012/11/1 David Bruant <bruant.d at gmail.com>
Currently, TrapGetOwnProperty returns an object, but when there is an actual trap, it goes through:
- NormalizeAndCompletePropertyDescriptor(Object) -> Object (step 6)
- ToCompletePropertyDescriptor(Object) -> PropertyDescriptor (step 3 of NormalizeAndCompletePropertyDescriptor)
- FromPropertyDescriptor(PropertyDescriptor) -> Object (step 4 of NormalizeAndCompletePropertyDescriptor)
We might as well get rid of the spec-only PropertyDescriptor, define an equivalently pre-condition/invariant enforcing ES5 construct and manipulate that both internally and in trap boundaries.
I think the deal-breaker, as Allen pointed out, is that what you call an ESUsablePropDesc would still need to be mutable (if it wants to mimic current property-descriptors-as-objects-with-some-invariants).
Plus, it doesn't get rid of the implicit conversions at proxy boundaries. In the case of defineProperty you'd still have to convert between a plain object passed in as 3rd arg, and an ESUsablePropDesc. In the case of getOwnPropertyDescriptor, to preserve current semantics, you must still copy the (mutable) ESUsablePropDesc to ensure that all calls to getOwnPropertyDescriptor return fresh, independent objects.
Granted, having an ESUsablePropDesc would avoid conversions in the case of proxies forwarding the operation to their target.
Thanks for drawing my attention to the auxiliary functions: it made me realize that Aux.3 NormalizePropertyDescriptor and Aux 4. NormalizeAndCompletePropertyDescriptor are only called in one place. I think that by in-lining their definitions we might get rid of some redundant conversions.
That said, the "redundancy" you point out, i.e. the conversion from Object -> PropDesc immediately followed by PropDesc -> Object is not really
redundant: it's needed to "normalize" the descriptor. Implementations can probably optimize so that they don't actually have to allocate an intermediate internal property descriptor, but immediately create an Object copy.
The way I see it now, ESUsablePropDesc would be a regular object with a bunch of getter/setters to enforce property descriptor invariants. Everything would remain compatible (unless people really cared that ES5 descriptors have data properties).
Unfortunately it's not a question of caring: currently, an ES5 meta-program can make the valid assumption that all "standard" attributes of a property descriptor object are data properties. Changing this assumption is not backwards-compatible.
2012/11/1 Allen Wirfs-Brock <allen at wirfs-brock.com>
The above isn't how this will be expressed in the final spec. Instead we will have
- Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name.
- Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
And all Proxy objects will have a [[GetOwnProperty]] internal methods that looks something like:
- Let O be the object upon which this internal method was invoked.
- Let desc be the result of calling TrapGetOwnProperty(O,name)
- Return ToPropertyDescriptor(desc)
(I left out details of exception handling)
Actually, while reviewing the spec, I realized that there is a good reason why the spec isn't currently specified this way (i.e. why the Proxy spec redefines Object.defineProperty and Object.getOwnPropertyDescriptor and does not just override [[DefineOwnProperty]] and [[GetOwnProperty]])
It is to avoid the lossy conversion that otherwise occurs between the return value of [[GetOwnProperty]] and Object.getOwnPropertyDescriptor.
With your alternative spec above, consider a call to |Object.getOwnPropertyDescriptor(proxy, name)|
- The Object.gOPD built-in calls proxy.[[GetOwnProperty]]
- [[GetOwnProperty]] invokes the trap, gets back an object
- [[GetOwnProperty]] converts the object into an internal descriptor and returns
- Object.gOPD takes this internal descriptor and turns it into an object again
Not only are steps 3. and 4. logical inverses, they lose information: recall that we had previously decided that custom attributes on property descriptors would be passed through. So if a proxy trap returns the descriptor {value:42, custom: true}, we want clients to still see the "custom" attribute. That's why we need to avoid coercing the trap result to an internal descriptor.
The case for Object.defineProperty is entirely analogous.
That's why there exist TrapDefineOwnProperty and TrapGetOwnProperty auxiliary functions: they abstract the common parts between the Object.* built-ins and the internal methods without doing conversions. This is so that the Object.* built-ins can call these auxiliaries directly, and avoid the conversions.
I realize you want to avoid explicit tests for proxies as much as possible. One way to do that would be to introduce two new built-ins, so that we end up with: [GetOwnProperty] -> PropDesc (same as ES5) [[GetOwnPropertyObject]] ( P) -> Object (called by Object.gOPD) [[DefineOwnProperty]] (P, PropDesc) -> Boolean (same as ES5) [[DefineOwnPropertyObject]] (P, Object) -> Boolean (called by
Object.defineProperty)
On Nov 2, 2012, at 4:20 AM, Tom Van Cutsem wrote:
Hi Allen,
2012/11/1 Allen Wirfs-Brock <allen at wirfs-brock.com> The above isn't how this will be expressed in the final spec. Instead we will have
- Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name.
- Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
And all Proxy objects will have a [[GetOwnProperty]] internal methods that looks something like:
- Let O be the object upon which this internal method was invoked.
- Let desc be the result of calling TrapGetOwnProperty(O,name)
- Return ToPropertyDescriptor(desc)
(I left out details of exception handling)
Actually, while reviewing the spec, I realized that there is a good reason why the spec isn't currently specified this way (i.e. why the Proxy spec redefines Object.defineProperty and Object.getOwnPropertyDescriptor and does not just override [[DefineOwnProperty]] and [[GetOwnProperty]])
It is to avoid the lossy conversion that otherwise occurs between the return value of [[GetOwnProperty]] and Object.getOwnPropertyDescriptor.
With your alternative spec above, consider a call to |Object.getOwnPropertyDescriptor(proxy, name)|
- The Object.gOPD built-in calls proxy.[[GetOwnProperty]]
- [[GetOwnProperty]] invokes the trap, gets back an object
- [[GetOwnProperty]] converts the object into an internal descriptor and returns
- Object.gOPD takes this internal descriptor and turns it into an object again
Not only are steps 3. and 4. logical inverses, they lose information: recall that we had previously decided that custom attributes on property descriptors would be passed through. So if a proxy trap returns the descriptor {value:42, custom: true}, we want clients to still see the "custom" attribute. That's why we need to avoid coercing the trap result to an internal descriptor.
Ah, yes. I wondered about that and wasn't sure when I wrote the above whether we were allowing custom attributes. I figured you would speak up if that was an issue...
So, yes, in that case Object.getOwnPropertyDescriptor and Object.defineProperty need to pass through object level descriptors from/to the corresponding proxy traps which means they can't normalize via a property descriptor record. So, it is perfectly appropriate for them to explicitly test if O is a proxy and act accordingly. I think, this should be the only place where an explicit Proxy test is required outside of the actual proxy object specification algorithms.
The case for Object.defineProperty is entirely analogous.
That's why there exist TrapDefineOwnProperty and TrapGetOwnProperty auxiliary functions: they abstract the common parts between the Object.* built-ins and the internal methods without doing conversions. This is so that the Object.* built-ins can call these auxiliaries directly, and avoid the conversions.
I realize you want to avoid explicit tests for proxies as much as possible. One way to do that would be to introduce two new built-ins, so that we end up with: [GetOwnProperty] -> PropDesc (same as ES5) [[GetOwnPropertyObject]] ( P) -> Object (called by Object.gOPD) [[DefineOwnProperty]] (P, PropDesc) -> Boolean (same as ES5) [[DefineOwnPropertyObject]] (P, Object) -> Boolean (called by Object.defineProperty)
No, we don't need the object variants. We should only use "internal methods" were we need an algorithm to be polymorphically overloaded across multiple implementation level object representations. In this case, it is only the proxy representation that needs the object variation so no polymorphic dispatch is required. Direct calls to your TrapGetOwn.../TrapDefine... routines is fine for the two places this actually occurs.
Le 31/10/2012 19:03, Allen Wirfs-Brock a écrit :
The part about precondition is particularly interesting and I agree it should be kept. However, it's possible to define ECMAScript contructs that enforce such pre-conditions. My point is that even for the spec, it's possible to define PropertyDescriptors by using ECMAScript constructs, without necessarily creating a new spec-only type.
My original concern was that the defineProperty and getOwnPropertyDescriptor traps I/O were interacting directly with PropertyDescriptor algorithms, but after re-reading the proxy spec page more carefully, I realized it is not the case. However, not being the case has a cost:
The proxies_spec page [1] redefines some ES5 15.2.3.* built-ins. Core of the current Object.getOwnPropertyDescriptor is : 3) Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name. 4) Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
Core of the proposed Object.getOwnPropertyDescriptor is : 3) If O is a proxy Return the result of calling TrapGetOwnProperty(O, name) 4) Let desc be the result of calling the [[GetOwnProperty]] internal method of O with argument name. 5) Return the result of calling FromPropertyDescriptor(desc) (8.10.4).
If [[GetOwnProperty]] returned the same type of thing than TrapGetOwnProperty (which returns an object), the spec 15.2.3.3 Object.getOwnPropertyDescriptor could remain the same. Also, I have the feeling that there is duplicated logic between TrapGetOwnProperty and FromPropertyDescriptor or unnecessary back and forth conversions between internal PropertyDescriptor and objects.
Currently, TrapGetOwnProperty returns an object, but when there is an actual trap, it goes through:
NormalizeAndCompletePropertyDescriptor)
NormalizeAndCompletePropertyDescriptor)
We might as well get rid of the spec-only PropertyDescriptor, define an equivalently pre-condition/invariant enforcing ES5 construct and manipulate that both internally and in trap boundaries.
I agree with what you wrote here. Basically, the thing that prevent potentially harmful descriptors-as-proxies to being harmful is that from getOwnPropertyDescriptor and to defineProperty traps, the "essence" of the descriptor is copied to a fresh object created internally.
Let's try to ASCII-art it. Here are the paths that property descriptors take. Arrows represent a internal type conversion and/or descriptor completion/normalization
Object.defineProperty on regular object:
ES Object (in user script) --> PropertyDescriptor (and stored as such)
Object.defineProperty on proxies (as I understand its current
specification): ES Object --> PropertyDescriptor --> ES Object (for trap argument) -->
PropertyDescriptor The last arrow is not compulsory, but we can decently assume that for most cases, the defineProperty trap will forward the operation to the target
What I'm suggesting is to do the following in both cases:
ES Object --> ESUsablePropDesc (used for proxy trap arguments)
I think I was wrong in suggesting using raw ES6 Maps (and I think I understand that's what Andreas meant regarding heterogeneous types). The idea of ESUsablePropDesc is that it preserves pre-conditions and invariants of property descriptors very much like the internal type currently does. It can be used both internally in the spec as well as in proxy traps. When forwarding the ESUsablePropDesc object to the target, there is no need for conversion once again. The engine generated it and enforced/normalized anything it needed already. Potentially, when using ESUsablePropDesc descriptors directly (which could become an option), the engine may not need to convert them, because it already has enforced invariants/normalization, prevented inconsistent states, etc.
The way I see it now, ESUsablePropDesc would be a regular object with a bunch of getter/setters to enforce property descriptor invariants. Everything would remain compatible (unless people really cared that ES5 descriptors have data properties). The proxy side of things would be more efficient, the door would be open to make non-proxy use of Object.defineProperty/getOwnPropertyDescriptor more efficient.
David
[1] harmony:proxies_spec