Insert implicit casts when setting interface targets

This preserves the invariant that method invocations and property sets
with an interface target handle static types at the call site.

This is a prerequisite to improving dart2js with implicit checks.
With this we can mark dynamic invocations a statically safe and
discover when instance methods don't need to check parameter types.

Change-Id: Ife7a3f947952b3c1f73316b922b2aa107343068e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/95400
Commit-Queue: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/io/kernel_source_information.dart b/pkg/compiler/lib/src/io/kernel_source_information.dart
index 3e63370..af69c56 100644
--- a/pkg/compiler/lib/src/io/kernel_source_information.dart
+++ b/pkg/compiler/lib/src/io/kernel_source_information.dart
@@ -109,6 +109,8 @@
       location = node.location;
       offset = node.fileOffset;
     }
+    assert(
+        location != null, "No location found for $node (${node.runtimeType})");
     return new KernelSourceLocation(location, offset, name);
   }
 
diff --git a/pkg/compiler/lib/src/ir/static_type.dart b/pkg/compiler/lib/src/ir/static_type.dart
index b8d423c..33ef11e 100644
--- a/pkg/compiler/lib/src/ir/static_type.dart
+++ b/pkg/compiler/lib/src/ir/static_type.dart
@@ -249,11 +249,35 @@
   ir.DartType visitPropertySet(ir.PropertySet node) {
     ir.DartType receiverType = visitNode(node.receiver);
     ir.DartType valueType = super.visitPropertySet(node);
-    if (node.interfaceTarget == null && receiverType is ir.InterfaceType) {
-      node.interfaceTarget = hierarchy
+    ir.Member interfaceTarget = node.interfaceTarget;
+    if (interfaceTarget == null && receiverType is ir.InterfaceType) {
+      interfaceTarget = hierarchy
           .getInterfaceMember(receiverType.classNode, node.name, setter: true);
+      if (interfaceTarget != null) {
+        ir.Class superclass = interfaceTarget.enclosingClass;
+        ir.Substitution receiverSubstitution =
+            ir.Substitution.fromInterfaceType(
+                getTypeAsInstanceOf(receiverType, superclass));
+        ir.DartType setterType =
+            receiverSubstitution.substituteType(interfaceTarget.setterType);
+        if (!typeEnvironment.isSubtypeOf(valueType, setterType)) {
+          // We need to insert an implicit cast to preserve the invariant that
+          // a property set with a known interface target is also statically
+          // checked.
+          ir.AsExpression implicitCast =
+              new ir.AsExpression(node.value, setterType)
+                ..isTypeError = true
+                ..parent = node;
+          node.value = implicitCast;
+          // Visit the newly created as expression; the original value has
+          // already been visited.
+          handleAsExpression(implicitCast, valueType);
+          valueType = setterType;
+        }
+        node.interfaceTarget = interfaceTarget;
+      }
     }
-    receiverType = _narrowInstanceReceiver(node.interfaceTarget, receiverType);
+    receiverType = _narrowInstanceReceiver(interfaceTarget, receiverType);
     handlePropertySet(node, receiverType, valueType);
     return valueType;
   }
@@ -415,6 +439,106 @@
     return false;
   }
 
