Environment guarantees determinism when retrieving its bindings

Added a little javadoc and tests.

RELNOTES: None
PiperOrigin-RevId: 185569985
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
index cafa8a0..73d79a0 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
@@ -31,9 +31,8 @@
 import com.google.devtools.build.lib.util.SpellChecker;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -125,7 +124,8 @@
     @Nullable
     private Label label;
 
-    private final Map<String, Object> bindings;
+    /** Bindings are maintained in order of creation. */
+    private final LinkedHashMap<String, Object> bindings;
 
     /** Constructs an uninitialized instance; caller must call {@link #initialize} before use. */
     public Frame() {
@@ -222,6 +222,9 @@
     /**
      * Returns a map of direct bindings of this {@code Frame}, ignoring parents.
      *
+     * <p>The bindings are returned in a deterministic order (for a given sequence of initial values
+     * and updates).
+     *
      * <p>For efficiency an unmodifiable view is returned. Callers should assume that the view is
      * invalidated by any subsequent modification to the {@code Frame}'s bindings.
      */
@@ -233,16 +236,19 @@
     /**
      * Returns a map containing all bindings of this {@code Frame} and of its transitive parents,
      * taking into account shadowing precedence.
+     *
+     * <p>The bindings are returned in a deterministic order (for a given sequence of initial values
+     * and updates).
      */
     public Map<String, Object> getTransitiveBindings() {
       checkInitialized();
       // Can't use ImmutableMap.Builder because it doesn't allow duplicates.
-      HashMap<String, Object> collectedBindings = new HashMap<>();
+      LinkedHashMap<String, Object> collectedBindings = new LinkedHashMap<>();
       accumulateTransitiveBindings(collectedBindings);
       return collectedBindings;
     }
 
-    private void accumulateTransitiveBindings(Map<String, Object> accumulator) {
+    private void accumulateTransitiveBindings(LinkedHashMap<String, Object> accumulator) {
       checkInitialized();
       // Put parents first, so child bindings take precedence.
       if (parent != null) {
@@ -328,7 +334,7 @@
     final Frame globalFrame;
 
     /** The set of known global variables of the caller. */
-    @Nullable final Set<String> knownGlobalVariables;
+    @Nullable final LinkedHashSet<String> knownGlobalVariables;
 
     Continuation(
         Continuation continuation,
@@ -336,7 +342,7 @@
         FuncallExpression caller,
         Frame lexicalFrame,
         Frame globalFrame,
-        Set<String> knownGlobalVariables) {
+        LinkedHashSet<String> knownGlobalVariables) {
       this.continuation = continuation;
       this.function = function;
       this.caller = caller;
@@ -377,6 +383,7 @@
       return transitiveContentHashCode;
     }
 
+    /** Retrieves all bindings, in a deterministic order. */
     public ImmutableMap<String, Object> getBindings() {
       return bindings;
     }
@@ -507,7 +514,7 @@
    * reads a global variable after which a local variable with the same name is assigned an
    * Exception needs to be thrown.
    */
-  @Nullable private Set<String> knownGlobalVariables;
+  @Nullable private LinkedHashSet<String> knownGlobalVariables;
 
   /**
    * When in a lexical (Skylark) frame, this lists the names of the functions in the call stack.
@@ -537,7 +544,7 @@
     // parent?
     lexicalFrame = new Frame(mutability(), null);
     globalFrame = globals;
-    knownGlobalVariables = new HashSet<>();
+    knownGlobalVariables = new LinkedHashSet<>();
   }
 
   /**
@@ -592,14 +599,12 @@
     return dynamicFrame.mutability();
   }
 
-  /** @return the current Frame, in which variable side-effects happen. */
+  /** Returns the current Frame, in which variable side-effects happen. */
   private Frame currentFrame() {
     return isGlobal() ? globalFrame : lexicalFrame;
   }
 
-  /**
-   * @return the global variables for the Environment (not including dynamic bindings).
-   */
+  /** Returns the global variables for the Environment (not including dynamic bindings). */
   public Frame getGlobals() {
     return globalFrame;
   }
@@ -755,7 +760,7 @@
     public Environment build() {
       Preconditions.checkArgument(!mutability.isFrozen());
       if (parent != null) {
-        Preconditions.checkArgument(parent.mutability().isFrozen());
+        Preconditions.checkArgument(parent.mutability().isFrozen(), "parent frame must be frozen");
       }
       Frame globalFrame = new Frame(mutability, parent);
       Frame dynamicFrame = new Frame(mutability, null);
@@ -907,7 +912,7 @@
   }
 
   /**
-   * @return the value from the environment whose name is "varname" if it exists, otherwise null.
+   * Returns the value from the environment whose name is "varname" if it exists, otherwise null.
    */
   public Object lookup(String varname) {
     // Lexical frame takes precedence, then globals, then dynamics.
@@ -932,8 +937,8 @@
   }
 
   /**
-   * @return true if varname is a known global variable,
-   * because it has been read in the context of the current function.
+   * Returns true if varname is a known global variable (i.e., it has been read in the context of
+   * the current function).
    */
   boolean isKnownGlobalVariable(String varname) {
     return knownGlobalVariables != null && knownGlobalVariables.contains(varname);
@@ -947,9 +952,12 @@
     eventHandler.handle(event);
   }
 
-  /** Returns a set of all names of variables that are accessible in this {@code Environment}. */
+  /**
+   * Returns a set of all names of variables that are accessible in this {@code Environment}, in a
+   * deterministic order.
+   */
   public Set<String> getVariableNames() {
-    Set<String> vars = new HashSet<>();
+    LinkedHashSet<String> vars = new LinkedHashSet<>();
     if (lexicalFrame != null) {
       vars.addAll(lexicalFrame.getTransitiveBindings().keySet());
     }
@@ -1016,6 +1024,10 @@
     }
   }
 
+  /**
+   * Computes a deterministic hash for the given base hash code and extension map (the map's order
+   * does not matter).
+   */
   private static String computeTransitiveContentHashCode(
       @Nullable String baseHashCode, Map<String, Extension> importedExtensions) {
     // Calculate a new hash from the hash of the loaded Extension-s.