evaluation order and the [[Invoke]] MOP operation

# Allen Wirfs-Brock (11 years ago)

See Bug 1593

In ES5, an expression such as:

obj.m(arg)

is visibly evaluated in this order:

1.     thisValue <- evaluate("obj")
2.    f <- thisValue.[[Get]]("m");
2.a        possible visible side-effets of getting property "m"
3.    arg1 <- evaluate("arg")
3.a         possible visible side-effects of evaluating "arg"
4.    f.[[Call]] (thisValue, ArgList(arg1))

In the current ES6 draft, using the [[Invoke]] operation the evaluation order is

1.     thisValue <- evaluate("obj")
2.    arg1 <- evaluate("arg")
2.a         possible visible side-effects of evaluating "arg".
3.    thisValue.[[Invoke]] ("m", ArgList(arg1))
4            f <- thisValue.[[Get]]("m");
4.a                possible visible side-effets of getting property "m"
5            f.[[Call]](thisValue, ArgList(arg1))

or, reducing it to just the observable side-effects( ignoring Proxies)

es5:

2.a        possible visible side-effets of accessing property "m"
3.a         possible visible side-effects of evaluating "arg"

es6

2.a         possible visible side-effects of evaluating "arg".
4.a                possible visible side-effets of getting property "m"

This is a possibly breaking change.

This ordering difference is inherent in the current design of the [[Invoke]] operation because it requires as arguments an evaluated argument list and an unevaluated method access.

Note that this ordering actually changed in ES5. ES<=3 specified the same observable order as the current ES6 draft. The ES5 spec. says in Annex D: "Edition 5 reverses the order of steps 2 and 3 of the algorithm. The original order as specified in Editions 1 through 3 was incorrectly specified such that side-effects of evaluating Arguments could affect the result of evaluating MemberExpression."

Other than backing out the addition of [[Invoke]] I see to alternative paths forward.

  1. Accept the breaking change.

The breaking change in ES5 doesn't seem to have raised significant real word compatibility issues. So, reversing that change is unlikely to cause such issues.

  1. Redesign [[Invoke]]

We could redesign [[Invoke]] to permit pre-evaluation of the method. It's new signature would be:

thisObj.[[Invoke]](propertyKey, function, argumentList)

and obj.m(arg) would evaluation as:

1.     thisValue <- evaluate("obj")
2.     f <- thisValue.[[Get]]("m");
2.a        possible visible side-effets of getting property "m",  may evaluate to undefined
3.    arg1 <- evaluate("arg")
3.a         possible visible side-effects of evaluating "arg"
4.    thisValue.[[Invoke]] ("m", f,  ArgList(arg1))

When [[Invoke]] is called, f might be undefined or it might be the value of obj.m. It is up to the [[Invoke]] handler to decide whether to use the value of f that was passed to it or to interpret "m" in some other manner. In the normal case, the evaluation order would be exactly as currently specified by ES5. In some weird Proxy cases there would be an potentially observable get access to obj.m that would not occur in using the current ES6 specification of [[Invoke]].

The simplest thing to do is just to leave things as currently specified and take the breaking change. However, alternative 2 seems valid and a bit intriguing.

Thoughts?

# Brandon Benvie (11 years ago)

On 8/19/2013 9:33 AM, Allen Wirfs-Brock wrote:

thisObj.[[Invoke]](propertyKey, function, argumentList)

This could allow [[Invoke]] to trap call and apply, if propertyKey was allowed to be undefined.

# Allen Wirfs-Brock (11 years ago)

On Aug 19, 2013, at 9:39 AM, Brandon Benvie wrote:

On 8/19/2013 9:33 AM, Allen Wirfs-Brock wrote:

thisObj.[[Invoke]](propertyKey, function, argumentList)

This could allow [[Invoke]] to trap call and apply, if propertyKey was allowed to be undefined.

I don't really want to get us off the topic WRT to the breaking change issue. But, WRT call/apply here is a couple fragments from a private exchange between TomVC and me:

On Jul 30, 2013, at 9:57 AM, Allen Wirfs-Brock wrote:

On Jul 30, 2013, at 1:25 AM, David Bruant wrote:

... This is not enough. this-binding ("proxy.getMonth()" when the target is a Date) is only half of the story. The other half remains unadressed ("Date.prototype.getMonth.call(proxy)"). Exotic objects aren't always used as "this" value. This is exactly Tom's feedback actually. We need the same solution for both cases (this and non-this usage of methods).

I also raised this objection and it is what led to the recent update to the virtual object API referenced above. What that change does is make it easier for ES programmer to create proxies that behave in this manner.

But, in the end we could not come up with a solution for the Date.prototype.getMonth.call(proxy) issue. The [[Invoke]] MOP operation/trap was added to minimize the actual occurrence of this scenario. You can't use 'call' to force a built-in to operate upon the wrong kind of object.

...

Is there a handler semantics with which it can fully work? That's all is needed.

The ForwardingHandler semantics is close to what you want. But it still doesn't work the way you would like for direct "call" invocation or for things like Array.isArray. The base issue for either of these is that they don't indirect through the proxy handler and hence the handler doesn't have an opportunity to replace the proxy reference with the target reference.

I wonder if we could respecify F.p.call/apply in a manner that would make David's getMonth.call use case work (at least some of the time).

[[Invoke]](P, ArgumentsList, Receiver) is current specified such that P must be a property key. What if we modify that so that P can be either a property key or a callable and that when a callable is passed the [[Get]] steps are skipped and P itself is used as the method.

Then call/apply could be respecified in terms of [[Invoke]] and ForwardingHandler could do the appropriate handler substitution.

Of course, this is only an asymptotically better solution, as it only improves the handler of Proxy objects in the this position. It wouldn't, may itself, fix Array.isArray.

Tom's thought were:

On Jul 31, 2013, at 3:58 AM, Tom Van Cutsem wrote:

While clever, I don't think this will fly, for the reason that many programmers use the explicit call syntax particularly so that they can guarantee that the method to be invoked is the built-in that they previously stashed away somewhere.

If now F.p.call is going to delegate the decision of what code is to be executed to the thisArg, that destroys the pattern.

I know Mark's SES is full of preamble code where he first obtains references to the actual built-ins, then invokes those built-ins using call to make sure he will be executing only the original built-in code, nothing else.

These two ideas for [[Invoke]] could be combined (pass both 'f' and 'p' and have call/apply use [[Invoke]]) but it still does't address Tom's concern.

# Mark S. Miller (11 years ago)

More data about whether changing this would break anything:

I reported code.google.com/p/v8/issues/detail?id=691 in May 2010.

It remains open -- probably because it has caused few actual problems.

# Mark S. Miller (11 years ago)

And because it remains open, whereas FF, Safari, and old Opera followed ES5, the cross-browser web must be compatible with either decision.

# Tom Van Cutsem (11 years ago)

(Pardon the late reply. Catching up.)

I'd also go with option 1 (accept that [[Invoke]] changes visible order of side-effects).

Option 2 pollutes the simple interface of [[Invoke]] for compatibility with edge-cases, which, as MarkM points out, are not fully web-compatible anyway. The symmetric operation, i.e. Reflect.invoke(obj, "m", f, args) would also make little sense.