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 }