+  /// Update the interface target on [node].
+  ///
+  /// This inserts any implicit cast of the arguments necessary to uphold the
+  /// invariant that a method invocation with an interface target handles
+  /// the static types at the call site.
+  void _updateMethodInvocationTarget(
+      ir.MethodInvocation node,
+      ArgumentTypes argumentTypes,
+      ir.DartType functionType,
+      ir.Member interfaceTarget) {
+    // TODO(johnniwinther): Handle incremental target improvement.
+    if (node.interfaceTarget != null) return;
+    node.interfaceTarget = interfaceTarget;
+    if (functionType is! ir.FunctionType) return;
+    ir.FunctionType parameterTypes = functionType;
+    Map<int, ir.DartType> neededPositionalChecks = {};
+    for (int i = 0; i < node.arguments.positional.length; i++) {
+      ir.DartType argumentType = argumentTypes.positional[i];
+      ir.DartType parameterType = parameterTypes.positionalParameters[i];
+      if (!typeEnvironment.isSubtypeOf(argumentType, parameterType)) {
+        neededPositionalChecks[i] = parameterType;
+      }
+    }
+    Map<int, ir.DartType> neededNamedChecks = {};
+    for (int argumentIndex = 0;
+        argumentIndex < node.arguments.named.length;
+        argumentIndex++) {
+      ir.NamedExpression namedArgument = node.arguments.named[argumentIndex];
+      ir.DartType argumentType = argumentTypes.named[argumentIndex];
+      ir.DartType parameterType = parameterTypes.namedParameters
+          .singleWhere((namedType) => namedType.name == namedArgument.name)
+          .type;
+      if (!typeEnvironment.isSubtypeOf(argumentType, parameterType)) {
+        neededNamedChecks[argumentIndex] = parameterType;
+      }
+    }
+    if (neededPositionalChecks.isEmpty && neededNamedChecks.isEmpty) {
+      // No implicit casts needed
+      return;
+    }
+
+    List<ir.VariableDeclaration> letVariables = [];
+
+    // Arguments need to be hoisted to an enclosing let expression in order
+    // to ensure that the arguments are evaluated before any implicit cast.
+
+    ir.Expression updateArgument(ir.Expression expression, ir.TreeNode parent,
+        ir.DartType argumentType, ir.DartType checkedParameterType) {
+      ir.VariableDeclaration variable =
+          new ir.VariableDeclaration.forValue(expression, type: argumentType);
+      // Visit the newly created variable declaration.
+      handleVariableDeclaration(variable);
+      letVariables.add(variable);
+      ir.VariableGet get = new ir.VariableGet(variable)..parent = parent;
+      // Visit the newly created variable get.
+      handleVariableGet(get, argumentType);
+      cachedStaticTypes[get] = argumentType;
+
+      if (checkedParameterType == null) {
+        return get;
+      }
+      // We need to insert an implicit cast to preserve the invariant that
+      // a method invocation with a known interface target is also
+      // statically checked.
+      ir.AsExpression implicitCast =
+          new ir.AsExpression(get, checkedParameterType)
+            ..isTypeError = true
+            ..parent = parent;
+      // Visit the newly created as expression; the original value has
+      // already been visited.
+      handleAsExpression(implicitCast, argumentType);
+      return implicitCast;
+    }
+
+    for (int index = 0; index < node.arguments.positional.length; index++) {
+      ir.DartType argumentType = argumentTypes.positional[index];
+      node.arguments.positional[index] = updateArgument(
+          node.arguments.positional[index],
+          node.arguments,
+          argumentType,
+          neededPositionalChecks[index]);
+    }
+    for (int argumentIndex = 0;
+        argumentIndex < node.arguments.named.length;
+        argumentIndex++) {
+      ir.NamedExpression namedArgument = node.arguments.named[argumentIndex];
+      ir.DartType argumentType = argumentTypes.named[argumentIndex];
+      namedArgument.value = updateArgument(namedArgument.value, namedArgument,
+          argumentType, neededNamedChecks[argumentIndex]);
+    }
+
+    ir.Expression dummy = new ir.NullLiteral();
+    node.replaceWith(dummy);
+    ir.Expression body = node;
+    for (ir.VariableDeclaration variable in letVariables.reversed) {
+      body = new ir.Let(variable, body);
+    }
+    dummy.replaceWith(body);
+  }
+
   /// Computes the result type of the method invocation [node] on a receiver of
   /// type [receiverType].
   ///
