Fix class reading of deficient constant types

char, byte, and short are encoded as ints in bytecode, so stringifying
bytecode constant chars didn't work.

Also fix a latent bug with unary operator evaluation.

MOE_MIGRATED_REVID=138215540
diff --git a/java/com/google/turbine/binder/ConstEvaluator.java b/java/com/google/turbine/binder/ConstEvaluator.java
index df8ae02..720586e 100644
--- a/java/com/google/turbine/binder/ConstEvaluator.java
+++ b/java/com/google/turbine/binder/ConstEvaluator.java
@@ -37,6 +37,7 @@
 import com.google.turbine.binder.sym.FieldSymbol;
 import com.google.turbine.diag.TurbineError;
 import com.google.turbine.model.Const;
+import com.google.turbine.model.Const.Value;
 import com.google.turbine.model.TurbineConstantTypeKind;
 import com.google.turbine.model.TurbineFlag;
 import com.google.turbine.tree.Tree;
@@ -329,70 +330,70 @@
     }
     switch (t.op()) {
       case NOT:
-        switch (expr.constantTypeKind()) {
-          case BOOLEAN:
-            return new Const.BooleanValue(!expr.asBoolean().value());
-          default:
-            throw new AssertionError(expr.constantTypeKind());
-        }
+        return unaryNegate(expr);
       case BITWISE_COMP:
-        switch (expr.constantTypeKind()) {
-          case INT:
-            return new Const.IntValue(~expr.asInteger().value());
-          case LONG:
-            return new Const.LongValue(~expr.asLong().value());
-          case BYTE:
-            return new Const.ByteValue((byte) ~expr.asByte().value());
-          case SHORT:
-            return new Const.ShortValue((short) ~expr.asShort().value());
-          case CHAR:
-            return new Const.CharValue((char) ~expr.asChar().value());
-          default:
-            throw new AssertionError(expr.constantTypeKind());
-        }
+        return bitwiseComp(expr);
       case UNARY_PLUS:
-        switch (expr.constantTypeKind()) {
-          case INT:
-            return new Const.IntValue(+expr.asInteger().value());
-          case LONG:
-            return new Const.LongValue(+expr.asLong().value());
-          case BYTE:
-            return new Const.ByteValue((byte) +expr.asByte().value());
-          case SHORT:
-            return new Const.ShortValue((short) +expr.asShort().value());
-          case CHAR:
-            return new Const.CharValue((char) +expr.asChar().value());
-          case FLOAT:
-            return new Const.FloatValue(+expr.asFloat().value());
-          case DOUBLE:
-            return new Const.DoubleValue(+expr.asDouble().value());
-          default:
-            throw new AssertionError(expr.constantTypeKind());
-        }
+        return unaryPlus(expr);
       case NEG:
-        switch (expr.constantTypeKind()) {
-          case INT:
-            return new Const.IntValue(-expr.asInteger().value());
-          case BYTE:
-            return new Const.ByteValue((byte) -expr.asByte().value());
-          case SHORT:
-            return new Const.ShortValue((short) -expr.asShort().value());
-          case CHAR:
-            return new Const.CharValue((char) -expr.asChar().value());
-          case LONG:
-            return new Const.LongValue(-expr.asLong().value());
-          case FLOAT:
-            return new Const.FloatValue(-expr.asFloat().value());
-          case DOUBLE:
-            return new Const.DoubleValue(-expr.asDouble().value());
-          default:
-            throw new AssertionError(expr.constantTypeKind());
-        }
+        return unaryMinus(expr);
       default:
         throw new AssertionError(t.op());
     }
   }
 
