/* * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * @test * @bug 8359412 8370922 * @key randomness * @summary Use the template framework library to generate random expressions. * @modules java.base/jdk.internal.misc * @modules jdk.incubator.vector * @library /test/lib / * @compile ../lib/verify/Verify.java * @run main/othervm -XX:+IgnoreUnrecognizedVMOptions -XX:CompileTaskTimeout=10000 compiler.igvn.ExpressionFuzzer */ package compiler.igvn; import java.util.List; import java.util.ArrayList; import java.util.Set; import java.util.Random; import java.util.Collections; import jdk.test.lib.Utils; import java.util.stream.Collectors; import java.util.stream.IntStream; import compiler.lib.compile_framework.*; import compiler.lib.template_framework.Template; import compiler.lib.template_framework.TemplateToken; import static compiler.lib.template_framework.Template.scope; import static compiler.lib.template_framework.Template.let; import static compiler.lib.template_framework.Template.$; import compiler.lib.template_framework.library.CodeGenerationDataNameType; import compiler.lib.template_framework.library.Expression; import compiler.lib.template_framework.library.Expression.Nesting; import compiler.lib.template_framework.library.Operations; import compiler.lib.template_framework.library.PrimitiveType; import compiler.lib.template_framework.library.TestFrameworkClass; import static compiler.lib.template_framework.library.CodeGenerationDataNameType.PRIMITIVE_TYPES; import static compiler.lib.template_framework.library.CodeGenerationDataNameType.SCALAR_NUMERIC_TYPES; // We generate random Expressions from primitive type operators. // // The goal is to generate random inputs with constrained TypeInt / TypeLong ranges / KnownBits, // and then verify the output value, ranges and bits. // // Should this test fail and make a lot of noise in the CI, you have two choices: // - Problem-list this test: but other tests may also use the same broken operators. // - Temporarily remove the operator from {@code Operations.PRIMITIVE_OPERATIONS}. // // Future Work [FUTURE]: // - Constrain also the unsigned bounds // - Some basic IR tests to ensure that the constraints / checksum mechanics work. // We may even have to add some IGVN optimizations to be able to better observe things right. // - Lower the CompileTaskTimeout, if possible. It is chosen conservatively (rather high) for now. public class ExpressionFuzzer { private static final Random RANDOM = Utils.getRandomInstance(); public static record MethodArgument(String name, CodeGenerationDataNameType type) {} public static record StringPair(String s0, String s1) {} public static void main(String[] args) { // Create a new CompileFramework instance. CompileFramework comp = new CompileFramework(); // Add a java source file. comp.addJavaSourceCode("compiler.igvn.templated.ExpressionFuzzerInnerTest", generate(comp)); // Compile the source file. comp.compile("--add-modules=jdk.incubator.vector"); // compiler.igvn.templated.InnterTest.main(new String[] {}); comp.invoke("compiler.igvn.templated.ExpressionFuzzerInnerTest", "main", new Object[] {new String[] { "--add-modules=jdk.incubator.vector", "--add-opens", "jdk.incubator.vector/jdk.incubator.vector=ALL-UNNAMED", "--add-opens", "java.base/java.lang=ALL-UNNAMED" }}); } // Generate a Java source file as String public static String generate(CompileFramework comp) { // Generate a list of test methods. List tests = new ArrayList<>(); // We are going to use some random numbers in our tests, so import some good methods for that. tests.add(PrimitiveType.generateLibraryRNG()); // Create the body for the test. We use it twice: compiled and reference. // Execute the expression and catch expected Exceptions. var bodyTemplate = Template.make("expression", "arguments", "checksum", (Expression expression, List arguments, String checksum) -> scope( """ try { """, "var val = ", expression.asToken(arguments), ";\n", "return #checksum(val);\n", expression.info.exceptions.stream().map(exception -> "} catch (" + exception + " e) { return e;\n" ).toList(), """ } finally { // Just so that javac is happy if there are no exceptions to catch. } """ )); // Machinery for the "checksum" method. // // We want to do output verification. We don't just want to check if the output value is correct, // but also if the signed/unsigned/KnownBits are correct of the TypeInt and TypeLong. For this, // we add some comparisons. If we get the ranges/bits wrong (too tight), then the comparisons // can wrongly constant fold, and we can detect that in the output array. List unsignedCmp = List.of( new StringPair("(val < ", ")"), new StringPair("(val > ", ")"), new StringPair("(val >= ", ")"), new StringPair("(val <= ", ")"), new StringPair("(val != ", ")"), new StringPair("(val == ", ")"), new StringPair("(val & ", ")") // Extract bits ); List intCmp = List.of( new StringPair("(val < ", ")"), new StringPair("(val > ", ")"), new StringPair("(val >= ", ")"), new StringPair("(val <= ", ")"), new StringPair("(val != ", ")"), new StringPair("(val == ", ")"), new StringPair("(val & ", ")"), // Extract bits new StringPair("(Integer.compareUnsigned(val, ", ") > 0)"), new StringPair("(Integer.compareUnsigned(val, ", ") < 0)"), new StringPair("(Integer.compareUnsigned(val, ", ") >= 0)"), new StringPair("(Integer.compareUnsigned(val, ", ") <= 0)") ); List longCmp = List.of( new StringPair("(val < ", ")"), new StringPair("(val > ", ")"), new StringPair("(val >= ", ")"), new StringPair("(val <= ", ")"), new StringPair("(val != ", ")"), new StringPair("(val == ", ")"), new StringPair("(val & ", ")"), // Extract bits new StringPair("(Long.compareUnsigned(val, ", ") > 0)"), new StringPair("(Long.compareUnsigned(val, ", ") < 0)"), new StringPair("(Long.compareUnsigned(val, ", ") >= 0)"), new StringPair("(Long.compareUnsigned(val, ", ") <= 0)") ); var integralCmpTemplate = Template.make("type", (CodeGenerationDataNameType type) -> { List cmps = switch(type.name()) { case "char" -> unsignedCmp; case "byte", "short", "int" -> intCmp; case "long" -> longCmp; default -> throw new RuntimeException("not handled: " + type.name()); }; StringPair cmp = cmps.get(RANDOM.nextInt(cmps.size())); return scope( ", ", cmp.s0(), type.con(), cmp.s1() ); }); // Checksum method: returns not just the value, but also does some range / bit checks. // This gives us enhanced verification on the range / bits of the result type. var checksumTemplate = Template.make("expression", "checksum", (Expression expression, String checksum) -> scope( let("returnType", expression.returnType), """ @ForceInline public static Object #checksum(#returnType val) { """, "return new Object[] {", switch(expression.returnType.name()) { // The integral values have signed/unsigned ranges and known bits. // Return val, but also some range and bits tests to see if those // ranges and bits are correct. case "byte", "short", "char", "int", "long" -> List.of("val", Collections.nCopies(20, integralCmpTemplate.asToken(expression.returnType))); // Float/Double have no range, just return the value: case "float", "double", "Float16" -> "val"; // Check if the boolean constant folded: case "boolean" -> "val, val == true, val == false"; default -> throw new RuntimeException("type not supported yet: " + expression.returnType.name()); } , "};\n", """ } """ )); // We need to prepare some random values to pass into the test method. We generate the values // once, and pass the same values into both the compiled and reference method. var valueTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> scope( "#type #name = ", (type instanceof PrimitiveType pt) ? pt.callLibraryRNG() : type.con(), ";\n" )); // At the beginning of the compiled and reference test methods we receive the arguments, // which have their full bottom_type (e.g. TypeInt: int). We now constrain the ranges and // bits, for the types that allow it. // // To ensure that both the compiled and reference method use the same constraint, we put // the computation in a ForceInline method. var constrainArgumentMethodTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> scope( """ @ForceInline public static #type constrain_#name(#type v) { """, switch(type.name()) { // These currently have no type ranges / bits. // Booleans do have an int-range, but restricting it would just make it constant, which // is not very useful: we would like to keep it variable here. We already mix in variable // arguments and constants in the testTemplate. case "boolean", "float", "double", "Float16" -> "return v;\n"; case "byte", "short", "char", "int", "long" -> List.of( // Sometimes constrain the signed range // v = min(max(v, CON1), CON2) (RANDOM.nextInt(2) == 0) ? List.of("v = (#type)Math.min(Math.max(v, ", type.con(),"), ", type.con() ,");\n") : List.of(), // Sometimes constrain the bits: // v = (v & CON1) | CON2 // Note: // and (&): forces some bits to zero // or (|): forces some bits to one (RANDOM.nextInt(2) == 0) ? List.of("v = (#type)((v & ", type.con(),") | ", type.con() ,");\n") : List.of(), // FUTURE: we could also constrain the unsigned bounds. "return v;\n"); default -> throw new RuntimeException("type not supported yet: " + type.name()); }, """ } """ )); var constrainArgumentTemplate = Template.make("name", (String name) -> scope( """ #name = constrain_#name(#name); """ )); // The template that generates the whole test machinery needed for testing a given expression. // Generates: // - @Test method: generate arguments and call compiled and reference test with it. // result verification (only if the result is known to be deterministic). // // - instantiate compiled and reference test methods. // - instantiate argument constraint methods (constrains test method arguments types). // - instantiate checksum method (summarizes value and bounds/bit checks). var testTemplate = Template.make("expression", (Expression expression) -> { // Fix the arguments for both the compiled and reference method. // We have a mix of variable and constant inputs to the expression. // The variable inputs are passed as method arguments to the test methods. List methodArguments = new ArrayList<>(); List expressionArguments = new ArrayList<>(); for (CodeGenerationDataNameType type : expression.argumentTypes) { switch (RANDOM.nextInt(2)) { case 0 -> { String name = $("arg" + methodArguments.size()); methodArguments.add(new MethodArgument(name, type)); expressionArguments.add(name); } default -> { expressionArguments.add(type.con()); } } } return scope( let("methodArguments", methodArguments.stream().map(ma -> ma.name).collect(Collectors.joining(", "))), let("methodArgumentsWithTypes", methodArguments.stream().map(ma -> ma.type + " " + ma.name).collect(Collectors.joining(", "))), """ @Test public static void $primitiveConTest() { // In each iteration, generate new random values for the method arguments. """, methodArguments.stream().map(ma -> valueTemplate.asToken(ma.name, ma.type)).toList(), """ Object v0 = ${primitiveConTest}_compiled(#methodArguments); Object v1 = ${primitiveConTest}_reference(#methodArguments); """, expression.info.isResultDeterministic ? "Verify.checkEQ(v0, v1);\n" : "// could fail - don't verify.\n", """ } @DontInline public static Object ${primitiveConTest}_compiled(#methodArgumentsWithTypes) { """, // The arguments now have the bottom_type. Constrain the ranges and bits. methodArguments.stream().map(ma -> constrainArgumentTemplate.asToken(ma.name)).toList(), // Generate the body with the expression, and calling the checksum. bodyTemplate.asToken(expression, expressionArguments, $("checksum")), """ } @DontCompile public static Object ${primitiveConTest}_reference(#methodArgumentsWithTypes) { """, methodArguments.stream().map(ma -> constrainArgumentTemplate.asToken(ma.name)).toList(), bodyTemplate.asToken(expression, expressionArguments, $("checksum")), """ } """, methodArguments.stream().map(ma -> constrainArgumentMethodTemplate.asToken(ma.name, ma.type)).toList(), checksumTemplate.asToken(expression, $("checksum")) ); }); // Generate expressions with the primitive types. for (PrimitiveType type : PRIMITIVE_TYPES) { // Prmitive expressions are most important, so let's create many expressions per output type. for (int i = 0; i < 10; i++) { // The depth determines roughly how many operations are going to be used in the expression. int depth = RANDOM.nextInt(1, 20); Expression expression = Expression.nestRandomly(type, Operations.PRIMITIVE_OPERATIONS, depth, Nesting.EXACT); tests.add(testTemplate.asToken(expression)); } } // Generate expressions with any scalar numeric types. for (CodeGenerationDataNameType type : SCALAR_NUMERIC_TYPES) { // The extended set of scalar numeric expressions (incl. special types such as Float16) are relevant // but don't currently warrant the same number amount of testing time, so we only create 2 cases // per type. Note: this still produces a lot of expressions, given that we have a lot of output // types, and even if the output type is "float", we can still use other types in the expression, // such as "float -> Float16 -> float". We can consider adjusting this arbitrary count in the future. for (int i = 0; i < 2; i++) { // The depth determines roughly how many operations are going to be used in the expression. int depth = RANDOM.nextInt(1, 20); Expression expression = Expression.nestRandomly(type, Operations.SCALAR_NUMERIC_OPERATIONS, depth, Nesting.EXACT); tests.add(testTemplate.asToken(expression)); } } // Create the test class, which runs all tests. return TestFrameworkClass.render( // package and class name. "compiler.igvn.templated", "ExpressionFuzzerInnerTest", // Set of imports. Set.of("compiler.lib.verify.*", "java.util.Random", "jdk.test.lib.Utils", "compiler.lib.generators.*", "jdk.incubator.vector.Float16"), // classpath, so the Test VM has access to the compiled class files. comp.getEscapedClassPathOfCompiledClasses(), // The list of tests. tests); } }