[Harmony Proxies] Non-extensible, sealed and frozen Proxies

# Tom Van Cutsem (14 years ago)

I promised to give an update on the work I've been doing to investigate to what extent the Proxy API may support non-extensible objects. In a nutshell: I now think it is possible for Proxies to emulate not just non-extensible objects, but also sealed and frozen objects, given appropriate invariant checks.

Before I continue, a brief history of how we got here: <history>

When Mark and I originally designed the Proxy API, we took care that proxies could not violate a number of invariants of ES5.1 objects, related to non-configurable properties and object extensibility (cf. ES5.1 section 8.6.2). The net result was that: a) proxies could not emulate non-configurable properties (i.e. all property descriptors returned by the get{Own}PropertyDescriptor trap must be configurable:true) b) Object.{preventExtensions,seal,freeze} would "fix" a proxy, after which it could no longer trap.

While b) seemed a reasonable restriction, a) was controversial and discussed both during earlier TC39 meetings and here on es-discuss (see a.o. < esdiscuss/2011-June/015114>). That

discussion revealed that proxies really need to be able to emulate non-configurable properties. One motivating use case was emulating Array's non-configurable |length| property.

To alleviate restriction a), Mark and I looked for alternatives, recorded as successive strawmen at < strawman:fixed_properties>.

A first strawman (the "short-circuiting approach" on the wiki page) was based on having a proxy "cache" non-configurable properties, such that if the proxy hit the cache, it would no longer trap the handler. That design proved to be flawed, for several reasons, as pointed out by David Bruant, IIRC.

Out of that discussion grew a second approach (the "trap-and-enforce approach" on the wiki page). In that approach, the handler can fully emulate non-configurable properties, but the handler is "monitored" for invariant violations. Because FF4+ proxies support emulation of non-configurable properties (without invariant checking), I was able to implement such a "monitoring" proxy handler in Javascript itself. That was the FixedHandler I proposed earlier: < esdiscuss/2011-July/015686>.

That approach was well received, but it did not alleviate restriction b) (see esdiscuss/2011-July/015944).

IIRC, the motivation here was again Array: emulating the magical |length| property even if the Array is made non-extensible. </history>

Over the past few weeks I have been working on an extension of the FixedHandler that allows proxies to additionally emulate non-extensible objects. These proxies keep track of previously exposed non-configurable properties (as in the earlier FixedHandler design), but additionally have an "isExtensible" flag. Once a proxy is fixed, the flag is set to false. Via the return value of the fix() trap, the proxy knows the set of own property names of the non-extensible object it needs to emulate. It then becomes easy for e.g. the getOwnPropertyDescriptor trap to check that |undefined| is returned for any |name| that is not in the set of own properties. Combined with a number of other checks in a handful of other traps, that is how the ES5.1 invariants w.r.t. [[Extensible]] are enforced.

As it turns out, once a proxy can emulate both non-configurable properties and non-extensible objects, the ability to emulate sealed and frozen objects falls out naturally.

The full implementation of such "non-extensible proxies" is here: < code.google.com/p/es-lab/source/browse/trunk/src/proxies/FixedTrappingProxy.js

The file's comments near the top contain a detailed description of the invariants enforced by this type of proxy.

I (lightly) tested this implementation on FF6 (the implementation depends on both proxies and WeakMaps). For those interested: console-based test: < code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.js

browser-based test: < code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.html

Given the intricacy of the invariant checks, this code can use more eyeballs. There's also a number of unresolved TODO's that merit discussion, and some hints on a redesign of the fix() protocol. In particular, Mark and I have been discussing a protocol where a handler can either tell the proxy that it wants to continue trapping, even after being fixed, or that it wants to "become" a regular object as before, with no more overhead for invariant checks. I think this gives handler writers the "best of both worlds" in terms of flexibility and performance.

On a final note: while implementing this I realized that the FixedHandler is a partial self-hosted implementation of the Proxy API in Javascript. That prompted me to go "all the way" and additionally make the normalization on the input arguments and return value of traps explicit, even though that would not have been strictly necessary. My hope is that people may get a better understanding of the interaction between proxies and handlers if they can study a self-hosted implementation of the Proxy API.

# David Bruant (14 years ago)

Le 01/09/2011 17:40, Tom Van Cutsem a écrit :

(...)

A first strawman (the "short-circuiting approach" on the wiki page) was based on having a proxy "cache" non-configurable properties, such that if the proxy hit the cache, it would no longer trap the handler. That design proved to be flawed, for several reasons, as pointed out by David Bruant, IIRC.

If i remember well, the main argument was that in the case of a forwarding proxy, if non-configurable properties related trap call go in the cache without actually calling the trap, then the forwarding proxy cannot forward the operation to the target (and then fails at its "forwarding" mission).

(...)

The full implementation of such "non-extensible proxies" is here: code.google.com/p/es-lab/source/browse/trunk/src/proxies/FixedTrappingProxy.js

Thanks :-)

The file's comments near the top contain a detailed description of the invariants enforced by this type of proxy.

Invariants on own properties trap (getOwnPropertyDescriptor, defineProperty, delete, hasOwn) are a somewhat direct interpretation of the invariants on the ES5.1 spec. However, invariants imposed on potentially inherited properties may be a bit more controversial. For instance, for the get trap, the fact that the value is unchanged is enforced (L.641, !Object.is). This is a "natural" extension of the ES5.1 invariant "If a property is described as a data property and its [[Writable]] and [[Configurable]] are both false, then the SameValue (according to 9.12) must be returned for the [[Value]] attribute of the property on all calls to [[GetOwnProperty]].", however, nothing in the spec enforces that the [[Get]] (get trap) should use [[GetOwnProperty]] and then be affected by the invariant. It is just how it happened to be spec'ed in 8.12.3 and host objects could decide to do otherwise.