+  private Value unaryNegate(Value expr) {
+    switch (expr.constantTypeKind()) {
+      case BOOLEAN:
+        return new Const.BooleanValue(!expr.asBoolean().value());
+      default:
+        throw new AssertionError(expr.constantTypeKind());
+    }
+  }
+
+  private Value bitwiseComp(Value expr) {
+    expr = promoteUnary(expr);
+    switch (expr.constantTypeKind()) {
+      case INT:
+        return new Const.IntValue(~expr.asInteger().value());
+      case LONG:
+        return new Const.LongValue(~expr.asLong().value());
+      default:
+        throw new AssertionError(expr.constantTypeKind());
+    }
+  }
+
+  private Value unaryPlus(Value expr) {
+    expr = promoteUnary(expr);
+    switch (expr.constantTypeKind()) {
+      case INT:
+        return new Const.IntValue(+expr.asInteger().value());
+      case LONG:
+        return new Const.LongValue(+expr.asLong().value());
+      case FLOAT:
+        return new Const.FloatValue(+expr.asFloat().value());
+      case DOUBLE:
+        return new Const.DoubleValue(+expr.asDouble().value());
+      default:
+        throw new AssertionError(expr.constantTypeKind());
+    }
+  }
+
+  private Value unaryMinus(Value expr) {
+    expr = promoteUnary(expr);
+    switch (expr.constantTypeKind()) {
+      case INT:
+        return new Const.IntValue(-expr.asInteger().value());
+      case LONG:
+        return new Const.LongValue(-expr.asLong().value());
+      case FLOAT:
+        return new Const.FloatValue(-expr.asFloat().value());
+      case DOUBLE:
+        return new Const.DoubleValue(-expr.asDouble().value());
+      default:
+        throw new AssertionError(expr.constantTypeKind());
+    }
+  }
   private Const.Value evalCast(TypeCast t) {
     Const.Value expr = evalValue(t.expr());
     if (expr == null) {
diff --git a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
index 16aabc5..240c6c1 100644
--- a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
+++ b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
@@ -320,9 +320,25 @@
                 Type type = null;
                 int access = cfi.access();
                 Const.Value value = cfi.value();
-                if (value != null && cfi.descriptor().equals("Z")) {
-                  // boolean constants are encoded as integers
-                  value = new Const.BooleanValue(value.asInteger().value() == 1);
+                if (value != null) {
+                  // TODO(b/32626659): this is not bug-compatible with javac
+                  switch (cfi.descriptor()) {
+                    case "Z":
+                      // boolean constants are encoded as integers
+                      value = new Const.BooleanValue(value.asInteger().value() != 0);
+                      break;
+                    case "C":
+                      value = new Const.CharValue(value.asChar().value());
+                      break;
+                    case "S":
+                      value = new Const.ShortValue(value.asShort().value());
+                      break;
+                    case "B":
+                      value = new Const.ByteValue(value.asByte().value());
+                      break;
+                    default:
+                      break;
+                  }
                 }
                 fields.add(new FieldInfo(fieldSym, type, access, ImmutableList.of(), null, value));
               }
diff --git a/javatests/com/google/turbine/lower/LowerIntegrationTest.java b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
index 99d4123..a7f1e26 100644
--- a/javatests/com/google/turbine/lower/LowerIntegrationTest.java
+++ b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
@@ -263,6 +263,7 @@
       "long_expression.test",
       "const_nonfinal.test",
       "enum_abstract.test",
+      "deficient_types_classfile.test",
     };
     List<Object[]> tests =
         ImmutableList.copyOf(testCases).stream().map(x -> new Object[] {x}).collect(toList());
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
index 42f395d..9bfe29f 100644
--- a/javatests/com/google/turbine/lower/LowerTest.java
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -43,17 +43,24 @@
 import com.google.turbine.parse.Parser;
 import com.google.turbine.type.Type;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.TypePath;
@@ -61,6 +68,8 @@
 @RunWith(JUnit4.class)
 public class LowerTest {
 
+  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
   private static final ImmutableList<Path> BOOTCLASSPATH =
       ImmutableList.of(Paths.get(System.getProperty("java.home")).resolve("lib/rt.jar"));
 
@@ -329,4 +338,35 @@
     assertThat(path[0].getStep(1)).isEqualTo(TypePath.ARRAY_ELEMENT);
     assertThat(path[0].getStepArgument(1)).isEqualTo(0);
   }
+
+  @Test
+  public void invalidBooleanConstant() throws Exception {
+    Path lib = temporaryFolder.newFile("lib.jar").toPath();
+    try (OutputStream os = Files.newOutputStream(lib);
+        JarOutputStream jos = new JarOutputStream(os)) {
+      jos.putNextEntry(new JarEntry("Lib.class"));
+
+      ClassWriter cw = new ClassWriter(0);
+      cw.visit(52, Opcodes.ACC_SUPER, "Lib", null, "java/lang/Object", null);
+      FieldVisitor fv =
+          cw.visitField(
+              Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, "CONST", "Z", null, Integer.MAX_VALUE);
+      fv.visitEnd();
+      cw.visitEnd();
+      jos.write(cw.toByteArray());
+    }
+
+    ImmutableMap<String, String> input =
+        ImmutableMap.of(
+            "Test.java", "class Test { static final boolean CONST = Lib.CONST || false; }");
+
+    Map<String, byte[]> actual =
+        IntegrationTestSupport.runTurbine(input, ImmutableList.of(lib), BOOTCLASSPATH);
+
+    Map<String, byte[]> expected =
+        IntegrationTestSupport.runJavac(input, ImmutableList.of(lib), BOOTCLASSPATH);
+
+    assertThat(IntegrationTestSupport.dump(actual))
+        .isEqualTo(IntegrationTestSupport.dump(expected));
+  }
 }
diff --git a/javatests/com/google/turbine/lower/testdata/deficient_types_classfile.test b/javatests/com/google/turbine/lower/testdata/deficient_types_classfile.test
new file mode 100644
index 0000000..2ed33dd
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/deficient_types_classfile.test
@@ -0,0 +1,13 @@
+%%% Lib.java %%%
+class Lib {
+  static final char C = '/';
+  static final char S = (short) 1;
+  static final char B = (byte) 2;
+}
+
+=== Test.java ===
+class Test {
+  static final String A = "" + Lib.C;
+  static final String B = "" + Lib.S;
+  static final String C = "" + Lib.B;
+}
\ No newline at end of file