Prepare js_ast for more advanced use of DeferredExpression

Change-Id: I008dc7ce21437475c2fa1d5505b42f9d4a4aa166
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102363
Commit-Queue: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/compiler/lib/src/io/position_information.dart b/pkg/compiler/lib/src/io/position_information.dart
index 732b841..63851e9 100644
--- a/pkg/compiler/lib/src/io/position_information.dart
+++ b/pkg/compiler/lib/src/io/position_information.dart
@@ -647,18 +647,18 @@
   /// (@ marks the current JavaScript position and ^ point to the mapped Dart
   /// code position.)
   static CallPosition getSemanticPositionForCall(js.Call node) {
-    if (node.target is js.PropertyAccess) {
-      js.PropertyAccess access = node.target;
+    js.Expression access = js.undefer(node.target);
+    if (access is js.PropertyAccess) {
       js.Node target = access;
       bool pureAccess = false;
       while (target is js.PropertyAccess) {
         js.PropertyAccess targetAccess = target;
-        if (targetAccess.receiver is js.VariableUse ||
-            targetAccess.receiver is js.This) {
+        js.Node receiver = js.undefer(targetAccess.receiver);
+        if (receiver is js.VariableUse || receiver is js.This) {
           pureAccess = true;
           break;
         } else {
-          target = targetAccess.receiver;
+          target = receiver;
         }
       }
       if (pureAccess) {
@@ -672,19 +672,19 @@
         return new CallPosition(
             access.selector, CodePositionKind.START, SourcePositionKind.INNER);
       }
-    } else if (node.target is js.VariableUse || node.target is js.This) {
+    } else if (access is js.VariableUse || access is js.This) {
       // m()   this()
       // ^     ^
       return new CallPosition(
           node, CodePositionKind.START, SourcePositionKind.START);
-    } else if (node.target is js.Fun ||
-        node.target is js.New ||
-        node.target is js.NamedFunction) {
+    } else if (access is js.Fun ||
+        access is js.New ||
+        access is js.NamedFunction) {
       // function(){}()  new Function("...")()   function foo(){}()
       //             ^                      ^                    ^
       return new CallPosition(
           node.target, CodePositionKind.END, SourcePositionKind.INNER);
-    } else if (node.target is js.Binary || node.target is js.Call) {
+    } else if (access is js.Binary || access is js.Call) {
       // (0,a)()   m()()
       //      ^       ^
       return new CallPosition(
@@ -1271,6 +1271,11 @@
     statementOffset = null;
   }
 
+  @override
+  visitDeferredExpression(js.DeferredExpression node) {
+    visit(node.value);
+  }
+
   Offset getOffsetForNode(js.Node node, int codeOffset) {
     if (codeOffset == null) {
       CodePosition codePosition = codePositions[node];
diff --git a/pkg/js_ast/lib/src/nodes.dart b/pkg/js_ast/lib/src/nodes.dart
index 0bec49b..28923cf 100644
--- a/pkg/js_ast/lib/src/nodes.dart
+++ b/pkg/js_ast/lib/src/nodes.dart
@@ -981,7 +981,7 @@
 /// In particular, there is no guarantee that implementations of [compareTo]
 /// will implement some form of lexicographic ordering like [String.compareTo].
 abstract class Name extends Literal
-    implements Declaration, Parameter, Comparable {
+    implements Declaration, Parameter, Comparable<Name> {
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitName(this);
 
   R accept1<R, A>(NodeVisitor1<R, A> visitor, A arg) =>
@@ -1750,10 +1750,11 @@
 }
 
 class Property extends Node {
-  final Literal name;
+  final Expression name;
   final Expression value;
 
-  Property(this.name, this.value);
+  Property(this.name, this.value)
+      : assert(name is Literal || name is DeferredExpression);
 
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitProperty(this);
 
@@ -1987,3 +1988,9 @@
 
   void visitChildren1<R, A>(NodeVisitor1<R, A> visitor, A arg) {}
 }
+
+/// Returns the value of [node] if it is a [DeferredExpression]. Otherwise
+/// returns the [node] itself.
+Node undefer(Node node) {
+  return node is DeferredExpression ? undefer(node.value) : node;
+}
diff --git a/pkg/js_ast/lib/src/printer.dart b/pkg/js_ast/lib/src/printer.dart
index 9ff26a6..d5eba7b 100644
--- a/pkg/js_ast/lib/src/printer.dart
+++ b/pkg/js_ast/lib/src/printer.dart
@@ -12,7 +12,7 @@
   final bool preferSemicolonToNewlineInMinifiedOutput;
   final Renamer renamerForNames;
 
-  JavaScriptPrintingOptions(
+  const JavaScriptPrintingOptions(
       {this.shouldCompressOutput: false,
       this.minifyLocalVariables: false,
       this.preferSemicolonToNewlineInMinifiedOutput: false,
@@ -223,6 +223,9 @@
 
   void startNode(Node node) {
     currentNode = new EnterExitNode(currentNode, node);
+    if (node is DeferredExpression) {
+      startNode(node.value);
+    }
   }
 
   void enterNode() {
@@ -230,6 +233,9 @@
   }
 
   void endNode(Node node) {
+    if (node is DeferredExpression) {
+      endNode(node.value);
+    }
     assert(currentNode.node == node);
     currentNode = currentNode.exitNode(context, _charCount);
   }
@@ -715,7 +721,7 @@
 
   static bool _isSmallInitialization(Node node) {
     if (node is VariableInitialization) {
-      Node value = node.value;
+      Node value = undefer(node.value);
       if (value == null) return true;
       if (value is This) return true;
       if (value is LiteralNull) return true;
@@ -723,17 +729,79 @@
       if (value is LiteralString && value.value.length <= 8) return true;
       if (value is ObjectInitializer && value.properties.isEmpty) return true;
       if (value is ArrayInitializer && value.elements.isEmpty) return true;
+      if (value is Name && value.name.length <= 8) return true;
     }
     return false;
   }
 
+  void _outputIncDec(String op, Expression variable, [Expression alias]) {
+    if (op == '+') {
+      if (lastCharCode == charCodes.$PLUS) out(" ", isWhitespace: true);
+      out('++');
+    } else {
+      if (lastCharCode == charCodes.$MINUS) out(" ", isWhitespace: true);
+      out('--');
+    }
+    if (alias != null) startNode(alias);
+    visitNestedExpression(variable, UNARY,
+        newInForInit: inForInit, newAtStatementBegin: false);
+    if (alias != null) endNode(alias);
+  }
+
   @override
   visitAssignment(Assignment assignment) {
+    /// To print assignments like `a = a + 1` and `a = a + b` compactly as
+    /// `++a` and `a += b` in the face of [DeferredExpression]s we detect the
+    /// pattern of the undeferred assignment.
+    String op = assignment.op;
+    Node leftHandSide = undefer(assignment.leftHandSide);
+    Node rightHandSide = undefer(assignment.value);
+    if ((op == '+' || op == '-') &&
+        leftHandSide is VariableUse &&
+        rightHandSide is LiteralNumber &&
+        rightHandSide.value == "1") {
+      // Output 'a += 1' as '++a' and 'a -= 1' as '--a'.
+      _outputIncDec(op, assignment.leftHandSide);
+      return;
+    } else if (leftHandSide is VariableUse && rightHandSide is Binary) {
+      Node rLeft = undefer(rightHandSide.left);
+      Node rRight = undefer(rightHandSide.right);
+      String op = rightHandSide.op;
+      if (op == '+' ||
+          op == '-' ||
+          op == '/' ||
+          op == '*' ||
+          op == '%' ||
+          op == '^' ||
+          op == '&' ||
+          op == '|') {
+        if (rLeft is VariableUse && rLeft.name == leftHandSide.name) {
+          // Output 'a = a + 1' as '++a' and 'a = a - 1' as '--a'.
+          if ((op == '+' || op == '-') &&
+              rRight is LiteralNumber &&
+              rRight.value == "1") {
+            _outputIncDec(op, assignment.leftHandSide, rightHandSide.left);
+            return;
+          }
+          // Output 'a = a + b' as 'a += b'.
+          startNode(rightHandSide.left);
+          visitNestedExpression(assignment.leftHandSide, CALL,
+              newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+          endNode(rightHandSide.left);
+          spaceOut();
+          out(op);
+          out("=");
+          spaceOut();
+          visitNestedExpression(rRight, ASSIGNMENT,
+              newInForInit: inForInit, newAtStatementBegin: false);
+          return;
+        }
+      }
+    }
     visitNestedExpression(assignment.leftHandSide, CALL,
         newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
     if (assignment.value != null) {
       spaceOut();
-      String op = assignment.op;
       if (op != null) out(op);
       out("=");
       spaceOut();
@@ -972,34 +1040,33 @@
   void visitAccess(PropertyAccess access) {
     visitNestedExpression(access.receiver, CALL,
         newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
-    Node selector = access.selector;
+    Node selector = undefer(access.selector);
     if (selector is LiteralString) {
-      LiteralString selectorString = selector;
-      String fieldWithQuotes = selectorString.value;
+      String fieldWithQuotes = selector.value;
       if (isValidJavaScriptId(fieldWithQuotes)) {
         if (access.receiver is LiteralNumber &&
             lastCharCode != charCodes.$CLOSE_PAREN) {
           out(" ", isWhitespace: true);
         }
         out(".");
-        startNode(selector);
+        startNode(access.selector);
         out(fieldWithQuotes.substring(1, fieldWithQuotes.length - 1));
-        endNode(selector);
+        endNode(access.selector);
         return;
       }
     } else if (selector is Name) {
-      if (access.receiver is LiteralNumber &&
-          lastCharCode != charCodes.$CLOSE_PAREN) {
+      Node receiver = undefer(access.receiver);
+      if (receiver is LiteralNumber && lastCharCode != charCodes.$CLOSE_PAREN) {
         out(" ", isWhitespace: true);
       }
       out(".");
-      startNode(selector);
+      startNode(access.selector);
       selector.accept(this);
-      endNode(selector);
+      endNode(access.selector);
       return;
     }
     out("[");
-    visitNestedExpression(selector, EXPRESSION,
+    visitNestedExpression(access.selector, EXPRESSION,
         newInForInit: false, newAtStatementBegin: false);
     out("]");
   }
@@ -1156,18 +1223,18 @@
   @override
   void visitProperty(Property node) {
     startNode(node.name);
-    if (node.name is LiteralString) {
-      LiteralString nameString = node.name;
-      String name = nameString.value;
-      if (isValidJavaScriptId(name)) {
-        out(name.substring(1, name.length - 1));
+    Node name = undefer(node.name);
+    if (name is LiteralString) {
+      String text = name.value;
+      if (isValidJavaScriptId(text)) {
+        out(text.substring(1, text.length - 1));
       } else {
-        out(name);
+        out(text);
       }
-    } else if (node.name is Name) {
+    } else if (name is Name) {
       node.name.accept(this);
     } else {
-      assert(node.name is LiteralNumber);
+      assert(name is LiteralNumber);
       LiteralNumber nameNumber = node.name;
       out(nameNumber.value);
     }
diff --git a/pkg/js_ast/lib/src/template.dart b/pkg/js_ast/lib/src/template.dart
index 0503e9f..a7dcf48 100644
--- a/pkg/js_ast/lib/src/template.dart
+++ b/pkg/js_ast/lib/src/template.dart
@@ -236,7 +236,7 @@
     var nameOrPosition = node.nameOrPosition;
     return (arguments) {
       var value = arguments[nameOrPosition];
-      if (value is Literal) return value;
+      if (value is Literal || value is DeferredExpression) return value;
       error('Interpolated value #$nameOrPosition is not a Literal: $value');
     };
   }
diff --git a/pkg/js_ast/test/deferred_expression_test.dart b/pkg/js_ast/test/deferred_expression_test.dart
new file mode 100644
index 0000000..397aaaa
--- /dev/null
+++ b/pkg/js_ast/test/deferred_expression_test.dart
@@ -0,0 +1,160 @@
+// 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';
+import 'package:js_ast/js_ast.dart';
+
+main() {
+  Map<Expression, DeferredExpression> map = {};
+  VariableUse variableUse = new VariableUse('variable');
+  DeferredExpression deferred =
+      map[variableUse] = new _DeferredExpression(variableUse);
+  VariableUse variableUseAlias = new VariableUse('variable');
+  map[variableUseAlias] = new _DeferredExpression(variableUseAlias);
+
+  map[deferred] = new _DeferredExpression(deferred);
+  Literal literal = new LiteralString('"literal"');
+  map[literal] = new _DeferredExpression(literal);
+
+  test(map, '#', [variableUse], 'variable');
+  test(map, '#', [deferred], 'variable');
+  test(map, '{#: #}', [literal, variableUse], '{literal: variable}');
+  test(map, '{#: #}', [literal, deferred], '{literal: variable}');
+  test(map, '#.#', [variableUse, literal], 'variable.literal');
+  test(map, '#.#', [deferred, literal], 'variable.literal');
+  test(map, '# = # + 1', [variableUse, variableUseAlias], '++variable');
+  test(map, '# = # + 1', [deferred, variableUseAlias], '++variable');
+  test(map, '# = # - 1', [variableUse, variableUseAlias], '--variable');
+  test(map, '# = # - 1', [deferred, variableUseAlias], '--variable');
+  test(map, '# = # + 2', [variableUse, variableUseAlias], 'variable += 2');
+  test(map, '# = # + 2', [deferred, variableUseAlias], 'variable += 2');
+}
+
+void test(Map<Expression, DeferredExpression> map, String template,
+    List<Expression> arguments, String expectedOutput) {
+  Expression directExpression =
+      js.expressionTemplateFor(template).instantiate(arguments);
+  _Context directContext = new _Context();
+  Printer directPrinter =
+      new Printer(const JavaScriptPrintingOptions(), directContext);
+  directPrinter.visit(directExpression);
+  Expect.equals(expectedOutput, directContext.text);
+
+  Expression deferredExpression = js
+      .expressionTemplateFor(template)
+      .instantiate(arguments.map((e) => map[e]).toList());
+  _Context deferredContext = new _Context();
+  Printer deferredPrinter =
+      new Printer(const JavaScriptPrintingOptions(), deferredContext);
+  deferredPrinter.visit(deferredExpression);
+  Expect.equals(expectedOutput, deferredContext.text);
+
+  for (Expression argument in arguments) {
+    DeferredExpression deferred = map[argument];
+    Expect.isTrue(
+        directContext.enterPositions.containsKey(argument),
+        "Argument ${DebugPrint(argument)} not found in direct enter positions: "
+        "${directContext.enterPositions.keys}");
+    Expect.isTrue(
+        deferredContext.enterPositions.containsKey(argument),
+        "Argument ${DebugPrint(argument)} not found in "
+        "deferred enter positions: "
+        "${deferredContext.enterPositions.keys}");
+    Expect.isTrue(
+        deferredContext.enterPositions.containsKey(deferred),
+        "Argument ${DebugPrint(deferred)} not found in "
+        "deferred enter positions: "
+        "${deferredContext.enterPositions.keys}");
+    Expect.equals(directContext.enterPositions[argument],
+        deferredContext.enterPositions[argument]);
+    Expect.equals(directContext.enterPositions[argument],
+        deferredContext.enterPositions[deferred]);
+
+    Expect.isTrue(
+        directContext.exitPositions.containsKey(argument),
+        "Argument ${DebugPrint(argument)} not found in direct enter positions: "
+        "${directContext.exitPositions.keys}");
+    Expect.isTrue(
+        deferredContext.exitPositions.containsKey(argument),
+        "Argument ${DebugPrint(argument)} not found in "
+        "deferred enter positions: "
+        "${deferredContext.exitPositions.keys}");
+    Expect.isTrue(
+        deferredContext.exitPositions.containsKey(deferred),
+        "Argument ${DebugPrint(deferred)} not found in "
+        "deferred enter positions: "
+        "${deferredContext.exitPositions.keys}");
+    Expect.equals(directContext.exitPositions[argument],
+        deferredContext.exitPositions[argument]);
+    Expect.equals(directContext.exitPositions[argument],
+        deferredContext.exitPositions[deferred]);
+  }
+}
+
+class _DeferredExpression extends DeferredExpression {
+  final Expression value;
+
+  _DeferredExpression(this.value);
+
+  @override
+  int get precedenceLevel => value.precedenceLevel;
+}
+
+class _Context implements JavaScriptPrintingContext {
+  StringBuffer sb = new StringBuffer();
+  List<String> errors = [];
+  Map<Node, int> enterPositions = {};
+  Map<Node, _Position> exitPositions = {};
+
+  @override
+  void emit(String string) {
+    sb.write(string);
+  }
+
+  @override
+  void enterNode(Node node, int startPosition) {
+    enterPositions[node] = startPosition;
+  }
+
+  @override
+  void exitNode(
+      Node node, int startPosition, int endPosition, int closingPosition) {
+    exitPositions[node] =
+        new _Position(startPosition, endPosition, closingPosition);
+    Expect.equals(enterPositions[node], startPosition);
+  }
+
+  @override
+  void error(String message) {
+    errors.add(message);
+  }
+
+  String get text => sb.toString();
+}
+
+class _Position {
+  final int startPosition;
+  final int endPosition;
+  final int closingPosition;
+
+  _Position(this.startPosition, this.endPosition, this.closingPosition);
+
+  int get hashCode =>
+      13 * startPosition.hashCode +
+      17 * endPosition.hashCode +
+      19 * closingPosition.hashCode;
+
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    return other is _Position &&
+        startPosition == other.startPosition &&
+        endPosition == other.endPosition &&
+        closingPosition == other.closingPosition;
+  }
+
+  String toString() {
+    return '_Position(start=$startPosition,'
+        'end=$endPosition,closing=$closingPosition)';
+  }
+}