I'm not really sure what is the best to do. Either specify these invariants in the spec or remove the checks from the implementation.

// - properties returned by the fix() trap are merged with fixed properties, // ensuring no incompatible changes can be made when calling freeze, seal, // preventExtensions // will throw if any of the props returned already exist in // fixedProps and are incompatible with existing attributes Object.defineProperties(this.fixedProps, props); I agree with this behavior. However it has some consequences. In order to avoid throws, the proxy author has to remember which properties are non-configurable (to avoid collisions when returning props) and the values of these (to avoid throwing) which is a duplication of what the engine has to remember to enforce the invariant. In order to avoid the duplication, the engine could provide this information in some way (additional trap argument? :-s).

Also, for traps to know whether a proxy has been fixed, the additional proxy argument will be a good news (Object.isExtensible(proxy) from within a trap).

I also like the idea of passing the type of operation (freeze, preventExtensions, seal) as a string which can allow, for instance from the fix trap of a forwarding proxy to do Objectoperation.

I (lightly) tested this implementation on FF6 (the implementation depends on both proxies and WeakMaps). For those interested: console-based test: code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.js browser-based test: code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.html

Given the intricacy of the invariant checks, this code can use more eyeballs. There's also a number of unresolved TODO's that merit discussion, and some hints on a redesign of the fix() protocol. In particular, Mark and I have been discussing a protocol where a handler can either tell the proxy that it wants to continue trapping, even after being fixed, or that it wants to "become" a regular object as before, with no more overhead for invariant checks. I think this gives handler writers the "best of both worlds" in terms of flexibility and performance.

