On Sep 17, 2013, at 9:36 AM, Jason Orendorff wrote:

> On Fri, Sep 13, 2013 at 11:36 AM, Tom Van Cutsem <[email protected]> wrote:
>> MarkM and I talked about conditional invocation offline and we convinced
>> ourselves to argue for the status-quo (i.e. continue to encode conditional
>> invocations as [[Get]]+[[Call]]).
>> 
>> The most compelling argument we can think of is that [[Get]]+[[Call]] is
>> also the pattern JavaScript programmers use to express conditional method
>> calls today:
>> 
>> var f = obj[name];
>> if (f) { f.call(…); } else { … }
>> 
>> No matter whether or how we extend the MOP, such code exists and will
>> continue to exist, and well-designed proxies that want to re-bind |this|
>> must already deal with this pattern by implementing the "get" trap
>> correctly.
> 
> Tom, it seems to me if there’s such a thing as “implementing the "get"
> trap correctly”, i.e. such that method calls work, that removes the
> main motivation for having [[Invoke]] in the first place. To recap
> what else [[Invoke]] achieves, in the light of that:
> 
> * improved performance for proxies, because method calls go through
> one proxy handler trap rather than two, and no temporary function
> object is allocated
> 
> * proxies can observe a bit about the caller (whether or not it's an
> [[Invoke]] call site), when a method is called
> 
> If that's all, it seems like we should definitely remove [[Invoke]]
> and the .invoke trap. The MOP was already complicated enough. The
> performance argument is a non-starter, and the other “feature” is
> entirely undesirable.

I've actually become convinced that [[Get]] and [[Invoke]] are the correct 
primitives and and that the worry about "conditional invoke" was a false 
concern.

A method invocation such as:
    obj.foo(arg)
is currently specified as being roughly equivalent to:
    obj.[[Invoke]]("foo", [arg], obj);
and the ordinary implementation if [[Invoke]] decomposes into:
    let func = obj.[[Get]]("foo");
    let result = func.[[Call]](obj,[arg])

However, a proxy may do other things in its 'invoke' handler including 
replacing the this value and/or arguments passed to the [[Call]].  There are 
strong use cases for the variability of such  translations which were the 
recent motivation for re-introducing [[Invoke]].  It don't think we need to go 
around the loop of reconsidering those use cases again.  They are valid and we 
will end up at the same place.

The problem is that we see within the ES specification a few instances of a 
[[Get]]/[[Call]] sequence that look more typically like:
    let func = obj.[[Get]]("foo");
    let result;
    if (typeof(func) == "function") result = func.[[Call]](obj,[arg])
    else /* compute result some other way */

Our concern started with" "oh no, the [[Get]]/[[Call]] is inside of [[Invoke]] 
how can we get  between them. This led to various proposals for new MOP 
operations that split up [[Invoke]] or allowed a conditional test to be 
injected into [[Invoke]]. I think this is the wrong way to look at the problem. 
 We were being mislead by legacy [[Get]]/[[Call]] pattern and not looking at 
the actual conceptual intent of these code sequences.  In pure ECMAScrpt and 
dealing at  the level of object abstractions, this is what such use cases are 
really trying to expression:

   If (typeof(obj.foo) == "function") result = obj.foo(arg);
   else //something else ...;

or, in prose: If the 'foo' property of obj is a method, invoke that method on 
obj with arg as the argument.

or in pseudo-ES-pseudo code:
    let func = obj.[[Get]]("foo");
    let result;
    if (typeof(func) == "function") result = obj.[[Invoke]]("foo",[arg],obj)
    else /* compute result some other way */

In other words, the appropriate conversion of the pattern we observed in the ES 
spec. isn't [[Get]]+test+conditional call to [[Call]].  It is 
[[Get]]+test+conditional call to [[Invoke]].

>From this thread, there are two concerns I anticipate.  The first is that a 
>double property lookup of "foo" is being performed.   Conceptually I don't 
>have a problem with that as the double property access accurately represents 
>the object level concept that is being expressed.  However, there might also 
>be a perforce concern about the double lookup, particularly for its usage in 
>ToPrimitive.  I believe this is a non-problem as implementations can easily 
>avoid performing the double lookup it is an actual performance issue.  A 
>typical specified usage of this pattern can be implemented something like:

// If (typeof(obj.valueOf) == "function") return obj.valueOf();
if (obj is not an ordinary object) goto slowpath;
//in practice some sort of guard like this is likely to be used in front of 
every MOP operation
//A more specified test would be if object does not use both the ordinary 
[[Get]] and [[Invoke]] implementations
let func = InlinedOrdinaryGet(obj,"valueOf"); //and if "valueOf" resolves to a 
getter, invoke it twice,yuck.  Perhaps poison optimization if any ordinary obj 
valueOf accessors defined
if (func is an ordinary function object) return inlinedOrdinaryCall(obj);
else if (func has a [[Call]] internal method) return func.[[Call]](obj); 
//exotic function needs full MOP [[Call]] dispatch
else goto nextcase;
slowpath:
//func is not an ordinary object so need to dispatch MOP calls on it
//only slow path does double lookup
let func = obj.[[Get]]("valueOf");
if (func has a [[Call]] internal method) return obj.[[Invoke]]("valueOf",[ ], 
obj); //full MOP [[Invoke]] dispatch
nextcase:
   ...

In other words, the cases in the specification for  ordinary objects can be 
implemented roughly like current implementations.  No double lookup need be 
performed and any extra guards are exactly the guards that are needed to 
support the existence of proxies or other exotic objects that over-ride 
[[Get]], [[Invoke]], or [[Call]].

I'm not particularly concerned about double lookup performance for similar use 
cases code in JS code.  For ordinary objects, I expect normal  PIC mechanism to 
eliminate most of the double lookup overhead and any hot code paths.

However, a concern that was mentioned is that JS programmers routinely express 
the conditional method call pattern as:
       If (typeof(func=obj.foo) == "function") result = func.call(obj, arg);
rather than
       If (typeof(obj.foo) == "function") result = obj.foo(arg);

I appreciate this concern.  However, without having or using [[Invoke]] this 
pattern may run into the same sort of bugs that motivated us to recently add 
[[Invoke]].  We need to educate JS programmers that with ES6 and the 
availability of Proxies and other features that obj.foo() is not exactly the 
same thing as obj.foo.call(foo) and that the latter formulation should 
generally be avoided except in expert situations.   the spread operator makes 
it particularly easy to use () instead of apply The new "call" function is () 
and except for very special circumstances where a different this values is 
being supplied obj.method() is how methods should be invoked.

I really don't think we need to debate this much longer.  We just need to stay 
the course with [[Invoke]] and I can update the spec. to replace 
[[Get]]+[[Invoke]] rather than [[Get]]+[[Call]] for this conditional 
situations. I may also added add a note suggesting that the extra [[Get]] can 
be eliminated.

Allen










    
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to