Introduce CfVersion to ensure correct comparisons.

Bug: 170796381
Bug: 169918924
Cherry picked commit: bdc905442f42e6d0107a4079d18f8d696335246c
Change-Id: I481afa535ca71147f056d0fb3d43f37e7ccb6a42
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index 17b7485..e43f1da 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfThrow;
@@ -145,7 +146,7 @@
               ParameterAnnotationsList.empty(),
               code,
               false,
-              50,
+              CfVersion.V1_6,
               false);
       if (method.isStatic() || method.isDirectMethod()) {
         directMethods.add(throwingMethod);
diff --git a/src/main/java/com/android/tools/r8/cf/CfVersion.java b/src/main/java/com/android/tools/r8/cf/CfVersion.java
new file mode 100644
index 0000000..84228b9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/CfVersion.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf;
+
+import java.util.Comparator;
+import org.objectweb.asm.Opcodes;
+
+public final class CfVersion implements Comparable<CfVersion> {
+
+  public static final CfVersion V1_1 = new CfVersion(Opcodes.V1_1);
+  public static final CfVersion V1_2 = new CfVersion(Opcodes.V1_2);
+  public static final CfVersion V1_3 = new CfVersion(Opcodes.V1_3);
+  public static final CfVersion V1_4 = new CfVersion(Opcodes.V1_4);
+  public static final CfVersion V1_5 = new CfVersion(Opcodes.V1_5);
+  public static final CfVersion V1_6 = new CfVersion(Opcodes.V1_6);
+  public static final CfVersion V1_7 = new CfVersion(Opcodes.V1_7);
+  public static final CfVersion V1_8 = new CfVersion(Opcodes.V1_8);
+  public static final CfVersion V9 = new CfVersion(Opcodes.V9);
+  public static final CfVersion V10 = new CfVersion(Opcodes.V10);
+  public static final CfVersion V11 = new CfVersion(Opcodes.V11);
+
+  private final int version;
+
+  // Private constructor in case we want to canonicalize versions.
+  private CfVersion(int version) {
+    this.version = version;
+  }
+
+  public static CfVersion fromRaw(int rawVersion) {
+    return new CfVersion(rawVersion);
+  }
+
+  public int major() {
+    return version & 0xFFFF;
+  }
+
+  public int minor() {
+    return version >> 16;
+  }
+
+  public int raw() {
+    return version;
+  }
+
+  public static CfVersion maxAllowNull(CfVersion v1, CfVersion v2) {
+    assert v1 != null || v2 != null;
+    if (v1 == null) {
+      return v2;
+    }
+    if (v2 == null) {
+      return v1;
+    }
+    return v1.max(v2);
+  }
+
+  public CfVersion max(CfVersion other) {
+    return isLessThan(other) ? other : this;
+  }
+
+  public boolean isEqual(CfVersion other) {
+    return version == other.version;
+  }
+
+  public boolean isLessThan(CfVersion other) {
+    return compareTo(other) < 0;
+  }
+
+  public boolean isLessThanOrEqual(CfVersion other) {
+    return compareTo(other) <= 0;
+  }
+
+  public boolean isGreaterThan(CfVersion other) {
+    return compareTo(other) > 0;
+  }
+
+  public boolean isGreaterThanOrEqual(CfVersion other) {
+    return compareTo(other) >= 0;
+  }
+
+  @Override
+  public int compareTo(CfVersion o) {
+    return Comparator.comparingInt(CfVersion::major)
+        .thenComparingInt(CfVersion::minor)
+        .compare(this, o);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof CfVersion)) {
+      return false;
+    }
+    return isEqual((CfVersion) o);
+  }
+
+  @Override
+  public int hashCode() {
+    return version;
+  }
+
+  @Override
+  public String toString() {
+    return minor() != 0 ? ("" + major() + "." + minor()) : ("" + major());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index 77a82df..e3801e6 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dex;
 
+import com.android.tools.r8.cf.CfVersion;
+
 public class Constants {
 
   public static final byte[] DEX_FILE_MAGIC_PREFIX = {'d', 'e', 'x', '\n'};
@@ -16,7 +18,7 @@
   public static final int MAX_VDEX_VERSION = 11;
 
   // We apply Java 6 class file constraints on DEX files.
-  public static final int CORRESPONDING_CLASS_FILE_VERSION = 50;
+  public static final CfVersion CORRESPONDING_CLASS_FILE_VERSION = CfVersion.V1_6;
 
   public static final int DEX_MAGIC_SIZE = 8;
 
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index de1c2b9..05dffcf 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -7,10 +7,9 @@
 import static com.android.tools.r8.graph.DexCode.FAKE_THIS_SUFFIX;
 import static com.android.tools.r8.ir.conversion.CfSourceCode.canThrowHelper;
 import static org.objectweb.asm.Opcodes.ACC_STATIC;
-import static org.objectweb.asm.Opcodes.V1_5;
-import static org.objectweb.asm.Opcodes.V1_6;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.cf.code.CfFrame;
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 import com.android.tools.r8.cf.code.CfFrameVerificationHelper;
@@ -285,7 +284,7 @@
 
   public void write(
       ProgramMethod method,
-      int classFileVersion,
+      CfVersion classFileVersion,
       AppView<?> appView,
       NamingLens namingLens,
       LensCodeRewriterUtils rewriter,
@@ -303,8 +302,9 @@
     }
     for (CfInstruction instruction : instructions) {
       if (instruction instanceof CfFrame
-          && (classFileVersion <= V1_5
-              || (classFileVersion == V1_6 && !options.shouldKeepStackMapTable()))) {
+          && (classFileVersion.isLessThan(CfVersion.V1_6)
+              || (classFileVersion.isEqual(CfVersion.V1_6)
+                  && !options.shouldKeepStackMapTable()))) {
         continue;
       }
       instruction.write(
@@ -683,7 +683,7 @@
       stackMapStatus = StackMapStatus.INVALID_OR_NOT_PRESENT;
       return true;
     }
-    if (method.hasClassFileVersion() && method.getClassFileVersion() <= V1_6) {
+    if (method.hasClassFileVersion() && method.getClassFileVersion().isLessThan(CfVersion.V1_7)) {
       stackMapStatus = StackMapStatus.INVALID_OR_NOT_PRESENT;
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
index 36f43ba..e3584ec 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
@@ -105,16 +106,17 @@
    * Checks whether the constraints from
    * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.1 are met.
    */
-  public boolean areValid(int majorVersion, boolean isPackageInfo) {
+  public boolean areValid(CfVersion version, boolean isPackageInfo) {
     if (isInterface()) {
       // We ignore the super flags prior to JDK 9, as so did the VM.
-      if ((majorVersion >= 53) && isSuper()) {
+      if (version.isGreaterThanOrEqual(CfVersion.V9) && isSuper()) {
         return false;
       }
       // When not coming from DEX input we require interfaces to be abstract - except for
       // package-info classes - as both old versions of javac and other tools can produce
       // package-info classes that are interfaces but not abstract.
-      if (((majorVersion > Constants.CORRESPONDING_CLASS_FILE_VERSION) && !isAbstract())
+      if (version.isGreaterThanOrEqual(Constants.CORRESPONDING_CLASS_FILE_VERSION)
+          && !isAbstract()
           && !isPackageInfo) {
         return false;
       }
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 c30f69c..1e628e8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -12,6 +12,7 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.NO_KOTLIN_INFO;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfConstString;
@@ -147,7 +148,7 @@
   private CompilationState compilationState = CompilationState.NOT_PROCESSED;
   private MethodOptimizationInfo optimizationInfo = DefaultMethodOptimizationInfo.DEFAULT_INSTANCE;
   private CallSiteOptimizationInfo callSiteOptimizationInfo = CallSiteOptimizationInfo.bottom();
-  private int classFileVersion;
+  private CfVersion classFileVersion = null;
   private KotlinMethodLevelInfo kotlinMemberInfo = NO_KOTLIN_INFO;
 
   private DexEncodedMethod defaultInterfaceMethodImplementation = null;
@@ -228,7 +229,7 @@
       DexAnnotationSet annotations,
       ParameterAnnotationsList parameterAnnotationsList,
       Code code) {
-    this(method, accessFlags, annotations, parameterAnnotationsList, code, false, -1);
+    this(method, accessFlags, annotations, parameterAnnotationsList, code, false, null);
   }
 
   public DexEncodedMethod(
@@ -238,7 +239,7 @@
       ParameterAnnotationsList parameterAnnotationsList,
       Code code,
       boolean d8R8Synthesized) {
-    this(method, accessFlags, annotations, parameterAnnotationsList, code, d8R8Synthesized, -1);
+    this(method, accessFlags, annotations, parameterAnnotationsList, code, d8R8Synthesized, null);
   }
 
   public DexEncodedMethod(
@@ -248,7 +249,7 @@
       ParameterAnnotationsList parameterAnnotationsList,
       Code code,
       boolean d8R8Synthesized,
-      int classFileVersion) {
+      CfVersion classFileVersion) {
     this(
         method,
         accessFlags,
@@ -267,7 +268,7 @@
       ParameterAnnotationsList parameterAnnotationsList,
       Code code,
       boolean d8R8Synthesized,
-      int classFileVersion,
+      CfVersion classFileVersion,
       boolean deprecated) {
     super(annotations);
     this.method = method;
@@ -757,21 +758,21 @@
     code = null;
   }
 
-  public int getClassFileVersion() {
+  public CfVersion getClassFileVersion() {
     checkIfObsolete();
-    assert classFileVersion >= 0;
+    assert classFileVersion != null;
     return classFileVersion;
   }
 
   public boolean hasClassFileVersion() {
     checkIfObsolete();
-    return classFileVersion >= 0;
+    return classFileVersion != null;
   }
 
-  public void upgradeClassFileVersion(int version) {
+  public void upgradeClassFileVersion(CfVersion version) {
     checkIfObsolete();
-    assert version >= 0;
-    classFileVersion = Math.max(classFileVersion, version);
+    assert version != null;
+    classFileVersion = CfVersion.maxAllowNull(classFileVersion, version);
   }
 
   public String qualifiedName() {
@@ -1394,7 +1395,7 @@
 
   public void copyMetadata(DexEncodedMethod from) {
     checkIfObsolete();
-    if (from.classFileVersion > classFileVersion) {
+    if (from.hasClassFileVersion()) {
       upgradeClassFileVersion(from.getClassFileVersion());
     }
   }
@@ -1418,7 +1419,7 @@
     private CompilationState compilationState;
     private MethodOptimizationInfo optimizationInfo;
     private KotlinMethodLevelInfo kotlinMemberInfo;
-    private final int classFileVersion;
+    private final CfVersion classFileVersion;
     private boolean d8R8Synthesized;
 
     private Builder(DexEncodedMethod from) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 13246eb..d94b060 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.CompilationError;
@@ -47,7 +48,7 @@
 
   private final ProgramResource.Kind originKind;
   private final Collection<DexProgramClass> synthesizedFrom;
-  private int initialClassFileVersion = -1;
+  private CfVersion initialClassFileVersion = null;
   private boolean deprecated = false;
   private KotlinClassLevelInfo kotlinInfo = NO_KOTLIN_INFO;
 
@@ -551,17 +552,18 @@
     return this;
   }
 
-  public void setInitialClassFileVersion(int initialClassFileVersion) {
-    assert this.initialClassFileVersion == -1 && initialClassFileVersion > 0;
+  public void setInitialClassFileVersion(CfVersion initialClassFileVersion) {
+    assert this.initialClassFileVersion == null;
+    assert initialClassFileVersion != null;
     this.initialClassFileVersion = initialClassFileVersion;
   }
 
   public boolean hasClassFileVersion() {
-    return initialClassFileVersion > -1;
+    return initialClassFileVersion != null;
   }
 
-  public int getInitialClassFileVersion() {
-    assert initialClassFileVersion > -1;
+  public CfVersion getInitialClassFileVersion() {
+    assert initialClassFileVersion != null;
     return initialClassFileVersion;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index ea58f8d..fcc0870 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -8,12 +8,11 @@
 import static org.objectweb.asm.ClassReader.SKIP_DEBUG;
 import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
 import static org.objectweb.asm.Opcodes.ACC_DEPRECATED;
-import static org.objectweb.asm.Opcodes.V1_6;
-import static org.objectweb.asm.Opcodes.V9;
 
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
@@ -195,7 +194,7 @@
     private final ReparseContext context = new ReparseContext();
 
     // DexClass data.
-    private int version;
+    private CfVersion version;
     private boolean deprecated;
     private DexType type;
     private ClassAccessFlags accessFlags;
@@ -234,7 +233,7 @@
     public void visitInnerClass(String name, String outerName, String innerName, int access) {
       if (outerName != null && innerName != null) {
         String separator = DescriptorUtils.computeInnerClassSeparator(outerName, name, innerName);
-        if (separator == null && getMajorVersion() < V9) {
+        if (separator == null && version.isLessThan(CfVersion.V9)) {
           application.options.reporter.info(
               new StringDiagnostic(
                   StringUtils.lines(
@@ -285,43 +284,54 @@
           + name;
     }
 
-    private String illegalClassFilePostfix(int version) {
+    private String illegalClassFilePostfix(CfVersion version) {
       return "Class file version " + version;
     }
 
     private String illegalClassFileMessage(
-        ClassAccessFlags accessFlags, String name, int version, String message) {
+        ClassAccessFlags accessFlags, String name, CfVersion version, String message) {
       return illegalClassFilePrefix(accessFlags, name)
           + " " + message
           + ". " + illegalClassFilePostfix(version) + ".";
     }
 
     @Override
-    public void visit(int version, int access, String name, String signature, String superName,
+    public void visit(
+        int rawVersion,
+        int access,
+        String name,
+        String signature,
+        String superName,
         String[] interfaces) {
-      this.version = version;
-      if (InternalOptions.SUPPORTED_CF_MAJOR_VERSION < getMajorVersion()) {
-        throw new CompilationError("Unsupported class file version: " + getMajorVersion(), origin);
+      version = CfVersion.fromRaw(rawVersion);
+      if (InternalOptions.SUPPORTED_CF_VERSION.isLessThan(version)) {
+        throw new CompilationError("Unsupported class file version: " + version, origin);
       }
       this.deprecated = AsmUtils.isDeprecated(access);
       accessFlags = ClassAccessFlags.fromCfAccessFlags(cleanAccessFlags(access));
       type = application.getTypeFromName(name);
       // Check if constraints from
       // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.1 are met.
-      if (!accessFlags.areValid(getMajorVersion(), name.endsWith("/package-info"))) {
+      if (!accessFlags.areValid(version, name.endsWith("/package-info"))) {
         throw new CompilationError(
-            illegalClassFileMessage(accessFlags, name, version,
-                "has invalid access flags. Found: " + accessFlags.toString()), origin);
+            illegalClassFileMessage(
+                accessFlags,
+                name,
+                version,
+                "has invalid access flags. Found: " + accessFlags.toString()),
+            origin);
       }
       if (superName == null && !name.equals(Constants.JAVA_LANG_OBJECT_NAME)) {
         throw new CompilationError(
-            illegalClassFileMessage(accessFlags, name, version,
-                "is missing a super type"), origin);
+            illegalClassFileMessage(accessFlags, name, version, "is missing a super type"), origin);
       }
       if (accessFlags.isInterface()
           && !Objects.equals(superName, Constants.JAVA_LANG_OBJECT_NAME)) {
         throw new CompilationError(
-            illegalClassFileMessage(accessFlags, name, version,
+            illegalClassFileMessage(
+                accessFlags,
+                name,
+                version,
                 "must extend class java.lang.Object. Found: " + Objects.toString(superName)),
             origin);
       }
@@ -443,7 +453,7 @@
       }
       if (enclosingMember == null
           && (clazz.isLocalClass() || clazz.isAnonymousClass())
-          && getMajorVersion() > V1_6) {
+          && CfVersion.V1_6.isLessThan(version)) {
         application.options.warningMissingEnclosingMember(clazz.type, clazz.origin, version);
       }
       if (!clazz.isLibraryClass()) {
@@ -521,14 +531,6 @@
       return annotations;
     }
 
-    private int getMajorVersion() {
-      return version & 0xFFFF;
-    }
-
-    private int getMinorVersion() {
-      return ((version >> 16) & 0xFFFF);
-    }
-
     public boolean isInANest() {
       return !nestMembers.isEmpty() || nestHost != null;
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
index 9baf6ad..f9a5fbe 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotationSet;
@@ -134,10 +135,11 @@
     // Tree map as must be sorted.
     Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
 
-    int classFileVersion = -1;
+    CfVersion classFileVersion = null;
     for (DexEncodedMethod constructor : constructors) {
       if (constructor.hasClassFileVersion()) {
-        classFileVersion = Integer.max(classFileVersion, constructor.getClassFileVersion());
+        classFileVersion =
+            CfVersion.maxAllowNull(classFileVersion, constructor.getClassFileVersion());
       }
       DexMethod movedConstructor = moveConstructor(constructor);
       lensBuilder.mapMethod(movedConstructor, movedConstructor);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
index 0f9fdc1..7501fe0 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -141,11 +142,11 @@
 
     Int2ReferenceSortedMap<DexMethod> classIdToMethodMap = new Int2ReferenceAVLTreeMap<>();
 
-    int classFileVersion = -1;
+    CfVersion classFileVersion = null;
     for (ProgramMethod method : methods) {
       if (method.getDefinition().hasClassFileVersion()) {
-        classFileVersion =
-            Integer.max(classFileVersion, method.getDefinition().getClassFileVersion());
+        CfVersion methodVersion = method.getDefinition().getClassFileVersion();
+        classFileVersion = CfVersion.maxAllowNull(classFileVersion, methodVersion);
       }
       DexMethod newMethod = moveMethod(method);
       lensBuilder.mapMethod(newMethod, newMethod);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 6382b86..ced929d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -14,9 +14,9 @@
 import static com.android.tools.r8.ir.analysis.type.TypeElement.getNull;
 import static com.android.tools.r8.ir.analysis.type.TypeElement.getSingle;
 import static com.android.tools.r8.ir.analysis.type.TypeElement.getWide;
-import static org.objectweb.asm.Opcodes.V1_8;
 
 import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
@@ -2199,7 +2199,7 @@
                     local,
                     readType);
           } else {
-            assert method.getDefinition().getClassFileVersion() < V1_8;
+            assert method.getDefinition().getClassFileVersion().isLessThan(CfVersion.V1_8);
             hasIncorrectStackMapTypes = true;
           }
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index c937356..02b521f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
@@ -67,7 +68,7 @@
 public class EnumUnboxingRewriter {
 
   public static final String ENUM_UNBOXING_UTILITY_METHOD_PREFIX = "$enumboxing$";
-  private static final int REQUIRED_CLASS_FILE_VERSION = 52;
+  private static final CfVersion REQUIRED_CLASS_FILE_VERSION = CfVersion.V1_8;
 
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory factory;
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index e28734b..0021144 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -4,11 +4,10 @@
 package com.android.tools.r8.jar;
 
 import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
-import static org.objectweb.asm.Opcodes.V1_6;
-import static org.objectweb.asm.Opcodes.V1_8;
 
 import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.errors.CodeSizeOverflowDiagnostic;
@@ -82,6 +81,8 @@
 
   public final ProguardMapSupplier proguardMapSupplier;
 
+  private static final CfVersion MIN_VERSION_FOR_COMPILER_GENERATED_CODE = CfVersion.V1_6;
+
   public CfApplicationWriter(
       AppView<?> appView,
       Marker marker,
@@ -163,8 +164,8 @@
     }
     String sourceDebug = getSourceDebugExtension(clazz.annotations());
     writer.visitSource(clazz.sourceFile != null ? clazz.sourceFile.toString() : null, sourceDebug);
-    int version = getClassFileVersion(clazz);
-    if (version >= V1_8) {
+    CfVersion version = getClassFileVersion(clazz);
+    if (version.isGreaterThanOrEqual(CfVersion.V1_8)) {
       // JDK8 and after ignore ACC_SUPER so unset it.
       clazz.accessFlags.unsetSuper();
     } else {
@@ -188,7 +189,7 @@
     for (int i = 0; i < clazz.interfaces.values.length; i++) {
       interfaces[i] = namingLens.lookupInternalName(clazz.interfaces.values[i]);
     }
-    writer.visit(version, access, name, signature, superName, interfaces);
+    writer.visit(version.raw(), access, name, signature, superName, interfaces);
     writeAnnotations(writer::visitAnnotation, clazz.annotations().annotations);
     ImmutableMap<DexString, DexValue> defaults = getAnnotationDefaults(clazz.annotations());
 
@@ -233,7 +234,7 @@
         options.reporter, handler -> consumer.accept(ByteDataView.of(result), desc, handler));
   }
 
-  private int getClassFileVersion(DexEncodedMethod method) {
+  private CfVersion getClassFileVersion(DexEncodedMethod method) {
     if (!method.hasClassFileVersion()) {
       // In this case bridges have been introduced for the Cf back-end,
       // which do not have class file version.
@@ -242,18 +243,22 @@
           || options.cfToCfDesugar;
       // TODO(b/146424042): We may call static methods on interface classes so we have to go for
       //  Java 8.
-      return options.cfToCfDesugar ? V1_8 : 0;
+      assert MIN_VERSION_FOR_COMPILER_GENERATED_CODE.isLessThan(CfVersion.V1_8);
+      return options.cfToCfDesugar ? CfVersion.V1_8 : MIN_VERSION_FOR_COMPILER_GENERATED_CODE;
     }
     return method.getClassFileVersion();
   }
 
-  private int getClassFileVersion(DexProgramClass clazz) {
-    int version = clazz.hasClassFileVersion() ? clazz.getInitialClassFileVersion() : V1_6;
+  private CfVersion getClassFileVersion(DexProgramClass clazz) {
+    CfVersion version =
+        clazz.hasClassFileVersion()
+            ? clazz.getInitialClassFileVersion()
+            : MIN_VERSION_FOR_COMPILER_GENERATED_CODE;
     for (DexEncodedMethod method : clazz.directMethods()) {
-      version = Math.max(version, getClassFileVersion(method));
+      version = version.max(getClassFileVersion(method));
     }
     for (DexEncodedMethod method : clazz.virtualMethods()) {
-      version = Math.max(version, getClassFileVersion(method));
+      version = version.max(getClassFileVersion(method));
     }
     return version;
   }
@@ -344,7 +349,7 @@
 
   private void writeMethod(
       ProgramMethod method,
-      int classFileVersion,
+      CfVersion classFileVersion,
       LensCodeRewriterUtils rewriter,
       ClassWriter writer,
       ImmutableMap<DexString, DexValue> defaults) {
@@ -505,7 +510,7 @@
 
   private void writeCode(
       ProgramMethod method,
-      int classFileVersion,
+      CfVersion classFileVersion,
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
     CfCode code = method.getDefinition().getCode().asCfCode();
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index acb52c5..fb1eeeb 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult.isOverriding;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.DexCallSite;
@@ -567,29 +568,30 @@
     return definition;
   }
 
-  private int largestInputCfVersion = -1;
+  private CfVersion largestInputCfVersion = null;
 
   public boolean canUseConstClassInstructions(InternalOptions options) {
     if (!options.isGeneratingClassFiles()) {
       return true;
     }
-    if (largestInputCfVersion == -1) {
+    if (largestInputCfVersion == null) {
       computeLargestCfVersion();
     }
     return options.canUseConstClassInstructions(largestInputCfVersion);
   }
 
   private synchronized void computeLargestCfVersion() {
-    if (largestInputCfVersion != -1) {
+    if (largestInputCfVersion != null) {
       return;
     }
     for (DexProgramClass clazz : classes()) {
       // Skip synthetic classes which may not have a specified version.
       if (clazz.hasClassFileVersion()) {
-        largestInputCfVersion = Math.max(largestInputCfVersion, clazz.getInitialClassFileVersion());
+        largestInputCfVersion =
+            CfVersion.maxAllowNull(largestInputCfVersion, clazz.getInitialClassFileVersion());
       }
     }
-    assert largestInputCfVersion != -1;
+    assert largestInputCfVersion != null;
   }
 
   public boolean isLiveProgramClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 0815577..0052577 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -1252,7 +1252,7 @@
               ParameterAnnotationsList.empty(),
               code,
               true,
-              method.hasClassFileVersion() ? method.getClassFileVersion() : -1);
+              method.hasClassFileVersion() ? method.getClassFileVersion() : null);
       bridge.setLibraryMethodOverride(method.isLibraryMethodOverride());
       if (method.accessFlags.isPromotedToPublic()) {
         // The bridge is now the public method serving the role of the original method, and should
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index af11559..6ae8c56 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.Version;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
@@ -97,7 +98,7 @@
     ON
   }
 
-  public static final int SUPPORTED_CF_MAJOR_VERSION = Opcodes.V11;
+  public static final CfVersion SUPPORTED_CF_VERSION = CfVersion.V11;
   public static final int SUPPORTED_DEX_VERSION =
       AndroidApiLevel.LATEST.getDexVersion().getIntValue();
 
@@ -696,10 +697,10 @@
 
   private static class TypeVersionPair {
 
-    final int version;
+    final CfVersion version;
     final DexType type;
 
-    public TypeVersionPair(int version, DexType type) {
+    public TypeVersionPair(CfVersion version, DexType type) {
       this.version = version;
       this.type = type;
     }
@@ -945,7 +946,7 @@
     }
   }
 
-  public void warningMissingEnclosingMember(DexType clazz, Origin origin, int version) {
+  public void warningMissingEnclosingMember(DexType clazz, Origin origin, CfVersion version) {
     TypeVersionPair pair = new TypeVersionPair(version, clazz);
     synchronized (missingEnclosingMembers) {
       missingEnclosingMembers.computeIfAbsent(origin, k -> new ArrayList<>()).add(pair);
@@ -1059,7 +1060,7 @@
             builder.append(", ");
           }
           builder.append(pair.type);
-          printOutdatedToolchain |= pair.version < 49;
+          printOutdatedToolchain |= pair.version.isLessThan(CfVersion.V1_5);
         }
         reporter.info(new StringDiagnostic(builder.toString(), origin));
       }
@@ -1365,14 +1366,14 @@
     return result;
   }
 
-  public boolean canUseConstClassInstructions(int cfVersion) {
+  public boolean canUseConstClassInstructions(CfVersion cfVersion) {
     assert isGeneratingClassFiles();
-    return cfVersion >= requiredCfVersionForConstClassInstructions();
+    return cfVersion.isGreaterThanOrEqual(requiredCfVersionForConstClassInstructions());
   }
 
-  public int requiredCfVersionForConstClassInstructions() {
+  public CfVersion requiredCfVersionForConstClassInstructions() {
     assert isGeneratingClassFiles();
-    return Opcodes.V1_5;
+    return CfVersion.V1_5;
   }
 
   public boolean canUseInvokePolymorphicOnVarHandle() {
diff --git a/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java b/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
index 6ea338c..c71ca8b 100644
--- a/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
+++ b/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
@@ -21,7 +21,7 @@
 @RunWith(Parameterized.class)
 public class FailCompilationOnFutureVersionsTest extends TestBase {
 
-  static final int UNSUPPORTED_CF_VERSION = InternalOptions.SUPPORTED_CF_MAJOR_VERSION + 1;
+  static final int UNSUPPORTED_CF_VERSION = InternalOptions.SUPPORTED_CF_VERSION.major() + 1;
   static final int UNSUPPORTED_DEX_VERSION = InternalOptions.SUPPORTED_DEX_VERSION + 1;
 
   private final TestParameters parameters;
diff --git a/src/test/java/com/android/tools/r8/cf/CfVersionTest.java b/src/test/java/com/android/tools/r8/cf/CfVersionTest.java
new file mode 100644
index 0000000..48871e8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/CfVersionTest.java
@@ -0,0 +1,64 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.cf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class CfVersionTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public CfVersionTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void test() throws Exception {
+    CfVersion v1_1 = CfVersion.V1_1;
+    assertEquals(Opcodes.V1_1, v1_1.raw());
+    assertEquals(45, v1_1.major());
+    assertEquals(3, v1_1.minor());
+
+    CfVersion v1_2 = CfVersion.V1_2;
+    assertEquals(Opcodes.V1_2, v1_2.raw());
+    assertEquals(46, v1_2.major());
+    assertEquals(0, v1_2.minor());
+
+    CfVersion v9 = CfVersion.V9;
+    assertEquals(Opcodes.V9, v9.raw());
+    assertEquals(53, v9.major());
+    assertEquals(0, v9.minor());
+
+    assertLessThan(v1_1, v1_2);
+    assertLessThan(v1_2, v9);
+  }
+
+  private static void assertLessThan(CfVersion less, CfVersion more) {
+    assertFalse(less.isEqual(more));
+    assertEquals(-1, less.compareTo(more));
+    assertEquals(1, more.compareTo(less));
+    assertTrue(less.isLessThan(more));
+    assertTrue(less.isLessThanOrEqual(more));
+    assertFalse(less.isGreaterThan(more));
+    assertFalse(less.isGreaterThanOrEqual(more));
+    assertFalse(more.isLessThan(less));
+    assertFalse(more.isLessThanOrEqual(less));
+    assertTrue(more.isGreaterThan(less));
+    assertTrue(more.isGreaterThanOrEqual(less));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/GetClassLdcClassTest.java b/src/test/java/com/android/tools/r8/cf/GetClassLdcClassTest.java
index c25dc40..da83514 100644
--- a/src/test/java/com/android/tools/r8/cf/GetClassLdcClassTest.java
+++ b/src/test/java/com/android/tools/r8/cf/GetClassLdcClassTest.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.cf;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverClassInline;
@@ -18,7 +17,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
-import org.objectweb.asm.Opcodes;
 
 @RunWith(Parameterized.class)
 public class GetClassLdcClassTest extends TestBase {
@@ -26,16 +24,16 @@
   static final String EXPECTED = StringUtils.lines(Runner.class.getName());
 
   private final TestParameters parameters;
-  private final int version;
+  private final CfVersion version;
 
   @Parameterized.Parameters(name = "{0}, cf:{1}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(),
-        new Integer[] {Opcodes.V1_4, Opcodes.V1_5});
+        new CfVersion[] {CfVersion.V1_1, CfVersion.V1_4, CfVersion.V1_5});
   }
 
-  public GetClassLdcClassTest(TestParameters parameters, int version) {
+  public GetClassLdcClassTest(TestParameters parameters, CfVersion version) {
     this.parameters = parameters;
     this.version = version;
   }
@@ -99,11 +97,11 @@
             inspector -> {
               if (parameters.isCfRuntime()) {
                 // We are assuming the runtimes we are testing are post CF SE 1.4 (version 48).
-                int cfVersionForRuntime = getVersion(inspector, TestClass.class);
-                assertNotEquals(Opcodes.V1_4, cfVersionForRuntime);
+                CfVersion cfVersionForRuntime = getVersion(inspector, TestClass.class);
+                assertTrue(CfVersion.V1_4.isLessThan(cfVersionForRuntime));
                 // Check that the downgraded class has been bumped to at least SE 1.5 (version 49).
-                int cfVersionAfterUpgrade = getVersion(inspector, Runner.class);
-                assertTrue(cfVersionAfterUpgrade >= Opcodes.V1_5);
+                CfVersion cfVersionAfterUpgrade = getVersion(inspector, Runner.class);
+                assertTrue(CfVersion.V1_4.isLessThan(cfVersionAfterUpgrade));
               }
               // Check that the method uses a const class instruction.
               assertTrue(
@@ -115,11 +113,11 @@
             });
   }
 
-  private static int getVersion(CodeInspector inspector, Class<?> clazz) {
+  private static CfVersion getVersion(CodeInspector inspector, Class<?> clazz) {
     return inspector.clazz(clazz).getDexProgramClass().getInitialClassFileVersion();
   }
 
-  private static void checkVersion(CodeInspector inspector, Class<?> clazz, int version) {
+  private static void checkVersion(CodeInspector inspector, Class<?> clazz, CfVersion version) {
     assertEquals(version, getVersion(inspector, clazz));
   }
 
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 4779b8e..d87eafd 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.ClassAccessFlags;
@@ -241,6 +242,10 @@
   }
 
   public ClassFileTransformer setVersion(int newVersion) {
+    return setVersion(CfVersion.fromRaw(newVersion));
+  }
+
+  public ClassFileTransformer setVersion(CfVersion newVersion) {
     return addClassTransformer(
         new ClassTransformer() {
           @Override
@@ -251,7 +256,7 @@
               String signature,
               String superName,
               String[] interfaces) {
-            super.visit(newVersion, access, name, signature, superName, interfaces);
+            super.visit(newVersion.raw(), access, name, signature, superName, interfaces);
           }
         });
   }