I really would like to see a native implementation of non-extensible with non-configurable properties proxies, because i am still not sure that there is an overhead in some cases. For instance, ES5.1 8.12.9 [[DefineOwnProperty]] for regular objects (es5.github.com/#x8.12.1 ) has very similar checks than in your code.

Regardless, it's a good thing to provide the choice. Regarding the open issue ("do we default to Object.create or do we want to allow for the possibility of Array.create etc.?"), I think that the choice should be given to create any sort of object (why not host objects such as NodeList?). One question would be: how could this (easily) be achieved?

# Tom Van Cutsem (14 years ago)

2011/9/4 David Bruant <david.bruant at labri.fr>

Le 01/09/2011 17:40, Tom Van Cutsem a écrit :

(...)

A first strawman (the "short-circuiting approach" on the wiki page) was based on having a proxy "cache" non-configurable properties, such that if the proxy hit the cache, it would no longer trap the handler. That design proved to be flawed, for several reasons, as pointed out by David Bruant, IIRC. If i remember well, the main argument was that in the case of a forwarding proxy, if non-configurable properties related trap call go in the cache without actually calling the trap, then the forwarding proxy cannot forward the operation to the target (and then fails at its "forwarding" mission).

Indeed. Also, for membranes, the "forwarding" mission included shutting off access to revoked objects, including property attributes. The short-circuiting proxies still allowed access to the attributes of revoked objects, which was (too) surprising.

The file's comments near the top contain a detailed description of the invariants enforced by this type of proxy. Invariants on own properties trap (getOwnPropertyDescriptor, defineProperty, delete, hasOwn) are a somewhat direct interpretation of the invariants on the ES5.1 spec. However, invariants imposed on potentially inherited properties may be a bit more controversial.

True. See my reply in Dave's Object.{getPropertyDescriptor, getPropertyNames} thread for a justification of the treatment of inherited properties.

For instance, for the get trap, the fact that the value is unchanged is enforced (L.641, !Object.is). This is a "natural" extension of the ES5.1 invariant "If a property is described as a data property and its [[Writable]] and [[Configurable]] are both false, then the SameValue (according to 9.12) must be returned for the [[Value]] attribute of the property on all calls to [[GetOwnProperty]].", however, nothing in the spec enforces that the [[Get]] (get trap) should use [[GetOwnProperty]] and then be affected by the invariant. It is just how it happened to be spec'ed in 8.12.3 and host objects could decide to do otherwise.

It's a valid point, and one I had not considered. Before proxies, there was probably no question that [[Get]] was implicitly defined in terms of [[GetOwnProperty]], so the invariant stretched to cover [[Get]] as well.

Proxies decouple these internal methods and allow for arbitrary inconsistencies. The purpose of this FixedHandler exercise is to do away with these inconsistencies.

My interpretation is that the ES5.1 invariant does require [[Get]] and [[GetOwnProperty]] to be consistent. Otherwise there is little point in having the invariant in practice (since arguably the vast majority of property value accesses happen through [[Get]]).

Your remark made me realize that Object.defineProperty actually implicitly performs the SameValue check as well (ES5.1 section 8.12.9, step 10.a.ii.1). That means I can probably rephrase the check in the "get" trap as follows:

if (fixedDesc !== undefined &&         // getting an existing,
    !fixedDesc.configurable &&          // non-configurable,
    "value" in fixedDesc) {                 // own data property,
  // check to see whether the values match up
  Object.defineProperty(this.fixedProps, name, {value: res });
}

That makes this check more consistent with other checks (and probably faster, since this uses the built-in SameValue algorithm).

I'm not really sure what is the best to do. Either specify these

invariants in the spec or remove the checks from the implementation.

// - properties returned by the fix() trap are merged with fixed properties, // ensuring no incompatible changes can be made when calling freeze, seal, // preventExtensions // will throw if any of the props returned already exist in // fixedProps and are incompatible with existing attributes Object.defineProperties(this.fixedProps, props); I agree with this behavior. However it has some consequences. In order to avoid throws, the proxy author has to remember which properties are non-configurable (to avoid collisions when returning props) and the values of these (to avoid throwing) which is a duplication of what the engine has to remember to enforce the invariant. In order to avoid the duplication, the engine could provide this information in some way (additional trap argument? :-s).

For proxy handlers that are emulating "proper" ES5.1 objects, there is no need to remember which properties were previously exposed as non-configurable own properties: just return whatever properties are emulated, and as long as these properties were consistently emulated in the past, the FixedHandler won't throw. Note that Object.defineProperties tolerates redefining existing properties as long as all attributes are consistent. It's OK to redefine a non-configurable property as being non-configurable. No need to avoid collisions.

Also, for traps to know whether a proxy has been fixed, the additional proxy argument will be a good news (Object.isExtensible(proxy) from within a trap).

Good point.

I also like the idea of passing the type of operation (freeze,

preventExtensions, seal) as a string which can allow, for instance from the fix trap of a forwarding proxy to do Objectoperation.

Yes, that was exactly my reasoning as well.

I (lightly) tested this implementation on FF6 (the implementation depends on both proxies and WeakMaps). For those interested: console-based test: < code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.js

browser-based test: < code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.html

Given the intricacy of the invariant checks, this code can use more eyeballs. There's also a number of unresolved TODO's that merit discussion, and some hints on a redesign of the fix() protocol. In particular, Mark and I have been discussing a protocol where a handler can either tell the proxy that it wants to continue trapping, even after being fixed, or that it wants to "become" a regular object as before, with no more overhead for invariant checks. I think this gives handler writers the "best of both worlds" in terms of flexibility and performance. I really would like to see a native implementation of non-extensible with non-configurable properties proxies, because i am still not sure that there is an overhead in some cases. For instance, ES5.1 8.12.9 [[DefineOwnProperty]] for regular objects (es5.github.com/#x8.12.1 ) has very similar checks than in your code.

The good news of this experiment is that for "property-specific" traps, invariants can be checked in constant time. For the traps that return arrays of strings, the invariant checking is (asymptotically) no more expensive than the time required to normalize the result.

The bad news is that constant factors matter. How much I cannot say, that requires the experience of a VM implementor. My hunch is that most of these checks can be performed cheaply, but I'm not an authority here.

Regardless, it's a good thing to provide the choice. Regarding the open issue ("do we default to Object.create or do we want to allow for the possibility of Array.create etc.?"), I think that the choice should be given to create any sort of object (why not host objects such as NodeList?). One question would be: how could this (easily) be achieved?

I've been thinking about a couple of ways, but there remain open issues:

  1. Have fix() not just return a property descriptor map, but a tuple of [constructor, property descriptor map]. To create the instance it needs to become, the proxy invokes the returned constructor's "create" method with as first argument the proxy's prototype and as second argument the prop. desc. map.

For example:

fix: function(operation) { // I want to become an array return [Array, { ... } ]; }

  1. Add the constructor to use upon fixing as an additional argument to Proxy.create{Function}, e.g.:

var p = Proxy.create(handler, proto, Array); // fix() trap will use Array.create

The thing is: we can't let proxy authors just pass any arbitrary object as the "constructor", e.g.

var myconstructor = { create: function(proto, props) { ... } }; Proxy.create(handler, proto, myconstructor);

This is because myconstructor could return arbitrary live objects from its call to "create", and the "become" trick with proxies really only works when the proxy can transplant its brains with a newborn object.

We could state that the "constructor" argument must be equal to Object, Array, and perhaps a handful of other primitive constructors, but that would probably rule out NodeList. How can we get some guarantee that a function is effectively going to produce and return a newborn object?

# Mark S. Miller (14 years ago)

On Tue, Sep 6, 2011 at 8:08 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote: [...]

Regardless, it's a good thing to provide the choice. Regarding the open issue ("do we default to Object.create or do we want to allow for the possibility of Array.create etc.?"), I think that the choice should be given to create any sort of object (why not host objects such as NodeList?). One question would be: how could this (easily) be achieved?

I've been thinking about a couple of ways, but there remain open issues:

  1. Have fix() not just return a property descriptor map, but a tuple of [constructor, property descriptor map]. To create the instance it needs to become, the proxy invokes the returned constructor's "create" method with as first argument the proxy's prototype and as second argument the prop. desc. map.

For example:

fix: function(operation) { // I want to become an array return [Array, { ... } ]; }

Array.isArray and other [[Class]] checks (e.g., the Flanagan device of using ({}).toString.call) need to give stable answers. Especially because they currently always do give stable answers.

  1. Add the constructor to use upon fixing as an additional argument to Proxy.create{Function}, e.g.:

var p = Proxy.create(handler, proto, Array); // fix() trap will use Array.create

The thing is: we can't let proxy authors just pass any arbitrary object as the "constructor", e.g.

var myconstructor = { create: function(proto, props) { ... } }; Proxy.create(handler, proto, myconstructor);

This is because myconstructor could return arbitrary live objects from its call to "create", and the "become" trick with proxies really only works when the proxy can transplant its brains with a newborn object.

We could state that the "constructor" argument must be equal to Object, Array, and perhaps a handful of other primitive constructors, but that would probably rule out NodeList. How can we get some guarantee that a function is effectively going to produce and return a newborn object?

How about we adapt ideas from the "<|" proposal? Proxy.create already takes a proto argument. The third argument could say whether the [[Class]] of the proxy should be "Object" or should be the same as that proto. No constructors.

# Tom Van Cutsem (14 years ago)

2011/9/6 Mark S. Miller <erights at google.com>

On Tue, Sep 6, 2011 at 8:08 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

[...]

  1. Have fix() not just return a property descriptor map, but a tuple of [constructor, property descriptor map]. To create the instance it needs to become, the proxy invokes the returned constructor's "create" method with as first argument the proxy's prototype and as second argument the prop. desc. map.

For example:

fix: function(operation) { // I want to become an array return [Array, { ... } ]; }

Array.isArray and other [[Class]] checks (e.g., the Flanagan device of using ({}).toString.call) need to give stable answers. Especially because they currently always do give stable answers.

Ok, that rules out this design.

  1. Add the constructor to use upon fixing as an additional argument to

Proxy.create{Function}, e.g.:

var p = Proxy.create(handler, proto, Array); // fix() trap will use Array.create

[...]

We could state that the "constructor" argument must be equal to Object, Array, and perhaps a handful of other primitive constructors, but that would probably rule out NodeList. How can we get some guarantee that a function is effectively going to produce and return a newborn object?

How about we adapt ideas from the "<|" proposal? Proxy.create already takes a proto argument. The third argument could say whether the [[Class]] of the proxy should be "Object" or should be the same as that proto. No constructors.

That would probably also allow Object.prototype.toString.call(proxy) to return, e.g. [object Array], which is another outstanding issue.

I can see this working for Object and Array. Functions are covered by function proxies. Do we need to consider other primitives?

I still don't see how it would work out for NodeList or other host objects. Then again, I don't know whether there is a use case that requires this.

# David Bruant (14 years ago)

Le 06/09/2011 18:15, Mark S. Miller a écrit :

On Tue, Sep 6, 2011 at 8:08 AM, Tom Van Cutsem <tomvc.be at gmail.com <mailto:tomvc.be at gmail.com>> wrote: [...]

    Regardless, it's a good thing to provide the choice. Regarding
    the open
    issue ("do we default to Object.create or do we want to allow
    for the
    possibility of Array.create etc.?"), I think that the choice
    should be
    given to create any sort of object (why not host objects such as
    NodeList?). One question would be: how could this (easily) be
    achieved?


I've been thinking about a couple of ways, but there remain open
issues:

1) Have fix() not just return a property descriptor map, but a
tuple of [constructor, property descriptor map]. To create the
instance it needs to become, the proxy invokes the returned
constructor's "create" method with as first argument the proxy's
prototype and as second argument the prop. desc. map.

For example:

fix: function(operation) {
  // I want to become an array
  return [Array, { ... } ];
}

Array.isArray and other [[Class]] checks (e.g., the Flanagan device of using ({}).toString.call) need to give stable answers. Especially because they currently always do give stable answers.

Indeed, so if a [[Class]] is given to a proxy, it has to be done at proxy creation so that the proxy and its fixed version are stable regarding [[Class]] checks.

2) Add the constructor to use upon fixing as an additional
argument to Proxy.create{Function}, e.g.:

var p = Proxy.create(handler, proto, Array);
// fix() trap will use Array.create


The thing is: we can't let proxy authors just pass any arbitrary
object as the "constructor", e.g.

var myconstructor = { create: function(proto, props) { ... } };
Proxy.create(handler, proto, myconstructor);

This is because myconstructor could return arbitrary live objects
from its call to "create", and the "become" trick with proxies
really only works when the proxy can transplant its brains with a
newborn object.

We could state that the "constructor" argument must be equal to
Object, Array, and perhaps a handful of other primitive
constructors, but that would probably rule out NodeList. How can
we get some guarantee that a function is effectively going to
produce and return a newborn object?

How about we adapt ideas from the "<|" proposal?

This proposal is only about initializing the prototype. I don't see what can be adapted to work on [[Class]].

Proxy.create already takes a proto argument. The third argument could say whether the [[Class]] of the proxy should be "Object" or should be the same as that proto. No constructors.

The notion of "same as the proto" is ambiguous. Do you mean that Proxy.create should guess which [[Class]] should be provided based on the prototype object? If I use an object which inherits from Array.prototype, can I still be considered as an Array? If there is a [[Class]] A with usually a prototype Pa and a [[Class]] B with Pb which inherits from Pa. Can I still create an A object with Pb as prototype?

Passing a string and doing consistency checks sounds like a more flexible idea.

# David Bruant (14 years ago)

Le 07/09/2011 15:38, Tom Van Cutsem a écrit :

2011/9/6 Mark S. Miller <erights at google.com <mailto:erights at google.com>>

On Tue, Sep 6, 2011 at 8:08 AM, Tom Van Cutsem <tomvc.be at gmail.com
<mailto:tomvc.be at gmail.com>> wrote:

    [...]
    1) Have fix() not just return a property descriptor map, but a
    tuple of [constructor, property descriptor map]. To create the
    instance it needs to become, the proxy invokes the returned
    constructor's "create" method with as first argument the
    proxy's prototype and as second argument the prop. desc. map.

    For example:

    fix: function(operation) {
      // I want to become an array
      return [Array, { ... } ];
    }


Array.isArray and other [[Class]] checks (e.g., the Flanagan
device of using ({}).toString.call) need to give stable answers.
Especially because they currently always do give stable answers.

Ok, that rules out this design.

    2) Add the constructor to use upon fixing as an additional
    argument to Proxy.create{Function}, e.g.:

    var p = Proxy.create(handler, proto, Array);
    // fix() trap will use Array.create

    [...]

    We could state that the "constructor" argument must be equal
    to Object, Array, and perhaps a handful of other primitive
    constructors, but that would probably rule out NodeList. How
    can we get some guarantee that a function is effectively going
    to produce and return a newborn object?


How about we adapt ideas from the "<|" proposal? Proxy.create
already takes a proto argument. The third argument could say
whether the [[Class]] of the proxy should be "Object" or should be
the same as that proto. No constructors.

That would probably also allow Object.prototype.toString.call(proxy) to return, e.g. [object Array], which is another outstanding issue.

I can see this working for Object and Array. Functions are covered by function proxies. Do we need to consider other primitives?

I still don't see how it would work out for NodeList or other host objects. Then again, I don't know whether there is a use case that requires this.

Emulating the DOM, probably. It would still be possible to have non-extensible trapping proxies, but not turn the proxy into a native DOM Document/Node/Element/HTMLElement/DocumentFragment, etc... This could create some bad performance penalty for DOM Traversers [1] for instance (or querySelectorAll). Would it even be possible to have an "hybrid" DOM tree with native DOM Node and emulated DOM Node?

Also, one very annoying aspect of the DOM is the inability to use what could be expected to be a constructor as such. Both "Document()" and "new Document()" throw a TypeError (FF6 and latest Chrome). This complicates asking an engine to turn a proxy into a given native DOM host object by passing a "constructor" since apparently, not all host objects have some.

David

[1] www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html

# Tom Van Cutsem (14 years ago)

2011/9/7 David Bruant <david.bruant at labri.fr>

** Le 07/09/2011 15:38, Tom Van Cutsem a écrit :

[...]

I still don't see how it would work out for NodeList or other host objects. Then again, I don't know whether there is a use case that requires this.

Emulating the DOM, probably. It would still be possible to have non-extensible trapping proxies, but not turn the proxy into a native DOM Document/Node/Element/HTMLElement/DocumentFragment, etc... This could create some bad performance penalty for DOM Traversers [1] for instance (or querySelectorAll). Would it even be possible to have an "hybrid" DOM tree with native DOM Node and emulated DOM Node?

So a sealed/frozen DOM element requires no more interposition at all?

Also, how straightforward would it be to brain-transplant a Proxy with, say, a NodeList, as opposed to brain-transplanting a Proxy with a regular Object?

# Mark S. Miller (14 years ago)

On Wed, Sep 7, 2011 at 6:40 AM, David Bruant <david.bruant at labri.fr> wrote:

**

How about we adapt ideas from the "<|" proposal?

This proposal is only about initializing the prototype. I don't see what can be adapted to work on [[Class]].

Agreed that it's quite different. My think was a bit muddy.

The [[Class]] magic that <| does derives from having a literal expression on the right side, determining the [[Class]] of the result while not determining the [[Prototype]] of the result. The cool thing about depending on a literal in this position is we get to delegate to the literal initialization logic the issue of initializing the instance so it has the right own invariants. The proposal works because none of the invariants associated with any existing [[Class]] rely on the instance's [[Prototype]]. The cost if relying on syntax in this way is we can't leverage this to create and initialize, say, a true Date which inherits from something other than Date.prototype. We could if we introduced a Date literal, but that's a terrible way out of the dilemma.

The Date example also points out what's wrong about just inheriting the [[Class]] and its associated internal vtable. Things which check for the Date [[Class]] assume the Date invariants are maintained. A proxy emulation of a Date must uphold these invariants.

Which suggests a bizarre idea. What if the optional third argument were the object to serve the role of Tom's fixedProps object? Then the proxy's [[Class]] would be the fixedProp's [[Class]], and Tom's enforcement mechanism would magically enforce whatever invariants are associated with [[Class]]. This should work for NodeLists and other host objects as well.

# Mark S. Miller (14 years ago)

On Wed, Sep 7, 2011 at 7:10 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

2011/9/7 David Bruant <david.bruant at labri.fr>

** Le 07/09/2011 15:38, Tom Van Cutsem a écrit :

[...]

I still don't see how it would work out for NodeList or other host objects. Then again, I don't know whether there is a use case that requires this.

Emulating the DOM, probably. It would still be possible to have non-extensible trapping proxies, but not turn the proxy into a native DOM Document/Node/Element/HTMLElement/DocumentFragment, etc... This could create some bad performance penalty for DOM Traversers [1] for instance (or querySelectorAll). Would it even be possible to have an "hybrid" DOM tree with native DOM Node and emulated DOM Node?

So a sealed/frozen DOM element requires no more interposition at all?

I don't understand the question or how you got there.

Certainly, for security purposes, sealed/frozen DOM nodes still carry tremendous authority, and so we still need to interpose proxy-wrappers for them.

# David Bruant (14 years ago)

Le 07/09/2011 17:39, Mark S. Miller a écrit :

On Wed, Sep 7, 2011 at 7:10 AM, Tom Van Cutsem <tomvc.be at gmail.com <mailto:tomvc.be at gmail.com>> wrote:

2011/9/7 David Bruant <david.bruant at labri.fr
<mailto:david.bruant at labri.fr>>

    Le 07/09/2011 15:38, Tom Van Cutsem a écrit :
    [...]


    I still don't see how it would work out for NodeList or other
    host objects. Then again, I don't know whether there is a use
    case that requires this.
    Emulating the DOM, probably. It would still be possible to
    have non-extensible trapping proxies, but not turn the proxy
    into a native DOM
    Document/Node/Element/HTMLElement/DocumentFragment, etc...
    This could create some bad performance penalty for DOM
    Traversers [1] for instance (or querySelectorAll). Would it
    even be possible to have an "hybrid" DOM tree with native DOM
    Node and emulated DOM Node?


So a sealed/frozen DOM element requires no more interposition at all?

I don't understand the question or how you got there.

I'm not sure I understand your question either. I think I should refine my point. With a proxy-emulated DOM element, would it be possible to do?

var myProxyEmulatedElement = Proxy.create(...); document.body.appendChild(myProxyEmulatedElement);

Currently, an exception is thrown for document.body.appendChild({}); (same for a proxy). Reading the DOM 3 spec [1], I realize that it corresponds to no DOMException (which is normal I think), but also that this behavior doesn't seem spec'ed. Anyway, I think that this would be a nice feature to have. Maybe I'm wrong. But that wasn't my initial point.

My point was that if it's not possible to turn proxies into native DOM objects, then things that traverse DOM trees (and are expected to do so quickly, otherwise the author had rather writing the traversal herself) could suffer from performance penalty.

Certainly, for security purposes, sealed/frozen DOM nodes still carry tremendous authority, and so we still need to interpose proxy-wrappers for them.

Good point.

Also, how straightforward would it be to brain-transplant a Proxy
with, say, a NodeList, as opposed to brain-transplanting a Proxy
with a regular Object?

Maybe it's not straightforward. Maybe it's so difficult and annoying that implementors are going to veto it by never doing it. Maybe that it's impossible actually. But the question is worth asking.

David

[1] www.w3.org/TR/DOM-Level-3-Core/core.html#ID

# Kevin Reid (14 years ago)

On Sep 7, 2011, at 9:58, David Bruant wrote:

I still don't see how it would work out for NodeList or other host objects. Then again, I don't know whether there is a use case that requires this. Emulating the DOM, probably.

For virtualization of things like the DOM, reusing the host's [[Class]], or being able to fix a proxy into a host object (if I correctly understand that those are what is being discussed), is not actually what is wanted. Rather, it needs to be possible to create a separate set of objects which have the same relationships among themselves. That is, given that in browsers document.[[Class]] == HTMLDocument, we want to be able to define MyHTMLDocument such that myDocument.[[Class]] == MyHTMLDocument.

# Tom Van Cutsem (14 years ago)

2011/9/7 Mark S. Miller <erights at google.com>

On Wed, Sep 7, 2011 at 7:10 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

2011/9/7 David Bruant <david.bruant at labri.fr>

** Emulating the DOM, probably. It would still be possible to have non-extensible trapping proxies, but not turn the proxy into a native DOM Document/Node/Element/HTMLElement/DocumentFragment, etc... This could create some bad performance penalty for DOM Traversers [1] for instance (or querySelectorAll). Would it even be possible to have an "hybrid" DOM tree with native DOM Node and emulated DOM Node?

So a sealed/frozen DOM element requires no more interposition at all?

I don't understand the question or how you got there.

I asked earlier "I don't know whether there is a use case that requires [proxies "becoming" host objects when fixed]." To which David replied emulating the DOM. I found that odd, since a proxy that "becomes" a DOM node loses the ability to interpose as well. Hence my question asking whether DOM elements can do without interposition when fixed.

Judging from the rest of the conversation, I think David and I misunderstood. David, correct me if I'm wrong, but what I now think you meant is that proxies should at least be able to fake the [[Class]] of a DOM node, not necessarily that a proxy should "become" a plain DOM object and lose its ability to trap.

Certainly, for security purposes, sealed/frozen DOM nodes still carry tremendous authority, and so we still need to interpose proxy-wrappers for them.

That's what I thought.

# David Flanagan (14 years ago)

On 9/1/11 8:40 AM, Tom Van Cutsem wrote:

I promised to give an update on the work I've been doing to investigate to what extent the Proxy API may support non-extensible objects. In a nutshell: I now think it is possible for Proxies to emulate not just non-extensible objects, but also sealed and frozen objects, given appropriate invariant checks.

Since this thread has started discussing NodeList and other DOM-related issues, and since I'm using Proxies to implement the DOM in pure JavaScript (see andreasgal/dom.js) I'll weigh in here with a few thoughts.

First though, I'd like to say that Tom's original post in this thread was a thing of beauty. Thanks, Tom, for the clearly articulated preface and history!

David Bruant wrote:

Regarding the open issue ("do we default to Object.create or do we want to allow for the possibility of Array.create etc.?"), I think that the choice should be given to create any sort of object (why not host objects such as NodeList?). One question would be: how could this (easily) be achieved?

That seems like a can of worms that shouldn't be opened. I think the "become" option for fix() should only be used for objects that are sufficiently locked-down that they can be emulated with a plain-old object with appropriately set [[Prototype]] and [[Class]]. If I'm using a Proxy to emulate Array and its magical length property and preventExtensions() is called, I still need magical length behavior because properties can be removed. I suggest that it is not worth allowing a proxy to "become" an Array in this case. The proxy would just have to keep trapping so it can continue to provide the desired magical behavior. But if my emulated array is frozen or sealed, then I can safely become a plain object with prototype Array.prototype and [[Class]] "Array". It seems to me that if a proxy needs to "become" something other than a plain object, then it isn't really fixed, and it should keep trapping.

As for [[Class]], I'm not sure if we really need to care about it, or if we only really care about the behavior of Object.prototype.toString().
For my work with dom.js I'm unable to comply with all WebIDL requirements because Proxies do not allow me to specify the [[Class]] for the proxy. I only care about the return value of Object.prototype.toString(), however, so Allen's proposal (only barely articulated here: strawman:es5_internal_nominal_typing) to allow the return value of O.p.toString() to be set via object properties would solve my problem.

In the Array case above, however, I suspect that an array-like Proxy that wanted to "become" an Array when frozen or sealed would like Array.isArray() to work for it, and so would like to be able to set [[Class]]. Allen is proposing to change the way that Array.isArray() works, however, so it wouldn't be tied to [[Class]] anymore. If he succeeds at that, then I think proxies (as currently defined) would never return true when passed to Array.isArray(). And if Array.isArray() can never return true for proxy objects, then presumably a proxy should not be able to fix() itself to "become" a true array object for which Array.isArray() does return true.

If Allen is successful in abolishing [[Class]], then maybe you'll have to add Proxy.createArray() along with Proxy.create() and Proxy.createFunction()?

If Allen is not successful, then it seems to me that we ought to be able to specify a [[Class]] value when calling Proxy.create() and that that value ought to be used when the proxy "becomes" a plain object via fix().

This, I think, is what David Bruant proposed when he wrote:

Indeed, so if a [[Class]] is given to a proxy, it has to be done at proxy creation so that the proxy and its fixed version are stable regarding [[Class]] checks.

Tom Van Cutsem wrote:

How about we adapt ideas from the "<|" proposal? Proxy.create already takes a proto argument. The third argument could say whether the [[Class]] of the proxy should be "Object" or should be the same as that proto. No constructors.

That would probably also allow Object.prototype.toString.call(proxy) to return, e.g. [object Array], which is another outstanding issue.

I can see this working for Object and Array. Functions are covered by function proxies. Do we need to consider other primitives?

I still don't see how it would work out for NodeList or other host objects. Then again, I don't know whether there is a use case that requires this.

WebIDL requires NodeList objects to have a [[Class]] of "NodeList", Document object to have a [[Class]] of "Document", and so on. Of course, since [[Class]] is only observable through Object.prototype.toString(), Allen's proposal to parameterize Object.prototype.toString() would address this, or proxies could address this directly.

As for the more fundamental question: is there a use case for a proxy to "become" a host object? I don't think there is much of a use case (at least not for DOM objects). In my work with dom.js, the goal is to replace the native DOM entirely with a new implementation written in JS. If it works, then my proxy-based NodeList implementation would be the only one there is: there wouldn't be any other kind of NodeList for it to become.

Or, consider the case of a proxy that forwards to a native DOM Element object. When frozen, it wouldn't make sense for it to "become" a new Element object because that new object wouldn't appear in the correct position in the document tree.

Furthermore, the question of what the fix() method of a NodeList proxy should do is getting ahead of the DOM itself. I don't think the behavior of DOM interfaces have been defined when preventExtensions, seal, or freeze() are called on them. A fundamental feature of NodeList, for example, is that it is usually "live". So if we're allowed to freeze a NodeList, we must either break the liveness contract and turn it into a static snapshot, or we must make the node from which the nodelist was derived non-modifiable. This is unexpected action-at-a-distance, and the DOM doesn't even have a well-defined notion of readonly nodes, so neither option is really viable. For now, at least, I suspect that trying to freeze a nodelist should just throw an exception.

David Bruant asked:

Would it even be possible to have an "hybrid" DOM tree with native DOM Node and emulated DOM Node?

DOM Level 3 tried to define ways for document nodes to be shared between implementations, I think. But that was really Java and XML stuff, and it has been dropped from the DOM Core spec. I don't think that anyone has a goal to allow a proxy-based implementation of Element, for example, to be inserted into a native Document. My understanding is that that is never going to work. So the Proxy spec does not need to work to enable that.

 David Flanagan
# Tom Van Cutsem (14 years ago)

Thanks David for putting some of my fears to rest ;-)

Essentially you and Kevin concluded similarly that it's not necessary to create some hybrid DOM emulation that would work with interchangeable native and proxied DOM elements. So, let's close the "becoming a host object" can of worms and instead focus on parameterizing [[Class]].

Simply parameterizing Object.prototype.toString should be easy. We've been over this before. An extra String argument to Proxy.create, perhaps. If the proxy becomes fixed, the newborn Object could inherit this String parameter so that O.p.toString remains stable. That would hint at an |Object.create(proto, pdmap, class)| primitive, where |class| is simply a string such that O.p.toString on such an object just returns "[Object <class>]".

Re. Array.isArray(aProxy) returning true: if truly necessary, it can still be done by overriding Array.isArray such that it checks whether its argument is an emulated array (e.g. via a WeakMap). Not ideal, but it works without complicating either arrays or proxies.

Cheers, Tom

2011/9/7 David Flanagan <dflanagan at mozilla.com>

# David Bruant (14 years ago)

Le 07/09/2011 17:35, Mark S. Miller a écrit :

On Wed, Sep 7, 2011 at 6:40 AM, David Bruant <david.bruant at labri.fr <mailto:david.bruant at labri.fr>> wrote:

How about we adapt ideas from the "<|" proposal?
This proposal is only about initializing the prototype. I don't
see what can be adapted to work on [[Class]].

Agreed that it's quite different. My think was a bit muddy.

The [[Class]] magic that <| does derives from having a literal expression on the right side, determining the [[Class]] of the result while not determining the [[Prototype]] of the result. The cool thing about depending on a literal in this position is we get to delegate to the literal initialization logic the issue of initializing the instance so it has the right own invariants. The proposal works because none of the invariants associated with any existing [[Class]] rely on the instance's [[Prototype]]. The cost if relying on syntax in this way is we can't leverage this to create and initialize, say, a true Date which inherits from something other than Date.prototype. We could if we introduced a Date literal, but that's a terrible way out of the dilemma.

The Date example also points out what's wrong about just inheriting the [[Class]] and its associated internal vtable. Things which check for the Date [[Class]] assume the Date invariants are maintained. A proxy emulation of a Date must uphold these invariants.

Which suggests a bizarre idea. What if the optional third argument were the object to serve the role of Tom's fixedProps object? Then the proxy's [[Class]] would be the fixedProp's [[Class]], and Tom's enforcement mechanism would magically enforce whatever invariants are associated with [[Class]]. This should work for NodeLists and other host objects as well.

So the third argument would be the object the proxy would become. This sounds like a reasonable idea but is a bit different from the original fix proposal which created a fresh object based on an object description (pdmap). I guess I should ask: why in the original design did the author had to return a description of the object to be returned rather than creating the object himself and return it? To avoid returning a proxy, maybe?

# Tom Van Cutsem (14 years ago)

2011/9/8 David Bruant <david.bruant at labri.fr>

** So the third argument would be the object the proxy would become. This sounds like a reasonable idea but is a bit different from the original fix proposal which created a fresh object based on an object description (pdmap). I guess I should ask: why in the original design did the author had to return a description of the object to be returned rather than creating the object himself and return it? To avoid returning a proxy, maybe?

In order for the proxy to safely "become" a target object, the target object should be newborn (i.e. no other object should have a live reference to the target object). Otherwise, we violate the stability of ===. Consider:

var o = {}; var p = Proxy.create(handler, proto, o); o === p // false Object.preventExtension(p); // assume p now becomes o o === p // true

That's why fix() returns a description of a fresh object to create, rather than a fresh object itself.

# Mark S. Miller (14 years ago)

On Thu, Sep 8, 2011 at 7:55 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

2011/9/8 David Bruant <david.bruant at labri.fr>

** So the third argument would be the object the proxy would become.

[...]

Otherwise, we violate the stability of ===. [...]

I am not proposing anything like a Smalltalk become.

But first, to avoid confusing, we need consistent terminology for the two present roles for [[Class]].

  1. What Object.prototype.toString.call() prints between "[object " and "]".

  2. An internal nominal type, associated with a set of fixed internal methods and possibly mutable internal state variables. The nominal type's integrity relies on invariants maintained on these internal state variables. These invariants are maintained both by these internal methods and by built in functions, like Date.prototype.setFullYear(), that first check this nominal type before mutating the object.

To elaborate on the proposal I'm making here,

a) The proxy does not actually become the fixedProps object. Rather, it would treat Object.getPropertyDescriptors(fixedProps) as it now treats the property descriptor map returned by fix().

