1 /*
2 * Copyright (c) 2024, 2025, 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 jdk.incubator.code.bytecode.BytecodeGenerator;
25 import jdk.incubator.code.dialect.core.CoreOp;
26 import jdk.internal.classfile.components.ClassPrinter;
27 import org.junit.jupiter.api.Assertions;
28 import org.junit.jupiter.api.Disabled;
29 import org.junit.jupiter.api.Test;
30
31 import java.lang.classfile.*;
32 import java.lang.classfile.attribute.CodeAttribute;
33 import java.lang.classfile.instruction.*;
34 import java.lang.invoke.MethodHandles;
35 import java.net.URI;
36 import java.nio.file.FileSystem;
37 import java.nio.file.FileSystems;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.util.*;
41 import java.util.stream.Collectors;
42 import java.util.stream.Stream;
43
44 /*
45 * @test
46 * @modules jdk.incubator.code/jdk.incubator.code.internal
47 * @modules java.base/java.lang.invoke:open
48 * @modules java.base/jdk.internal.classfile.components
49 * @enablePreview
50 * @run junit TestSmallCorpus
51 */
52 public class TestSmallCorpus {
53
54 private static final String ROOT_PATH = "modules/java.base/";
55 private static final String CLASS_NAME_SUFFIX = ".class";
56 private static final String METHOD_NAME = null;
57 private static final int ROUNDS = 3;
58
59 private static final FileSystem JRT = FileSystems.getFileSystem(URI.create("jrt:/"));
60 private static final ClassFile CF = ClassFile.of();
61 private static final int COLUMN_WIDTH = 150;
62 private static final MethodHandles.Lookup TRUSTED_LOOKUP;
63 static {
64 try {
65 var lf = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
66 lf.setAccessible(true);
67 TRUSTED_LOOKUP = (MethodHandles.Lookup)lf.get(null);
68 } catch (ReflectiveOperationException e) {
69 throw new RuntimeException(e);
70 }
71 }
72
73 private MethodModel bytecode;
74 CoreOp.FuncOp reflection;
75 private int stable, unstable;
76 private Long[] stats = new Long[6];
77
78 @Disabled
79 @Test
80 public void testRoundTripStability() throws Exception {
81 stable = 0;
82 unstable = 0;
83 Arrays.fill(stats, 0l);
84 for (Path p : Files.walk(JRT.getPath(ROOT_PATH))
85 .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(CLASS_NAME_SUFFIX))
86 .toList()) {
87 testRoundTripStability(p);
88 }
89
90 System.out.println("""
91 statistics original generated
92 code length: %1$,10d %4$,10d
93 max locals: %2$,10d %5$,10d
94 max stack: %3$,10d %6$,10d
95 """.formatted((Object[])stats));
96
97 // Roundtrip is 100% stable after 3 rounds, no exceptions, no verification errors
98 Assertions.assertTrue(stable > 54500 && unstable == 0, String.format("stable: %d unstable: %d", stable, unstable));
99 }
100
101 private void testRoundTripStability(Path path) throws Exception {
102 var clm = CF.parse(path);
103 for (var originalModel : clm.methods()) {
104 if (originalModel.code().isPresent() && (METHOD_NAME == null || originalModel.methodName().equalsString(METHOD_NAME))) try {
105 bytecode = originalModel;
106 reflection = null;
107 MethodModel prevBytecode = null;
108 CoreOp.FuncOp prevReflection = null;
109 for (int round = 1; round <= ROUNDS; round++) try {
110 prevBytecode = bytecode;
111 prevReflection = reflection;
112 lift();
113 verifyReflection();
114 generate();
115 verifyBytecode();
116 } catch (UnsupportedOperationException uoe) {
117 throw uoe;
118 } catch (Throwable t) {
119 System.out.println(" at " + path + " " + originalModel.methodName() + originalModel.methodType() + " round " + round);
120 throw t;
121 }
122 if (ROUNDS > 0) {
123 var normPrevBytecode = normalize(prevBytecode);
124 var normBytecode = normalize(bytecode);
125 if (normPrevBytecode.equals(normBytecode)) {
126 stable++;
127 } else {
128 unstable++;
129 System.out.println("Unstable code " + path + " " + originalModel.methodName() + originalModel.methodType() + " after " + ROUNDS +" round(s)");
130 if (prevReflection != null) printInColumns(prevReflection, reflection);
131 printInColumns(normPrevBytecode, normBytecode);
132 System.out.println();
133 }
134 var ca = (CodeAttribute)originalModel.code().get();
135 stats[0] += ca.codeLength();
136 stats[1] += ca.maxLocals();
137 stats[2] += ca.maxStack();
138 ca = (CodeAttribute)bytecode.code().get();
139 stats[3] += ca.codeLength();
140 stats[4] += ca.maxLocals();
141 stats[5] += ca.maxStack();
142 }
143 } catch (UnsupportedOperationException uoe) {
144 // InvokeOp when InvokeKind == SUPER
145 }
146 }
147 }
148
149 private void verifyReflection() {
150 var errors = Verifier.verify(TRUSTED_LOOKUP, reflection);
151 if (!errors.isEmpty()) {
152 printBytecode();
153 System.out.println("Code reflection model verification failed:");
154 errors.forEach(e -> System.out.println(e.getMessage()));
155 System.out.println(errors.getFirst().getPrintedContext());
156 throw new AssertionError("Code reflection model verification failed");
157 }
158 }
159
160 private void verifyBytecode() {
161 var errors = ClassFile.of().verify(bytecode.parent().get()).stream()
162 .filter(e -> !e.getMessage().contains("Illegal call to internal method")).toList();
163 if (!errors.isEmpty()) {
164 printReflection();
165 System.out.println("Bytecode verification failed:");
166 errors.forEach(e -> System.out.println(e.getMessage()));
167 printBytecode();
168 throw new AssertionError("Bytecode verification failed");
169 }
170 }
171
172 private static void printInColumns(CoreOp.FuncOp first, CoreOp.FuncOp second) {
173 printInColumns(first.toText().lines().toList(), second.toText().lines().toList());
174 }
175
176 private static void printInColumns(List<String> first, List<String> second) {
177 System.out.println("-".repeat(COLUMN_WIDTH ) + "--+-" + "-".repeat(COLUMN_WIDTH ));
178 for (int i = 0; i < first.size() || i < second.size(); i++) {
179 String f = i < first.size() ? first.get(i) : "";
180 String s = i < second.size() ? second.get(i) : "";
181 System.out.println(" " + f + (f.length() < COLUMN_WIDTH ? " ".repeat(COLUMN_WIDTH - f.length()) : "") + (f.equals(s) ? " | " : " x ") + s);
182 }
183 }
184
185 private void lift() {
186 try {
187 reflection = BytecodeLift.lift(bytecode);
188 } catch (Throwable t) {
189 printReflection();
190 printBytecode();
191 System.out.println("Lift failed");
192 throw t;
193 }
194 }
195
196 private void generate() {
197 try {
198 bytecode = CF.parse(BytecodeGenerator.generateClassData(
199 TRUSTED_LOOKUP,
200 reflection)).methods().getFirst();
201 } catch (UnsupportedOperationException uoe) {
202 throw uoe;
203 } catch (Throwable t) {
204 printBytecode();
205 printReflection();
206 System.out.println("Generation failed");
207 throw t;
208 }
209 }
210
211 private void printBytecode() {
212 ClassPrinter.toYaml(bytecode, ClassPrinter.Verbosity.CRITICAL_ATTRIBUTES, System.out::print);
213 }
214
215 private void printReflection() {
216 if (reflection != null) System.out.println(reflection.toText());
217 }
218
219 public static List<String> normalize(MethodModel mm) {
220 record El(int index, String format, Label... targets) {
221 public El(int index, Instruction i, Object format, Label... targets) {
222 this(index, trim(i.opcode()) + " " + format, targets);
223 }
224 public String toString(Map<Label, Integer> targetsMap) {
225 return "%3d: ".formatted(index) + (targets.length == 0 ? format : format.formatted(Stream.of(targets).map(l -> targetsMap.get(l)).toArray()));
226 }
227 }
228
229 Map<Label, Integer> targetsMap = new HashMap<>();
230 List<El> elements = new ArrayList<>();
231 Label lastLabel = null;
232 int i = 0;
233 for (var e : mm.code().orElseThrow()) {
234 var er = switch (e) {
235 case LabelTarget lt -> {
236 lastLabel = lt.label();
237 yield null;
238 }
239 case ExceptionCatch ec ->
240 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());
241 case BranchInstruction ins ->
242 new El(i++, ins, "@%d", ins.target());
243 case ConstantInstruction ins ->
244 new El(i++, "LDC " + ins.constantValue());
245 case FieldInstruction ins ->
246 new El(i++, ins, ins.owner().asInternalName() + "." + ins.name().stringValue());
247 case InvokeDynamicInstruction ins ->
248 new El(i++, ins, ins.name().stringValue() + ins.typeSymbol() + " " + ins.bootstrapMethod() + "(" + ins.bootstrapArgs() + ")");
249 case InvokeInstruction ins ->
250 new El(i++, ins, ins.owner().asInternalName() + "::" + ins.name().stringValue() + ins.typeSymbol().displayDescriptor());
251 case LoadInstruction ins ->
252 new El(i++, ins, "#" + ins.slot());
253 case StoreInstruction ins ->
254 new El(i++, ins, "#" + ins.slot());
255 case IncrementInstruction ins ->
256 new El(i++, ins, "#" + ins.slot() + " " + ins.constant());
257 case LookupSwitchInstruction ins ->
258 new El(i++, ins, "default: @%d" + ins.cases().stream().map(c -> ", " + c.caseValue() + ": @%d").collect(Collectors.joining()),
259 Stream.concat(Stream.of(ins.defaultTarget()), ins.cases().stream().map(SwitchCase::target)).toArray(Label[]::new));
260 case NewMultiArrayInstruction ins ->
261 new El(i++, ins, ins.arrayType().asInternalName() + "(" + ins.dimensions() + ")");
262 case NewObjectInstruction ins ->
263 new El(i++, ins, ins.className().asInternalName());
264 case NewPrimitiveArrayInstruction ins ->
265 new El(i++, ins, ins.typeKind());
266 case NewReferenceArrayInstruction ins ->
267 new El(i++, ins, ins.componentType().asInternalName());
268 case TableSwitchInstruction ins ->
269 new El(i++, ins, "default: @%d" + ins.cases().stream().map(c -> ", " + c.caseValue() + ": @%d").collect(Collectors.joining()),
270 Stream.concat(Stream.of(ins.defaultTarget()), ins.cases().stream().map(SwitchCase::target)).toArray(Label[]::new));
271 case TypeCheckInstruction ins ->
272 new El(i++, ins, ins.type().asInternalName());
273 case Instruction ins ->
274 new El(i++, ins, "");
275 default -> null;
276 };
277 if (er != null) {
278 if (lastLabel != null) {
279 targetsMap.put(lastLabel, elements.size());
280 lastLabel = null;
281 }
282 elements.add(er);
283 }
284 }
285 return elements.stream().map(el -> el.toString(targetsMap)).toList();
286 }
287
288 private static String trim(Opcode opcode) {
289 var name = opcode.toString();
290 int i = name.indexOf('_');
291 return i > 2 ? name.substring(0, i) : name;
292 }
293 }