starlark: add StarlarkList.Builder

...and use it to reduce copying during construction.

+ Tests

See CL 339475495 for corresponding Dict change.

PiperOrigin-RevId: 340344770
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkAttributesCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkAttributesCollection.java
index ca59e2e..ab0dbfa 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkAttributesCollection.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkAttributesCollection.java
@@ -199,8 +199,7 @@
       }
       filesBuilder.put(
           skyname,
-          StarlarkList.copyOf(
-              /*mutability=*/ null,
+          StarlarkList.immutableCopyOf(
               context.getRuleContext().getPrerequisiteArtifacts(a.getName()).list()));
 
       if (type == BuildType.LABEL && !a.getTransitionFactory().isSplit()) {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleContext.java
index d6aa055..079ab04 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleContext.java
@@ -186,13 +186,13 @@
         if (type.getLabelClass() != LabelClass.OUTPUT) {
           continue;
         }
-        ImmutableList.Builder<Artifact> artifactsBuilder = ImmutableList.builder();
+        StarlarkList.Builder<Artifact> artifactsBuilder = StarlarkList.builder();
         for (OutputFile outputFile : ruleContext.getRule().getOutputFileMap().get(attrName)) {
           Artifact artifact = ruleContext.createOutputArtifact(outputFile);
           artifactsBuilder.add(artifact);
           artifactLabelMapBuilder.put(artifact, outputFile.getLabel());
         }
-        ImmutableList<Artifact> artifacts = artifactsBuilder.build();
+        StarlarkList<Artifact> artifacts = artifactsBuilder.buildImmutable();
 
         if (type == BuildType.OUTPUT) {
           if (artifacts.size() == 1) {
@@ -201,7 +201,7 @@
             outputs.addOutput(attrName, Starlark.NONE);
           }
         } else if (type == BuildType.OUTPUT_LIST) {
-          outputs.addOutput(attrName, StarlarkList.immutableCopyOf(artifacts));
+          outputs.addOutput(attrName, artifacts);
         } else {
           throw new IllegalArgumentException(
               "Type of " + attrName + "(" + type + ") is not output type ");
diff --git a/src/main/java/net/starlark/java/eval/Eval.java b/src/main/java/net/starlark/java/eval/Eval.java
index 29bbec7..5f20a32 100644
--- a/src/main/java/net/starlark/java/eval/Eval.java
+++ b/src/main/java/net/starlark/java/eval/Eval.java
@@ -748,7 +748,7 @@
   private static Object evalComprehension(StarlarkThread.Frame fr, Comprehension comp)
       throws EvalException, InterruptedException {
     final Dict<Object, Object> dict = comp.isDict() ? Dict.of(fr.thread.mutability()) : null;
-    final ArrayList<Object> list = comp.isDict() ? null : new ArrayList<>();
+    final StarlarkList.Builder<Object> list = comp.isDict() ? null : StarlarkList.builder();
 
     // Save previous value (if any) of local variables bound in a 'for' clause
     // so we can restore them later.
@@ -836,7 +836,7 @@
       }
     }
 
-    return comp.isDict() ? dict : StarlarkList.copyOf(fr.thread.mutability(), list);
+    return comp.isDict() ? dict : list.build(fr.thread.mutability());
   }
 
   private static final Object[] EMPTY = {};
diff --git a/src/main/java/net/starlark/java/eval/MethodLibrary.java b/src/main/java/net/starlark/java/eval/MethodLibrary.java
index 934b5c8..a1893c4 100644
--- a/src/main/java/net/starlark/java/eval/MethodLibrary.java
+++ b/src/main/java/net/starlark/java/eval/MethodLibrary.java
@@ -15,13 +15,10 @@
 package net.starlark.java.eval;
 
 import com.google.common.base.Ascii;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Iterator;
-import java.util.List;
 import java.util.NoSuchElementException;
 import net.starlark.java.annot.Param;
 import net.starlark.java.annot.ParamType;
@@ -754,27 +751,27 @@
       extraPositionals = @Param(name = "args", doc = "lists to zip."),
       useStarlarkThread = true)
   public StarlarkList<?> zip(Sequence<?> args, StarlarkThread thread) throws EvalException {
-    Iterator<?>[] iterators = new Iterator<?>[args.size()];
-    for (int i = 0; i < args.size(); i++) {
-      iterators[i] = Starlark.toIterable(args.get(i)).iterator();
-    }
-    ArrayList<Tuple> result = new ArrayList<>();
-    boolean allHasNext;
-    do {
-      allHasNext = !args.isEmpty();
-      List<Object> elem = Lists.newArrayListWithExpectedSize(args.size());
-      for (Iterator<?> iterator : iterators) {
-        if (iterator.hasNext()) {
-          elem.add(iterator.next());
-        } else {
-          allHasNext = false;
+    StarlarkList.Builder<Tuple> result = StarlarkList.builder();
+    int ncols = args.size();
+    if (ncols > 0) {
+      Iterator<?>[] iterators = new Iterator<?>[ncols];
+      for (int i = 0; i < ncols; i++) {
+        iterators[i] = Starlark.toIterable(args.get(i)).iterator();
+      }
+      rows:
+      for (; ; ) {
+        Object[] elem = new Object[ncols];
+        for (int i = 0; i < ncols; i++) {
+          Iterator<?> it = iterators[i];
+          if (!it.hasNext()) {
+            break rows;
+          }
+          elem[i] = it.next();
         }
+        result.add(Tuple.wrap(elem));
       }
-      if (allHasNext) {
-        result.add(Tuple.copyOf(elem));
-      }
-    } while (allHasNext);
-    return StarlarkList.copyOf(thread.mutability(), result);
+    }
+    return result.build(thread.mutability());
   }
 
   /** Starlark bool type. */
diff --git a/src/main/java/net/starlark/java/eval/StarlarkList.java b/src/main/java/net/starlark/java/eval/StarlarkList.java
index 87ac690..c11bfcc 100644
--- a/src/main/java/net/starlark/java/eval/StarlarkList.java
+++ b/src/main/java/net/starlark/java/eval/StarlarkList.java
@@ -85,9 +85,9 @@
 
   private static final Object[] EMPTY_ARRAY = {};
 
-  private StarlarkList(@Nullable Mutability mutability, Object[] elems) {
+  private StarlarkList(@Nullable Mutability mutability, Object[] elems, int size) {
     this.elems = elems;
-    this.size = elems.length;
+    this.size = size;
     this.mutability = mutability == null ? Mutability.IMMUTABLE : mutability;
   }
 
@@ -97,7 +97,7 @@
    * instance may do so.
    */
   static <T> StarlarkList<T> wrap(@Nullable Mutability mutability, Object[] elems) {
-    return new StarlarkList<>(mutability, elems);
+    return new StarlarkList<>(mutability, elems, elems.length);
   }
 
   @Override
@@ -181,6 +181,79 @@
     return wrap(null, Arrays.copyOf(elems, elems.length));
   }
 
+  /** Returns a new empty StarlarkList builder. */
+  public static <T> Builder<T> builder() {
+    return new Builder<>();
+  }
+
+  /** A reusable builder for StarlarkLists. */
+  public static final class Builder<T> {
+    private Object[] elems = EMPTY;
+    private int size;
+    private boolean shared; // whether elems is nonempty and shared with some StarlarkList
+
+    private static final Object[] EMPTY = {};
+
+    /** Adds an element to the builder. */
+    public Builder<T> add(T v) {
+      ensureCapacity(1);
+      elems[size++] = Starlark.checkValid(v);
+      return this;
+    }
+
+    /** Adds all the iterable's entries to the builder. */
+    public Builder<T> addAll(Iterable<? extends T> seq) {
+      if (!(seq instanceof Collection)) {
+        // iterable of unknown size
+        for (T elem : seq) {
+          add(elem);
+        }
+
+      } else if (size == 0) {
+        // first time: bulk copy, avoiding iterator
+        elems = ((Collection<? extends T>) seq).toArray();
+        for (Object elem : elems) {
+          Starlark.checkValid(elem);
+        }
+        size = elems.length;
+
+      } else {
+        // general case
+        ensureCapacity(((Collection) seq).size());
+        for (Object elem : seq) {
+          elems[size++] = Starlark.checkValid(elem);
+        }
+      }
+      return this;
+    }
+
+    // Ensures elems is unshared with sufficient capacity for n additional elements.
+    private void ensureCapacity(int n) {
+      int cap = elems.length;
+      if (shared || size + n > cap) {
+        int newcap = Math.max(size + n, cap + (cap >> 1) + 1); // grow by at least 50%
+        elems = Arrays.copyOf(elems, newcap);
+        shared = false;
+      }
+    }
+
+    /** Returns a new immutable StarlarkList containing the elements added so far. */
+    public StarlarkList<T> buildImmutable() {
+      return build(null);
+    }
+
+    /**
+     * Returns a new StarlarkList containing the elements added so far. The result has the specified
+     * mutability; null means immutable.
+     */
+    public StarlarkList<T> build(@Nullable Mutability mu) {
+      // TODO(adonovan): opt: if elems.length ≫ size, reallocate smaller.
+      ensureCapacity(0);
+      shared = size > 0;
+      return new StarlarkList<T>(mu, elems, size);
+    }
+  }
+
   @Override
   public Mutability mutability() {
     return mutability;
diff --git a/src/main/java/net/starlark/java/eval/StringModule.java b/src/main/java/net/starlark/java/eval/StringModule.java
index 448bcb9..a40f768 100644
--- a/src/main/java/net/starlark/java/eval/StringModule.java
+++ b/src/main/java/net/starlark/java/eval/StringModule.java
@@ -17,10 +17,8 @@
 import com.google.common.base.Ascii;
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import net.starlark.java.annot.Param;
@@ -339,7 +337,7 @@
     if (maxSplitO != Starlark.NONE) {
       maxSplit = Starlark.toInt(maxSplitO, "maxsplit");
     }
-    ArrayList<String> res = new ArrayList<>();
+    StarlarkList.Builder<String> res = StarlarkList.builder();
     int start = 0;
     while (true) {
       int end = self.indexOf(sep, start);
@@ -350,7 +348,7 @@
       res.add(self.substring(start, end));
       start = end + sep.length();
     }
-    return StarlarkList.copyOf(thread.mutability(), res);
+    return res.build(thread.mutability());
   }
 
   @StarlarkMethod(
@@ -646,7 +644,7 @@
       name = "splitlines",
       doc =
           "Splits the string at line boundaries ('\\n', '\\r\\n', '\\r') "
-              + "and returns the result as a list.",
+              + "and returns the result as a new mutable list.",
       parameters = {
         @Param(name = "self", doc = "This string."),
         @Param(
@@ -654,9 +652,10 @@
             name = "keepends",
             defaultValue = "False",
             doc = "Whether the line breaks should be included in the resulting list.")
-      })
-  public Sequence<String> splitLines(String self, Boolean keepEnds) throws EvalException {
-    List<String> result = new ArrayList<>();
+      },
+      useStarlarkThread = true)
+  public Sequence<String> splitLines(String self, boolean keepEnds, StarlarkThread thread) {
+    StarlarkList.Builder<String> result = StarlarkList.builder();
     Matcher matcher = SPLIT_LINES_PATTERN.matcher(self);
     while (matcher.find()) {
       String line = matcher.group("line");
@@ -671,9 +670,9 @@
         result.add(line);
       }
     }
-    // TODO(adonovan): spec should state immutability.
-    // Python[23] and go.starlark.net return a mutable list.
-    return StarlarkList.immutableCopyOf(result);
+    // TODO(adonovan): spec should state that result is mutable,
+    // as in Python[23] and go.starlark.net.
+    return result.build(thread.mutability());
   }
 
   @StarlarkMethod(
@@ -856,12 +855,13 @@
               + "Equivalent to <code>[s[i] for i in range(len(s))]</code>, except that the "
               + "returned value might not be a list.",
       parameters = {@Param(name = "self", doc = "This string.")})
-  public Sequence<String> elems(String self) throws EvalException {
-    ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
+  public Sequence<String> elems(String self) {
+    // TODO(adonovan): opt: return a new type that is lazily iterable.
+    StarlarkList.Builder<String> res = StarlarkList.builder();
     for (char c : self.toCharArray()) {
-      builder.add(String.valueOf(c));
+      res.add(String.valueOf(c));
     }
-    return StarlarkList.immutableCopyOf(builder.build());
+    return res.buildImmutable();
   }
 
   @StarlarkMethod(
diff --git a/src/test/java/net/starlark/java/eval/StarlarkListTest.java b/src/test/java/net/starlark/java/eval/StarlarkListTest.java
index 4505f6d..9685696 100644
--- a/src/test/java/net/starlark/java/eval/StarlarkListTest.java
+++ b/src/test/java/net/starlark/java/eval/StarlarkListTest.java
@@ -329,4 +329,55 @@
     wrapped[0] = "goodbye";
     assertThat((List<String>) mutableList).containsExactly("goodbye");
   }
+
+  @Test
+  public void testBuilder() throws EvalException {
+    // add
+    StarlarkList<String> list1 =
+        StarlarkList.<String>builder().add("a").add("b").add("c").buildImmutable();
+    assertThat((List<?>) list1).containsExactly("a", "b", "c");
+    assertThrows(EvalException.class, () -> list1.addElement("d")); // immutable
+
+    // addAll(Collection)
+    StarlarkList<String> list2 =
+        StarlarkList.<String>builder()
+            .addAll(ImmutableList.of("a", "b"))
+            .addAll(ImmutableList.of("c", "d"))
+            .buildImmutable();
+    assertThat((List<?>) list2).containsExactly("a", "b", "c", "d");
+
+    // addAll(Iterable)
+    StarlarkList<String> list3 =
+        StarlarkList.<String>builder()
+            .addAll(() -> ImmutableList.of("a", "b", "c").iterator())
+            .buildImmutable();
+    assertThat((List<?>) list3).containsExactly("a", "b", "c");
+
+    // mutability and builder re-use
+    StarlarkList.Builder<String> builder = StarlarkList.<String>builder().add("a").add("b");
+    StarlarkList<String> list4 = builder.buildImmutable();
+    assertThat((List<?>) list4).containsExactly("a", "b");
+    builder.add("c"); // continue building
+    StarlarkList<String> list5 = builder.buildImmutable();
+    assertThat((List<?>) list5).containsExactly("a", "b", "c");
+    assertThat((List<?>) list4).containsExactly("a", "b"); // unchanged
+    Mutability mu = Mutability.create("test");
+    StarlarkList<String> list6 = builder.build(mu);
+    list6.addElement("d");
+    mu.close();
+    assertThrows(EvalException.class, () -> list6.addElement("e")); // frozen
+    assertThat((List<?>) list6).containsExactly("a", "b", "c", "d");
+  }
+
+  @Test
+  public void testListsFromSameBuilderDoNotShareBackingArray() throws EvalException {
+    Mutability mu = Mutability.create("test");
+    StarlarkList.Builder<String> builder = StarlarkList.<String>builder().add("a").add("b");
+    StarlarkList<String> x = builder.build(mu);
+    x.setElementAt(1, "c");
+    StarlarkList<String> y = builder.build(mu);
+    y.setElementAt(1, "C");
+    assertThat((List<?>) x).containsExactly("a", "c");
+    assertThat((List<?>) y).containsExactly("a", "C");
+  }
 }