1 /* 2 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import java.io.StringWriter; 25 import java.lang.classfile.ClassFile; 26 import java.lang.classfile.Instruction; 27 import java.lang.classfile.Label; 28 import java.lang.classfile.MethodModel; 29 import java.lang.classfile.Opcode; 30 import java.lang.classfile.attribute.CodeAttribute; 31 import jdk.internal.classfile.components.ClassPrinter; 32 import java.lang.classfile.instruction.*; 33 import java.lang.invoke.MethodHandles; 34 import jdk.incubator.code.bytecode.BytecodeGenerator; 35 import jdk.incubator.code.bytecode.BytecodeLift; 36 import jdk.incubator.code.interpreter.Verifier; 37 import jdk.incubator.code.op.CoreOp; 38 import java.net.URI; 39 import java.nio.file.FileSystem; 40 import java.nio.file.FileSystems; 41 import java.nio.file.Files; 42 import java.nio.file.Path; 43 import java.util.*; 44 import java.util.stream.Collectors; 45 import java.util.stream.Stream; 46 import org.testng.Assert; 47 import org.testng.annotations.Ignore; 48 import org.testng.annotations.Test; 49 50 /* 51 * @test 52 * @modules jdk.incubator.code 53 * @modules java.base/java.lang.invoke:open 54 * @modules java.base/jdk.internal.classfile.components 55 * @enablePreview 56 * @run testng TestSmallCorpus 57 */ 58 public class TestSmallCorpus { 59 60 private static final String ROOT_PATH = "modules/java.base/"; 61 private static final String CLASS_NAME_SUFFIX = ".class"; 62 private static final String METHOD_NAME = null; 63 private static final int ROUNDS = 3; 64 65 private static final FileSystem JRT = FileSystems.getFileSystem(URI.create("jrt:/")); 66 private static final ClassFile CF = ClassFile.of(); 67 private static final int COLUMN_WIDTH = 150; 68 private static final MethodHandles.Lookup TRUSTED_LOOKUP; 69 static { 70 try { 71 var lf = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); 72 lf.setAccessible(true); 73 TRUSTED_LOOKUP = (MethodHandles.Lookup)lf.get(null); 74 } catch (ReflectiveOperationException e) { 75 throw new RuntimeException(e); 76 } 77 } 78 79 private MethodModel bytecode; 80 CoreOp.FuncOp reflection; 81 private int stable, unstable; 82 private Long[] stats = new Long[6]; 83 84 @Ignore 85 @Test 86 public void testRoundTripStability() throws Exception { 87 stable = 0; 88 unstable = 0; 89 Arrays.fill(stats, 0l); 90 for (Path p : Files.walk(JRT.getPath(ROOT_PATH)) 91 .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(CLASS_NAME_SUFFIX)) 92 .toList()) { 93 testRoundTripStability(p); 94 } 95 96 System.out.println(""" 97 statistics original generated 98 code length: %1$,10d %4$,10d 99 max locals: %2$,10d %5$,10d 100 max stack: %3$,10d %6$,10d 101 """.formatted((Object[])stats)); 102 103 // Roundtrip is 100% stable after 3 rounds, no exceptions, no verification errors 104 Assert.assertTrue(stable > 54500 && unstable == 0, String.format("stable: %d unstable: %d", stable, unstable)); 105 } 106 107 private void testRoundTripStability(Path path) throws Exception { 108 var clm = CF.parse(path); 109 for (var originalModel : clm.methods()) { 110 if (originalModel.code().isPresent() && (METHOD_NAME == null || originalModel.methodName().equalsString(METHOD_NAME))) try { 111 bytecode = originalModel; 112 reflection = null; 113 MethodModel prevBytecode = null; 114 CoreOp.FuncOp prevReflection = null; 115 for (int round = 1; round <= ROUNDS; round++) try { 116 prevBytecode = bytecode; 117 prevReflection = reflection; 118 lift(); 119 verifyReflection(); 120 generate(); 121 verifyBytecode(); 122 } catch (UnsupportedOperationException uoe) { 123 throw uoe; 124 } catch (Throwable t) { 125 System.out.println(" at " + path + " " + originalModel.methodName() + originalModel.methodType() + " round " + round); 126 throw t; 127 } 128 if (ROUNDS > 0) { 129 var normPrevBytecode = normalize(prevBytecode); 130 var normBytecode = normalize(bytecode); 131 if (normPrevBytecode.equals(normBytecode)) { 132 stable++; 133 } else { 134 unstable++; 135 System.out.println("Unstable code " + path + " " + originalModel.methodName() + originalModel.methodType() + " after " + ROUNDS +" round(s)"); 136 if (prevReflection != null) printInColumns(prevReflection, reflection); 137 printInColumns(normPrevBytecode, normBytecode); 138 System.out.println(); 139 } 140 var ca = (CodeAttribute)originalModel.code().get(); 141 stats[0] += ca.codeLength(); 142 stats[1] += ca.maxLocals(); 143 stats[2] += ca.maxStack(); 144 ca = (CodeAttribute)bytecode.code().get(); 145 stats[3] += ca.codeLength(); 146 stats[4] += ca.maxLocals(); 147 stats[5] += ca.maxStack(); 148 } 149 } catch (UnsupportedOperationException uoe) { 150 // InvokeOp when InvokeKind == SUPER 151 } 152 } 153 } 154 155 private void verifyReflection() { 156 var errors = Verifier.verify(TRUSTED_LOOKUP, reflection); 157 if (!errors.isEmpty()) { 158 printBytecode(); 159 System.out.println("Code reflection model verification failed:"); 160 errors.forEach(e -> System.out.println(e.getMessage())); 161 System.out.println(errors.getFirst().getPrintedContext()); 162 throw new AssertionError("Code reflection model verification failed"); 163 } 164 } 165 166 private void verifyBytecode() { 167 var errors = ClassFile.of().verify(bytecode.parent().get()).stream() 168 .filter(e -> !e.getMessage().contains("Illegal call to internal method")).toList(); 169 if (!errors.isEmpty()) { 170 printReflection(); 171 System.out.println("Bytecode verification failed:"); 172 errors.forEach(e -> System.out.println(e.getMessage())); 173 printBytecode(); 174 throw new AssertionError("Bytecode verification failed"); 175 } 176 } 177 178 private static void printInColumns(CoreOp.FuncOp first, CoreOp.FuncOp second) { 179 StringWriter fw = new StringWriter(); 180 first.writeTo(fw); 181 StringWriter sw = new StringWriter(); 182 second.writeTo(sw); 183 printInColumns(fw.toString().lines().toList(), sw.toString().lines().toList()); 184 } 185 186 private static void printInColumns(List<String> first, List<String> second) { 187 System.out.println("-".repeat(COLUMN_WIDTH ) + "--+-" + "-".repeat(COLUMN_WIDTH )); 188 for (int i = 0; i < first.size() || i < second.size(); i++) { 189 String f = i < first.size() ? first.get(i) : ""; 190 String s = i < second.size() ? second.get(i) : ""; 191 System.out.println(" " + f + (f.length() < COLUMN_WIDTH ? " ".repeat(COLUMN_WIDTH - f.length()) : "") + (f.equals(s) ? " | " : " x ") + s); 192 } 193 } 194 195 private void lift() { 196 try { 197 reflection = BytecodeLift.lift(bytecode); 198 } catch (Throwable t) { 199 printReflection(); 200 printBytecode(); 201 System.out.println("Lift failed"); 202 throw t; 203 } 204 } 205 206 private void generate() { 207 try { 208 bytecode = CF.parse(BytecodeGenerator.generateClassData( 209 TRUSTED_LOOKUP, 210 reflection)).methods().getFirst(); 211 } catch (UnsupportedOperationException uoe) { 212 throw uoe; 213 } catch (Throwable t) { 214 printBytecode(); 215 printReflection(); 216 System.out.println("Generation failed"); 217 throw t; 218 } 219 } 220 221 private void printBytecode() { 222 ClassPrinter.toYaml(bytecode, ClassPrinter.Verbosity.CRITICAL_ATTRIBUTES, System.out::print); 223 } 224 225 private void printReflection() { 226 if (reflection != null) System.out.println(reflection.toText()); 227 } 228 229 public static List<String> normalize(MethodModel mm) { 230 record El(int index, String format, Label... targets) { 231 public El(int index, Instruction i, Object format, Label... targets) { 232 this(index, trim(i.opcode()) + " " + format, targets); 233 } 234 public String toString(Map<Label, Integer> targetsMap) { 235 return "%3d: ".formatted(index) + (targets.length == 0 ? format : format.formatted(Stream.of(targets).map(l -> targetsMap.get(l)).toArray())); 236 } 237 } 238 239 Map<Label, Integer> targetsMap = new HashMap<>(); 240 List<El> elements = new ArrayList<>(); 241 Label lastLabel = null; 242 int i = 0; 243 for (var e : mm.code().orElseThrow()) { 244 var er = switch (e) { 245 case LabelTarget lt -> { 246 lastLabel = lt.label(); 247 yield null; 248 } 249 case ExceptionCatch ec -> 250 new El(i++, "ExceptionCatch start: @%d end: @%d handler: @%d" + ec.catchType().map(ct -> " catch type: " + ct.asInternalName()).orElse(""), ec.tryStart(), ec.tryEnd(), ec.handler()); 251 case BranchInstruction ins -> 252 new El(i++, ins, "@%d", ins.target()); 253 case ConstantInstruction ins -> 254 new El(i++, "LDC " + ins.constantValue()); 255 case FieldInstruction ins -> 256 new El(i++, ins, ins.owner().asInternalName() + "." + ins.name().stringValue()); 257 case InvokeDynamicInstruction ins -> 258 new El(i++, ins, ins.name().stringValue() + ins.typeSymbol() + " " + ins.bootstrapMethod() + "(" + ins.bootstrapArgs() + ")"); 259 case InvokeInstruction ins -> 260 new El(i++, ins, ins.owner().asInternalName() + "::" + ins.name().stringValue() + ins.typeSymbol().displayDescriptor()); 261 case LoadInstruction ins -> 262 new El(i++, ins, "#" + ins.slot()); 263 case StoreInstruction ins -> 264 new El(i++, ins, "#" + ins.slot()); 265 case IncrementInstruction ins -> 266 new El(i++, ins, "#" + ins.slot() + " " + ins.constant()); 267 case LookupSwitchInstruction ins -> 268 new El(i++, ins, "default: @%d" + ins.cases().stream().map(c -> ", " + c.caseValue() + ": @%d").collect(Collectors.joining()), 269 Stream.concat(Stream.of(ins.defaultTarget()), ins.cases().stream().map(SwitchCase::target)).toArray(Label[]::new)); 270 case NewMultiArrayInstruction ins -> 271 new El(i++, ins, ins.arrayType().asInternalName() + "(" + ins.dimensions() + ")"); 272 case NewObjectInstruction ins -> 273 new El(i++, ins, ins.className().asInternalName()); 274 case NewPrimitiveArrayInstruction ins -> 275 new El(i++, ins, ins.typeKind()); 276 case NewReferenceArrayInstruction ins -> 277 new El(i++, ins, ins.componentType().asInternalName()); 278 case TableSwitchInstruction ins -> 279 new El(i++, ins, "default: @%d" + ins.cases().stream().map(c -> ", " + c.caseValue() + ": @%d").collect(Collectors.joining()), 280 Stream.concat(Stream.of(ins.defaultTarget()), ins.cases().stream().map(SwitchCase::target)).toArray(Label[]::new)); 281 case TypeCheckInstruction ins -> 282 new El(i++, ins, ins.type().asInternalName()); 283 case Instruction ins -> 284 new El(i++, ins, ""); 285 default -> null; 286 }; 287 if (er != null) { 288 if (lastLabel != null) { 289 targetsMap.put(lastLabel, elements.size()); 290 lastLabel = null; 291 } 292 elements.add(er); 293 } 294 } 295 return elements.stream().map(el -> el.toString(targetsMap)).toList(); 296 } 297 298 private static String trim(Opcode opcode) { 299 var name = opcode.toString(); 300 int i = name.indexOf('_'); 301 return i > 2 ? name.substring(0, i) : name; 302 } 303 }