[LIR] Move LIR to after synthetic finalization

This change requires that LIR code can be hashed and compared to create
a deterministic order for each code object.

Bug: b/225838009
Change-Id: I79a36e36c76b95d352ef0e194b33ddabd1648caf
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index a306d434..bbaa2b9 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -710,15 +710,15 @@
           appView.getArtProfileCollection().withoutMissingItems(appView));
       appView.setStartupProfile(appView.getStartupProfile().withoutMissingItems(appView));
 
-      // TODO(b/225838009): Support LIR in synthetic finalization (needs hashing and compareTo).
-      PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-
       if (appView.appInfo().hasLiveness()) {
         SyntheticFinalization.finalizeWithLiveness(appView.withLiveness(), executorService, timing);
       } else {
         SyntheticFinalization.finalizeWithClassHierarchy(appView, executorService, timing);
       }
 
+      // TODO(b/225838009): Move further down.
+      PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+
       // Read any -applymapping input to allow for repackaging to not relocate the classes.
       timing.begin("read -applymapping file");
       appView.loadApplyMappingSeedMapper();
diff --git a/src/main/java/com/android/tools/r8/graph/DexCallSite.java b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
index 2f84316..b40ef2b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCallSite.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
@@ -11,6 +11,8 @@
 import com.android.tools.r8.graph.DexValue.DexValueMethodType;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.lightir.LirConstant;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.StructuralItem;
 import com.android.tools.r8.utils.structural.StructuralMapping;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
@@ -193,6 +195,21 @@
     return new HashBuilder().build();
   }
 
+  @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.CALL_SITE;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexCallSite) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    acceptHashing(visitor);
+  }
+
   private final class HashBuilder {
     private ByteArrayOutputStream bytes;
     private ObjectOutputStream out;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 5452bf2..3e1856d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -295,6 +295,9 @@
       // This call is to remain order consistent with the 'withNullableItem' code.
       return visitor.visitBool(code1 != null, code2 != null);
     }
+    if (code1.isLirCode() && code2.isLirCode()) {
+      return code1.asLirCode().acceptCompareTo(code2.asLirCode(), visitor);
+    }
     if (code1.isCfWritableCode() && code2.isCfWritableCode()) {
       return code1.asCfWritableCode().acceptCompareTo(code2.asCfWritableCode(), visitor);
     }
@@ -309,6 +312,8 @@
     if (code == null) {
       // The null code does not contribute to the hash. This should be distinct from non-null as
       // code otherwise has a non-empty instruction payload.
+    } else if (code.isLirCode()) {
+      code.asLirCode().acceptHashing(visitor);
     } else if (code.isCfWritableCode()) {
       code.asCfWritableCode().acceptHashing(visitor);
     } else {
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 9fbbdb1..7628bd6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
@@ -50,10 +51,18 @@
   }
 
   @Override
-  public void acceptHashing(HashingVisitor visitor) {
-    visitor.visitDexType(holder);
-    visitor.visitDexString(name);
-    getReferencedTypes().forEach(visitor::visitDexType);
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.FIELD;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexField) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    acceptHashing(visitor);
   }
 
   @Override
@@ -177,6 +186,11 @@
   }
 
   @Override
+  public void acceptHashing(HashingVisitor visitor) {
+    visitor.visitDexField(this);
+  }
+
+  @Override
   public boolean match(DexField field) {
     return field.name == name && field.type == type;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index 1ad1e1b..a8c8ddf 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
@@ -63,9 +64,22 @@
 
   @Override
   public void acceptHashing(HashingVisitor visitor) {
-    visitor.visitDexType(holder);
-    visitor.visitDexString(name);
-    getReferencedTypes().forEach(visitor::visitDexType);
+    visitor.visitDexMethod(this);
+  }
+
+  @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.METHOD;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexMethod) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    acceptHashing(visitor);
   }
 
   public DexType getArgumentType(int argumentIndex, boolean isStatic) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
index a61182e..e48785c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.ir.code.InvokeType;
 import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.StructuralMapping;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Objects;
@@ -18,6 +20,21 @@
 public class DexMethodHandle extends IndexedDexItem
     implements NamingLensComparable<DexMethodHandle>, LirConstant {
 
+  @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.METHOD_HANDLE;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexMethodHandle) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    acceptHashing(visitor);
+  }
+
   public enum MethodHandleType {
     STATIC_PUT((short) 0x00),
     STATIC_GET((short) 0x01),
diff --git a/src/main/java/com/android/tools/r8/graph/DexProto.java b/src/main/java/com/android/tools/r8/graph/DexProto.java
index 3e9d428..6ca01ec 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProto.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProto.java
@@ -6,6 +6,8 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.StructuralMapping;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.Iterables;
@@ -17,6 +19,7 @@
 
   public static final DexProto SENTINEL = new DexProto(null, null, null);
 
+  // TODO(b/172206529): Consider removing shorty.
   public final DexString shorty;
   public final DexType returnType;
   public final DexTypeList parameters;
@@ -28,10 +31,7 @@
   }
 
   private static void specify(StructuralSpecification<DexProto, ?> spec) {
-    spec.withItem(DexProto::getReturnType)
-        .withItem(p -> p.parameters)
-        // TODO(b/172206529): Consider removing shorty.
-        .withItem(p1 -> p1.shorty);
+    spec.withItem(DexProto::getReturnType).withItem(p -> p.parameters);
   }
 
   @Override
