[vm/compiler] Avoid invalid code motion

In Dart doing a type check against one value can constrain the type
of another value in an indirect fashion. This possibility needs to be
taken into account when inserting Redefinitions to inhibit the code
motion. This CL addresses two previously ignored situations:

* Given a generic container `C<T> c` which holds a value `T v` doing a
type check against `c` might narrow the type of `v`.
* Given two generic classes `C<T>` and `H<T>`, doing a type-check
against an instance of type `C<X> c` might narrow the type of an
unrelated instance of `H<X> v` within the same scope.

The second situation is currently limited to a situation when we do an
invocation on `this` and the target of the call has constrained generic
parameter (e.g. because the target of the call is in the subclass which
instantiates the base class with a more specific type
`class C_ extends C<int>`). Calls on `this` are special because
they are allowed to bypass argument type checks which are
usually performed due to covariance.

In both cases we need to ensure that all uses of `v` which are
using narrowed type are pinned to stay within the true successor
of the type-check against `c`.

Previously we would only insert redefinitions for the value that is
being type checked, but this is not enough and this CL tries to
address the newly discovered cases:

* When replacing loads in load optimizer we must ensure that
if replaced load was dependant on the redefinition (and thus
was pinned) the replacement is similarly pinned (if the type of the
load might have been narrowed). If neccessary we must insert a
redefinition for the loaded value itself.
* When performing inlining of calls on `this` we must insert
redefinitions for all parameters that might have their types
narrowed.
* When replacing phis which have redefinitions as all inputs.

Fixes https://github.com/flutter/flutter/issues/91370
Fixes https://github.com/dart-lang/sdk/issues/43652

