Allen Wirfs-Brock <mailto:[email protected]>
September 19, 2013 1:39 PM
Seems very unlikely. The @@hasInstance access is new so it isn't a
backwards compat issue. BTW, these are static counts. For example, a
typical call to ToPrimitive will only make a single such conditional
invoke.
Arguably allowing a String or Number wrapper to be transparently
proxied and hence work like a unproxied wrapper WRT ToPrimitive is one
the reasons we need to do this.
Allen
Tom Van Cutsem <mailto:[email protected]>
September 19, 2013 1:25 PM
2013/9/19 Brendan Eich <[email protected] <mailto:[email protected]>>
Refactoring [[Get]]+[[Call]] to [[Get]]+[[Invoke]] seems fine
by me. It better expresses the intent, and the change should
only be observable to proxies.
Is this so? Wouldn't an ordinary object with a getter be able to
observe the two lookups? Indeed wouldn't the spec require this?
You're right. The change would lead to observably different behavior
for conditionally invoked getters.
Not sure how much of a backwards compat issue that would be. Allen
previously wrote upstream in this thread:
There are currently 6 such places (not counting the Proxy trap
invocators):
3 in ToPrimitive (ie toSrting/valueOf)
1 in instanceof to access @@hasInstance (legacy conpat. for
missing @@hasInstance)
1 in [ ].toString to conditionally invoke 'join' method
1 in JSON.stringify conditionally invoke 'toJSON'
How likely is it that these are getters with side-effects?
Tom Van Cutsem <mailto:[email protected]>
September 19, 2013 9:53 AM
2013/9/19 Allen Wirfs-Brock <[email protected]
<mailto:[email protected]>>
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.
Refactoring [[Get]]+[[Call]] to [[Get]]+[[Invoke]] seems fine by me.
It better expresses the intent, and the change should only be
observable to proxies.
Re. the fact that |proxy.method.call(proxy)| would not re-bind |this|:
I've come to think that if |this|-rebinding is crucial to your proxy's
use case, you probably need to go "all the way" and just use
membranes. Membranes contain all the necessary logic to rebind |this|
as well as any other parameters.
That said, I believe we could do strictly without invoke(), but given
that method invocation is so primary to JS, I believe we make the
right choice by exposing it in the MOP.
By comparison, we also added a has() trap to trap the in-operator,
while we could have also just triggered a series of more fundamental
traps to figure out the result, and the in-operator is far less common
than method invocation in a typical JS program.
Regards,
Tom
Allen Wirfs-Brock <mailto:[email protected]>
September 19, 2013 5:00 AM
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
Jason Orendorff <mailto:[email protected]>
September 17, 2013 2:36 PM
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.
-j