[dart2js] Basic chaining of field assignments

      t4 = t3.tooltip;
      t2._tooltipText = t4;
      this.currenttooltip = t4;
--->
      this.currenttooltip = t2._tooltipText = t3.tooltip;


        future = new P._Future(0, $.Zone__current, [P.bool]);
        this._stateData = future;
        return future;
--->
        return this._stateData = new P._Future(0, $.Zone__current, [P.bool]);

Change-Id: I8a1c4dae85b8a10f8b2c099668033f0c1ea9b6c4
Reviewed-on: https://dart-review.googlesource.com/c/93342
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/ssa/codegen.dart b/pkg/compiler/lib/src/ssa/codegen.dart
index f93696b..9136155 100644
--- a/pkg/compiler/lib/src/ssa/codegen.dart
+++ b/pkg/compiler/lib/src/ssa/codegen.dart
@@ -521,11 +521,9 @@
         if (current.isControlFlow()) {
           return TYPE_STATEMENT;
         }
-        // HFieldSet generates code on the form x.y = ..., which isn't
-        // valid in a declaration, but it also always have no uses, so
-        // it's caught by that test too.
-        assert(current is! HFieldSet || current.usedBy.isEmpty);
-        if (current.usedBy.isEmpty) {
+        // HFieldSet generates code on the form "x.y = ...", which isn't valid
+        // in a declaration.
+        if (current.usedBy.isEmpty || current is HFieldSet) {
           result = TYPE_EXPRESSION;
         }
         current = current.next;
diff --git a/pkg/compiler/lib/src/ssa/codegen_helpers.dart b/pkg/compiler/lib/src/ssa/codegen_helpers.dart
index 78ddc04..34fe26e 100644
--- a/pkg/compiler/lib/src/ssa/codegen_helpers.dart
+++ b/pkg/compiler/lib/src/ssa/codegen_helpers.dart
@@ -20,6 +20,8 @@
   final CompilerOptions _options;
   HGraph graph;
 
+  Set<HFieldSet> _processedFieldSetters = Set();
+
   SsaInstructionSelection(
       this._options, this._closedWorld, this._interceptorData);
 
@@ -195,6 +197,81 @@
   }
 
   HInstruction visitFieldSet(HFieldSet setter) {
+    void tryChainAssignment() {
+      // Try to use result of field assignment
+      //
+      //     t1 = v;  x.f = t1;  ... t1 ...  -->  t1 = x.f = v;  ... t1 ...
+      //
+
+      // We grow the chain ahead of the block-scan, so we may have already
+      // processed the chain.
+      if (_processedFieldSetters.contains(setter)) return;
+      _processedFieldSetters.add(setter);
+
+      final value = setter.value;
+
+      // Single use is this setter so there will be no other uses to chain.
+      if (value.usedBy.length <= 1) return;
+
+      HFieldSet chain = setter;
+      setter.instructionType = value.instructionType;
+      for (HInstruction current = setter.next;;) {
+        if (current is HFieldSet) {
+          HFieldSet nextSetter = current;
+          if (nextSetter.value == value && nextSetter.receiver != value) {
+            _processedFieldSetters.add(nextSetter);
+            nextSetter.changeUse(value, chain);
+            nextSetter.instructionType = value.instructionType;
+            chain = nextSetter;
+            current = nextSetter.next;
+            continue;
+          }
+        } else if (current is HReturn) {
+          if (current.inputs.single == value) {
+            current.changeUse(value, chain);
+            return;
+          }
+        }
+        break;
+      }
+
+      if (value.usedBy.length <= 1) return; // [setter] is only remaining use.
+
+      // Chain to other places.
+      var uses = DominatedUses.of(value, chain, excludeDominator: true);
+
+      if (uses.isEmpty) return;
+
+      if (uses.isSingleton) {
+        var use = uses.single;
+        if (use is HPhi) {
+          // Filter out back-edges - that causes problems for variable
+          // assignment.
+          // TODO(sra): Better analysis to permit phis that are part of a
+          // forwards-only tree.
+          if (use.block.id < chain.block.id) return;
+          if (use.usedBy.any((node) => node is HPhi)) return;
+          use.changeUse(value, chain);
+          return;
+        }
+      }
+
+      if (value is HConstant) return;
+      if (value.nonCheck() is HParameterValue) return;
+
+      // TODO(sra): Consider chaining to other places.
+      //
+      // 1. If there are many remaining uses, all of them dominated by [chain],
+      //    we should replace them with [chain] and let that value get the
+      //    variable name.
+      //
+      // 2. Chains with one remaining potential use have the potential to
+      //    generate huge expression containing many assignments. This will be
+      //    smaller but nearly impossible to read. What interior positions
+      //    should we chain into?
+      return;
+    }
+
     // Pattern match
     //     t1 = x.f; t2 = t1 + 1; x.f = t2; use(t2)   -->  ++x.f
     //     t1 = x.f; t2 = t1 op y; x.f = t2; use(t2)  -->  x.f op= y
@@ -219,6 +296,7 @@
     }
 
     HInstruction noMatchingRead() {
+      tryChainAssignment();
       // If we have other HFieldSet optimizations, they go here.
       return null;
     }
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index 5b0006f..cc28749 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -1327,6 +1327,7 @@
 
   bool get isEmpty => _instructions.isEmpty;
   bool get isNotEmpty => !isEmpty;
+  int get length => _instructions.length;
 
   /// Changes all the uses in the set to [newInstruction].
   void replaceWith(HInstruction newInstruction) {
@@ -1350,6 +1351,8 @@
 
   HInstruction get single => _instructions.single;
 
+  Iterable<HInstruction> get instructions => _instructions;
+
   void _addUse(HInstruction user, int inputIndex) {
     _instructions.add(user);
     _indexes.add(inputIndex);
@@ -1921,7 +1924,9 @@
   HInstruction get value => inputs[1];
   accept(HVisitor visitor) => visitor.visitFieldSet(this);
 
-  bool isJsStatement() => true;
+  // HFieldSet is an expression if it has a user.
+  bool isJsStatement() => usedBy.isEmpty;
+
   String toString() => "FieldSet(element=$element,type=$instructionType)";
 }
 
diff --git a/tests/compiler/dart2js/codegen/load_elimination_test.dart b/tests/compiler/dart2js/codegen/load_elimination_test.dart
index fa16f0f..eb5c0a9 100644
--- a/tests/compiler/dart2js/codegen/load_elimination_test.dart
+++ b/tests/compiler/dart2js/codegen/load_elimination_test.dart
@@ -235,7 +235,7 @@
 
 main() {
   runTests() async {
-    test(String code, String expected) async {
+    test(String code, Pattern expected) async {
       String generated = await compile(code,
           disableInlining: false, disableTypeInference: false);
       Expect.isTrue(
@@ -250,7 +250,7 @@
     await test(TEST_4, 'return t1 + t1');
     await test(TEST_5, 'return 84');
     await test(TEST_6, 'return 84');
-    await test(TEST_7, 'return 32');
+    await test(TEST_7, RegExp('return( .* =)? 32'));
     await test(TEST_8, 'return a.a');
     await test(TEST_9, 'return a.a');
     await test(TEST_10, 'return 2');