Handle effectively constant fields in class stub generator

Closes #36222

Change-Id: Ic8b7a3963e1364903b4b2d7871ff0b12cb134572
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/97082
Commit-Queue: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/js_emitter/class_stub_generator.dart b/pkg/compiler/lib/src/js_emitter/class_stub_generator.dart
index fb31e92..81b9f9e 100644
--- a/pkg/compiler/lib/src/js_emitter/class_stub_generator.dart
+++ b/pkg/compiler/lib/src/js_emitter/class_stub_generator.dart
@@ -9,6 +9,7 @@
 import '../elements/entities.dart';
 import '../js/js.dart' as jsAst;
 import '../js/js.dart' show js;
+import '../js_backend/field_analysis.dart';
 import '../js_backend/namer.dart' show Namer;
 import '../js_backend/interceptor_data.dart' show InterceptorData;
 import '../options.dart';
@@ -108,8 +109,14 @@
         }
         return js('#.#()', [receiver, getterName]);
       } else {
-        jsAst.Name fieldName = _namer.instanceFieldPropertyName(member);
-        return js('#.#', [receiver, fieldName]);
+        FieldAnalysisData fieldData =
+            _closedWorld.fieldAnalysis.getFieldData(member);
+        if (fieldData.isEffectivelyConstant) {
+          return _emitter.constantReference(fieldData.constantValue);
+        } else {
+          jsAst.Name fieldName = _namer.instanceFieldPropertyName(member);
+          return js('#.#', [receiver, fieldName]);
+        }
       }
     }
 
diff --git a/tests/compiler/dart2js/codegen/model_data/capture.dart b/tests/compiler/dart2js/codegen/model_data/capture.dart
index ca9279e..5e44fb0 100644
--- a/tests/compiler/dart2js/codegen/model_data/capture.dart
+++ b/tests/compiler/dart2js/codegen/model_data/capture.dart
@@ -4,7 +4,7 @@
 
 /*element: method1:params=0*/
 @pragma('dart2js:noInline')