@@ -436,20 +560,17 @@
       ir.Member member =
           hierarchy.getInterfaceMember(receiverType.classNode, node.name);
       if (_isApplicable(node.arguments, member)) {
-        interfaceTarget = node.interfaceTarget = member;
+        interfaceTarget = member;
       }
     }
     if (interfaceTarget != null) {
-      if (isSpecialCasedBinaryOperator(interfaceTarget)) {
-        ir.DartType argumentType = argumentTypes.positional[0];
-        return typeEnvironment.getTypeOfOverloadedArithmetic(
-            receiverType, argumentType);
-      }
       ir.Class superclass = interfaceTarget.enclosingClass;
-      receiverType = getTypeAsInstanceOf(receiverType, superclass);
-      ir.DartType getterType = ir.Substitution.fromInterfaceType(receiverType)
-          .substituteType(interfaceTarget.getterType);
+      ir.Substitution receiverSubstitution = ir.Substitution.fromInterfaceType(
+          getTypeAsInstanceOf(receiverType, superclass));
+      ir.DartType getterType =
+          receiverSubstitution.substituteType(interfaceTarget.getterType);
       if (getterType is ir.FunctionType) {
+        ir.FunctionType functionType = getterType;
         List<ir.DartType> typeArguments = node.arguments.types;
         if (interfaceTarget is ir.Procedure &&
             interfaceTarget.function.typeParameters.isNotEmpty &&
@@ -457,15 +578,22 @@
           // If this was a dynamic call the invocation does not have the
           // inferred default type arguments so we need to create them here
           // to perform a valid substitution.
-          ir.Substitution substitution =
-              ir.Substitution.fromInterfaceType(receiverType);
           typeArguments = interfaceTarget.function.typeParameters
-              .map((t) => substitution.substituteType(t.defaultType))
+              .map((t) => receiverSubstitution.substituteType(t.defaultType))
               .toList();
         }
-        return ir.Substitution.fromPairs(
-                getterType.typeParameters, typeArguments)
-            .substituteType(getterType.returnType);
+        getterType = ir.Substitution.fromPairs(
+                functionType.typeParameters, typeArguments)
+            .substituteType(functionType.withoutTypeParameters);
+      }
+      _updateMethodInvocationTarget(
+          node, argumentTypes, getterType, interfaceTarget);
+      if (isSpecialCasedBinaryOperator(interfaceTarget)) {
+        ir.DartType argumentType = argumentTypes.positional[0];
+        return typeEnvironment.getTypeOfOverloadedArithmetic(
+            receiverType, argumentType);
+      } else if (getterType is ir.FunctionType) {
+        return getterType.returnType;
       } else {
         return const ir.DynamicType();
       }
diff --git a/tests/compiler/dart2js/impact/data/injected_cast.dart b/tests/compiler/dart2js/impact/data/injected_cast.dart
new file mode 100644
index 0000000..526414c
--- /dev/null
+++ b/tests/compiler/dart2js/impact/data/injected_cast.dart
@@ -0,0 +1,220 @@
+// Copyright (c) 2019, 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.
+
+class A {}
+
+class B {}
+
+class C {}
+
+class D {}
+
+class E {}
+
+/*element: Class1.:static=[Object.(0)]*/
+class Class1 {
+  /*element: Class1.field1:type=[inst:JSBool,inst:JSNull,param:A]*/
+  A field1;
+}
+
+/*element: method1:
+ dynamic=[Class1.field1=],
+ type=[
+  impl:A,
+  inst:JSBool,
+  is:Class1]
+*/
+method1(dynamic o, dynamic value) {
+  if (o is! Class1) return;
+  o.field1 = value;
+}
+
+/*element: Class2.:static=[Object.(0)]*/
+class Class2<T> {
+  /*element: Class2.field2:
+   static=*,
+   type=[inst:*,param:Class2.T]
+   */
+  T field2;
+}
+
+/*element: method2:
+ dynamic=[Class2.field2=],
+ static=*,
+ type=[
+  impl:A,
+  inst:*,
+  is:Class2<A>]
+*/
+method2(dynamic o, dynamic value) {
+  if (o is! Class2<A>) return;
+  o.field2 = value;
+}
+
+/*element: Class3.:static=[Object.(0)]*/
+class Class3 {
+  /*element: Class3.method3:type=[inst:JSBool,inst:JSNull,param:A,param:B,param:C]*/
+  method3(A a, [B b, C c]) {}
+}
+
+/*element: method3:
+ dynamic=[Class3.method3(3)],
+ type=[
+  impl:A,
+  impl:C,
+  inst:JSBool,
+  is:Class3,
+  param:B]
+*/
+method3(dynamic o, dynamic a, B b, dynamic c) {
+  if (o is! Class3) return;
+  o.method3(a, b, c);
+}
+
+/*element: Class4.:static=[Object.(0)]*/
+class Class4 {
+  /*element: Class4.method4:
+   type=[inst:JSBool,inst:JSNull,param:A,param:B,param:C]
+  */
+  method4(A a, {B b, C c}) {}
+}
+
+/*element: method4:
+ dynamic=[Class4.method4(1,b,c)],
+ type=[
+  impl:A,
+  impl:C,
+  inst:JSBool,
+  is:Class4,
+  param:B]
+*/
+method4(dynamic o, dynamic a, B b, dynamic c) {
+  if (o is! Class4) return;
+  o.method4(a, c: c, b: b);
+}
+
+/*element: Class5.:static=[Object.(0)]*/
+class Class5<T1, T2> {
+  /*element: Class5.method5:
+   static=*,
+   type=[
+    inst:*,
+    param:C,
+    param:Class5.T1,
+    param:Class5.T2,
+    param:Object,
+    param:method5.S1,
+    param:method5.S2]
+  */
+  method5<S1, S2>(T1 a, [T2 b, C c, S1 d, S2 e]) {}
+}
+
+/*element: method5:
+ dynamic=[Class5.method5<D,E>(5)],
+ static=*,
+ type=[
+  impl:A,
+  impl:D,
+  inst:*,
+  is:Class5<A,B>,
+  param:B,
+  param:C,
+  param:E]
+*/
+method5(dynamic o, dynamic a, B b, C c, dynamic d, E e) {
+  if (o is! Class5<A, B>) return;
+  o.method5<D, E>(a, b, c, d, e);
+}
+
+/*element: Class6.:static=[Object.(0)]*/
+class Class6<T1, T2> {
+  /*element: Class6.method6:
+   static=*,
+   type=[
+    inst:*,
+    param:C,
+    param:Class6.T1,
+    param:Class6.T2,
+    param:Object,
+    param:method6.S1,
+    param:method6.S2]
+  */
+  method6<S1, S2>(T1 a, {T2 b, C c, S1 d, S2 e}) {}
+}
+
+/*element: method6:
+ dynamic=[Class6.method6<D,E>(1,b,c,d,e)],
+ static=*,
+ type=[
+  impl:A,
+  impl:D,
+  inst:*,
+  is:Class6<A,B>,
+  param:B,
+  param:C,
+  param:E]
+*/
+method6(dynamic o, dynamic a, B b, C c, dynamic d, E e) {
+  if (o is! Class6<A, B>) return;
+  o.method6<D, E>(a, d: d, b: b, e: e, c: c);
+}
+
+/*element: Class7.:static=[Object.(0)]*/
+class Class7 {
+  /*element: Class7.f:type=[inst:JSNull]*/
+  A Function(A) get f => null;
+}
+
+/*element: method7:
+ dynamic=[Class7.f(1),call(1)],
+ type=[impl:A,inst:JSBool,is:Class7]
+*/
+method7(dynamic o, dynamic a) {
+  if (o is! Class7) return;
+  o.f(a);
+}
+
+/*element: F.:static=[Object.(0)]*/
+class F<T> {
+  /*element: F.method:static=*,type=[inst:*,param:List<F.T>]*/
+  T method(List<T> list) => null;
+
+  /*element: F.field:static=*,type=[inst:*,param:F.T]*/
+  T field;
+}
+
+/*element: G.:static=[F.(0)]*/
+class G extends F<int> {}
+
+/*element: method8:
+ dynamic=[G.method(1)],
+ static=*,
+ type=[impl:List<int>,inst:*,is:G,param:Iterable<int>]
+*/
+method8(dynamic g, Iterable<int> iterable) {
+  if (g is! G) return null;
+  return g.method(iterable);
+}
+
+/*element: method9:
+ dynamic=[G.field=],
+ type=[impl:int,inst:JSBool,inst:JSNull,is:G,param:num]
+*/
+method9(dynamic g, num value) {
+  if (g is! G) return null;
+  return g.field = value;
+}
+
+/*element: main:**/
+main() {
+  method1(new Class1(), null);
+  method2(new Class2<A>(), null);
+  method3(new Class3(), null, null, null);
+  method4(new Class4(), null, null, null);
+  method5(new Class5<A, B>(), null, null, null, null, null);
+  method6(new Class6<A, B>(), null, null, null, null, null);
+  method7(new Class7(), null);
+  method8(new G(), null);
+  method9(new G(), null);
+}
diff --git a/tests/compiler/dart2js/inference/data/map_tracer_keys.dart b/tests/compiler/dart2js/inference/data/map_tracer_keys.dart
index af96537..576a02a 100644
--- a/tests/compiler/dart2js/inference/data/map_tracer_keys.dart
+++ b/tests/compiler/dart2js/inference/data/map_tracer_keys.dart
@@ -58,16 +58,16 @@
 test2() {
   dynamic theMap = {'a': 2.2, 'b': 3.3, 'c': 4.4};
   theMap
-      /*update: Map([subclass=JsLinkedHashMap], key: Union([exact=JSExtendableArray], [exact=JSString]), value: [null|exact=JSDouble])*/
+      /*update: Map([subclass=JsLinkedHashMap], key: [exact=JSString], value: [null|exact=JSDouble])*/
       [aList2] = 5.5;
   /*iterator: [exact=LinkedHashMapKeyIterable]*/
   /*current: [exact=LinkedHashMapKeyIterator]*/
   /*moveNext: [exact=LinkedHashMapKeyIterator]*/
   for (var key in theMap.
-      /*Map([subclass=JsLinkedHashMap], key: Union([exact=JSExtendableArray], [exact=JSString]), value: [null|exact=JSDouble])*/
+      /*Map([subclass=JsLinkedHashMap], key: [exact=JSString], value: [null|exact=JSDouble])*/
       keys) {
     aDouble2 = theMap
-        /*Map([subclass=JsLinkedHashMap], key: Union([exact=JSExtendableArray], [exact=JSString]), value: [null|exact=JSDouble])*/
+        /*Map([subclass=JsLinkedHashMap], key: [exact=JSString], value: [null|exact=JSDouble])*/
         [key];
   }
   // We have to reference it somewhere, so that it always gets resolved.
diff --git a/tests/compiler/dart2js/optimization/data/index.dart b/tests/compiler/dart2js/optimization/data/index.dart
index 12586ad..b6dd480 100644
--- a/tests/compiler/dart2js/optimization/data/index.dart
+++ b/tests/compiler/dart2js/optimization/data/index.dart
@@ -40,6 +40,7 @@
 /*strong.element: mutableDynamicListDynamicIndex:Specializer=[!Index]*/
 /*omit.element: mutableDynamicListDynamicIndex:Specializer=[Index]*/
 @pragma('dart2js:noInline')
+@pragma('dart2js:disableFinal')
 mutableDynamicListDynamicIndex(dynamic index) {
   dynamic list = [0];
   return list[index];
diff --git a/tests/compiler/dart2js/rti/emission/generic_methods_dynamic_02.dart b/tests/compiler/dart2js/rti/emission/generic_methods_dynamic_02.dart
index e091714..947dcca 100644
--- a/tests/compiler/dart2js/rti/emission/generic_methods_dynamic_02.dart
+++ b/tests/compiler/dart2js/rti/emission/generic_methods_dynamic_02.dart
@@ -20,6 +20,7 @@
   List<T> bar<T>(Iterable<T> t) => <T>[t.first];
 }
 
+@pragma('dart2js:disableFinal')
 main() {
   B b = new B();
   C c = new C();
diff --git a/tests/compiler/dart2js_extra/injected_cast_test.dart b/tests/compiler/dart2js_extra/injected_cast_test.dart
new file mode 100644
index 0000000..3ab671c
--- /dev/null
+++ b/tests/compiler/dart2js_extra/injected_cast_test.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2019, 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:expect/expect.dart';
+
+var field;
+
+class Class {
+  @pragma('dart2js:noInline')
+  method(int i, int j, int k) {}
+}
+
+@pragma('dart2js:noInline')
+test1(dynamic c, num n) {
+  if (c is Class) {
+    c.method(field = 41, n, field = 42);
+  }
+}
+
+@pragma('dart2js:noInline')
+test2(dynamic c, num n) {
+  if (c is! Class) return;
+  c.method(field = 86, n, field = 87);
+}
+
+main() {
+  try {
+    test1(new Class(), 0.5);
+    field = 123;
+    field = 123;
+  } catch (e) {}
+  // CFE inserts the implicit cast directly on the argument expression, making
+  // it fail before later arguments are evaluated.
+  Expect.equals(41, field);
+
+  try {
+    test2(new Class(), 0.5);
+    field = 321;
+    field = 321;
+  } catch (e) {}
+  // dart2js inserts implicit casts, but does so after evaluating all arguments
+  // to ensure semantics match what it would be like to check the types when
+  // entering the callee.
+  Expect.equals(87, field);
+}