diff --git a/.gitignore b/.gitignore
index 064a6aa..20cd526 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,10 @@
.idea
*.iml
+.project
+.settings
+.classpath
+
out
generated
generated-*
diff --git a/README.md b/README.md
index 3e2f755..947055c 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ So far we have "trouble" with...
* [Noto Sans](noto-sans)
* [weird Tycho/JDT behavior](tycho-jdt)
* [XML transformations](xml-transformer)
+* [Symbolic References](symbolic-references)
## Observe!
diff --git a/pom.xml b/pom.xml
index 307cee6..fb528fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,7 @@
noto-sans
tycho-jdt
xml-transformer
+ symbolic-references
diff --git a/symbolic-references/README.md b/symbolic-references/README.md
new file mode 100644
index 0000000..6835d0f
--- /dev/null
+++ b/symbolic-references/README.md
@@ -0,0 +1,32 @@
+# Symbolic References
+
+This is related to the class loading tests, and really falls into the "Should Break" category,
+as previous behaviour was not strictly to-spec.
+
+## Stricter interpretation of Symbolic References in .class files
+
+Java 9 appears to be more strict in its interpretation of the term "Symbolic Reference" in
+class files. The spec says:
+
+> For a nonarray class or an interface, the name is the fully qualified name of the class or interface.
+
+(This is expected in "binary format", with forward-slashes instead of periods, for example
+`java/lang/String`.)
+
+In Java 8, you could get away with using a _Type Descriptor_ when really you should have
+used a symbolic reference. A good example is CHECKCAST (which is used in this test).
+Under Java 8, the instruction could have referenced a constant pool entry with either
+`java/lang/String` (to spec) or `Ljava/lang/String;` (a descriptor, and not strictly
+to spec) and it would work fine.
+
+Under Java 9, such classes will not load. You'll instead receive a `ClassFormatError`:
+
+```
+[ERROR] loadClassWithDescriptorCheckcast Time elapsed: 0.031 s <<< ERROR!
+java.lang.RuntimeException: Unrecoverable Error
+ at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13)
+Caused by: java.lang.reflect.InvocationTargetException
+ at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13)
+Caused by: java.lang.ClassFormatError: Illegal class name "Ljava/lang/String;" in class file TestClassImpl
+ at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13)
+```
diff --git a/symbolic-references/pom.xml b/symbolic-references/pom.xml
new file mode 100644
index 0000000..109764e
--- /dev/null
+++ b/symbolic-references/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ wtf.java9
+ symbolic-references
+ 1.0-SNAPSHOT
+
+
+ wtf.java9
+ seriously-wtf
+ 1.0-SNAPSHOT
+ ../pom.xml
+
+
+
+
+ org.ow2.asm
+ asm
+ 6.2
+
+
+ org.ow2.asm
+ asm-tree
+ 6.2
+
+
+
diff --git a/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClass.java b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClass.java
new file mode 100644
index 0000000..35316a3
--- /dev/null
+++ b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClass.java
@@ -0,0 +1,5 @@
+package wtf.java9.symbolic_references;
+
+public interface TestClass {
+ public String generatedMethod(Object o);
+}
diff --git a/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClassGenerator.java b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClassGenerator.java
new file mode 100644
index 0000000..2414300
--- /dev/null
+++ b/symbolic-references/src/main/java/wtf/java9/symbolic_references/TestClassGenerator.java
@@ -0,0 +1,97 @@
+package wtf.java9.symbolic_references;
+
+import static org.objectweb.asm.Opcodes.*;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.tree.ClassNode;
+
+public class TestClassGenerator {
+ private static final Method defineClass;
+ static {
+ try {
+ defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Unrecoverable Error: NoSuchMethodException 'defineClass' on ClassLoader.\n"
+ + "This is most likely an environment issue.", e);
+ }
+ defineClass.setAccessible(true);
+ }
+
+ public Class extends TestClass> generateClass() {
+ return defineClass(getClass().getClassLoader());
+
+ }
+
+ byte[] generateBytecode() {
+ ClassNode cv = new ClassNode();
+
+ cv.visit(V1_8,
+ ACC_PUBLIC | ACC_SYNTHETIC | ACC_SUPER,
+ "TestClassImpl",
+ null,
+ "java/lang/Object",
+ new String[] { "wtf/java9/symbolic_references/TestClass" });
+
+ generateConstructor(cv);
+ generateGeneratedMethod(cv);
+
+ cv.visitEnd();
+
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+ cv.accept(writer);
+ return writer.toByteArray();
+ }
+
+ void generateConstructor(ClassVisitor cv) {
+ MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC,
+ "",
+ "()V",
+ "",
+ null);
+
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitEnd();
+ }
+
+ void generateGeneratedMethod(ClassVisitor cv) {
+ MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC,
+ "generatedMethod",
+ "(Ljava/lang/Object;)Ljava/lang/String;",
+ "",
+ null);
+
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 1);
+
+ // The following line initiates the failure. Java 9 is
+ // more strict here and requires an internal name
+ // rather than a descriptor. So change to "java/lang/String".
+ mv.visitTypeInsn(CHECKCAST, "Ljava/lang/String;");
+
+ mv.visitInsn(ARETURN);
+ mv.visitEnd();
+ }
+
+ @SuppressWarnings("unchecked")
+ Class extends TestClass> defineClass(ClassLoader loader) {
+ byte[] code = generateBytecode();
+ try {
+ return (Class extends TestClass>)defineClass.invoke(loader, "TestClassImpl", code, 0, code.length);
+ } catch (InvocationTargetException e) {
+ System.err.println("InvocationTargetException: in defineClass: " + e.getMessage());
+ throw new RuntimeException("Unrecoverable Error", e);
+ } catch (IllegalAccessException e) {
+ System.err.println("IllegalAccessException: in defineClass: " + e.getMessage());
+ throw new RuntimeException("Unrecoverable Error", e);
+ }
+ }
+
+}
diff --git a/symbolic-references/src/test/java/wtf/java9/symbolic_references/CheckCastTest.java b/symbolic-references/src/test/java/wtf/java9/symbolic_references/CheckCastTest.java
new file mode 100644
index 0000000..cfec34a
--- /dev/null
+++ b/symbolic-references/src/test/java/wtf/java9/symbolic_references/CheckCastTest.java
@@ -0,0 +1,19 @@
+package wtf.java9.symbolic_references;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import wtf.java9.symbolic_references.TestClass;
+import wtf.java9.symbolic_references.TestClassGenerator;
+
+public class CheckCastTest {
+
+ /* Works under Java < 9, fails with ClassFormatError under Java 9. */
+ @Test
+ public void loadClassWithDescriptorCheckcast() throws Exception {
+ Class extends TestClass> clz = new TestClassGenerator().generateClass();
+ TestClass testClass = clz.newInstance();
+ assertThat(testClass.generatedMethod("hello")).isEqualTo("hello");
+ }
+}