-method1([a]) => /*params=0*/ () => a;
+method1([a]) => /*access=[a],params=0*/ () => a;
 
 class Class {
   /*element: Class.f:emitted*/
@@ -13,14 +13,14 @@
 
   /*element: Class.capture:params=0*/
   @pragma('dart2js:noInline')
-  Class.capture([a]) : f = (/*params=0*/ () => a);
+  Class.capture([a]) : f = (/*access=[a],params=0*/ () => a);
 
   // TODO(johnniwinther): Remove the redundant assignment of elided boxed
   // parameters.
   /*element: Class.box:assign=[a,a],params=0*/
   @pragma('dart2js:noInline')
   Class.box([a])
-      : f = (/*assign=[a],params=0*/ () {
+      : f = (/*access=[_box_0],assign=[a],params=0*/ () {
           a = 42;
         });
 
@@ -30,12 +30,12 @@
 class Subclass extends Class {
   /*element: Subclass.capture:params=0*/
   @pragma('dart2js:noInline')
-  Subclass.capture([a]) : super.internal(/*params=0*/ () => a);
+  Subclass.capture([a]) : super.internal(/*access=[a],params=0*/ () => a);
 
   /*element: Subclass.box:assign=[a,a],params=0*/
   @pragma('dart2js:noInline')
   Subclass.box([a])
-      : super.internal(/*assign=[a],params=0*/ () {
+      : super.internal(/*access=[_box_0],assign=[a],params=0*/ () {
           a = 42;
         });
 }
diff --git a/tests/compiler/dart2js/codegen/model_data/regress_36222.dart b/tests/compiler/dart2js/codegen/model_data/regress_36222.dart
new file mode 100644
index 0000000..8ab6ea3
--- /dev/null
+++ b/tests/compiler/dart2js/codegen/model_data/regress_36222.dart
@@ -0,0 +1,27 @@
+// 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.
+
+typedef int BinaryFunc(int x, int y);
+
+class A {
+  const A({this.foo = A.defaultFoo});
+
+  /*element: A.defaultFoo:params=2*/
+  static int defaultFoo(int x, int y) {
+    return x + y;
+  }
+
+  /*element: A.foo:elided,stubCalls=[foo$2:call$2(arg0,arg1),foo$2:main_A_defaultFoo$closure(0)]*/
+  final BinaryFunc foo;
+}
+
+/*element: test:calls=[foo$2(2)],params=1*/
+@pragma('dart2js:assumeDynamic')
+@pragma('dart2js:noInline')
+test(dynamic a) => a.foo(1, 2);
+
+/*element: main:calls=[test(1)],params=0*/
+main() {
+  test(new A());
+}
diff --git a/tests/compiler/dart2js/codegen/model_test.dart b/tests/compiler/dart2js/codegen/model_test.dart
index 84cda3f..af0b3c8 100644
--- a/tests/compiler/dart2js/codegen/model_test.dart
+++ b/tests/compiler/dart2js/codegen/model_test.dart
@@ -59,6 +59,8 @@
   static const String parameterCount = 'params';
   static const String call = 'calls';
   static const String parameterStub = 'stubs';
+  static const String callStubCall = 'stubCalls';
+  static const String callStubAccesses = 'stubAccesses';
   static const String isEmitted = 'emitted';
   static const String isElided = 'elided';
   static const String assignment = 'assign';
@@ -83,6 +85,75 @@
       : _programLookup = new ProgramLookup(compiler),
         super(reporter, actualMap);
 
+  void registerCalls(Features features, String tag, js.Node node,
+      {String prefix = '', Set<js.PropertyAccess> handledAccesses}) {
+    forEachNode(node, onCall: (js.Call node) {
+      js.Node target = node.target;
+      if (target is js.PropertyAccess) {
+        js.Node selector = target.selector;
+        bool fixedNameCall = false;
+        String name;
+        if (selector is js.Name) {
+          name = selector.key;
+          fixedNameCall = selector is StringBackedName;
+        } else if (selector is js.LiteralString) {
+          /// Call to fixed backend name, so we include the argument
+          /// values to test encoding of optional parameters in native
+          /// methods.
+          name = selector.value.substring(1, selector.value.length - 1);
+          fixedNameCall = true;
+        }
+        if (name != null) {
+          if (fixedNameCall) {
+            String arguments = node.arguments.map(js.nodeToString).join(',');
+            features.addElement(tag, '${prefix}${name}(${arguments})');
+          } else {
+            features.addElement(
+                tag, '${prefix}${name}(${node.arguments.length})');
+          }
+          handledAccesses?.add(target);
+        }
+      }
+    });
+  }
+
+  void registerAccesses(Features features, String tag, js.Node code,
+      {String prefix = '', Set<js.PropertyAccess> handledAccesses}) {
+    forEachNode(code, onPropertyAccess: (js.PropertyAccess node) {
+      if (handledAccesses?.contains(node) ?? false) {
+        return;
+      }
+
+      js.Node receiver = node.receiver;
+      String receiverName;
+      if (receiver is js.VariableUse) {
+        receiverName = receiver.name;
+        if (receiverName == receiverName.toUpperCase() &&
+            receiverName != r'$') {
+          // Skip holder access.
+          receiverName = null;
+        }
+      } else if (receiver is js.This) {
+        receiverName = 'this';
+      }
+
+      js.Node selector = node.selector;
+      String name;
+      if (selector is js.Name) {
+        name = selector.key;
+      } else if (selector is js.LiteralString) {
+        /// Call to fixed backend name, so we include the argument
+        /// values to test encoding of optional parameters in native
+        /// methods.
+        name = selector.value.substring(1, selector.value.length - 1);
+      }
+
+      if (receiverName != null && name != null) {
+        features.addElement(tag, '${prefix}${name}');
+      }
+    });
+  }
+
   Features getMemberValue(MemberEntity member) {
     if (member is FieldEntity) {
       Field field = _programLookup.getField(member);
@@ -115,6 +186,16 @@
         registerFlags(Tags.getterFlags, field.getterFlags);
         registerFlags(Tags.setterFlags, field.setterFlags);
 
+        Class cls = _programLookup.getClass(member.enclosingClass);
+        for (StubMethod stub in cls.callStubs) {
+          if (stub.element == member) {
+            registerCalls(features, Tags.callStubCall, stub.code,
+                prefix: '${stub.name.key}:');
+            registerAccesses(features, Tags.callStubAccesses, stub.code,
+                prefix: '${stub.name.key}:');
+          }
+        }
+
         return features;
       }
       StaticField staticField = _programLookup.getStaticField(member);
@@ -137,42 +218,12 @@
 
         Set<js.PropertyAccess> handledAccesses = new Set();
 
-        void registerCalls(String tag, js.Node node, [String prefix = '']) {
-          forEachNode(node, onCall: (js.Call node) {
-            js.Node target = node.target;
-            if (target is js.PropertyAccess) {
-              js.Node selector = target.selector;
-              bool fixedNameCall = false;
-              String name;
-              if (selector is js.Name) {
-                name = selector.key;
-                fixedNameCall = selector is StringBackedName;
-              } else if (selector is js.LiteralString) {
-                /// Call to fixed backend name, so we include the argument
-                /// values to test encoding of optional parameters in native
-                /// methods.
-                name = selector.value.substring(1, selector.value.length - 1);
-                fixedNameCall = true;
-              }
-              if (name != null) {
-                if (fixedNameCall) {
-                  String arguments =
-                      node.arguments.map(js.nodeToString).join(',');
-                  features.addElement(tag, '${prefix}${name}(${arguments})');
-                } else {
-                  features.addElement(
-                      tag, '${prefix}${name}(${node.arguments.length})');
-                }
-                handledAccesses.add(target);
-              }
-            }
-          });
-        }
-
-        registerCalls(Tags.call, code);
+        registerCalls(features, Tags.call, code,
+            handledAccesses: handledAccesses);
         if (method is DartMethod) {
           for (ParameterStubMethod stub in method.parameterStubs) {
-            registerCalls(Tags.parameterStub, stub.code, '${stub.name.key}:');
+            registerCalls(features, Tags.parameterStub, stub.code,
+                prefix: '${stub.name.key}:', handledAccesses: handledAccesses);
           }
         }
 
@@ -193,37 +244,8 @@
           }
         });
 
-        forEachNode(code, onPropertyAccess: (js.PropertyAccess node) {
-          if (handledAccesses.contains(node)) {
-            return;
-          }
-
-          js.Node receiver = node.receiver;
-          String receiverName;
-          if (receiver is js.VariableUse) {
-            receiverName = receiver.name;
-            if (receiverName == receiverName.toUpperCase() &&
-                receiverName != r'$') {
-              // Skip holder access.
-              receiverName = null;
-            }
-          }
-
-          js.Node selector = node.selector;
-          String name;
-          if (selector is js.Name) {
-            name = selector.key;
-          } else if (selector is js.LiteralString) {
-            /// Call to fixed backend name, so we include the argument
-            /// values to test encoding of optional parameters in native
-            /// methods.
-            name = selector.value.substring(1, selector.value.length - 1);
-          }
-
-          if (receiverName != null && name != null) {
-            features.addElement(Tags.propertyAccess, '${name}');
-          }
-        });
+        registerAccesses(features, Tags.propertyAccess, code,
+            handledAccesses: handledAccesses);
 
         forEachNode(code, onSwitch: (js.Switch node) {
           features.add(Tags.switchCase);
diff --git a/tests/compiler/dart2js/field_analysis/jdata/regress_36222.dart b/tests/compiler/dart2js/field_analysis/jdata/regress_36222.dart
new file mode 100644
index 0000000..d798780
--- /dev/null
+++ b/tests/compiler/dart2js/field_analysis/jdata/regress_36222.dart
@@ -0,0 +1,24 @@
+// 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.
+
+typedef int BinaryFunc(int x, int y);
+
+class A {
+  const A({this.foo = A.defaultFoo});
+
+  static int defaultFoo(int x, int y) {
+    return x + y;
+  }
+
+  /*element: A.foo:constant=FunctionConstant(A.defaultFoo)*/
+  final BinaryFunc foo;
+}
+
+@pragma('dart2js:assumeDynamic')
+@pragma('dart2js:noInline')
+test(dynamic a) => a.foo(1, 2);
+
+main() {
+  test(new A());
+}
diff --git a/tests/compiler/dart2js/field_analysis/kdata/regress_36222.dart b/tests/compiler/dart2js/field_analysis/kdata/regress_36222.dart
new file mode 100644
index 0000000..6794ac8
--- /dev/null
+++ b/tests/compiler/dart2js/field_analysis/kdata/regress_36222.dart
@@ -0,0 +1,24 @@
+// 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.
+
+typedef int BinaryFunc(int x, int y);
+
+class A {
+  const A({this.foo = A.defaultFoo});
+
+  static int defaultFoo(int x, int y) {
+    return x + y;
+  }
+
+  /*element: A.foo:A.=foo:FunctionConstant(A.defaultFoo),initial=NullConstant*/
+  final BinaryFunc foo;
+}
+
+@pragma('dart2js:assumeDynamic')
+@pragma('dart2js:noInline')
+test(dynamic a) => a.foo(1, 2);
+
+main() {
+  test(new A());
+}
diff --git a/tests/compiler/dart2js_extra/regress_36222_test.dart b/tests/compiler/dart2js_extra/regress_36222_test.dart
new file mode 100644
index 0000000..f030833
--- /dev/null
+++ b/tests/compiler/dart2js_extra/regress_36222_test.dart
@@ -0,0 +1,23 @@
+// 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.
+
+typedef int BinaryFunc(int x, int y);
+
+class A {
+  const A({this.foo = A.defaultFoo});
+
+  static int defaultFoo(int x, int y) {
+    return x + y;
+  }
+
+  final BinaryFunc foo;
+}
+
+@pragma('dart2js:assumeDynamic')
+@pragma('dart2js:noInline')
+test(dynamic a) => a.foo(1, 2);
+
+main() {
+  test(new A());
+}