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.PrintWriter;
 25 import java.io.StringWriter;
 26 import java.lang.classfile.ClassFile;
 27 import java.lang.classfile.Instruction;
 28 import java.lang.classfile.Label;
 29 import java.lang.classfile.MethodModel;
 30 import java.lang.classfile.Opcode;
 31 import java.lang.classfile.instruction.*;
 32 import java.lang.invoke.MethodHandles;
 33 import java.lang.reflect.AccessFlag;
 34 import java.lang.reflect.code.Op;
 35 import java.lang.reflect.code.OpTransformer;
 36 import java.lang.reflect.code.analysis.SSA;
 37 import java.lang.reflect.code.bytecode.BytecodeGenerator;
 38 import java.lang.reflect.code.bytecode.BytecodeLift;
 39 import java.lang.reflect.code.op.CoreOp;
 40 import java.net.URI;
 41 import java.nio.file.FileSystem;
 42 import java.nio.file.FileSystems;
 43 import java.nio.file.Files;
 44 import java.nio.file.Path;
 45 import java.util.*;
 46 import java.util.stream.Collectors;
 47 import java.util.stream.Stream;
 48 import org.testng.Assert;
 49 import org.testng.annotations.Ignore;
 50 import org.testng.annotations.Test;
 51 
 52 /*
 53  * @test
 54  * @enablePreview
 55  * @run testng TestSmallCorpus
 56  */
 57 public class TestSmallCorpus {
 58 
 59     private static final FileSystem JRT = FileSystems.getFileSystem(URI.create("jrt:/"));
 60     private static final ClassFile CF = ClassFile.of(ClassFile.DebugElementsOption.DROP_DEBUG,
 61                                                      ClassFile.LineNumbersOption.DROP_LINE_NUMBERS);
 62     private static final int COLUMN_WIDTH = 150;
 63 
 64     private int passed, notMatching;
 65     private Map<String, Map<String, Integer>> errorStats;
 66 
 67     @Ignore
 68     @Test
 69     public void testDoubleRoundtripStability() throws Exception {
 70         passed = 0;
 71         notMatching = 0;
 72         errorStats = new LinkedHashMap<>();
 73         for (Path p : Files.walk(JRT.getPath("modules/java.base/java"))
 74                 .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".class"))
 75                 .toList()) {
 76             testDoubleRoundtripStability(p);
 77         }
 78 
 79         for (var stats : errorStats.entrySet()) {
 80             System.out.println(String.format("""
 81 
 82             %s errors:
 83             -----------------------------------------------------
 84             """, stats.getKey()));
 85             stats.getValue().entrySet().stream().sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())).forEach(e -> System.out.println(e.getValue() +"x " + e.getKey() + "\n"));
 86         }
 87 
 88         // @@@ There is still several failing cases and a lot of errors
 89         Assert.assertTrue(notMatching < 31 && passed > 5400, String.format("""
 90 
 91                     passed: %d
 92                     not matching: %d
 93                     %s
 94                 """,
 95                 passed,
 96                 notMatching,
 97                 errorStats.entrySet().stream().map(e -> e.getKey() +
 98                         " errors: "
 99                         + e.getValue().values().stream().mapToInt(Integer::intValue).sum()).collect(Collectors.joining("\n    "))
100                 ));
101     }
102 
103     private void testDoubleRoundtripStability(Path path) throws Exception {
104         var clm = CF.parse(path);
105         for (var originalModel : clm.methods()) {
106             if (originalModel.flags().has(AccessFlag.STATIC) && originalModel.code().isPresent()) try {
107                 CoreOp.FuncOp firstLift = lift(originalModel);
108                 try {
109                     CoreOp.FuncOp firstTransform = transform(firstLift);
110                     try {
111                         MethodModel firstModel = lower(firstTransform);
112                         try {
113                             CoreOp.FuncOp secondLift = lift(firstModel);
114                             try {
115                                 CoreOp.FuncOp secondTransform = transform(secondLift);
116                                 try {
117                                     MethodModel secondModel = lower(secondTransform);
118 
119                                     // testing only methods passing through
120                                     var firstNormalized = normalize(firstModel);
121                                     var secondNormalized = normalize(secondModel);
122                                     if (!firstNormalized.equals(secondNormalized)) {
123                                         notMatching++;
124                                         System.out.println(clm.thisClass().asInternalName() + "::" + originalModel.methodName().stringValue() + originalModel.methodTypeSymbol().displayDescriptor());
125                                         printInColumns(firstLift, secondLift);
126                                         printInColumns(firstTransform, secondTransform);
127                                         printInColumns(firstNormalized, secondNormalized);
128                                         System.out.println();
129                                     } else {
130                                         passed++;
131                                     }
132                                 } catch (Exception e) {
133                                     error("second lower", e);
134                                 }
135                             } catch (Exception e) {
136                                 error("second transform", e);
137                             }
138                         } catch (Exception e) {
139                             error("second lift", e);
140                         }
141                     } catch (Exception e) {
142                         error("first lower", e);
143                     }
144                 } catch (Exception e) {
145                     error("first transform", e);
146                 }
147             } catch (Exception e) {
148                 error("first lift", e);
149             }
150         }
151     }
152     private static void printInColumns(CoreOp.FuncOp first, CoreOp.FuncOp second) {
153         StringWriter fw = new StringWriter();
154         first.writeTo(fw);
155         StringWriter sw = new StringWriter();
156         second.writeTo(sw);
157         printInColumns(fw.toString().lines().toList(), sw.toString().lines().toList());
158     }
159 
160     private static void printInColumns(List<String> first, List<String> second) {
161         System.out.println("-".repeat(COLUMN_WIDTH ) + "--+-" + "-".repeat(COLUMN_WIDTH ));
162         for (int i = 0; i < first.size() || i < second.size(); i++) {
163             String f = i < first.size() ? first.get(i) : "";
164             String s = i < second.size() ? second.get(i) : "";
165             System.out.println(" " + f + (f.length() < COLUMN_WIDTH ? " ".repeat(COLUMN_WIDTH - f.length()) : "") + (f.equals(s) ? " | " : " x ") + s);
166         }
167     }
168 
169     private static CoreOp.FuncOp lift(MethodModel mm) {
170         return BytecodeLift.lift(mm);
171     }
172 
173     private static CoreOp.FuncOp transform(CoreOp.FuncOp func) {
174         return SSA.transform(func.transform(OpTransformer.LOWERING_TRANSFORMER));
175     }
176 
177     private static MethodModel lower(CoreOp.FuncOp func) {
178         return CF.parse(BytecodeGenerator.generateClassData(
179                 MethodHandles.lookup(),
180                 func)).methods().get(0);
181     }
182 
183 
184     public static List<String> normalize(MethodModel mm) {
185         record El(int index, String format, Label... targets) {
186             public El(int index, Instruction i, Object format, Label... targets) {
187                 this(index, trim(i.opcode()) + " " + format, targets);
188             }
189             public String toString(Map<Label, Integer> targetsMap) {
190                 return "%3d: ".formatted(index) + (targets.length == 0 ? format : format.formatted(Stream.of(targets).map(l -> targetsMap.get(l)).toArray()));
191             }
192         }
193 
194         Map<Label, Integer> targetsMap = new HashMap<>();
195         List<El> elements = new ArrayList<>();
196         Label lastLabel = null;
197         int i = 0;
198         for (var e : mm.code().orElseThrow()) {
199             var er = switch (e) {
200                 case LabelTarget lt -> {
201                     lastLabel = lt.label();
202                     yield null;
203                 }
204                 case ExceptionCatch ec ->
205                     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());
206                 case BranchInstruction ins ->
207                     new El(i++, ins, "@%d", ins.target());
208                 case ConstantInstruction ins ->
209                     new El(i++, "LDC " + ins.constantValue());
210                 case FieldInstruction ins ->
211                     new El(i++, ins, ins.owner().asInternalName() + "." + ins.name().stringValue());
212                 case InvokeDynamicInstruction ins ->
213                     new El(i++, ins, ins.name().stringValue() + ins.typeSymbol() + " " + ins.bootstrapMethod() + "(" + ins.bootstrapArgs() + ")");
214                 case InvokeInstruction ins ->
215                     new El(i++, ins, ins.owner().asInternalName() + "::" + ins.name().stringValue() + ins.typeSymbol().displayDescriptor());
216                 case LoadInstruction ins ->
217                     new El(i++, ins, "#" + ins.slot());
218                 case StoreInstruction ins ->
219                     new El(i++, ins, "#" + ins.slot());
220                 case IncrementInstruction ins ->
221                     new El(i++, ins, "#" + ins.slot() + " " + ins.constant());
222                 case LookupSwitchInstruction ins ->
223                     new El(i++, ins, "default: @%d" + ins.cases().stream().map(c -> ", " + c.caseValue() + ": @%d").collect(Collectors.joining()),
224                             Stream.concat(Stream.of(ins.defaultTarget()), ins.cases().stream().map(SwitchCase::target)).toArray(Label[]::new));
225                 case NewMultiArrayInstruction ins ->
226                     new El(i++, ins, ins.arrayType().asInternalName() + "(" + ins.dimensions() + ")");
227                 case NewObjectInstruction ins ->
228                     new El(i++, ins, ins.className().asInternalName());
229                 case NewPrimitiveArrayInstruction ins ->
230                     new El(i++, ins, ins.typeKind());
231                 case NewReferenceArrayInstruction ins ->
232                     new El(i++, ins, ins.componentType().asInternalName());
233                 case TableSwitchInstruction ins ->
234                     new El(i++, ins, "default: @%d" + ins.cases().stream().map(c -> ", " + c.caseValue() + ": @%d").collect(Collectors.joining()),
235                             Stream.concat(Stream.of(ins.defaultTarget()), ins.cases().stream().map(SwitchCase::target)).toArray(Label[]::new));
236                 case TypeCheckInstruction ins ->
237                     new El(i++, ins, ins.type().asInternalName());
238                 case Instruction ins ->
239                     new El(i++, ins, "");
240                 default -> null;
241             };
242             if (er != null) {
243                 if (lastLabel != null) {
244                     targetsMap.put(lastLabel, elements.size());
245                     lastLabel = null;
246                 }
247                 elements.add(er);
248             }
249         }
250         return elements.stream().map(el -> el.toString(targetsMap)).toList();
251     }
252 
253     private static String trim(Opcode opcode) {
254         var name = opcode.toString();
255         int i = name.indexOf('_');
256         return i > 2 ? name.substring(0, i) : name;
257     }
258 
259     private void error(String category, Exception e) {
260         StringWriter sw = new StringWriter();
261         e.printStackTrace(new PrintWriter(sw));
262         errorStats.computeIfAbsent(category, _ -> new HashMap<>())
263                   .compute(sw.toString(), (_, i) -> i == null ? 1 : i + 1);
264     }
265 }