Proxy-induced impurity of internal methods

Tom Van Cutsem tomvc.be at gmail.com
Mon Oct 10 06:38:38 PDT 2011


Hi Andreas,

These are all good points. Some comments below:

2011/10/5 Andreas Rossberg <rossberg at google.com>

> Proxies invalidate one fundamental assumption of the current ES spec,
> namely that (most) internal methods are effectively pure. That has a
> couple of consequences which the current proxy proposal and semantics
> seem to ignore, but which we need to address.
>
>
> OBSERVABILITY & EFFICIENCY
>
> In ES5, internal methods essentially are an implementation detail of
> the spec. AFAICS, there is no way their interaction is actually
> observable in user code. This gives JS implementations significant
> leeway in implementing objects (and they make use of it).
>
> This changes drastically with proxies. In particular, since most
> internal methods may now invoke traps directly or indirectly, we can
> suddenly observe many internal steps of property lookup and similar
> operations through potential side effects of these traps. (Previously,
> only the invocation of getters or setters was observable).


> Take the following simple example:
>
> ----------------
> var desc = {configurable: true, get: function() {return 8}, set:
> function() {return true}}
> var handler = {getPropertyDescriptor: function() {seq += "G"; return desc}}
> var p = Proxy.create(handler)
> var o = Object.create(p)
>
> var seq = ""
> o.x
> var seq1 = seq
> seq = ""
> o.x = 0
> var seq2 = seq
> ----------------
>
> According to the proxy spec, we should see seq1=="G" and seq2=="GG".
> In my local version of V8, I currently see seq1=="G" and seq2=="G".
> In Firefox 7, I see seq1=="GG" and seq2=="GG".
>

This point was previously noted, see: <
http://wiki.ecmascript.org/doku.php?id=strawman:proxy_set_trap>
It was brought up at the March 2011 meeting, and IIRC we were in agreement
that the spec. should be adapted to remove the redundant
getPropertyDescriptor call.


> Obviously, both implementations are unfaithful to the spec, albeit in
> reverse ways. At least for V8, implementing the correct behaviour may
> require significant changes.
>
> Also, I wonder whether the current semantics forcing seq2=="GG" really
> is what we want, given that it is unnecessarily inefficient (note that
> it also involves converting the property descriptor twice, which in
> turn can spawn numerous calls into user code). Optimizing this would
> require purity analysis on trap functions, which seems difficult in
> general.
>

I agree. This is clearly a case where the ES5 spec was written assuming this
was all unobservable.


> HIDDEN ASSUMPTIONS
>
> In a number of places, the ES5 spec makes hidden assumptions about the
> purity of internal method calls, and derives certain invariants from
> that, which break with proxies.
>
> For example, in the spec of [[Put]] (8.12.5), step 5.a asserts that
> desc.[[Set]] cannot be undefined. That is true in ES5, but no longer
> with proxies. Unsurprisingly, both Firefox and V8 do funny things for
> the following example:
>
> ----------------
> var handler = {
>  getPropertyDescriptor: function() {
>    Object.defineProperty(o, "x", {get: function() { return 5 }})
>    return {set: function() {}}
>  }
> }
> var p = Proxy.create(handler)
> var o = Object.create(p)
> o.x = 4
> ----------------
>
> Firefox 7: InternalError on line 1: too much recursion
> V8: TypeError: Trap #<error> of proxy handler #<Object> returned
> non-configurable descriptor for property x
>

(are you sure this tests the right behavior? It seems the V8 TypeError is
simply due to the fact that the descriptor returned from
getPropertyDescriptor is configurable.)

More generally, there is no guarantee anymore that the result of
> [[CanPut]] in step 1 of [[Put]] is in any way consistent with what we
> see in later steps. In this light (and due to the efficiency reasons I
> mentioned earlier), we might want to consider rethinking the
> CanPut/Put split.
>

I agree. In fact, proxies already abandon the CanPut/Put split: they
implement CanPut simply by always returning true, and perform all of their
assignment logic in [[Put]]. Related to this refactoring: Mark has
previously proposed introducing a [[Set]] trap that simply returns a
boolean, indicating whether or not the assignment succeeded. The [[Put]]
trap would simply call [[Set]], converting a false result into a TypeError
when appropriate (cf. <
http://wiki.ecmascript.org/doku.php?id=harmony:proxy_defaulthandler#alternative_implementation_for_default_set_trap>).
We don't have consensus on this yet. I would propose to discuss the
CanPut/Put refactoring and the [[Set]] alternative together during the Nov.
meeting.


> This is just one case. There may be other problematic places in other
> operations. Most of them are probably more subtle, i.e. the spec still
> prescribes some behaviour, but that does not necessarily make any
> sense for certain cases (and would be hard to implement to the
> letter). We probably need to check the whole spec very carefully.
>
>
> FIXING PROXIES
>
> A particularly worrisome side effect is fixing a proxy. The proxy
> semantics contains a lot of places saying "If O is a trapping proxy,
> do steps I-J." However, there generally is no guarantee that O remains
> a trapping proxy through all of I-J!
>
> Again, an example:
>
> ----------------
> var handler = {
>  get set() { Object.freeze(p); return undefined },
>  fix: function() { return {} }
> }
> var p = Proxy.create(handler)
> p.x
> ----------------
>
> Firefox 7: TypeError on line 1: getPropertyDescriptor is not a function
> V8: TypeError: Object #<Object> has no method 'getPropertyDescriptor'
>
> The current proxy semantics has an (informal) restriction forbidding
> reentrant fixing of the same object, but that is only a very special
> case of the broader problem. Firefox 7 rejects fixing a proxy while
> one of (most) its traps is executing (this seems to be a recent
> change, and the above case probably is an oversight). But it is not
> clear to me what the exact semantics is there, and whether it is
> enough as a restriction. V8 currently even crashes on a few contorted
> examples.
>

You are right, the restriction on recursive fixing should be generalized
such that fixing is disallowed as long as there are _any_ remaining active
trap invocations, unless we can make sure that fixing in the middle of any
step leads to a sane outcome.

On the upside: Mark and I have been making good progress on an alternative
proxy proposal that completely decouples
Object.{freeze,seal,preventExtensions} from fixing a proxy. That would mean
that Object.{freeze,seal,preventExtensions} would no longer suffer from the
above restriction - they don't implicitly fix the proxy anymore. There would
be a distinct Object.stopTrapping operation that cannot be performed while
the proxy is actively trapping. That would confine the problem to this (much
more specific) operation.

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


More information about the es-discuss mailing list