@@ -137,4 +137,19 @@
     builder.append(lens.lookupDescriptor(returnType));
     return builder.toString();
   }
+
+  @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.PROTO;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexProto) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    acceptHashing(visitor);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index f00c125..eea89b2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -98,6 +98,21 @@
     visitor.visitDexString(this);
   }
 
+  @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.STRING;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexString) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    visitor.visitDexString(this);
+  }
+
   public ThrowingCharIterator<UTFDataFormatException> iterator() {
     return new ThrowingCharIterator<UTFDataFormatException>() {
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index c7f8aa2..204d05e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.lightir.LirConstant;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
@@ -111,6 +112,21 @@
   }
 
   @Override
+  public LirConstantOrder getLirConstantOrder() {
+    return LirConstantOrder.TYPE;
+  }
+
+  @Override
+  public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+    return acceptCompareTo((DexType) other, visitor);
+  }
+
+  @Override
+  public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+    acceptHashing(visitor);
+  }
+
+  @Override
   public DexType getContextType() {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index f633d53..d26c44c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -4,11 +4,19 @@
 
 package com.android.tools.r8.ir.code;
 
-public class IRMetadata {
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
+
+public class IRMetadata implements StructuralItem<IRMetadata> {
 
   private long first;
   private long second;
 
+  private static void specify(StructuralSpecification<IRMetadata, ?> spec) {
+    spec.withLong(s -> s.first).withLong(s -> s.second);
+  }
+
   public IRMetadata() {}
 
   private IRMetadata(long first, long second) {
@@ -20,6 +28,16 @@
     return new IRMetadata(0xFFFFFFFFFFFFFFFFL, 0xFFFFFFFFFFFFFFFFL);
   }
 
+  @Override
+  public IRMetadata self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<IRMetadata> getStructuralMapping() {
+    return IRMetadata::specify;
+  }
+
   private boolean get(int bit) {
     if (bit < 64) {
       return isAnySetInFirst(1L << bit);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index cc457a3..d21044f 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -40,6 +40,11 @@
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -102,7 +107,8 @@
    * <p>The instruction encoding assumes the instruction operand payload size is u1, so the data
    * payload is stored in the constant pool instead.
    */
-  public static class FillArrayPayload extends InstructionPayload {
+  public static class FillArrayPayload extends InstructionPayload
+      implements StructuralItem<FillArrayPayload> {
     public final int element_width;
     public final long size;
     public final short[] data;
@@ -112,20 +118,80 @@
       this.size = size;
       this.data = data;
     }
+
+    @Override
+    public FillArrayPayload self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<FillArrayPayload> getStructuralMapping() {
+      return FillArrayPayload::specify;
+    }
+
+    private static void specify(StructuralSpecification<FillArrayPayload, ?> spec) {
+      spec.withInt(s -> s.element_width).withLong(s -> s.size).withShortArray(s -> s.data);
+    }
+
+    @Override
+    public LirConstantOrder getLirConstantOrder() {
+      return LirConstantOrder.FILL_ARRAY;
+    }
+
+    @Override
+    public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+      return acceptCompareTo((FillArrayPayload) other, visitor);
+    }
+
+    @Override
+    public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+      acceptHashing(visitor);
+    }
   }
 
-  public static class IntSwitchPayload extends InstructionPayload {
+  public static class IntSwitchPayload extends InstructionPayload
+      implements StructuralItem<IntSwitchPayload> {
     public final int[] keys;
     public final int[] targets;
 
+    private static void specify(StructuralSpecification<IntSwitchPayload, ?> spec) {
+      spec.withIntArray(s -> s.keys).withIntArray(s -> s.targets);
+    }
+
     public IntSwitchPayload(int[] keys, int[] targets) {
       assert keys.length == targets.length;
       this.keys = keys;
       this.targets = targets;
     }
+
+    @Override
+    public IntSwitchPayload self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<IntSwitchPayload> getStructuralMapping() {
+      return IntSwitchPayload::specify;
+    }
+
+    @Override
+    public LirConstantOrder getLirConstantOrder() {
+      return LirConstantOrder.INT_SWITCH;
+    }
+
+    @Override
+    public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+      return acceptCompareTo((IntSwitchPayload) other, visitor);
+    }
+
+    @Override
+    public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+      acceptHashing(visitor);
+    }
   }
 
-  public static class StringSwitchPayload extends InstructionPayload {
+  public static class StringSwitchPayload extends InstructionPayload
+      implements StructuralItem<StringSwitchPayload> {
     public final int[] keys;
     public final int[] targets;
 
@@ -134,6 +200,35 @@
       this.keys = keys;
       this.targets = targets;
     }
+
+    private static void specify(StructuralSpecification<StringSwitchPayload, ?> spec) {
+      spec.withIntArray(s -> s.keys).withIntArray(s -> s.targets);
+    }
+
+    @Override
+    public StringSwitchPayload self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<StringSwitchPayload> getStructuralMapping() {
+      return StringSwitchPayload::specify;
+    }
+
+    @Override
+    public LirConstantOrder getLirConstantOrder() {
+      return LirConstantOrder.STRING_SWITCH;
+    }
+
+    @Override
+    public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+      acceptHashing(visitor);
+    }
+
+    @Override
+    public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+      return acceptCompareTo((StringSwitchPayload) other, visitor);
+    }
   }
 
   public static class NameComputationPayload extends InstructionPayload {
@@ -142,6 +237,19 @@
     public NameComputationPayload(NameComputationInfo<?> nameComputationInfo) {
       this.nameComputationInfo = nameComputationInfo;
     }
+
+    @Override
+    public LirConstantOrder getLirConstantOrder() {
+      return LirConstantOrder.NAME_COMPUTATION;
+    }
+
+    @Override
+    public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+      return 0;
+    }
+
+    @Override
+    public void internalLirConstantAcceptHashing(HashingVisitor visitor) {}
   }
 
   public static class RecordFieldValuesPayload extends InstructionPayload {
@@ -150,6 +258,21 @@
     public RecordFieldValuesPayload(DexField[] fields) {
       this.fields = fields;
     }
+
+    @Override
+    public LirConstantOrder getLirConstantOrder() {
+      return LirConstantOrder.RECORD_FIELD_VALUES;
+    }
+
+    @Override
+    public int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor) {
+      return visitor.visitItemArray(fields, ((RecordFieldValuesPayload) other).fields);
+    }
+
+    @Override
+    public void internalLirConstantAcceptHashing(HashingVisitor visitor) {
+      visitor.visitItemArray(fields);
+    }
   }
 
   public LirBuilder(
diff --git a/src/main/java/com/android/tools/r8/lightir/LirCode.java b/src/main/java/com/android/tools/r8/lightir/LirCode.java
index 00930a8..e99b870 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirCode.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirCode.java
@@ -30,21 +30,34 @@
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
+import com.android.tools.r8.lightir.LirConstant.LirConstantStructuralAcceptor;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.RetracerForCodePrinting;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAcceptor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.ImmutableMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-public class LirCode<EV> extends Code implements Iterable<LirInstructionView> {
+public class LirCode<EV> extends Code
+    implements StructuralItem<LirCode<EV>>, Iterable<LirInstructionView> {
 
-  public abstract static class PositionEntry {
+  public abstract static class PositionEntry implements StructuralItem<PositionEntry> {
 
     private final int fromInstructionIndex;
 
@@ -57,6 +70,37 @@
     }
 
     public abstract Position getPosition(DexMethod method);
+
+    abstract int getOrder();
+
+    abstract int internalAcceptCompareTo(PositionEntry other, CompareToVisitor visitor);
+
+    abstract void internalAcceptHashing(HashingVisitor visitor);
+
+    @Override
+    public final PositionEntry self() {
+      return this;
+    }
+
+    @Override
+    public final StructuralMapping<PositionEntry> getStructuralMapping() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public final int acceptCompareTo(PositionEntry other, CompareToVisitor visitor) {
+      int diff = visitor.visitInt(getOrder(), other.getOrder());
+      if (diff != 0) {
+        return diff;
+      }
+      return internalAcceptCompareTo(other, visitor);
+    }
+
+    @Override
+    public final void acceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(getOrder());
+      internalAcceptHashing(visitor);
+    }
   }
 
   public static class LinePositionEntry extends PositionEntry {
@@ -75,6 +119,21 @@
     public Position getPosition(DexMethod method) {
       return SourcePosition.builder().setMethod(method).setLine(line).build();
     }
+
+    @Override
+    int getOrder() {
+      return 0;
+    }
+
+    @Override
+    int internalAcceptCompareTo(PositionEntry other, CompareToVisitor visitor) {
+      return visitor.visitInt(line, ((LinePositionEntry) other).line);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(line);
+    }
   }
 
   public static class StructuredPositionEntry extends PositionEntry {
@@ -89,9 +148,24 @@
     public Position getPosition(DexMethod method) {
       return position;
     }
+
+    @Override
+    int getOrder() {
+      return 1;
+    }
+
+    @Override
+    int internalAcceptCompareTo(PositionEntry other, CompareToVisitor visitor) {
+      return position.acceptCompareTo(((StructuredPositionEntry) other).position, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      position.acceptHashing(visitor);
+    }
   }
 
-  public static class TryCatchTable {
+  public static class TryCatchTable implements StructuralItem<TryCatchTable> {
     private final Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers;
 
     public TryCatchTable(Int2ReferenceMap<CatchHandlers<Integer>> tryCatchHandlers) {
@@ -107,9 +181,47 @@
     public void forEachHandler(BiConsumer<Integer, CatchHandlers<Integer>> fn) {
       tryCatchHandlers.forEach(fn);
     }
+
+    @Override
+    public TryCatchTable self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<TryCatchTable> getStructuralMapping() {
+      return TryCatchTable::specify;
+    }
+
+    private static void specify(StructuralSpecification<TryCatchTable, ?> spec) {
+      spec.withInt2CustomItemMap(
+          s -> s.tryCatchHandlers,
+          new StructuralAcceptor<>() {
+            @Override
+            public int acceptCompareTo(
+                CatchHandlers<Integer> item1,
+                CatchHandlers<Integer> item2,
+                CompareToVisitor visitor) {
+              int diff = visitor.visitItemCollection(item1.getGuards(), item2.getGuards());
+              if (diff != 0) {
+                return diff;
+              }
+              return ComparatorUtils.compareLists(item1.getAllTargets(), item2.getAllTargets());
+            }
+
+            @Override
+            public void acceptHashing(CatchHandlers<Integer> item, HashingVisitor visitor) {
+              List<Integer> targets = item.getAllTargets();
+              for (int i = 0; i < targets.size(); i++) {
+                visitor.visitDexType(item.getGuard(i));
+                visitor.visitInt(targets.get(i));
+              }
+            }
+          });
+    }
   }
 
-  public static class DebugLocalInfoTable<EV> {
+  public static class DebugLocalInfoTable<EV> implements StructuralItem<DebugLocalInfoTable<EV>> {
+    // TODO(b/225838009): Once EV is removed use an int2ref map here.
     private final Map<EV, DebugLocalInfo> valueToLocalMap;
     private final Int2ReferenceMap<int[]> instructionToEndUseMap;
 
@@ -135,6 +247,71 @@
     public void forEachLocalDefinition(BiConsumer<EV, DebugLocalInfo> fn) {
       valueToLocalMap.forEach(fn);
     }
+
+    @Override
+    public DebugLocalInfoTable<EV> self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<DebugLocalInfoTable<EV>> getStructuralMapping() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public int acceptCompareTo(DebugLocalInfoTable<EV> other, CompareToVisitor visitor) {
+      int size = valueToLocalMap.size();
+      int diff = Integer.compare(size, other.valueToLocalMap.size());
+      if (diff != 0) {
+        return diff;
+      }
+      if ((instructionToEndUseMap == null) != (other.instructionToEndUseMap == null)) {
+        return instructionToEndUseMap == null ? -1 : 1;
+      }
+      if (instructionToEndUseMap != null) {
+        assert other.instructionToEndUseMap != null;
+        diff =
+            ComparatorUtils.compareInt2ReferenceMap(
+                instructionToEndUseMap,
+                other.instructionToEndUseMap,
+                ComparatorUtils::compareIntArray);
+        if (diff != 0) {
+          return diff;
+        }
+      }
+      // We know EV is only instantiated with Integer so this is safe.
+      assert !(valueToLocalMap instanceof Int2ReferenceMap);
+      Int2ReferenceOpenHashMap<DebugLocalInfo> map1 = new Int2ReferenceOpenHashMap<>(size);
+      Int2ReferenceOpenHashMap<DebugLocalInfo> map2 = new Int2ReferenceOpenHashMap<>(size);
+      valueToLocalMap.forEach((k, v) -> map1.put((int) k, v));
+      other.valueToLocalMap.forEach((k, v) -> map2.put((int) k, v));
+      return ComparatorUtils.compareInt2ReferenceMap(
+          map1, map2, (i1, i2) -> i1.acceptCompareTo(i2, visitor));
+    }
+
+    @Override
+    public void acceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(valueToLocalMap.size());
+      ArrayList<Integer> keys = new ArrayList<>(valueToLocalMap.size());
+      valueToLocalMap.forEach((k, v) -> keys.add((int) k));
+      keys.sort(Integer::compareTo);
+      for (int key : keys) {
+        visitor.visitInt(key);
+        valueToLocalMap.get(key).acceptHashing(visitor);
+      }
+      if (instructionToEndUseMap != null) {
+        // Instead of sorting the end map we just sum the keys and values which is commutative.
+        IntBox keySum = new IntBox();
+        IntBox valueSum = new IntBox();
+        instructionToEndUseMap.forEach(
+            (k, v) -> {
+              keySum.increment(k);
+              valueSum.increment(Arrays.hashCode(v));
+            });
+        visitor.visitInt(keySum.get());
+        visitor.visitInt(valueSum.get());
+      }
+    }
   }
 
   private final LirStrategyInfo<EV> strategyInfo;
@@ -171,6 +348,20 @@
     return new LirBuilder<>(method, strategy, options);
   }
 
+  private static <EV> void specify(StructuralSpecification<LirCode<EV>, ?> spec) {
+    // strategyInfo is compiler meta-data (constant for a given compilation unit).
+    // useDexEstimationStrategy is compiler meta-data (constant for a given compilation unit).
+    spec.withItem(c -> c.irMetadata)
+        .withCustomItemArray(c -> c.constants, LirConstantStructuralAcceptor.getInstance())
+        .withItemArray(c -> c.positionTable)
+        .withInt(c -> c.argumentCount)
+        .withByteArray(c -> c.instructions)
+        .withInt(c -> c.instructionCount)
+        .withNullableItem(c -> c.tryCatchTable)
+        .withNullableItem(c -> c.debugLocalInfoTable)
+        .withAssert(c -> c.metadataMap == null);
+  }
+
   /** Should be constructed using {@link LirBuilder}. */
   LirCode(
       IRMetadata irMetadata,
@@ -205,6 +396,16 @@
   }
 
   @Override
+  public LirCode<EV> self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<LirCode<EV>> getStructuralMapping() {
+    return LirCode::specify;
+  }
+
+  @Override
   protected int computeHashCode() {
     throw new Unreachable("LIR code should not be subject to hashing.");
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirConstant.java b/src/main/java/com/android/tools/r8/lightir/LirConstant.java
index 6c3dc4d..41a7115 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirConstant.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirConstant.java
@@ -4,5 +4,68 @@
 
 package com.android.tools.r8.lightir;
 
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAcceptor;
+
 /** Interface for items that can be put in the LIR constant pool. */
-public interface LirConstant {}
+public interface LirConstant {
+
+  enum LirConstantOrder {
+    // DexItem derived constants.
+    STRING,
+    TYPE,
+    FIELD,
+    METHOD,
+    PROTO,
+    METHOD_HANDLE,
+    CALL_SITE,
+    // Payload constants.
+    INT_SWITCH,
+    STRING_SWITCH,
+    FILL_ARRAY,
+    NAME_COMPUTATION,
+    RECORD_FIELD_VALUES
+  }
+
+  class LirConstantStructuralAcceptor implements StructuralAcceptor<LirConstant> {
+    private static final LirConstantStructuralAcceptor INSTANCE =
+        new LirConstantStructuralAcceptor();
+
+    public static LirConstantStructuralAcceptor getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public int acceptCompareTo(LirConstant item1, LirConstant item2, CompareToVisitor visitor) {
+      int diff =
+          visitor.visitInt(
+              item1.getLirConstantOrder().ordinal(), item2.getLirConstantOrder().ordinal());
+      if (diff != 0) {
+        return diff;
+      }
+      return item1.internalLirConstantAcceptCompareTo(item2, visitor);
+    }
+
+    @Override
+    public void acceptHashing(LirConstant item, HashingVisitor visitor) {
+      visitor.visitInt(item.getLirConstantOrder().ordinal());
+      item.internalLirConstantAcceptHashing(visitor);
+    }
+  }
+
+  /** Total order on the subtypes to ensure compareTo order on the general type. */
+  LirConstantOrder getLirConstantOrder();
+
+  /**
+   * Implementation of compareTo on the actual subtypes.
+   *
+   * <p>The implementation can assume that 'other' is of the same subtype, since it must hold that
+   *
+   * <pre>this.getOrder() == other.getOrder()</pre>
+   */
+  int internalLirConstantAcceptCompareTo(LirConstant other, CompareToVisitor visitor);
+
+  /** Implementation of hashing on the actual subtypes. */
+  void internalLirConstantAcceptHashing(HashingVisitor visitor);
+}
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/ClassNameComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/ClassNameComputationInfo.java
index 59e398c..34a9076 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/ClassNameComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/ClassNameComputationInfo.java
@@ -15,8 +15,14 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 
-public class ClassNameComputationInfo extends NameComputationInfo<DexType> {
+public class ClassNameComputationInfo extends NameComputationInfo<DexType>
+    implements StructuralItem<ClassNameComputationInfo> {
 
   public enum ClassNameMapping {
     NONE,
@@ -102,6 +108,10 @@
   private final int arrayDepth;
   private final ClassNameMapping mapping;
 
+  public static void specify(StructuralSpecification<ClassNameComputationInfo, ?> spec) {
+    spec.withInt(s -> s.arrayDepth).withInt(s -> s.mapping.ordinal());
+  }
+
   private ClassNameComputationInfo(ClassNameMapping mapping) {
     this(mapping, 0);
   }
@@ -139,6 +149,31 @@
   }
 
   @Override
+  public ClassNameComputationInfo self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<ClassNameComputationInfo> getStructuralMapping() {
+    return ClassNameComputationInfo::specify;
+  }
+
+  @Override
+  Order getOrder() {
+    return Order.CLASSNAME;
+  }
+
+  @Override
+  int internalAcceptCompareTo(NameComputationInfo<?> other, CompareToVisitor visitor) {
+    return StructuralItem.super.acceptCompareTo((ClassNameComputationInfo) other, visitor);
+  }
+
+  @Override
+  void internalAcceptHashing(HashingVisitor visitor) {
+    StructuralItem.super.acceptHashing(visitor);
+  }
+
+  @Override
   public boolean needsToComputeName() {
     return mapping.needsToComputeClassName();
   }
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/FieldNameComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/FieldNameComputationInfo.java
index f9f8cc0..a99056e 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/FieldNameComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/FieldNameComputationInfo.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
 
 public class FieldNameComputationInfo extends NameComputationInfo<DexField> {
 
@@ -29,6 +31,22 @@
   }
 
   @Override
+  Order getOrder() {
+    return Order.FIELDNAME;
+  }
+
+  @Override
+  int internalAcceptCompareTo(NameComputationInfo<?> other, CompareToVisitor visitor) {
+    assert this == other;
+    return 0;
+  }
+
+  @Override
+  void internalAcceptHashing(HashingVisitor visitor) {
+    // Nothing to hash.
+  }
+
+  @Override
   public boolean needsToComputeName() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
index 11014f6..02e29aa 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
@@ -9,9 +9,18 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
 
 public abstract class NameComputationInfo<T extends DexReference> {
 
+  enum Order {
+    CLASSNAME,
+    FIELDNAME,
+    RECORD_MATCH,
+    RECORD_MISMATCH
+  }
+
   public final DexString computeNameFor(
       DexReference reference,
       DexDefinitionSupplier definitions,
@@ -39,6 +48,25 @@
   abstract DexString internalComputeNameFor(
       T reference, DexDefinitionSupplier definitions, NamingLens namingLens);
 
+  abstract Order getOrder();
+
+  public int acceptCompareTo(NameComputationInfo<?> other, CompareToVisitor visitor) {
+    int diff = visitor.visitInt(getOrder().ordinal(), other.getOrder().ordinal());
+    if (diff != 0) {
+      return diff;
+    }
+    return internalAcceptCompareTo(other, visitor);
+  }
+
+  public void acceptHashing(HashingVisitor visitor) {
+    visitor.visitInt(getOrder().ordinal());
+    internalAcceptHashing(visitor);
+  }
+
+  abstract int internalAcceptCompareTo(NameComputationInfo<?> other, CompareToVisitor visitor);
+
+  abstract void internalAcceptHashing(HashingVisitor visitor);
+
   public abstract boolean needsToComputeName();
 
   public abstract boolean needsToRegisterReference();
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java
index 253aa47..9d93047 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/RecordFieldNamesComputationInfo.java
@@ -14,7 +14,14 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAcceptor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.IntFunction;
@@ -43,7 +50,8 @@
   }
 
   private static class MissMatchingRecordFieldNamesComputationInfo
-      extends RecordFieldNamesComputationInfo {
+      extends RecordFieldNamesComputationInfo
+      implements StructuralItem<MissMatchingRecordFieldNamesComputationInfo> {
 
     private final String[] fieldNames;
 
@@ -52,6 +60,27 @@
       this.fieldNames = fieldNames;
     }
 
+    private static void specify(
+        StructuralSpecification<MissMatchingRecordFieldNamesComputationInfo, ?> spec) {
+      spec.withItemArray(s -> s.fields)
+          .withCustomItem(
+              s -> s.fieldNames,
+              new StructuralAcceptor<>() {
+                @Override
+                public int acceptCompareTo(
+                    String[] item1, String[] item2, CompareToVisitor visitor) {
+                  return ComparatorUtils.arrayComparator(String::compareTo).compare(item1, item2);
+                }
+
+                @Override
+                public void acceptHashing(String[] item, HashingVisitor visitor) {
+                  for (String s : item) {
+                    visitor.visitJavaString(s);
+                  }
+                }
+              });
+    }
+
     @Override
     public DexString internalComputeNameFor(
         DexType type,
@@ -60,15 +89,47 @@
         NamingLens namingLens) {
       return internalComputeNameFor(type, definitions, graphLens, i -> fieldNames[i]);
     }
+
+    @Override
+    Order getOrder() {
+      return Order.RECORD_MISMATCH;
+    }
+
+    @Override
+    public MissMatchingRecordFieldNamesComputationInfo self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<MissMatchingRecordFieldNamesComputationInfo> getStructuralMapping() {
+      return MissMatchingRecordFieldNamesComputationInfo::specify;
+    }
+
+    @Override
+    int internalAcceptCompareTo(NameComputationInfo<?> other, CompareToVisitor visitor) {
+      return StructuralItem.super.acceptCompareTo(
+          (MissMatchingRecordFieldNamesComputationInfo) other, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      StructuralItem.super.acceptHashing(visitor);
+    }
   }
 
   private static class MatchingRecordFieldNamesComputationInfo
-      extends RecordFieldNamesComputationInfo {
+      extends RecordFieldNamesComputationInfo
+      implements StructuralItem<MatchingRecordFieldNamesComputationInfo> {
 
     public MatchingRecordFieldNamesComputationInfo(DexField[] fields) {
       super(fields);
     }
 
+    private static void specify(
+        StructuralSpecification<MatchingRecordFieldNamesComputationInfo, ?> spec) {
+      spec.withItemArray(s -> s.fields);
+    }
+
     @Override
     public DexString internalComputeNameFor(
         DexType type,
@@ -86,6 +147,32 @@
                   .name
                   .toString());
     }
+
+    @Override
+    Order getOrder() {
+      return Order.RECORD_MATCH;
+    }
+
+    @Override
+    public MatchingRecordFieldNamesComputationInfo self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<MatchingRecordFieldNamesComputationInfo> getStructuralMapping() {
+      return MatchingRecordFieldNamesComputationInfo::specify;
+    }
+
+    @Override
+    int internalAcceptCompareTo(NameComputationInfo<?> other, CompareToVisitor visitor) {
+      return StructuralItem.super.acceptCompareTo(
+          (MatchingRecordFieldNamesComputationInfo) other, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      StructuralItem.super.acceptHashing(visitor);
+    }
   }
 
   static DexString dexStringFromFieldNames(List<String> fieldNames, DexItemFactory factory) {
diff --git a/src/main/java/com/android/tools/r8/utils/ComparatorUtils.java b/src/main/java/com/android/tools/r8/utils/ComparatorUtils.java
index 59cac42..36ce846 100644
--- a/src/main/java/com/android/tools/r8/utils/ComparatorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ComparatorUtils.java
@@ -4,6 +4,8 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.errors.Unreachable;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 
@@ -14,13 +16,19 @@
   }
 
   public static <T> Comparator<List<T>> listComparator(Comparator<T> comparator) {
-    return (List<T> xs, List<T> ys) -> {
-      int diff = Integer.compare(xs.size(), ys.size());
-      for (int i = 0; i < xs.size() && diff == 0; i++) {
-        diff = comparator.compare(xs.get(i), ys.get(i));
-      }
-      return diff;
-    };
+    return (List<T> xs, List<T> ys) -> compareLists(xs, ys, comparator);
+  }
+
+  public static <T extends Comparable<T>> int compareLists(List<T> xs, List<T> ys) {
+    return compareLists(xs, ys, T::compareTo);
+  }
+
+  public static <T> int compareLists(List<T> xs, List<T> ys, Comparator<T> comparator) {
+    int diff = Integer.compare(xs.size(), ys.size());
+    for (int i = 0; i < xs.size() && diff == 0; i++) {
+      diff = comparator.compare(xs.get(i), ys.get(i));
+    }
+    return diff;
   }
 
   // Compare pair-wise integers in sequenced order, i.e., (A1, A2), (B1, B2), (C1, C2), ...
@@ -68,4 +76,52 @@
       throw new Unreachable();
     };
   }
+
+  public static <S> Comparator<Int2ReferenceMap<S>> int2ReferenceMapComparator(
+      Comparator<S> comparator) {
+    return (map1, map2) -> compareInt2ReferenceMap(map1, map2, comparator);
+  }
+
+  public static <S> int compareInt2ReferenceMap(
+      Int2ReferenceMap<S> map1, Int2ReferenceMap<S> map2, Comparator<S> comparator) {
+    int sizeDiff = Integer.compare(map1.size(), map2.size());
+    if (sizeDiff != 0) {
+      return sizeDiff;
+    }
+    if (map1.isEmpty()) {
+      assert map2.isEmpty();
+      return 0;
+    }
+    Integer minMissing1 = findSmallestMissingKey(map1, map2);
+    Integer minMissing2 = findSmallestMissingKey(map2, map1);
+    if (minMissing1 != null) {
+      assert minMissing2 != null;
+      return minMissing1.compareTo(minMissing2);
+    }
+    // Keys are equal so compare the values point-wise sorted by the keys.
+    ArrayList<Integer> keys = new ArrayList<>(map1.keySet());
+    keys.sort(Integer::compareTo);
+    for (int key : keys) {
+      S item1 = map1.get(key);
+      S item2 = map2.get(key);
+      int diff = comparator.compare(item1, item2);
+      if (diff != 0) {
+        return diff;
+      }
+    }
+    return 0;
+  }
+
+  private static <S> Integer findSmallestMissingKey(
+      Int2ReferenceMap<S> map1, Int2ReferenceMap<S> map2) {
+    boolean hasMissing = false;
+    int missing = Integer.MAX_VALUE;
+    for (int key : map1.keySet()) {
+      if (!map2.containsKey(key)) {
+        missing = hasMissing ? Math.min(missing, key) : key;
+        hasMissing = true;
+      }
+    }
+    return hasMissing ? missing : null;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
index c721599..a476214 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
@@ -174,6 +174,22 @@
     }
 
     @Override
+    public ItemSpecification<T> withByteArray(Function<T, byte[]> getter) {
+      if (order == 0) {
+        byte[] is1 = getter.apply(item1);
+        byte[] is2 = getter.apply(item2);
+        int minLength = Math.min(is1.length, is2.length);
+        for (int i = 0; i < minLength && order == 0; i++) {
+          order = parent.visitInt(is1[i], is2[i]);
+        }
+        if (order == 0) {
+          order = parent.visitInt(is1.length, is2.length);
+        }
+      }
+      return this;
+    }
+
+    @Override
     public ItemSpecification<T> withShortArray(Function<T, short[]> getter) {
       if (order == 0) {
         short[] is1 = getter.apply(item1);
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
index 244d404..c13e0b8 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
@@ -85,6 +85,11 @@
   }
 
   @Override
+  public HashCodeVisitor<T> withByteArray(Function<T, byte[]> getter) {
+    return amend(Arrays.hashCode(getter.apply(item)));
+  }
+
+  @Override
   public HashCodeVisitor<T> withShortArray(Function<T, short[]> getter) {
     return amend(Arrays.hashCode(getter.apply(item)));
   }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
index 538ddcd..7054a40 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
@@ -36,6 +36,8 @@
     visitItemIterator(items.iterator(), S::acceptHashing);
   }
 
+  public abstract void visitJavaString(String string);
+
   public abstract void visitDexString(DexString string);
 
   public abstract void visitDexType(DexType type);
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
index 303f212..b355673 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.nio.charset.StandardCharsets;
 import java.util.Iterator;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -62,6 +63,11 @@
   }
 
   @Override
+  public void visitJavaString(String string) {
+    hash.putBytes(string.getBytes(StandardCharsets.UTF_8));
+  }
+
+  @Override
   public void visitDexString(DexString string) {
     hash.putBytes(string.content);
   }
@@ -139,6 +145,15 @@
     }
 
     @Override
+    public ItemSpecification<T> withByteArray(Function<T, byte[]> getter) {
+      byte[] ints = getter.apply(item);
+      for (int i = 0; i < ints.length; i++) {
+        parent.visitInt(ints[i]);
+      }
+      return this;
+    }
+
+    @Override
     public ItemSpecification<T> withShortArray(Function<T, short[]> getter) {
       short[] ints = getter.apply(item);
       for (int i = 0; i < ints.length; i++) {
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
index 69d8796..1c0104d 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
@@ -4,8 +4,11 @@
 package com.android.tools.r8.utils.structural;
 
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
@@ -59,6 +62,11 @@
     return withCustomItemIterator(getter.andThen(Collection::iterator), acceptor, acceptor);
   }
 
+  public final <S> V withCustomItemArray(Function<T, S[]> getter, StructuralAcceptor<S> acceptor) {
+    return withCustomItemIterator(
+        getter.andThen(a -> Arrays.asList(a).iterator()), acceptor, acceptor);
+  }
+
   /**
    * Specification for a "specified" item.
    *
@@ -112,6 +120,45 @@
         });
   }
 
+  private <S> V withInt2CustomItemMap(
+      Function<T, Int2ReferenceMap<S>> getter,
+      CompareToAccept<S> compare,
+      HashingAccept<S> hasher) {
+    return withCustomItem(
+        getter,
+        new StructuralAcceptor<>() {
+          @Override
+          public int acceptCompareTo(
+              Int2ReferenceMap<S> map1, Int2ReferenceMap<S> map2, CompareToVisitor visitor) {
+            return ComparatorUtils.compareInt2ReferenceMap(
+                map1, map2, (s1, s2) -> compare.acceptCompareTo(s1, s2, visitor));
+          }
+
+          @Override
+          public void acceptHashing(Int2ReferenceMap<S> map, HashingVisitor visitor) {
+            // We might want to optimize this to avoid sorting. Potentiality compute the min-max
+            // range and use a fori, or collect some number of the smallest keys and hash just
+            // those.
+            ArrayList<Integer> keys = new ArrayList<>(map.keySet());
+            keys.sort(Integer::compareTo);
+            for (int key : keys) {
+              visitor.visitInt(key);
+              hasher.acceptHashing(map.get(key), visitor);
+            }
+          }
+        });
+  }
+
+  public final <S> V withInt2CustomItemMap(
+      Function<T, Int2ReferenceMap<S>> getter, StructuralAcceptor<S> acceptor) {
+    return withInt2CustomItemMap(getter, acceptor, acceptor);
+  }
+
+  public final <S extends StructuralItem<S>> V withInt2ItemMap(
+      Function<T, Int2ReferenceMap<S>> getter) {
+    return withInt2CustomItemMap(getter, S::acceptCompareTo, S::acceptHashing);
+  }
+
   /**
    * Helper to declare an assert on the item.
    *
@@ -131,6 +178,8 @@
 
   public abstract V withIntArray(Function<T, int[]> getter);
 
+  public abstract V withByteArray(Function<T, byte[]> getter);
+
   public abstract V withShortArray(Function<T, short[]> getter);
 
   public abstract V withDexReference(Function<T, DexReference> getter);
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index ed2dd4e..128178a 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -334,9 +334,9 @@
     Set<MethodReference> expectedSynthetics =
         ImmutableSet.of(
             SyntheticItemsTestUtils.syntheticBackportMethod(
-                User1.class, 0, Boolean.class.getMethod("compare", boolean.class, boolean.class)),
+                User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class)),
             SyntheticItemsTestUtils.syntheticBackportMethod(
-                User1.class, 1, Character.class.getMethod("compare", char.class, char.class)),
+                User1.class, 0, Character.class.getMethod("compare", char.class, char.class)),
             SyntheticItemsTestUtils.syntheticBackportMethod(
                 User2.class, 0, Integer.class.getMethod("compare", int.class, int.class)));
     assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
index 2577312..592281a 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/RecordProfileRewritingTest.java
@@ -252,7 +252,7 @@
 
     // int hashCode()
     ClassSubject hashCodeHelperClassSubject =
-        inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 0));
+        inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 1));
     assertThat(hashCodeHelperClassSubject, notIf(isPresent(), canUseRecords));
 
     MethodSubject hashCodeHelperMethodSubject = hashCodeHelperClassSubject.uniqueMethod();
@@ -269,7 +269,7 @@
 
     // String toString()
     ClassSubject toStringHelperClassSubject =
-        inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 1));
+        inspector.clazz(SyntheticItemsTestUtils.syntheticRecordHelperClass(PERSON_REFERENCE, 0));
     assertThat(toStringHelperClassSubject, notIf(isPresent(), canUseRecords));
 
     MethodSubject toStringHelperMethodSubject = toStringHelperClassSubject.uniqueMethod();