You can sometimes use the 'wrong' constant interceptor rather than look up the interceptor.
class A extends Interceptor {
foo() { ... }
bar() { print('A.foo'); }
}
class B extends A {
bar() { print('B.foo'); }
}
The general code for calling r.foo() would be:
methods = getInterceptor(r);
methods.foo$0(r)
We might statically know that 'r is A' is true.
We still have to use getInterceptor if the execution can operate in a way that can distinguish that the virtual call's receiver is an A or a B.
This would happen, e.g. if the body of A.foo() is { this.bar(); }
The body for A.foo() would be compiled to
A: {
foo$0: function (receiver) { this.bar$0(receiver); }
this happens because of an optimization to re-use the current interceptor (bound to 'methods' at the call site and 'this' within the intercepted method) to avoid the expense of calling getInterceptor() again.
When can we call 'A.foo' directly, even though the receiver may be a B? i.e. when will the constant interceptor for A suffice for the call, and allow the code to avoid getInterceptor:
methods = A.prototype;
methods.foo$0(r);
It is a flow-sensitive transitive property of the call graph from A.foo. It must be basically impossible for 'this' (the cached interceptor value) to flow to a selector that uses the value to invoke a method defined below A in the hierarchy.
It might be that a flow-insensitive approximation is good enough to detect many cases.
element = document.querySelector("...");
methods = J.getInterceptor$x(element)
methods.scrollIntoView$1(element, C.ScrollAlignment_CENTER);
The scrollIntoView method is defined only on Element, and querySelector() is known to return Element|null.
If we know that scrollIntoView is simple enough, we could calculate 'methods' via one of the the cheaper statements
methods = (element == null) ? null : Element.prototype
or
methods = element && Element.prototype;
The result would either be null, or Elements.prototype.
Lets look at scrollIntoView:
Element: {
...
scrollIntoView$1: function(receiver, alignment) {
var hasScrollIntoViewIfNeeded = !!receiver.scrollIntoViewIfNeeded;
if (alignment === C.ScrollAlignment_TOP)
receiver.scrollIntoView(true);
else if (alignment === C.ScrollAlignment_BOTTOM)
receiver.scrollIntoView(false);
else if (hasScrollIntoViewIfNeeded)
if (alignment === C.ScrollAlignment_CENTER)
receiver.scrollIntoViewIfNeeded(true);
else
receiver.scrollIntoViewIfNeeded();
else
receiver.scrollIntoView();
},
There is no use of 'this' in there so we are safe! But we don't have this final compiled code to inspect at the point of calling getInterceptor(), and there is no use of 'this' only because all method calls that used 'this' as a receiver were inlined because these methods were also declared on Element, and sufficiently small. There could be harmless uses of 'this' had the inlining not happened.
To analyse at a higher level, we would have collect the selectors in the target method, find the methods they can reach, and add the selectors for those methods until we had reached closure, or found a method that is defined below Element in the hierarchy.
It is not clear how well a flow-insensitive algorithm would work. Type inference gives a weak form of flow sensitivity. In this example, the call graph closure would be effective only if we know from type inference that the occurrences of operator== all have a receiver of type ScrollAlignment and reach only the default implementation, Object.==. If we had to use untyped selectors, it is likely one of the many implementations of operator== would use some method (transitively) with the same name as one defined below Element.
Aliasing information (another summary of flow) might also help reduce false positives but we don't have any global alias information.
You can sometimes use the 'wrong' constant interceptor rather than look up the interceptor.
class A extends Interceptor { foo() { ... } bar() { print('A.foo'); } }
class B extends A { bar() { print('B.foo'); } }
The general code for calling r.foo() would be:
methods = getInterceptor(r); methods.foo$0(r)
We might statically know that 'r is A' is true. We still have to use getInterceptor if the execution can operate in a way that can distinguish that the virtual call's receiver is an A or a B. This would happen, e.g. if the body of A.foo() is { this.bar(); }
The body for A.foo() would be compiled to
A: { foo$0: function (receiver) { this.bar$0(receiver); }
this happens because of an optimization to re-use the current interceptor (bound to 'methods' at the call site and 'this' within the intercepted method) to avoid the expense of calling getInterceptor() again.
When can we call 'A.foo' directly, even though the receiver may be a B? i.e. when will the constant interceptor for A suffice for the call, and allow the code to avoid getInterceptor:
methods = A.prototype; methods.foo$0(r);
It is a flow-sensitive transitive property of the call graph from A.foo. It must be basically impossible for 'this' (the cached interceptor value) to flow to a selector that uses the value to invoke a method defined below A in the hierarchy. It might be that a flow-insensitive approximation is good enough to detect many cases.
A motivating example from a customer's app:
The Dart code contains:
document.querySelector("...").scrollIntoView(ScrollAlignment.CENTER);
This compiles to essentially:
element = document.querySelector("..."); methods = J.getInterceptor$x(element) methods.scrollIntoView$1(element, C.ScrollAlignment_CENTER);
The scrollIntoView method is defined only on Element, and querySelector() is known to return Element|null. If we know that scrollIntoView is simple enough, we could calculate 'methods' via one of the the cheaper statements
methods = (element == null) ? null : Element.prototype or methods = element && Element.prototype;
The result would either be null, or Elements.prototype.
Lets look at scrollIntoView:
Element: { ... scrollIntoView$1: function(receiver, alignment) { var hasScrollIntoViewIfNeeded = !!receiver.scrollIntoViewIfNeeded; if (alignment === C.ScrollAlignment_TOP) receiver.scrollIntoView(true); else if (alignment === C.ScrollAlignment_BOTTOM) receiver.scrollIntoView(false); else if (hasScrollIntoViewIfNeeded) if (alignment === C.ScrollAlignment_CENTER) receiver.scrollIntoViewIfNeeded(true); else receiver.scrollIntoViewIfNeeded(); else receiver.scrollIntoView(); },
There is no use of 'this' in there so we are safe! But we don't have this final compiled code to inspect at the point of calling getInterceptor(), and there is no use of 'this' only because all method calls that used 'this' as a receiver were inlined because these methods were also declared on Element, and sufficiently small. There could be harmless uses of 'this' had the inlining not happened.
To analyse at a higher level, we would have collect the selectors in the target method, find the methods they can reach, and add the selectors for those methods until we had reached closure, or found a method that is defined below Element in the hierarchy.
It is not clear how well a flow-insensitive algorithm would work. Type inference gives a weak form of flow sensitivity. In this example, the call graph closure would be effective only if we know from type inference that the occurrences of operator== all have a receiver of type ScrollAlignment and reach only the default implementation, Object.==. If we had to use untyped selectors, it is likely one of the many implementations of operator== would use some method (transitively) with the same name as one defined below Element. Aliasing information (another summary of flow) might also help reduce false positives but we don't have any global alias information.