Skip to content

dart2js: better inference of 'this' type in global type analysis #40183

@rakudrama

Description

@rakudrama

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;
    },
  };

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions