-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
TL;DR: Our current AbstractValueDomain can be used to support better precision of this and potentially remove more unreachable code.
Consider this program
class A {
foo() => bar() + 1;
bar() => 100;
}
class B extends A {
foo() => bar() + 2;
bar() => 200;
}
main() {
demo(A());
demo(B());
}
void demo(A a) {
print(a.foo());
}The main program has a call to a.foo$0(), and the following classes:
G.A.prototype = {
foo$0: function() {
return this.bar$0() + 1;
},
bar$0: function() {
return 100;
}
};
G.B.prototype = {
foo$0: function() {
return 202;
},
bar$0: function() {
return 200;
}
};Why do A.foo$0 and B.foo$0 look different?
Why is B.bar$0 included although it can never be called?
It is evident that the call to B.bar in B.foo has been inlined and constant-folded.
In methods on B, we know that this has a type represented by the 'cone' subclasses(B)
which is {B}. This allows the target of this.bar() to be identified and inlined.
A.foo is not as well optimized. Type analysis assumes that this has the 'cone' type subclasses(A) which is {A,B}. There are two bar targets reachable with this set of receivers, so there is no single target to inline. Further, the impact for the call also has this 'cone' type, subclasses(A), which marks A.bar and B.bar as being potential runtime targets, causing these two functions to be compiled and included in the program.
Now, in A.foo, this can't actually have the type B. If the receiver of a call a.foo() is a B, the call will reach B.foo instead. Because of the override, in A.foo, this can only be a B if there is a call super.foo() from another method where this can be a B.
We can establish a set of inclusion constraints that can be solved to find the possible set of instantiated types for A.foo.this. In the general case these sets are too big, which is why we use 'cone' types. We are starting out with cone types for this when we should start out with more precise types and resort to cones if they get too big. In this small example, A.foo.this would start out with {A}, represented as exact(A), and stay there.
The inclusion constrains can be solved ahead of full inference and used as a narrowing on the this parameter, or can be built dynamically from the call graph, which is potentially more precise. For the latter, the call site needs to do the filtering, and the this parameter abstract type serves as the solved inclusion constraints.
This increased precision with the same type lattice would permit A.bar to be inlined, leaving
G.A.prototype = {
foo$0: function() {
return 101;
},
};
G.B.prototype = {
foo$0: function() {
return 202;
},
};