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

Tom Van Cutsem tomvc.be at gmail.com
Tue Sep 6 08:08:29 PDT 2011


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 Object[operation](target).
>

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:
> > <
> http://code.google.com/p/es-lab/source/browse/trunk/src/proxies/testFixedTrappingProxy.js
> >
> > browser-based test:
> > <
> http://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
> (http://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, { ... } ];
}

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?

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


More information about the es-discuss mailing list