b) From birth, the proxy has the same Object.prototype.toString.call() behavior as fixedProps.

c) From birth, the proxy has the same nominal type, internal methods as fixedProps. Also, internal mutable state properties should somehow be shared, since these internal mutable state properties are part of the invariants assumed by the internal methods and those built-ins internal-state-accessing built-ins like Date.prototype.setFullYear().

The state sharing implied by #c is unfortunate, and could be avoided in practice if we can find a variant of this proposal that ensures fixedProps is indeed fresh. I don't have any clever ideas of how to do that.

What the proxy doesn't get from the fixedProps:

d) The fixedProps' identity.

e) The fixedProps' [[Prototype]]

f) The fixedProps' [[Construct]] method if a construct trap is provided to a function proxy.

In enumerating these, I omitted the [[Call]] trap, realizing that we already treat the callTrap in many ways similarly to how we propose here to treat the fixedProps, for example, in the behavior of Function.prototype.toString.call(). Were we to adopt this proposal, we should probably promote the callTrap into having this role.

Were this proposal to be adopted,

Date.prototype.setFullYear.call(Proxy.create(someFunnyObject, handler,

new Date()), 1957)

should work. It is perhaps unsatisfying that it would work without trapping to the handler.

I don't know if I'm serious about any of this. It seems complicated and I'm not sure how much payoff there is. But it does seem to be a principled answer to many of the conundrums arising in this thread.

# Mark Miller (14 years ago)

On Thu, Sep 8, 2011 at 9:35 AM, Mark S. Miller <erights at google.com> wrote:

a) The proxy does not actually become the fixedProps object. Rather, it would treat Object.getPropertyDescriptors(fixedProps) as it now treats the property descriptor map returned by fix().

Critical mistake: I meant getOwnPropertyDescriptors