TEST=vm/dart{,_2}/flutter_regress_91370_il_test

Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-product-x64-try,vm-kernel-precomp-linux-release-x64-try
Change-Id: I89c1f165615dd827102e9f6af90365af7d8f32b2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/219241
Commit-Queue: Slava Egorov <vegorov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
diff --git a/runtime/tests/vm/dart/flutter_regress_91370_il_test.dart b/runtime/tests/vm/dart/flutter_regress_91370_il_test.dart
new file mode 100644
index 0000000..722da15
--- /dev/null
+++ b/runtime/tests/vm/dart/flutter_regress_91370_il_test.dart
@@ -0,0 +1,718 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:vm/testing/il_matchers.dart';
+
+bool shouldPrint = false;
+
+@pragma('vm:never-inline')
+void blackhole(Object v) {
+  if (shouldPrint) {
+    print(v);
+  }
+}
+
+class A {}
+
+class A0 extends A {
+  final double x;
+  final String str;
+
+  A0(this.x, this.str);
+
+  // Use [x] to prevent the field from being shaken out. We would like
+  // [x] to occupy the same location that [A1.str] takes so that
+  // type confusion / incorrect LICM would cause a crash when we load
+  // `o.[A1.str].length` on an object of type [A0]
+  String toString() => 'A0($x)';
+}
+
+class A1 extends A {
+  final String str;
+
+  A1(this.str);
+}
+
+class H<T> {
+  final T data;
+  H(this.data);
+}
+
+abstract class B<T extends A> {
+  final T v;
+
+  B(this.v);
+
+  int load(H<T> h);
+  int loadWithNamedParam({required H<T> h});
+
+  @pragma('vm:never-inline')
+  @pragma('vm:testing:print-flow-graph', '*LICM')
+  int testNarrowingThroughThisCallWithPositionalParam(H<T> h) {
+    var result = 0;
+    for (var i = 0; i < 10; i++) {
+      // We will perform polymorphic inlining of `load` because `this`
+      // is known to be either B0 or B1. In both cases we will have
+      // v.str.length loads fully inlined because inlined bodies
+      // have precise type information for v.
+      // Then we will hoist v.str.length out of the loop past
+      // class-id comparisons generated by the inlining leading
+      // to incorrect code which will crash.
+      result += load(h);
+    }
+    return result;
+  }
+
+  @pragma('vm:never-inline')
+  @pragma('vm:testing:print-flow-graph', '*LICM')
+  int testNarrowingThroughThisCallWithNamedParams(H<T> h) {
+    var result = 0;
+    for (var i = 0; i < 10; i++) {
+      // We will perform polymorphic inlining of `load` because `this`
+      // is known to be either B0 or B1. In both cases we will have
+      // v.str.length loads fully inlined because inlined bodies
+      // have precise type information for v.
+      // Then we will hoist v.str.length out of the loop past
+      // class-id comparisons generated by the inlining leading
+      // to incorrect code which will crash.
+      result += loadWithNamedParam(h: h);
+    }
+    return result;
+  }
+}
+
+class BImpl<T extends A> extends B<T> {
+  BImpl(T v) : super(v);
+
+  // These methods do not matter. They can just return 0.
+  int load(H<T> h) => 0;
+  int loadWithNamedParam({required H<T> h}) => 0;
+}
+
+class B0 extends B<A0> {
+  B0(A0 a) : super(a);
+
+  int load(H<A0> h) {
+    return h.data.str.length;
+  }
+
+  int loadWithNamedParam({String? a, required H<A0> h, String? z}) {
+    return h.data.str.length;
+  }
+}
+
+class B1 extends B<A1> {
+  B1(A1 a) : super(a);
+
+  int load(H<A1> h) {
+    return h.data.str.length;
+  }
+
+  int loadWithNamedParam({String? a, required H<A1> h, String? z}) {
+    return h.data.str.length;
+  }
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIsCheckOnSubclass(B b) {
+  int sum = 0;
+  b.v.toString();
+  for (var i = 0; i < 2; i++) {
+    if (b is B1) {
+      sum += b.v.str.length;
+    }
+  }
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIsCheckWithTypeArgPolymorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    if (b is B<A1>) {
+      sum += b.v.str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIsCheckWithTypeArgMonomorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    if (b is B<A1>) {
+      sum += b.v.str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughAsCheckWithTypeArgPolymorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    // Branch with a side-effect to avoid b as B<A1> hoisting.
+    if (sum == 42) throw '42';
+    sum += (b as B<A1>).v.str.length;
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    // Branch with a side-effect to avoid b as B<A1> hoisting.
+    if (sum == 42) throw '42';
+    sum += (sum == 22 ? b as B<A1> : b as B<A1>).v.str.length;
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIndexedLoadFromGrowableArray(List<A> l) {
+  int sum = 0;
+  final v = l[0];
+  for (var i = 0; i < 2; i++) {
+    if (l is List<A1>) {
+      sum += l[0].str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIndexedLoadFromFixedArray(List<A> l) {
+  int sum = 0;
+  final v = l[0];
+  for (var i = 0; i < 2; i++) {
+    if (l is List<A1>) {
+      sum += l[0].str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+void main(List<String> args) {
+  shouldPrint = args.contains("shouldPrint");
+
+  // Prevent shaking of BImpl.load and BImpl.loadWithNamed, if these methods
+  // are shaked (because they are not used) that would inhibit polymorphic
+  // inlining at testNarrowingThroughThisCall{,WithNamedParams}
+  BImpl(A1("")).load(H(A1("")));
+  BImpl(A1("")).loadWithNamedParam(h: H(A1("")));
+
+  for (var i = 0; i < 2; i++) {
+    final a1 = A1(i.toString());
+    final a0 = A0(i.toDouble(), i.toString());
+    B1(a1).testNarrowingThroughThisCallWithPositionalParam(H(a1));
+    B0(a0).testNarrowingThroughThisCallWithPositionalParam(H(a0));
+    B1(a1).testNarrowingThroughThisCallWithNamedParams(H(a1));
+    B0(a0).testNarrowingThroughThisCallWithNamedParams(H(a0));
+    testNarrowingThroughIsCheckOnSubclass(B1(a1));
+    testNarrowingThroughIsCheckOnSubclass(B0(a0));
+    testNarrowingThroughIsCheckWithTypeArgPolymorphic(B1(a1));
+    testNarrowingThroughIsCheckWithTypeArgPolymorphic(B0(a0));
+    testNarrowingThroughIsCheckWithTypeArgMonomorphic(BImpl<A1>(a1));
+    testNarrowingThroughIsCheckWithTypeArgMonomorphic(BImpl<A0>(a0));
+    testNarrowingThroughAsCheckWithTypeArgPolymorphic(B1(a1));
+    try {
+      testNarrowingThroughAsCheckWithTypeArgPolymorphic(B0(a0));
+      throw "Should be unreachable";
+    } on TypeError catch (e) {}
+    testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(B1(a1));
+    try {
+      testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(B0(a0));
+      throw "Should be unreachable";
+    } on TypeError catch (e) {}
+    testNarrowingThroughIndexedLoadFromGrowableArray([a1]);
+    testNarrowingThroughIndexedLoadFromGrowableArray([a0]);
+    testNarrowingThroughIndexedLoadFromFixedArray(
+        List<A1>.filled(1, a1, growable: false));
+    testNarrowingThroughIndexedLoadFromFixedArray(
+        List<A0>.filled(1, a0, growable: false));
+  }
+}
+
+void matchIL$testNarrowingThroughThisCallWithPositionalParam(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'h_raw' << match.Parameter(index: 1),
+      'h' << match.AssertAssignable('h_raw', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_cid' << match.LoadClassId('this'),
+          match.Branch(match.StrictCompare('this_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('this'),
+          // This redefinition was inserted by inlining.
+          'h_' << match.Redefinition('h'),
+          'v0' << match.LoadField('h_', slot: 'data'),
+          'v1' << match.LoadField('v0', slot: 'str'),
+          'v2' << match.LoadField('v1', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'h_raw' << match.Parameter(index: 1),
+      'h' << match.AssertAssignable('h_raw', match.any),
+      'this_cid' << match.LoadClassId('this'), // Hoisted.
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.StrictCompare('this_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          // After LICM redefinitions are removed.
+          'v0' << match.LoadField('h', slot: 'data'),
+          'v1' << match.LoadField('v0', slot: 'str'),
+          'v2' << match.LoadField('v1', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughThisCallWithNamedParams(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  // Graph shape is basically the same.
+  matchIL$testNarrowingThroughThisCallWithPositionalParam(
+      beforeLICM, afterLICM);
+}
+
+void matchIL$testNarrowingThroughIsCheckOnSubclass(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'b_cid' << match.LoadClassId('b'),
+          match.Branch(match.StrictCompare('b_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('b'),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      'b_cid' << match.LoadClassId('b'), // Hoisted.
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.StrictCompare('b_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughIsCheckWithTypeArgMonomorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'b_is_B<A1>' << match.InstanceOf('b', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('b_is_B<A1>', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('b'),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'b_is_B<A1>' << match.InstanceOf('b', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('b_is_B<A1>', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughAsCheckWithTypeArgPolymorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B5', ifFalse: 'B6'),
+        ]),
+    'B5' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B6' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B7' <<
+        match.block('Join', [
+          // This redefinition was inserted by PhiInstr::Canonicalize
+          // which removed Phi of two AssertAssignables above.
+          match.Redefinition('b'),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B5', ifFalse: 'B6'),
+        ]),
+    'B5' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B6' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B7' <<
+        match.block('Join', [
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughIndexedLoadFromGrowableArray(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a.data' << match.LoadField('this', slot: 'GrowableObjectArray.data'),
+      'a.data[0]' << match.LoadIndexed('a.data', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a.data[0]*' << match.Phi('a.data[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('this'),
+          // This redefinition was inserted by load forwarding.
+          'a.data[0]**' << match.Redefinition('a.data[0]*'),
+          'a.data[0].str' << match.LoadField('a.data[0]**', slot: 'str'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a.data' << match.LoadField('this', slot: 'GrowableObjectArray.data'),
+      'a.data[0]' << match.LoadIndexed('a.data', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a.data[0]*' << match.Phi('a.data[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'a.data[0].str' << match.LoadField('a.data[0]*', slot: 'str'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughIsCheckWithTypeArgPolymorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  matchIL$testNarrowingThroughIsCheckWithTypeArgMonomorphic(
+      beforeLICM, afterLICM);
+}
+
+void matchIL$testNarrowingThroughIndexedLoadFromFixedArray(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a[0]' << match.LoadIndexed('this', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a[0]*' << match.Phi('a[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('this'),
+          // This redefinition was inserted by load forwarding.
+          'a[0]**' << match.Redefinition('a[0]*'),
+          'a[0].str' << match.LoadField('a[0]**', slot: 'str'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a[0]' << match.LoadIndexed('this', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a[0]*' << match.Phi('a[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'a[0].str' << match.LoadField('a[0]*', slot: 'str'),
+        ]),
+  ], env: env);
+}
diff --git a/runtime/tests/vm/dart_2/flutter_regress_91370_il_test.dart b/runtime/tests/vm/dart_2/flutter_regress_91370_il_test.dart
new file mode 100644
index 0000000..722da15
--- /dev/null
+++ b/runtime/tests/vm/dart_2/flutter_regress_91370_il_test.dart
@@ -0,0 +1,718 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:vm/testing/il_matchers.dart';
+
+bool shouldPrint = false;
+
+@pragma('vm:never-inline')
+void blackhole(Object v) {
+  if (shouldPrint) {
+    print(v);
+  }
+}
+
+class A {}
+
+class A0 extends A {
+  final double x;
+  final String str;
+
+  A0(this.x, this.str);
+
+  // Use [x] to prevent the field from being shaken out. We would like
+  // [x] to occupy the same location that [A1.str] takes so that
+  // type confusion / incorrect LICM would cause a crash when we load
+  // `o.[A1.str].length` on an object of type [A0]
+  String toString() => 'A0($x)';
+}
+
+class A1 extends A {
+  final String str;
+
+  A1(this.str);
+}
+
+class H<T> {
+  final T data;
+  H(this.data);
+}
+
+abstract class B<T extends A> {
+  final T v;
+
+  B(this.v);
+
+  int load(H<T> h);
+  int loadWithNamedParam({required H<T> h});
+
+  @pragma('vm:never-inline')
+  @pragma('vm:testing:print-flow-graph', '*LICM')
+  int testNarrowingThroughThisCallWithPositionalParam(H<T> h) {
+    var result = 0;
+    for (var i = 0; i < 10; i++) {
+      // We will perform polymorphic inlining of `load` because `this`
+      // is known to be either B0 or B1. In both cases we will have
+      // v.str.length loads fully inlined because inlined bodies
+      // have precise type information for v.
+      // Then we will hoist v.str.length out of the loop past
+      // class-id comparisons generated by the inlining leading
+      // to incorrect code which will crash.
+      result += load(h);
+    }
+    return result;
+  }
+
+  @pragma('vm:never-inline')
+  @pragma('vm:testing:print-flow-graph', '*LICM')
+  int testNarrowingThroughThisCallWithNamedParams(H<T> h) {
+    var result = 0;
+    for (var i = 0; i < 10; i++) {
+      // We will perform polymorphic inlining of `load` because `this`
+      // is known to be either B0 or B1. In both cases we will have
+      // v.str.length loads fully inlined because inlined bodies
+      // have precise type information for v.
+      // Then we will hoist v.str.length out of the loop past
+      // class-id comparisons generated by the inlining leading
+      // to incorrect code which will crash.
+      result += loadWithNamedParam(h: h);
+    }
+    return result;
+  }
+}
+
+class BImpl<T extends A> extends B<T> {
+  BImpl(T v) : super(v);
+
+  // These methods do not matter. They can just return 0.
+  int load(H<T> h) => 0;
+  int loadWithNamedParam({required H<T> h}) => 0;
+}
+
+class B0 extends B<A0> {
+  B0(A0 a) : super(a);
+
+  int load(H<A0> h) {
+    return h.data.str.length;
+  }
+
+  int loadWithNamedParam({String? a, required H<A0> h, String? z}) {
+    return h.data.str.length;
+  }
+}
+
+class B1 extends B<A1> {
+  B1(A1 a) : super(a);
+
+  int load(H<A1> h) {
+    return h.data.str.length;
+  }
+
+  int loadWithNamedParam({String? a, required H<A1> h, String? z}) {
+    return h.data.str.length;
+  }
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIsCheckOnSubclass(B b) {
+  int sum = 0;
+  b.v.toString();
+  for (var i = 0; i < 2; i++) {
+    if (b is B1) {
+      sum += b.v.str.length;
+    }
+  }
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIsCheckWithTypeArgPolymorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    if (b is B<A1>) {
+      sum += b.v.str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIsCheckWithTypeArgMonomorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    if (b is B<A1>) {
+      sum += b.v.str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughAsCheckWithTypeArgPolymorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    // Branch with a side-effect to avoid b as B<A1> hoisting.
+    if (sum == 42) throw '42';
+    sum += (b as B<A1>).v.str.length;
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(B b) {
+  int sum = 0;
+  final v = b.v;
+  for (var i = 0; i < 2; i++) {
+    // Branch with a side-effect to avoid b as B<A1> hoisting.
+    if (sum == 42) throw '42';
+    sum += (sum == 22 ? b as B<A1> : b as B<A1>).v.str.length;
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIndexedLoadFromGrowableArray(List<A> l) {
+  int sum = 0;
+  final v = l[0];
+  for (var i = 0; i < 2; i++) {
+    if (l is List<A1>) {
+      sum += l[0].str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+@pragma('vm:never-inline')
+@pragma('vm:testing:print-flow-graph', '*LICM')
+int testNarrowingThroughIndexedLoadFromFixedArray(List<A> l) {
+  int sum = 0;
+  final v = l[0];
+  for (var i = 0; i < 2; i++) {
+    if (l is List<A1>) {
+      sum += l[0].str.length;
+    }
+  }
+  blackhole(v);
+  return sum;
+}
+
+void main(List<String> args) {
+  shouldPrint = args.contains("shouldPrint");
+
+  // Prevent shaking of BImpl.load and BImpl.loadWithNamed, if these methods
+  // are shaked (because they are not used) that would inhibit polymorphic
+  // inlining at testNarrowingThroughThisCall{,WithNamedParams}
+  BImpl(A1("")).load(H(A1("")));
+  BImpl(A1("")).loadWithNamedParam(h: H(A1("")));
+
+  for (var i = 0; i < 2; i++) {
+    final a1 = A1(i.toString());
+    final a0 = A0(i.toDouble(), i.toString());
+    B1(a1).testNarrowingThroughThisCallWithPositionalParam(H(a1));
+    B0(a0).testNarrowingThroughThisCallWithPositionalParam(H(a0));
+    B1(a1).testNarrowingThroughThisCallWithNamedParams(H(a1));
+    B0(a0).testNarrowingThroughThisCallWithNamedParams(H(a0));
+    testNarrowingThroughIsCheckOnSubclass(B1(a1));
+    testNarrowingThroughIsCheckOnSubclass(B0(a0));
+    testNarrowingThroughIsCheckWithTypeArgPolymorphic(B1(a1));
+    testNarrowingThroughIsCheckWithTypeArgPolymorphic(B0(a0));
+    testNarrowingThroughIsCheckWithTypeArgMonomorphic(BImpl<A1>(a1));
+    testNarrowingThroughIsCheckWithTypeArgMonomorphic(BImpl<A0>(a0));
+    testNarrowingThroughAsCheckWithTypeArgPolymorphic(B1(a1));
+    try {
+      testNarrowingThroughAsCheckWithTypeArgPolymorphic(B0(a0));
+      throw "Should be unreachable";
+    } on TypeError catch (e) {}
+    testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(B1(a1));
+    try {
+      testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(B0(a0));
+      throw "Should be unreachable";
+    } on TypeError catch (e) {}
+    testNarrowingThroughIndexedLoadFromGrowableArray([a1]);
+    testNarrowingThroughIndexedLoadFromGrowableArray([a0]);
+    testNarrowingThroughIndexedLoadFromFixedArray(
+        List<A1>.filled(1, a1, growable: false));
+    testNarrowingThroughIndexedLoadFromFixedArray(
+        List<A0>.filled(1, a0, growable: false));
+  }
+}
+
+void matchIL$testNarrowingThroughThisCallWithPositionalParam(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'h_raw' << match.Parameter(index: 1),
+      'h' << match.AssertAssignable('h_raw', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_cid' << match.LoadClassId('this'),
+          match.Branch(match.StrictCompare('this_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('this'),
+          // This redefinition was inserted by inlining.
+          'h_' << match.Redefinition('h'),
+          'v0' << match.LoadField('h_', slot: 'data'),
+          'v1' << match.LoadField('v0', slot: 'str'),
+          'v2' << match.LoadField('v1', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'h_raw' << match.Parameter(index: 1),
+      'h' << match.AssertAssignable('h_raw', match.any),
+      'this_cid' << match.LoadClassId('this'), // Hoisted.
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.StrictCompare('this_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          // After LICM redefinitions are removed.
+          'v0' << match.LoadField('h', slot: 'data'),
+          'v1' << match.LoadField('v0', slot: 'str'),
+          'v2' << match.LoadField('v1', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughThisCallWithNamedParams(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  // Graph shape is basically the same.
+  matchIL$testNarrowingThroughThisCallWithPositionalParam(
+      beforeLICM, afterLICM);
+}
+
+void matchIL$testNarrowingThroughIsCheckOnSubclass(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'b_cid' << match.LoadClassId('b'),
+          match.Branch(match.StrictCompare('b_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('b'),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      'b_cid' << match.LoadClassId('b'), // Hoisted.
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.StrictCompare('b_cid', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughIsCheckWithTypeArgMonomorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'b_is_B<A1>' << match.InstanceOf('b', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('b_is_B<A1>', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('b'),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'b_is_B<A1>' << match.InstanceOf('b', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('b_is_B<A1>', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughAsCheckWithTypeArgPolymorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughPhiOfAsChecksWithTypeArgPolymorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B5', ifFalse: 'B6'),
+        ]),
+    'B5' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B6' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B7' <<
+        match.block('Join', [
+          // This redefinition was inserted by PhiInstr::Canonicalize
+          // which removed Phi of two AssertAssignables above.
+          match.Redefinition('b'),
+          // This redefinition was inserted by load forwarding.
+          'b.v*' << match.Redefinition('b.v'),
+          'v0' << match.LoadField('b.v*', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'b' << match.Parameter(index: 0),
+      'b.v' << match.LoadField('b', slot: 'v'),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B3', ifFalse: 'B4'),
+        ]),
+    'B3' << match.block('Target', [match.Throw(match.any)]),
+    'B4' <<
+        match.block('Target', [
+          match.Branch(match.EqualityCompare(match.any, match.any, kind: '=='),
+              ifTrue: 'B5', ifFalse: 'B6'),
+        ]),
+    'B5' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B6' <<
+        match.block('Target', [
+          match.AssertAssignable('b', match.any, match.any, match.any),
+          match.Goto('B7'),
+        ]),
+    'B7' <<
+        match.block('Join', [
+          'v0' << match.LoadField('b.v', slot: 'str'),
+          'v1' << match.LoadField('v0', slot: 'String.length'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughIndexedLoadFromGrowableArray(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a.data' << match.LoadField('this', slot: 'GrowableObjectArray.data'),
+      'a.data[0]' << match.LoadIndexed('a.data', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a.data[0]*' << match.Phi('a.data[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('this'),
+          // This redefinition was inserted by load forwarding.
+          'a.data[0]**' << match.Redefinition('a.data[0]*'),
+          'a.data[0].str' << match.LoadField('a.data[0]**', slot: 'str'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a.data' << match.LoadField('this', slot: 'GrowableObjectArray.data'),
+      'a.data[0]' << match.LoadIndexed('a.data', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a.data[0]*' << match.Phi('a.data[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'a.data[0].str' << match.LoadField('a.data[0]*', slot: 'str'),
+        ]),
+  ], env: env);
+}
+
+void matchIL$testNarrowingThroughIsCheckWithTypeArgPolymorphic(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  matchIL$testNarrowingThroughIsCheckWithTypeArgMonomorphic(
+      beforeLICM, afterLICM);
+}
+
+void matchIL$testNarrowingThroughIndexedLoadFromFixedArray(
+    FlowGraph beforeLICM, FlowGraph afterLICM) {
+  final env = beforeLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a[0]' << match.LoadIndexed('this', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a[0]*' << match.Phi('a[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          match.Redefinition('this'),
+          // This redefinition was inserted by load forwarding.
+          'a[0]**' << match.Redefinition('a[0]*'),
+          'a[0].str' << match.LoadField('a[0]**', slot: 'str'),
+        ]),
+  ]);
+
+  afterLICM.match([
+    match.block('Graph'),
+    match.block('Function', [
+      'this' << match.Parameter(index: 0),
+      'a[0]' << match.LoadIndexed('this', match.any),
+      match.Goto('B1'),
+    ]),
+    'B1' <<
+        match.block('Join', [
+          'a[0]*' << match.Phi('a[0]', match.any),
+          match.CheckStackOverflow(),
+          match.Branch(match.RelationalOp(match.any, match.any, kind: '<'),
+              ifTrue: 'B2'),
+        ]),
+    'B2' <<
+        match.block('Target', [
+          'this_is_List' << match.InstanceOf('this', match.any, match.any),
+          match.Branch(
+              match.StrictCompare('this_is_List', match.any, kind: '==='),
+              ifTrue: 'B3'),
+        ]),
+    'B3' <<
+        match.block('Target', [
+          'a[0].str' << match.LoadField('a[0]*', slot: 'str'),
+        ]),
+  ], env: env);
+}
diff --git a/runtime/vm/compiler/backend/il.cc b/runtime/vm/compiler/backend/il.cc
index 9434956..4ca03ed 100644
--- a/runtime/vm/compiler/backend/il.cc
+++ b/runtime/vm/compiler/backend/il.cc
@@ -1135,18 +1135,18 @@
 
 // Returns true if the value represents a constant.
 bool Value::BindsToConstant() const {
-  return definition()->IsConstant();
+  return definition()->OriginalDefinition()->IsConstant();
 }
 
 // Returns true if the value represents constant null.
 bool Value::BindsToConstantNull() const {
-  ConstantInstr* constant = definition()->AsConstant();
+  ConstantInstr* constant = definition()->OriginalDefinition()->AsConstant();
   return (constant != NULL) && constant->value().IsNull();
 }
 
 const Object& Value::BoundConstant() const {
   ASSERT(BindsToConstant());
-  ConstantInstr* constant = definition()->AsConstant();
+  ConstantInstr* constant = definition()->OriginalDefinition()->AsConstant();
   ASSERT(constant != NULL);
   return constant->value();
 }
@@ -2545,7 +2545,8 @@
   if (!HasUses() && !flow_graph->is_licm_allowed()) {
     return NULL;
   }
-  if ((constrained_type() != nullptr) && Type()->IsEqualTo(value()->Type())) {
+  if (constrained_type() != nullptr &&
+      constrained_type()->IsEqualTo(value()->Type())) {
     return value()->definition();
   }
   return this;
@@ -5919,8 +5920,43 @@
   }
 }
 
+static bool AllInputsAreRedefinitions(PhiInstr* phi) {
+  for (intptr_t i = 0; i < phi->InputCount(); i++) {
+    if (phi->InputAt(i)->definition()->RedefinedValue() == nullptr) {
+      return false;
+    }
+  }
+  return true;
+}
+
 Definition* PhiInstr::Canonicalize(FlowGraph* flow_graph) {
   Definition* replacement = GetReplacementForRedundantPhi();
+  if (replacement != nullptr && flow_graph->is_licm_allowed() &&
+      AllInputsAreRedefinitions(this)) {
+    // If we are replacing a Phi which has redefinitions as all of its inputs
+    // then to maintain the redefinition chain we are going to insert a
+    // redefinition. If any input is *not* a redefinition that means that
+    // whatever properties were infered for a Phi also hold on a path
+    // that does not pass through any redefinitions so there is no need
+    // to redefine this value.
+    auto zone = flow_graph->zone();
+    auto redef = new (zone) RedefinitionInstr(new (zone) Value(replacement));
+    flow_graph->InsertAfter(block(), redef, /*env=*/nullptr, FlowGraph::kValue);
+
+    // Redefinition is not going to dominate the block entry itself, so we
+    // have to handle environment uses at the block entry specially.
+    Value* next_use;
+    for (Value* use = env_use_list(); use != nullptr; use = next_use) {
+      next_use = use->next_use();
+      if (use->instruction() == block()) {
+        use->RemoveFromUseList();
+        use->set_definition(replacement);
+        replacement->AddEnvUse(use);
+      }
+    }
+    return redef;
+  }
+
   return (replacement != nullptr) ? replacement : this;
 }
 
diff --git a/runtime/vm/compiler/backend/il.h b/runtime/vm/compiler/backend/il.h
index 2be44dc..4e7efb6 100644
--- a/runtime/vm/compiler/backend/il.h
+++ b/runtime/vm/compiler/backend/il.h
@@ -3770,6 +3770,8 @@
 
   virtual Value* RedefinedValue() const;
 
+  virtual void InferRange(RangeAnalysis* analysis, Range* range);
+
   PRINT_OPERANDS_TO_SUPPORT
 
  private:
@@ -4106,6 +4108,9 @@
   Code::EntryKind entry_kind() const { return entry_kind_; }
   void set_entry_kind(Code::EntryKind value) { entry_kind_ = value; }
 
+  void mark_as_call_on_this() { is_call_on_this_ = true; }
+  bool is_call_on_this() const { return is_call_on_this_; }
+
   DEFINE_INSTRUCTION_TYPE_CHECK(InstanceCallBase);
 
   bool receiver_is_not_smi() const { return receiver_is_not_smi_; }
@@ -4152,6 +4157,7 @@
   bool has_unique_selector_;
   Code::EntryKind entry_kind_ = Code::EntryKind::kNormal;
   bool receiver_is_not_smi_ = false;
+  bool is_call_on_this_ = false;
 
   DISALLOW_COPY_AND_ASSIGN(InstanceCallBaseInstr);
 };
@@ -4262,6 +4268,9 @@
     new_call->set_result_type(call->result_type());
     new_call->set_entry_kind(call->entry_kind());
     new_call->set_has_unique_selector(call->has_unique_selector());
+    if (call->is_call_on_this()) {
+      new_call->mark_as_call_on_this();
+    }
     return new_call;
   }
 
diff --git a/runtime/vm/compiler/backend/inliner.cc b/runtime/vm/compiler/backend/inliner.cc
index 2523166..38de868 100644
--- a/runtime/vm/compiler/backend/inliner.cc
+++ b/runtime/vm/compiler/backend/inliner.cc
@@ -629,18 +629,128 @@
   const Function& caller_function_;
 };
 
+static bool IsAThisCallThroughAnUncheckedEntryPoint(Definition* call) {
+  if (auto instance_call = call->AsInstanceCallBase()) {
+    return (instance_call->entry_kind() == Code::EntryKind::kUnchecked) &&
+           instance_call->is_call_on_this();
+  }
+  return false;
+}
+
+// Helper which returns true if callee potentially has a more specific
+// parameter type and thus a redefinition needs to be inserted.
+static bool CalleeParameterTypeMightBeMoreSpecific(
+    BitVector* is_generic_covariant_impl,
+    const FunctionType& interface_target_signature,
+    const FunctionType& callee_signature,
+    intptr_t first_arg_index,
+    intptr_t arg_index) {
+  if (arg_index > first_arg_index && is_generic_covariant_impl != nullptr &&
+      is_generic_covariant_impl->Contains(arg_index - first_arg_index)) {
+    const intptr_t param_index = arg_index - first_arg_index;
+    const intptr_t num_named_params =
+        callee_signature.NumOptionalNamedParameters();
+    const intptr_t num_params = callee_signature.NumParameters();
+    if (num_named_params == 0 &&
+        param_index >= interface_target_signature.NumParameters()) {
+      // An optional positional parameter which was added in the callee but
+      // not present in the interface target.
+      return false;
+    }
+
+    // Check if this argument corresponds to a named parameter. In this case
+    // we need to find correct index based on the name.
+    intptr_t interface_target_param_index = param_index;
+    if (num_named_params > 0 &&
+        (num_params - num_named_params) <= param_index) {
+      // This is a named parameter.
+      const String& name =
+          String::Handle(callee_signature.ParameterNameAt(param_index));
+      interface_target_param_index = -1;
+      for (intptr_t i = interface_target_signature.NumParameters() -
+                        interface_target_signature.NumOptionalNamedParameters(),
+                    n = interface_target_signature.NumParameters();
+           i < n; i++) {
+        if (interface_target_signature.ParameterNameAt(i) == name.ptr()) {
+          interface_target_param_index = i;
+          break;
+        }
+      }
+
+      // This is a named parameter which was added in the callee.
+      if (interface_target_param_index == -1) {
+        return false;
+      }
+    }
+    const AbstractType& callee_parameter_type =
+        AbstractType::Handle(callee_signature.ParameterTypeAt(param_index));
+    const AbstractType& interface_target_parameter_type =
+        AbstractType::Handle(interface_target_signature.ParameterTypeAt(
+            interface_target_param_index));
+    if (interface_target_parameter_type.ptr() != callee_parameter_type.ptr()) {
+      // This a conservative approximation.
+      return true;
+    }
+  }
+  return false;
+}
+
 static void ReplaceParameterStubs(Zone* zone,
                                   FlowGraph* caller_graph,
                                   InlinedCallData* call_data,
                                   const TargetInfo* target_info) {
   const bool is_polymorphic = call_data->call->IsPolymorphicInstanceCall();
+  const bool no_checks =
+      IsAThisCallThroughAnUncheckedEntryPoint(call_data->call);
   ASSERT(is_polymorphic == (target_info != NULL));
   FlowGraph* callee_graph = call_data->callee_graph;
   auto callee_entry = callee_graph->graph_entry()->normal_entry();
+  const Function& callee = callee_graph->function();
+
+  FunctionType& interface_target_signature = FunctionType::Handle();
+  FunctionType& callee_signature = FunctionType::Handle(callee.signature());
+
+  // If we are inlining a call on this and we are going to skip parameter checks
+  // then a situation can arise when parameter type in the callee has a narrower
+  // type than what interface target specifies, e.g.
+  //
+  //    class A<T> {
+  //      void f(T v);
+  //      void g(T v) { f(v); }
+  //    }
+  //    class B extends A<X> { void f(X v) { ... } }
+  //
+  // Conside when B.f is inlined into a callsite in A.g (e.g. due to polymorphic
+  // inlining). v is known to be X within the body of B.f, but not guaranteed to
+  // be X outside of it. Thus we must ensure that all operations with v that
+  // depend on its type being X are pinned to stay within the inlined body.
+  //
+  // We achieve that by inserting redefinitions for parameters which potentially
+  // have narrower types in callee compared to those in the interface target of
+  // the call.
+  BitVector* is_generic_covariant_impl = nullptr;
+  if (no_checks && callee.IsRegularFunction()) {
+    const Function& interface_target =
+        call_data->call->AsInstanceCallBase()->interface_target();
+
+    callee_signature = callee.signature();
+    interface_target_signature = interface_target.signature();
+
+    // If signatures match then there is nothing to do.
+    if (interface_target.signature() != callee.signature()) {
+      const intptr_t num_params = callee.NumParameters();
+      BitVector is_covariant(zone, num_params);
+      is_generic_covariant_impl = new (zone) BitVector(zone, num_params);
+
+      kernel::ReadParameterCovariance(callee_graph->function(), &is_covariant,
+                                      is_generic_covariant_impl);
+    }
+  }
 
   // Replace each stub with the actual argument or the caller's constant.
   // Nulls denote optional parameters for which no actual was given.
   const intptr_t first_arg_index = call_data->first_arg_index;
+
   // When first_arg_index > 0, the stub and actual argument processed in the
   // first loop iteration represent a passed-in type argument vector.
   GrowableArray<Value*>* arguments = call_data->arguments;
@@ -654,17 +764,29 @@
   }
   for (intptr_t i = 0; i < arguments->length(); ++i) {
     Value* actual = (*arguments)[i];
-    Definition* defn = NULL;
-    if (is_polymorphic && (i == first_arg_index)) {
-      // Replace the receiver argument with a redefinition to prevent code from
-      // the inlined body from being hoisted above the inlined entry.
+    Definition* defn = nullptr;
+
+    // Replace the receiver argument with a redefinition to prevent code from
+    // the inlined body from being hoisted above the inlined entry.
+    const bool is_polymorphic_receiver =
+        (is_polymorphic && (i == first_arg_index));
+
+    if (actual == nullptr) {
+      ASSERT(!is_polymorphic_receiver);
+      continue;
+    }
+
+    if (is_polymorphic_receiver ||
+        CalleeParameterTypeMightBeMoreSpecific(
+            is_generic_covariant_impl, interface_target_signature,
+            callee_signature, first_arg_index, i)) {
       RedefinitionInstr* redefinition =
           new (zone) RedefinitionInstr(actual->Copy(zone));
       redefinition->set_ssa_temp_index(caller_graph->alloc_ssa_temp_index());
       if (FlowGraph::NeedsPairLocation(redefinition->representation())) {
         caller_graph->alloc_ssa_temp_index();
       }
-      if (target_info->IsSingleCid()) {
+      if (is_polymorphic_receiver && target_info->IsSingleCid()) {
         redefinition->UpdateType(CompileType::FromCid(target_info->cid_start));
       }
       redefinition->InsertAfter(callee_entry);
@@ -674,13 +796,12 @@
       callee_entry->ReplaceInEnvironment(
           call_data->parameter_stubs->At(first_arg_stub_index + i),
           actual->definition());
-    } else if (actual != NULL) {
+    } else {
       defn = actual->definition();
     }
-    if (defn != NULL) {
-      call_data->parameter_stubs->At(first_arg_stub_index + i)
-          ->ReplaceUsesWith(defn);
-    }
+
+    call_data->parameter_stubs->At(first_arg_stub_index + i)
+        ->ReplaceUsesWith(defn);
   }
 
   // Replace remaining constants with uses by constants in the caller's
@@ -688,7 +809,7 @@
   auto defns = callee_graph->graph_entry()->initial_definitions();
   for (intptr_t i = 0; i < defns->length(); ++i) {
     ConstantInstr* constant = (*defns)[i]->AsConstant();
-    if (constant != NULL && constant->HasUses()) {
+    if (constant != nullptr && constant->HasUses()) {
       constant->ReplaceUsesWith(caller_graph->GetConstant(constant->value()));
     }
   }
@@ -696,12 +817,12 @@
   defns = callee_graph->graph_entry()->normal_entry()->initial_definitions();
   for (intptr_t i = 0; i < defns->length(); ++i) {
     ConstantInstr* constant = (*defns)[i]->AsConstant();
-    if (constant != NULL && constant->HasUses()) {
+    if (constant != nullptr && constant->HasUses()) {
       constant->ReplaceUsesWith(caller_graph->GetConstant(constant->value()));
     }
 
     SpecialParameterInstr* param = (*defns)[i]->AsSpecialParameter();
-    if (param != NULL && param->HasUses()) {
+    if (param != nullptr && param->HasUses()) {
       switch (param->kind()) {
         case SpecialParameterInstr::kContext: {
           ASSERT(!is_polymorphic);
@@ -1422,9 +1543,8 @@
     // Plug result in the caller graph.
     InlineExitCollector* exit_collector = call_data->exit_collector;
     exit_collector->PrepareGraphs(callee_graph);
-    exit_collector->ReplaceCall(callee_function_entry);
-
     ReplaceParameterStubs(zone(), caller_graph_, call_data, NULL);
+    exit_collector->ReplaceCall(callee_function_entry);
 
     ASSERT(!call_data->call->HasPushArguments());
   }
diff --git a/runtime/vm/compiler/backend/inliner_test.cc b/runtime/vm/compiler/backend/inliner_test.cc
index f5c59aa..4760a70 100644
--- a/runtime/vm/compiler/backend/inliner_test.cc
+++ b/runtime/vm/compiler/backend/inliner_test.cc
@@ -188,7 +188,8 @@
       {kMatchAndMoveStoreIndexed, &store_instr},
   }));
 
-  RELEASE_ASSERT(unbox_instr->InputAt(0)->definition() == value_param);
+  RELEASE_ASSERT(unbox_instr->InputAt(0)->definition()->OriginalDefinition() ==
+                 value_param);
   RELEASE_ASSERT(store_instr->InputAt(0)->definition() == list_param);
   RELEASE_ASSERT(store_instr->InputAt(2)->definition() == unbox_instr);
   RELEASE_ASSERT(unbox_instr->is_truncating());
diff --git a/runtime/vm/compiler/backend/range_analysis.cc b/runtime/vm/compiler/backend/range_analysis.cc
index 74a7257..7d02f87 100644
--- a/runtime/vm/compiler/backend/range_analysis.cc
+++ b/runtime/vm/compiler/backend/range_analysis.cc
@@ -3091,7 +3091,22 @@
   }
 }
 
+void AssertAssignableInstr::InferRange(RangeAnalysis* analysis, Range* range) {
+  const Range* value_range = value()->definition()->range();
+  if (!Range::IsUnknown(value_range)) {
+    *range = *value_range;
+  } else {
+    *range = Range::Full(RangeBoundary::kRangeBoundaryInt64);
+  }
+}
+
 static bool IsRedundantBasedOnRangeInformation(Value* index, Value* length) {
+  if (index->BindsToSmiConstant() && length->BindsToSmiConstant()) {
+    const auto index_val = index->BoundSmiConstant();
+    const auto length_val = length->BoundSmiConstant();
+    return (0 <= index_val && index_val < length_val);
+  }
+
   // Range of the index is unknown can't decide if the check is redundant.
   Range* index_range = index->definition()->range();
   if (index_range == nullptr) {
diff --git a/runtime/vm/compiler/backend/redundancy_elimination.cc b/runtime/vm/compiler/backend/redundancy_elimination.cc
index 75a83b4..67f8112 100644
--- a/runtime/vm/compiler/backend/redundancy_elimination.cc
+++ b/runtime/vm/compiler/backend/redundancy_elimination.cc
@@ -1921,6 +1921,72 @@
             (array_store->class_id() == kTypedDataFloat32x4ArrayCid));
   }
 
+  static bool AlreadyPinnedByRedefinition(Definition* replacement,
+                                          Definition* redefinition) {
+    Definition* defn = replacement;
+    if (auto load_field = replacement->AsLoadField()) {
+      defn = load_field->instance()->definition();
+    } else if (auto load_indexed = replacement->AsLoadIndexed()) {
+      defn = load_indexed->array()->definition();
+    }
+
+    Value* unwrapped;
+    while ((unwrapped = defn->RedefinedValue()) != nullptr) {
+      if (defn == redefinition) {
+        return true;
+      }
+      defn = unwrapped->definition();
+    }
+
+    return false;
+  }
+
+  Definition* ReplaceLoad(Definition* load, Definition* replacement) {
+    // When replacing a load from a generic field or from an array element
+    // check if instance we are loading from is redefined. If it is then
+    // we need to ensure that replacement is not going to break the
+    // dependency chain.
+    Definition* redef = nullptr;
+    if (auto load_field = load->AsLoadField()) {
+      auto instance = load_field->instance()->definition();
+      if (instance->RedefinedValue() != nullptr) {
+        if ((load_field->slot().kind() ==
+                 Slot::Kind::kGrowableObjectArray_data ||
+             (load_field->slot().IsDartField() &&
+              !AbstractType::Handle(load_field->slot().field().type())
+                   .IsInstantiated()))) {
+          redef = instance;
+        }
+      }
+    } else if (auto load_indexed = load->AsLoadIndexed()) {
+      if (load_indexed->class_id() == kArrayCid ||
+          load_indexed->class_id() == kImmutableArrayCid) {
+        auto instance = load_indexed->array()->definition();
+        if (instance->RedefinedValue() != nullptr) {
+          redef = instance;
+        }
+      }
+    }
+    if (redef != nullptr && !AlreadyPinnedByRedefinition(replacement, redef)) {
+      // Original load had a redefined instance and replacement does not
+      // depend on the same redefinition. Create a redefinition
+      // of the replacement to keep the dependency chain.
+      auto replacement_redefinition =
+          new (zone()) RedefinitionInstr(new (zone()) Value(replacement));
+      if (redef->IsDominatedBy(replacement)) {
+        graph_->InsertAfter(redef, replacement_redefinition, /*env=*/nullptr,
+                            FlowGraph::kValue);
+      } else {
+        graph_->InsertBefore(load, replacement_redefinition, /*env=*/nullptr,
+                             FlowGraph::kValue);
+      }
+      replacement = replacement_redefinition;
+    }
+
+    load->ReplaceUsesWith(replacement);
+    return replacement;
+  }
+
   // Compute sets of loads generated and killed by each block.
   // Additionally compute upwards exposed and generated loads for each block.
   // Exposed loads are those that can be replaced if a corresponding
@@ -2142,7 +2208,7 @@
                       defn->ssa_temp_index(), replacement->ssa_temp_index());
           }
 
-          defn->ReplaceUsesWith(replacement);
+          ReplaceLoad(defn, replacement);
           instr_it.RemoveCurrentFromGraph();
           forwarded_ = true;
           continue;
@@ -2515,7 +2581,7 @@
                       load->ssa_temp_index(), replacement->ssa_temp_index());
           }
 
-          load->ReplaceUsesWith(replacement);
+          replacement = ReplaceLoad(load, replacement);
           load->RemoveFromGraph();
           load->SetReplacement(replacement);
           forwarded_ = true;
diff --git a/runtime/vm/compiler/backend/type_propagator.cc b/runtime/vm/compiler/backend/type_propagator.cc
index 8e110dd..fbae33b 100644
--- a/runtime/vm/compiler/backend/type_propagator.cc
+++ b/runtime/vm/compiler/backend/type_propagator.cc
@@ -366,9 +366,14 @@
 }
 
 void FlowGraphTypePropagator::VisitAssertAssignable(
-    AssertAssignableInstr* instr) {
-  SetTypeOf(instr->value()->definition(),
-            new (zone()) CompileType(instr->ComputeType()));
+    AssertAssignableInstr* check) {
+  auto defn = check->value()->definition();
+  SetTypeOf(defn, new (zone()) CompileType(check->ComputeType()));
+  if (check->ssa_temp_index() == -1) {
+    flow_graph_->AllocateSSAIndexes(check);
+    GrowTypes(check->ssa_temp_index() + 1);
+  }
+  FlowGraph::RenameDominatedUses(defn, check, check);
 }
 
 void FlowGraphTypePropagator::VisitAssertBoolean(AssertBooleanInstr* instr) {
diff --git a/runtime/vm/compiler/compiler_pass.cc b/runtime/vm/compiler/compiler_pass.cc
index 7b15f39..4bbce2c 100644
--- a/runtime/vm/compiler/compiler_pass.cc
+++ b/runtime/vm/compiler/compiler_pass.cc
@@ -573,6 +573,7 @@
   flow_graph->function().set_inlining_depth(state->inlining_depth);
   // Remove redefinitions for the rest of the pipeline.
   flow_graph->RemoveRedefinitions();
+  flow_graph->Canonicalize();
 });
 
 COMPILER_PASS(GenerateCode, { state->graph_compiler->CompileGraph(); });
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
index 08e04f9..c1daced 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
@@ -1627,11 +1627,13 @@
     const InferredTypeMetadata* result_type,
     bool use_unchecked_entry,
     const CallSiteAttributesMetadata* call_site_attrs,
-    bool receiver_is_not_smi) {
+    bool receiver_is_not_smi,
+    bool is_call_on_this) {
   return flow_graph_builder_->InstanceCall(
       position, name, kind, type_args_len, argument_count, argument_names,
       checked_argument_count, interface_target, tearoff_interface_target,
-      result_type, use_unchecked_entry, call_site_attrs, receiver_is_not_smi);
+      result_type, use_unchecked_entry, call_site_attrs, receiver_is_not_smi,
+      is_call_on_this);
 }
 
 Fragment StreamingFlowGraphBuilder::ThrowException(TokenPosition position) {
@@ -2420,7 +2422,8 @@
   const TokenPosition position = ReadPosition();  // read position.
   if (p != nullptr) *p = position;
 
-  if (PeekTag() == kThisExpression) {
+  const bool is_call_on_this = PeekTag() == kThisExpression;
+  if (is_call_on_this) {
     is_unchecked_call = true;
   }
   instructions += BuildExpression();  // read receiver.
@@ -2464,7 +2467,8 @@
         Array::null_array(), kNumArgsChecked, interface_target,
         Function::null_function(),
         /*result_type=*/nullptr,
-        /*use_unchecked_entry=*/is_unchecked_call, &call_site_attributes);
+        /*use_unchecked_entry=*/is_unchecked_call, &call_site_attributes,
+        /*receiver_not_smi=*/false, is_call_on_this);
   }
 
   instructions += Drop();  // Drop result of the setter invocation.
@@ -2928,7 +2932,8 @@
 
   // Take note of whether the invocation is against the receiver of the current
   // function: in this case, we may skip some type checks in the callee.
-  if ((PeekTag() == kThisExpression) && !is_dynamic) {
+  const bool is_call_on_this = (PeekTag() == kThisExpression) && !is_dynamic;
+  if (is_call_on_this) {
     is_unchecked_call = true;
   }
   instructions += BuildExpression();  // read receiver.
@@ -3035,12 +3040,12 @@
                    argument_names, ICData::kNoRebind, &result_type,
                    type_args_len, /*use_unchecked_entry=*/is_unchecked_call);
   } else {
-    instructions +=
-        InstanceCall(position, *mangled_name, token_kind, type_args_len,
-                     argument_count, argument_names, checked_argument_count,
-                     *interface_target, Function::null_function(), &result_type,
-                     /*use_unchecked_entry=*/is_unchecked_call,
-                     &call_site_attributes, result_type.ReceiverNotInt());
+    instructions += InstanceCall(
+        position, *mangled_name, token_kind, type_args_len, argument_count,
+        argument_names, checked_argument_count, *interface_target,
+        Function::null_function(), &result_type,
+        /*use_unchecked_entry=*/is_unchecked_call, &call_site_attributes,
+        result_type.ReceiverNotInt(), is_call_on_this);
   }
 
   // Drop temporaries preserving result on the top of the stack.
@@ -3880,12 +3885,10 @@
     // We do not care whether the 'as' cast as implicitly added by the frontend
     // or explicitly written by the user, in both cases we use an assert
     // assignable.
-    instructions += LoadLocal(MakeTemporary());
     instructions += B->AssertAssignableLoadTypeArguments(
         position, type,
         is_type_error ? Symbols::Empty() : Symbols::InTypeCast(),
         AssertAssignableInstr::kInsertedByFrontend);
-    instructions += Drop();
   }
   return instructions;
 }
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
index 0f40918..47ed58b 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
@@ -199,7 +199,8 @@
       const InferredTypeMetadata* result_type = nullptr,
       bool use_unchecked_entry = false,
       const CallSiteAttributesMetadata* call_site_attrs = nullptr,
-      bool receiver_is_not_smi = false);
+      bool receiver_is_not_smi = false,
+      bool is_call_on_this = false);
 
   Fragment ThrowException(TokenPosition position);
   Fragment BooleanNegate();
diff --git a/runtime/vm/compiler/frontend/kernel_to_il.cc b/runtime/vm/compiler/frontend/kernel_to_il.cc
index efd2f8e..c993d10 100644
--- a/runtime/vm/compiler/frontend/kernel_to_il.cc
+++ b/runtime/vm/compiler/frontend/kernel_to_il.cc
@@ -347,7 +347,8 @@
     const InferredTypeMetadata* result_type,
     bool use_unchecked_entry,
     const CallSiteAttributesMetadata* call_site_attrs,
-    bool receiver_is_not_smi) {
+    bool receiver_is_not_smi,
+    bool is_call_on_this) {
   const intptr_t total_count = argument_count + (type_args_len > 0 ? 1 : 0);
   InputsArray* arguments = GetArguments(total_count);
   InstanceCallInstr* call = new (Z) InstanceCallInstr(
@@ -360,6 +361,9 @@
   if (use_unchecked_entry) {
     call->set_entry_kind(Code::EntryKind::kUnchecked);
   }
+  if (is_call_on_this) {
+    call->mark_as_call_on_this();
+  }
   if (call_site_attrs != nullptr && call_site_attrs->receiver_type != nullptr &&
       call_site_attrs->receiver_type->IsInstantiated()) {
     call->set_receivers_static_type(call_site_attrs->receiver_type);
@@ -1769,7 +1773,7 @@
   if (auto const alloc = definition->AsAllocateClosure()) {
     return !alloc->known_function().IsNull();
   }
-  return definition->IsLoadLocal();
+  return definition->IsLoadLocal() || definition->IsAssertAssignable();
 }
 
 Fragment FlowGraphBuilder::EvaluateAssertion() {
diff --git a/runtime/vm/compiler/frontend/kernel_to_il.h b/runtime/vm/compiler/frontend/kernel_to_il.h
index 6dfc141..42573d0 100644
--- a/runtime/vm/compiler/frontend/kernel_to_il.h
+++ b/runtime/vm/compiler/frontend/kernel_to_il.h
@@ -191,7 +191,8 @@
       const InferredTypeMetadata* result_type = nullptr,
       bool use_unchecked_entry = false,
       const CallSiteAttributesMetadata* call_site_attrs = nullptr,
-      bool receiver_is_not_smi = false);
+      bool receiver_is_not_smi = false,
+      bool is_call_on_this = false);
 
   Fragment FfiCall(const compiler::ffi::CallMarshaller& marshaller);