[[Invoke]] and implicit method calls

Allen Wirfs-Brock allen at wirfs-brock.com
Wed Sep 18 21:00:25 PDT 2013


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

> On Fri, Sep 13, 2013 at 11:36 AM, Tom Van Cutsem <tomvc.be at gmail.com> 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










    


More information about the es-discuss mailing list