Implement structural equality for structs

RELNOTES: Structs in Skylark are tested for structural equality instead of reference equality.

--
MOS_MIGRATED_REVID=139583726
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObject.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObject.java
index 830021d..65fff09 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObject.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObject.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.packages;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Ordering;
@@ -31,6 +32,9 @@
 import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.util.Preconditions;
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
 
@@ -175,6 +179,10 @@
 
   @Override
   public boolean isImmutable() {
+    // If the constructor is not yet exported the hash code of the object is subject to change
+    if (!constructor.isExported()) {
+      return false;
+    }
     for (Object item : values.values()) {
       if (!EvalUtils.isImmutable(item)) {
         return false;
@@ -183,6 +191,43 @@
     return true;
   }
 
+  @Override
+  public boolean equals(Object otherObject) {
+    if (!(otherObject instanceof SkylarkClassObject)) {
+      return false;
+    }
+    SkylarkClassObject other = (SkylarkClassObject) otherObject;
+    if (this == other) {
+      return true;
+    }
+    if (!this.constructor.equals(other.constructor)) {
+      return false;
+    }
+    // Compare objects' keys and values
+    if (!this.getKeys().equals(other.getKeys())) {
+      return false;
+    }
+    for (String key : getKeys()) {
+      if (!this.getValue(key).equals(other.getValue(key))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    List<String> keys = new ArrayList<>(getKeys());
+    Collections.sort(keys);
+    List<Object> objectsToHash = new ArrayList<>();
+    objectsToHash.add(constructor);
+    for (String key : keys) {
+      objectsToHash.add(key);
+      objectsToHash.add(getValue(key));
+    }
+    return Objects.hashCode(objectsToHash.toArray());
+  }
+
   /**
    * Convert the object to string using Skylark syntax. The output tries to be
    * reversible (but there is no guarantee, it depends on the actual values).
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObjectConstructor.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObjectConstructor.java
index e478833..8291d1f 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObjectConstructor.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkClassObjectConstructor.java
@@ -135,14 +135,31 @@
 
   @Override
   public int hashCode() {
+    if (isExported()) {
+      return getKey().hashCode();
+    }
     return System.identityHashCode(this);
   }
 
   @Override
-  public boolean equals(@Nullable Object other) {
-    return other == this;
+  public boolean equals(@Nullable Object otherObject) {
+    if (!(otherObject instanceof  SkylarkClassObjectConstructor)) {
+      return false;
+    }
+    SkylarkClassObjectConstructor other = (SkylarkClassObjectConstructor) otherObject;
+
+    if (this.isExported() && other.isExported()) {
+      return this.getKey().equals(other.getKey());
+    } else {
+      return this == other;
+    }
   }
 
+  @Override
+  public boolean isImmutable() {
+    // Hash code for non exported constructors may be changed
+    return isExported();
+  }
 
   /**
    * A representation of {@link SkylarkClassObjectConstructor}.
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
index 3f2118d..7bc8bcb 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleClassFunctionsTest.java
@@ -792,6 +792,26 @@
   }
 
   @Test
+  public void testStructEquality() throws Exception {
+    assertTrue((Boolean) eval("struct(a = 1, b = 2) == struct(b = 2, a = 1)"));
+    assertFalse((Boolean) eval("struct(a = 1) == struct(a = 1, b = 2)"));
+    assertFalse((Boolean) eval("struct(a = 1, b = 2) == struct(a = 1)"));
+    // Compare a recursive object to itself to make sure reference equality is checked
+    assertTrue((Boolean) eval("s = (struct(a = 1, b = [])); s.b.append(s); s == s"));
+    assertFalse((Boolean) eval("struct(a = 1, b = 2) == struct(a = 1, b = 3)"));
+    assertFalse((Boolean) eval("struct(a = 1) == [1]"));
+    assertFalse((Boolean) eval("[1] == struct(a = 1)"));
+    assertTrue((Boolean) eval("struct() == struct()"));
+    assertFalse((Boolean) eval("struct() == struct(a = 1)"));
+
+    eval("foo = provider(); bar = provider()");
+    assertFalse((Boolean) eval("struct(a = 1) == foo(a = 1)"));
+    assertFalse((Boolean) eval("foo(a = 1) == struct(a = 1)"));
+    assertFalse((Boolean) eval("foo(a = 1) == bar(a = 1)"));
+    assertTrue((Boolean) eval("foo(a = 1) == foo(a = 1)"));
+  }
+
+  @Test
   public void testStructAccessingFieldsFromSkylark() throws Exception {
     eval("x = struct(a = 1, b = 2)", "x1 = x.a", "x2 = x.b");
     assertThat(lookup("x1")).isEqualTo(1);