bazel syntax: eliminate LValue

This reduces the number of nodes allocated by the parser.

The logic of LValue is now moved into Eval, the tree-based evaluator.
A follow-up change will move all the Evaluator.eval methods into this class too,
so that the tree-based evaluator is entirely confined to this class.

ASTNode methods that used to return an LValue are now named getLHS.

PiperOrigin-RevId: 255950649
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
index 7a4273b..036210d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
@@ -15,7 +15,9 @@
 package com.google.devtools.build.lib.syntax;
 
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Location;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.function.Function;
 
@@ -67,14 +69,13 @@
   }
 
   void execAssignment(AssignmentStatement node) throws EvalException, InterruptedException {
-    Object rvalue = node.getExpression().eval(env);
-    node.getLValue().assign(rvalue, env, node.getLocation());
+    Object rvalue = node.getRHS().eval(env);
+    assign(node.getLHS(), rvalue, env, node.getLocation());
   }
 
   void execAugmentedAssignment(AugmentedAssignmentStatement node)
       throws EvalException, InterruptedException {
-    node.getLValue()
-        .assignAugmented(node.getOperator(), node.getExpression(), env, node.getLocation());
+    assignAugmented(node.getLHS(), node.getOperator(), node.getRHS(), env, node.getLocation());
   }
 
   void execIfBranch(IfStatement.ConditionalStatements node)
@@ -88,7 +89,7 @@
     EvalUtils.lock(o, node.getLocation());
     try {
       for (Object it : col) {
-        node.getVariable().assign(it, env, node.getLocation());
+        assign(node.getLHS(), it, env, node.getLocation());
 
         try {
           execStatements(node.getBlock());
@@ -230,4 +231,123 @@
       exec(statements.get(i));
     }
   }
+
+  /**
+   * Updates the environment bindings, and possibly mutates objects, so as to assign the given value
+   * to the given expression. The expression must be valid for an {@code LValue}.
+   */
+  // TODO(adonovan): make this a private instance method once all Expression.eval methods move here.
+  static void assign(Expression expr, Object value, Environment env, Location loc)
+      throws EvalException, InterruptedException {
+    if (expr instanceof Identifier) {
+      assignIdentifier((Identifier) expr, value, env);
+    } else if (expr instanceof IndexExpression) {
+      Object object = ((IndexExpression) expr).getObject().eval(env);
+      Object key = ((IndexExpression) expr).getKey().eval(env);
+      assignItem(object, key, value, env, loc);
+    } else if (expr instanceof ListLiteral) {
+      ListLiteral list = (ListLiteral) expr;
+      assignList(list, value, env, loc);
+    } else {
+      // Not possible for validated ASTs.
+      throw new EvalException(loc, "cannot assign to '" + expr + "'");
+    }
+  }
+
+  /** Binds a variable to the given value in the environment. */
+  private static void assignIdentifier(Identifier ident, Object value, Environment env)
+      throws EvalException {
+    env.updateAndExport(ident.getName(), value);
+  }
+
+  /**
+   * Adds or changes an object-key-value relationship for a list or dict.
+   *
+   * <p>For a list, the key is an in-range index. For a dict, it is a hashable value.
+   *
+   * @throws EvalException if the object is not a list or dict
+   */
+  @SuppressWarnings("unchecked")
+  private static void assignItem(
+      Object object, Object key, Object value, Environment env, Location loc) throws EvalException {
+    if (object instanceof SkylarkDict) {
+      SkylarkDict<Object, Object> dict = (SkylarkDict<Object, Object>) object;
+      dict.put(key, value, loc, env);
+    } else if (object instanceof SkylarkList.MutableList) {
+      SkylarkList.MutableList<Object> list = (SkylarkList.MutableList<Object>) object;
+      int index = EvalUtils.getSequenceIndex(key, list.size(), loc);
+      list.set(index, value, loc, env.mutability());
+    } else {
+      throw new EvalException(
+          loc,
+          "can only assign an element in a dictionary or a list, not in a '"
+              + EvalUtils.getDataTypeName(object)
+              + "'");
+    }
+  }
+
+  /**
+   * Recursively assigns an iterable value to a list literal.
+   *
+   * @throws EvalException if the list literal has length 0, or if the value is not an iterable of
+   *     matching length
+   */
+  private static void assignList(ListLiteral list, Object value, Environment env, Location loc)
+      throws EvalException, InterruptedException {
+    Collection<?> collection = EvalUtils.toCollection(value, loc, env);
+    int len = list.getElements().size();
+    if (len == 0) {
+      throw new EvalException(
+          loc, "lists or tuples on the left-hand side of assignments must have at least one item");
+    }
+    if (len != collection.size()) {
+      throw new EvalException(
+          loc,
+          String.format(
+              "assignment length mismatch: left-hand side has length %d, but right-hand side"
+                  + " evaluates to value of length %d",
+              len, collection.size()));
+    }
+    int i = 0;
+    for (Object item : collection) {
+      assign(list.getElements().get(i), item, env, loc);
+      i++;
+    }
+  }
+
+  /**
+   * Evaluates an augmented assignment that mutates this {@code LValue} with the given right-hand
+   * side's value.
+   *
+   * <p>The left-hand side expression is evaluated only once, even when it is an {@link
+   * IndexExpression}. The left-hand side is evaluated before the right-hand side to match Python's
+   * behavior (hence why the right-hand side is passed as an expression rather than as an evaluated
+   * value).
+   */
+  private static void assignAugmented(
+      Expression expr, Operator operator, Expression rhs, Environment env, Location loc)
+      throws EvalException, InterruptedException {
+    if (expr instanceof Identifier) {
+      Object result =
+          BinaryOperatorExpression.evaluateAugmented(
+              operator, expr.eval(env), rhs.eval(env), env, loc);
+      assignIdentifier((Identifier) expr, result, env);
+    } else if (expr instanceof IndexExpression) {
+      IndexExpression indexExpression = (IndexExpression) expr;
+      // The object and key should be evaluated only once, so we don't use expr.eval().
+      Object object = indexExpression.getObject().eval(env);
+      Object key = indexExpression.getKey().eval(env);
+      Object oldValue = IndexExpression.evaluate(object, key, env, loc);
+      // Evaluate rhs after lhs.
+      Object rhsValue = rhs.eval(env);
+      Object result =
+          BinaryOperatorExpression.evaluateAugmented(operator, oldValue, rhsValue, env, loc);
+      assignItem(object, key, result, env, loc);
+    } else if (expr instanceof ListLiteral) {
+      throw new EvalException(loc, "cannot perform augmented assignment on a list literal");
+    } else {
+      // Not possible for validated ASTs.
+      throw new EvalException(loc, "cannot perform augmented assignment on '" + expr + "'");
+    }
+  }
 }