diff --git a/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java b/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java new file mode 100644 index 0000000000000..60b11e8ffbc44 --- /dev/null +++ b/test/hotspot/jtreg/compiler/igvn/ExpressionFuzzer.java @@ -0,0 +1,349 @@ +/* + * 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 + * @summary Use the template framework library to generate random expressions. + * @modules java.base/jdk.internal.misc + * @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.body; +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.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; + +// 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(); + + // compiler.igvn.templated.InnterTest.main(new String[] {}); + comp.invoke("compiler.igvn.templated.ExpressionFuzzerInnerTest", "main", new Object[] {new String[] {}}); + } + + // 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) -> body( + """ + 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 body( + ", ", 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) -> body( + 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" -> "val"; + // Check if the boolean constant folded: + case "boolean" -> "val, val == true, val == false"; + default -> throw new RuntimeException("should only be primitive types"); + } + , "};\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) -> body( + "#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) -> body( + """ + @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" -> "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("should only be primitive types"); + }, + """ + } + """ + )); + + var constrainArgumentTemplate = Template.make("name", (String name) -> body( + """ + #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 body( + 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) { + 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); + 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.*"), + // classpath, so the Test VM has access to the compiled class files. + comp.getEscapedClassPathOfCompiledClasses(), + // The list of tests. + tests); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java new file mode 100644 index 0000000000000..360937c8f7fc8 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/Expression.java @@ -0,0 +1,474 @@ +/* + * 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. + */ + +package compiler.lib.template_framework.library; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.Random; +import jdk.test.lib.Utils; +import java.util.stream.Stream; +import java.util.stream.Collectors; + +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.TemplateToken; +import static compiler.lib.template_framework.Template.body; + +/** + * {@link Expression}s model Java expressions, that have a list of arguments with specified + * argument types, and a result with a specified result type. Once can {@link #make} a new + * {@link Expression} or use existing ones from {@link Operations}. + * + *

+ * The {@link Expression}s are composable, they can be explicitly {@link #nest}ed, or randomly + * combined using {@link #nestRandomly}. + * + *

+ * Finally, they can be used in a {@link Template} as a {@link TemplateToken} by calling + * {@link #asToken} with the required arguments. + */ +public class Expression { + private static final Random RANDOM = Utils.getRandomInstance(); + + /** + * Specifies the return type of the {@link Expression}. + */ + public final CodeGenerationDataNameType returnType; + + + /** + * Specifies the types of the arguments. + */ + public final List argumentTypes; + + final List strings; + + /** + * Provides additional information about the {@link Expression}. + */ + public final Info info; + + private Expression(CodeGenerationDataNameType returnType, + List argumentTypes, + List strings, + Info info) { + if (argumentTypes.size() + 1 != strings.size()) { + throw new RuntimeException("Must have one more string than argument."); + } + this.returnType = returnType; + this.argumentTypes = List.copyOf(argumentTypes); + this.strings = List.copyOf(strings); + this.info = info; + } + + + /** + * Specifies additional information for an {@link Expression}. + */ + public static class Info { + /** + * Set of exceptions the {@link Exception} could throw when executed. + * By default, we assume that an {@link Expression} throws no exceptions. + */ + public final Set exceptions; + + /** + * Specifies if the result of the {@link Expression} is guaranteed to + * be deterministic. This allows exact result verification, for example + * by comparing compiler and interpreter results. However, there are some + * operations that do not always return the same exact result, which can + * for example happen with {@code Float.floatToRawIntBits} in combination + * with more than one {@code NaN} bit representations. + * By default, we assume that an {@link Expression} is deterministic. + */ + public final boolean isResultDeterministic; + + /** + * Create a default {@link Info}. + */ + public Info() { + this.exceptions = Set.of(); + this.isResultDeterministic = true; + } + + private Info(Set exceptions, boolean isResultDeterministic) { + this.exceptions = Set.copyOf(exceptions); + this.isResultDeterministic = isResultDeterministic; + } + + /** + * Creates a new {@link Info} with additional exceptions that the {@link Expression} could throw. + * + * @param exceptions the exceptions to be added. + * @return a new {@link Info} instance with the added exceptions. + */ + public Info withExceptions(Set exceptions) { + exceptions = Stream.concat(this.exceptions.stream(), exceptions.stream()) + .collect(Collectors.toSet()); + return new Info(exceptions, this.isResultDeterministic); + } + + /** + * Creates a new {@link Info} that specifies that the {@link Exception} may return + * indeterministic results, which prevents exact result verification. + * + * @return a new {@link Info} instance that specifies indeterministic results. + */ + public Info withNondeterministicResult() { + return new Info(this.exceptions, false); + } + + Info combineWith(Info other) { + Info info = this.withExceptions(other.exceptions); + if (!other.isResultDeterministic) { + info = info.withNondeterministicResult(); + } + return info; + } + } + + /** + * Creates a new Expression with 1 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The last string, finishing the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1) { + return make(returnType, s0, t0, s1, new Info()); + } + + /** + * Creates a new Expression with 1 argument. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The last string, finishing the {@link Expression}. + * @param info Additional information about the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + Info info) { + return new Expression(returnType, List.of(t0), List.of(s0, s1), info); + } + + /** + * Creates a new Expression with 2 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The second string, to be placed before {@code t1}. + * @param t1 The type of the second argument. + * @param s2 The last string, finishing the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + CodeGenerationDataNameType t1, + String s2) { + return make(returnType, s0, t0, s1, t1, s2, new Info()); + } + + /** + * Creates a new Expression with 2 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The second string, to be placed before {@code t1}. + * @param t1 The type of the second argument. + * @param s2 The last string, finishing the {@link Expression}. + * @param info Additional information about the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + CodeGenerationDataNameType t1, + String s2, + Info info) { + return new Expression(returnType, List.of(t0, t1), List.of(s0, s1, s2), info); + } + + /** + * Creates a new Expression with 3 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The second string, to be placed before {@code t1}. + * @param t1 The type of the second argument. + * @param s2 The third string, to be placed before {@code t2}. + * @param t2 The type of the third argument. + * @param s3 The last string, finishing the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + CodeGenerationDataNameType t1, + String s2, + CodeGenerationDataNameType t2, + String s3) { + return make(returnType, s0, t0, s1, t1, s2, t2, s3, new Info()); + } + + /** + * Creates a new Expression with 3 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The second string, to be placed before {@code t1}. + * @param t1 The type of the second argument. + * @param s2 The third string, to be placed before {@code t2}. + * @param t2 The type of the third argument. + * @param s3 The last string, finishing the {@link Expression}. + * @param info Additional information about the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + CodeGenerationDataNameType t1, + String s2, + CodeGenerationDataNameType t2, + String s3, + Info info) { + return new Expression(returnType, List.of(t0, t1, t2), List.of(s0, s1, s2, s3), info); + } + + /** + * Creates a new Expression with 4 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The second string, to be placed before {@code t1}. + * @param t1 The type of the second argument. + * @param s2 The third string, to be placed before {@code t2}. + * @param t2 The type of the third argument. + * @param s3 The fourth string, to be placed before {@code t3}. + * @param t3 The type of the fourth argument. + * @param s4 The last string, finishing the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + CodeGenerationDataNameType t1, + String s2, + CodeGenerationDataNameType t2, + String s3, + CodeGenerationDataNameType t3, + String s4) { + return make(returnType, s0, t0, s1, t1, s2, t2, s3, t3, s4, new Info()); + } + + /** + * Creates a new Expression with 4 arguments. + * + * @param returnType The return type of the {@link Expression}. + * @param s0 The first string, to be placed before {@code t0}. + * @param t0 The type of the first argument. + * @param s1 The second string, to be placed before {@code t1}. + * @param t1 The type of the second argument. + * @param s2 The third string, to be placed before {@code t2}. + * @param t2 The type of the third argument. + * @param s3 The fourth string, to be placed before {@code t3}. + * @param t3 The type of the fourth argument. + * @param s4 The last string, finishing the {@link Expression}. + * @param info Additional information about the {@link Expression}. + * @return the new {@link Expression}. + */ + public static Expression make(CodeGenerationDataNameType returnType, + String s0, + CodeGenerationDataNameType t0, + String s1, + CodeGenerationDataNameType t1, + String s2, + CodeGenerationDataNameType t2, + String s3, + CodeGenerationDataNameType t3, + String s4, + Info info) { + return new Expression(returnType, List.of(t0, t1, t2, t3), List.of(s0, s1, s2, s3, s4), info); + } + + /** + * Creates a {@link TemplateToken} for the use in a {@link Template} by applying the + * {@code arguments} to the {@link Expression}. It is the users responsibility to + * ensure that the argument tokens match the required {@link #argumentTypes}. + * + * @param arguments the tokens to be passed as arguments into the {@link Expression}. + * @return a {@link TemplateToken} representing the {@link Expression} with applied arguments, + * for the use in a {@link Template}. + */ + public TemplateToken asToken(List arguments) { + if (arguments.size() != argumentTypes.size()) { + throw new IllegalArgumentException("Wrong number of arguments:" + + " expected: " + argumentTypes.size() + + " but got: " + arguments.size() + + " for " + this); + } + + // List of tokens: interleave strings and arguments. + List tokens = new ArrayList<>(); + for (int i = 0; i < argumentTypes.size(); i++) { + tokens.add(strings.get(i)); + tokens.add(arguments.get(i)); + } + tokens.add(strings.getLast()); + + var template = Template.make(() -> body( + tokens + )); + return template.asToken(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("Expression["); + + for (int i = 0; i < this.argumentTypes.size(); i++) { + sb.append("\""); + sb.append(this.strings.get(i)); + sb.append("\", "); + sb.append(this.argumentTypes.get(i).toString()); + sb.append(", "); + } + sb.append("\""); + sb.append(this.strings.getLast()); + sb.append("\"]"); + return sb.toString(); + } + + /** + * Create a nested {@link Expression} with a specified {@code returnType} from a + * set of {@code expressions}. + * + * @param returnType the type of the return value. + * @param expressions the list of {@link Expression}s from which we sample to create + * the nested {@link Expression}. + * @param maxNumberOfUsedExpressions the maximal number of {@link Expression}s from the + * {@code expressions} are nested. + * @return a new randomly nested {@link Expression}. + */ + public static Expression nestRandomly(CodeGenerationDataNameType returnType, + List expressions, + int maxNumberOfUsedExpressions) { + List filtered = expressions.stream().filter(e -> e.returnType.isSubtypeOf(returnType)).toList(); + + if (filtered.isEmpty()) { + throw new IllegalArgumentException("Found no exception with the specified returnType."); + } + + int r = RANDOM.nextInt(filtered.size()); + Expression expression = filtered.get(r); + + for (int i = 1; i < maxNumberOfUsedExpressions; i++) { + expression = expression.nestRandomly(expressions); + } + return expression; + } + + /** + * Nests a random {@link Expression} from {@code nestingExpressions} into a random argument of + * {@code this} {@link Expression}, ensuring compatibility of argument and return type. + * + * @param nestingExpressions list of expressions we sample from for the inner {@link Expression}. + * @return a new nested {@link Expression}. + */ + public Expression nestRandomly(List nestingExpressions) { + int argumentIndex = RANDOM.nextInt(this.argumentTypes.size()); + CodeGenerationDataNameType argumentType = this.argumentTypes.get(argumentIndex); + List filtered = nestingExpressions.stream().filter(e -> e.returnType.isSubtypeOf(argumentType)).toList(); + + if (filtered.isEmpty()) { + // Found no expression that has a matching returnType. + return this; + } + + int r = RANDOM.nextInt(filtered.size()); + Expression expression = filtered.get(r); + + return this.nest(argumentIndex, expression); + } + + /** + * Nests the {@code nestingExpression} into the specified {@code argumentIndex} of + * {@code this} {@link Expression}. + * + * @param argumentIndex the index specifying at which argument of {@code this} + * {@link Expression} we inser the {@code nestingExpression}. + * @param nestingExpression the inner {@link Expression}. + * @return a new nested {@link Expression}. + */ + public Expression nest(int argumentIndex, Expression nestingExpression) { + if (!nestingExpression.returnType.isSubtypeOf(this.argumentTypes.get(argumentIndex))) { + throw new IllegalArgumentException("Cannot nest expressions because of mismatched types."); + } + + List newArgumentTypes = new ArrayList<>(); + List newStrings = new ArrayList<>(); + // s0 t0 s1 [S0 T0 S1 T1 S2] s2 t2 s3 + for (int i = 0; i < argumentIndex; i++) { + newStrings.add(this.strings.get(i)); + newArgumentTypes.add(this.argumentTypes.get(i)); + } + newStrings.add(this.strings.get(argumentIndex) + + nestingExpression.strings.getFirst()); // concat s1 and S0 + newArgumentTypes.add(nestingExpression.argumentTypes.getFirst()); + for (int i = 1; i < nestingExpression.argumentTypes.size(); i++) { + newStrings.add(nestingExpression.strings.get(i)); + newArgumentTypes.add(nestingExpression.argumentTypes.get(i)); + } + newStrings.add(nestingExpression.strings.getLast() + + this.strings.get(argumentIndex + 1)); // concat S2 and s2 + for (int i = argumentIndex+1; i < this.argumentTypes.size(); i++) { + newArgumentTypes.add(this.argumentTypes.get(i)); + newStrings.add(this.strings.get(i+1)); + } + + return new Expression(this.returnType, newArgumentTypes, newStrings, this.info.combineWith(nestingExpression.info)); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/Operations.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/Operations.java new file mode 100644 index 0000000000000..53acf943b20d6 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/Operations.java @@ -0,0 +1,253 @@ +/* + * 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. + */ + +package compiler.lib.template_framework.library; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; + +import static compiler.lib.template_framework.library.PrimitiveType.BYTES; +import static compiler.lib.template_framework.library.PrimitiveType.SHORTS; +import static compiler.lib.template_framework.library.PrimitiveType.CHARS; +import static compiler.lib.template_framework.library.PrimitiveType.INTS; +import static compiler.lib.template_framework.library.PrimitiveType.LONGS; +import static compiler.lib.template_framework.library.PrimitiveType.FLOATS; +import static compiler.lib.template_framework.library.PrimitiveType.DOUBLES; +import static compiler.lib.template_framework.library.PrimitiveType.BOOLEANS; + +/** + * This class provides various lists of {@link Expression}s, that represent Java operators or library + * methods. For example, we represent arithmetic operations on primitive types. + */ +public final class Operations { + + // private constructor to avoid instantiation. + private Operations() {} + + /** + * Provides a lits of operations on {@link PrimitiveType}s, such as arithmetic, logical, + * and cast operations. + */ + public static final List PRIMITIVE_OPERATIONS = generatePrimitiveOperations(); + + private static List generatePrimitiveOperations() { + List ops = new ArrayList<>(); + + Expression.Info withArithmeticException = new Expression.Info().withExceptions(Set.of("ArithmeticException")); + Expression.Info withNondeterministicResult = new Expression.Info().withNondeterministicResult(); + + // Cast between all primitive types. Except for Boolean, we cannot cast from and to. + CodeGenerationDataNameType.INTEGRAL_AND_FLOATING_TYPES.stream().forEach(src -> { + CodeGenerationDataNameType.INTEGRAL_AND_FLOATING_TYPES.stream().forEach(dst -> { + ops.add(Expression.make(dst, "(" + dst.name() + ")(", src, ")")); + }); + }); + + // Ternary operator. + CodeGenerationDataNameType.INTEGRAL_AND_FLOATING_TYPES.stream().forEach(type -> { + ops.add(Expression.make(type, "(", BOOLEANS, "?", type, ":", type, ")")); + }); + + List.of(INTS, LONGS).stream().forEach(type -> { + // Arithmetic operators + ops.add(Expression.make(type, "(-(", type, "))")); + ops.add(Expression.make(type, "(", type, " + ", type, ")")); + ops.add(Expression.make(type, "(", type, " - ", type, ")")); + ops.add(Expression.make(type, "(", type, " * ", type, ")")); + ops.add(Expression.make(type, "(", type, " / ", type, ")", withArithmeticException)); + ops.add(Expression.make(type, "(", type, " % ", type, ")", withArithmeticException)); + + // Bitwise Operators (non short-circuit) + ops.add(Expression.make(type, "(~(", type, "))")); + ops.add(Expression.make(type, "(", type, " & ", type, ")")); + ops.add(Expression.make(type, "(", type, " | ", type, ")")); + ops.add(Expression.make(type, "(", type, " ^ ", type, ")")); + ops.add(Expression.make(type, "(", type, " << ", type, ")")); + ops.add(Expression.make(type, "(", type, " >> ", type, ")")); + ops.add(Expression.make(type, "(", type, " >>> ", type, ")")); + + // Relational / Comparison Operators + ops.add(Expression.make(BOOLEANS, "(", type, " == ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " != ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " > ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " < ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " >= ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " <= ", type, ")")); + }); + + CodeGenerationDataNameType.FLOATING_TYPES.stream().forEach(type -> { + // Arithmetic operators + ops.add(Expression.make(type, "(-(", type, "))")); + ops.add(Expression.make(type, "(", type, " + ", type, ")")); + ops.add(Expression.make(type, "(", type, " - ", type, ")")); + ops.add(Expression.make(type, "(", type, " * ", type, ")")); + ops.add(Expression.make(type, "(", type, " / ", type, ")")); + ops.add(Expression.make(type, "(", type, " % ", type, ")")); + + // Relational / Comparison Operators + ops.add(Expression.make(BOOLEANS, "(", type, " == ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " != ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " > ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " < ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " >= ", type, ")")); + ops.add(Expression.make(BOOLEANS, "(", type, " <= ", type, ")")); + }); + + // ------------ byte ------------- + // Cast and ternary operator handled above. + // Arithmetic operations are not performed in byte, but rather promoted to int. + + // ------------ Byte ------------- + ops.add(Expression.make(INTS, "Byte.compare(", BYTES, ", ", BYTES, ")")); + ops.add(Expression.make(INTS, "Byte.compareUnsigned(", BYTES, ", ", BYTES, ")")); + ops.add(Expression.make(INTS, "Byte.toUnsignedInt(", BYTES, ")")); + ops.add(Expression.make(LONGS, "Byte.toUnsignedLong(", BYTES, ")")); + + // ------------ char ------------- + // Cast and ternary operator handled above. + // Arithmetic operations are not performned in char, but rather promoted to int. + + // ------------ Character ------------- + ops.add(Expression.make(INTS, "Character.compare(", CHARS, ", ", CHARS, ")")); + ops.add(Expression.make(CHARS, "Character.reverseBytes(", CHARS, ")")); + + // ------------ short ------------- + // Cast and ternary operator handled above. + // Arithmetic operations are not performned in short, but rather promoted to int. + + // ------------ Short ------------- + ops.add(Expression.make(INTS, "Short.compare(", SHORTS, ", ", SHORTS, ")")); + ops.add(Expression.make(INTS, "Short.compareUnsigned(", SHORTS, ", ", SHORTS, ")")); + ops.add(Expression.make(SHORTS, "Short.reverseBytes(", SHORTS, ")")); + ops.add(Expression.make(INTS, "Short.toUnsignedInt(", SHORTS, ")")); + ops.add(Expression.make(LONGS, "Short.toUnsignedLong(", SHORTS, ")")); + + // ------------ int ------------- + // Cast and ternary operator handled above. + // Arithmetic, Bitwise, Relational / Comparison handled above. + + // ------------ Integer ------------- + ops.add(Expression.make(INTS, "Integer.bitCount(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.compare(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.compareUnsigned(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.compress(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.divideUnsigned(", INTS, ", ", INTS, ")", withArithmeticException)); + ops.add(Expression.make(INTS, "Integer.expand(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.highestOneBit(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.lowestOneBit(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.max(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.min(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.numberOfLeadingZeros(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.numberOfTrailingZeros(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.remainderUnsigned(", INTS, ", ", INTS, ")", withArithmeticException)); + ops.add(Expression.make(INTS, "Integer.reverse(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.reverseBytes(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.rotateLeft(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.rotateRight(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.signum(", INTS, ")")); + ops.add(Expression.make(INTS, "Integer.sum(", INTS, ", ", INTS, ")")); + ops.add(Expression.make(LONGS, "Integer.toUnsignedLong(", INTS, ")")); + + // ------------ long ------------- + // Cast and ternary operator handled above. + // Arithmetic, Bitwise, Relational / Comparison handled above. + + // ------------ Long ------------- + ops.add(Expression.make(INTS, "Long.bitCount(", LONGS, ")")); + ops.add(Expression.make(INTS, "Long.compare(", LONGS, ", ", LONGS, ")")); + ops.add(Expression.make(INTS, "Long.compareUnsigned(", LONGS, ", ", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.compress(", LONGS, ", ", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.divideUnsigned(", LONGS, ", ", LONGS, ")", withArithmeticException)); + ops.add(Expression.make(LONGS, "Long.expand(", LONGS, ", ", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.highestOneBit(", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.lowestOneBit(", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.max(", LONGS, ", ", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.min(", LONGS, ", ", LONGS, ")")); + ops.add(Expression.make(INTS, "Long.numberOfLeadingZeros(", LONGS, ")")); + ops.add(Expression.make(INTS, "Long.numberOfTrailingZeros(", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.remainderUnsigned(", LONGS, ", ", LONGS, ")", withArithmeticException)); + ops.add(Expression.make(LONGS, "Long.reverse(", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.reverseBytes(", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.rotateLeft(", LONGS, ", ", INTS, ")")); + ops.add(Expression.make(LONGS, "Long.rotateRight(", LONGS, ", ", INTS, ")")); + ops.add(Expression.make(INTS, "Long.signum(", LONGS, ")")); + ops.add(Expression.make(LONGS, "Long.sum(", LONGS, ", ", LONGS, ")")); + + // ------------ float ------------- + // Cast and ternary operator handled above. + // Arithmetic and Relational / Comparison handled above. + + // ------------ Float ------------- + ops.add(Expression.make(INTS, "Float.compare(", FLOATS, ", ", FLOATS, ")")); + ops.add(Expression.make(INTS, "Float.floatToIntBits(", FLOATS, ")")); + ops.add(Expression.make(INTS, "Float.floatToRawIntBits(", FLOATS, ")", withNondeterministicResult)); + // Note: there are multiple NaN values with different bit representations. + ops.add(Expression.make(FLOATS, "Float.float16ToFloat(", SHORTS, ")")); + ops.add(Expression.make(FLOATS, "Float.intBitsToFloat(", INTS, ")")); + ops.add(Expression.make(BOOLEANS, "Float.isFinite(", FLOATS, ")")); + ops.add(Expression.make(BOOLEANS, "Float.isInfinite(", FLOATS, ")")); + ops.add(Expression.make(BOOLEANS, "Float.isNaN(", FLOATS, ")")); + ops.add(Expression.make(FLOATS, "Float.max(", FLOATS, ", ", FLOATS, ")")); + ops.add(Expression.make(FLOATS, "Float.min(", FLOATS, ", ", FLOATS, ")")); + ops.add(Expression.make(FLOATS, "Float.sum(", FLOATS, ", ", FLOATS, ")")); + + // ------------ double ------------- + // Cast and ternary operator handled above. + // Arithmetic and Relational / Comparison handled above. + + // ------------ Double ------------- + ops.add(Expression.make(INTS, "Double.compare(", DOUBLES, ", ", DOUBLES, ")")); + ops.add(Expression.make(LONGS, "Double.doubleToLongBits(", DOUBLES, ")")); + // Note: there are multiple NaN values with different bit representations. + ops.add(Expression.make(LONGS, "Double.doubleToRawLongBits(", DOUBLES, ")", withNondeterministicResult)); + ops.add(Expression.make(DOUBLES, "Double.longBitsToDouble(", LONGS, ")")); + ops.add(Expression.make(BOOLEANS, "Double.isFinite(", DOUBLES, ")")); + ops.add(Expression.make(BOOLEANS, "Double.isInfinite(", DOUBLES, ")")); + ops.add(Expression.make(BOOLEANS, "Double.isNaN(", DOUBLES, ")")); + ops.add(Expression.make(DOUBLES, "Double.max(", DOUBLES, ", ", DOUBLES, ")")); + ops.add(Expression.make(DOUBLES, "Double.min(", DOUBLES, ", ", DOUBLES, ")")); + ops.add(Expression.make(DOUBLES, "Double.sum(", DOUBLES, ", ", DOUBLES, ")")); + + // ------------ boolean ------------- + // Cast and ternary operator handled above. + // There are no boolean arithmetic operators + + // Logical operators + ops.add(Expression.make(BOOLEANS, "(!(", BOOLEANS, "))")); + ops.add(Expression.make(BOOLEANS, "(", BOOLEANS, " || ", BOOLEANS, ")")); + ops.add(Expression.make(BOOLEANS, "(", BOOLEANS, " && ", BOOLEANS, ")")); + ops.add(Expression.make(BOOLEANS, "(", BOOLEANS, " ^ ", BOOLEANS, ")")); + + // ------------ Boolean ------------- + ops.add(Expression.make(INTS, "Boolean.compare(", BOOLEANS, ", ", BOOLEANS, ")")); + ops.add(Expression.make(BOOLEANS, "Boolean.logicalAnd(", BOOLEANS, ", ", BOOLEANS, ")")); + ops.add(Expression.make(BOOLEANS, "Boolean.logicalOr(", BOOLEANS, ", ", BOOLEANS, ")")); + ops.add(Expression.make(BOOLEANS, "Boolean.logicalXor(", BOOLEANS, ", ", BOOLEANS, ")")); + + // TODO: Math and other classes. + + // Make sure the list is not modifiable. + return List.copyOf(ops); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java index 3bf6c7f62886e..46a9d5bbabe67 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/PrimitiveType.java @@ -31,6 +31,9 @@ import compiler.lib.generators.RestrictableGenerator; import compiler.lib.template_framework.DataName; +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.TemplateToken; +import static compiler.lib.template_framework.Template.body; /** * The {@link PrimitiveType} models Java's primitive types, and provides a set @@ -148,4 +151,91 @@ public boolean isFloating() { case FLOAT, DOUBLE -> true; }; } + + /** + * Calls the corresponding pseudo random number generator from + * {@link #generateLibraryRNG}, for the given type. Accordingly, + * one must generate {@link #generateLibraryRNG} into the same + * test if one wants to use this method. + * + * Note: if you simply need a compile time constant, then please + * use {@link #con} instead. + * + * @return the token representing the method call to obtain a + * random value for the given type at runtime. + */ + public Object callLibraryRNG() { + return switch (kind) { + case BYTE -> "LibraryRNG.nextByte()"; + case SHORT -> "LibraryRNG.nextShort()"; + case CHAR -> "LibraryRNG.nextChar()"; + case INT -> "LibraryRNG.nextInt()"; + case LONG -> "LibraryRNG.nextLong()"; + case FLOAT -> "LibraryRNG.nextFloat()"; + case DOUBLE -> "LibraryRNG.nextDouble()"; + case BOOLEAN -> "LibraryRNG.nextBoolean()"; + }; + } + + /** + * Generates the {@code LibraryRNG} class, which makes a set of pseudo + * random number generators available, wrapping {@link Generators}. This + * is supposed to be used in tandem with {@link #callLibraryRNG}. + * + * Note: you must ensure that all required imports are performed: + * {@code java.util.Random} + * {@code jdk.test.lib.Utils} + * {@code compiler.lib.generators.*} + * + * @return a TemplateToken that holds all the {@code LibraryRNG} class. + */ + public static TemplateToken generateLibraryRNG() { + var template = Template.make(() -> body( + """ + public static class LibraryRNG { + private static final Random RANDOM = Utils.getRandomInstance(); + private static final RestrictableGenerator GEN_BYTE = Generators.G.safeRestrict(Generators.G.ints(), Byte.MIN_VALUE, Byte.MAX_VALUE); + private static final RestrictableGenerator GEN_CHAR = Generators.G.safeRestrict(Generators.G.ints(), Character.MIN_VALUE, Character.MAX_VALUE); + private static final RestrictableGenerator GEN_SHORT = Generators.G.safeRestrict(Generators.G.ints(), Short.MIN_VALUE, Short.MAX_VALUE); + private static final RestrictableGenerator GEN_INT = Generators.G.ints(); + private static final RestrictableGenerator GEN_LONG = Generators.G.longs(); + private static final Generator GEN_DOUBLE = Generators.G.doubles(); + private static final Generator GEN_FLOAT = Generators.G.floats(); + + public static byte nextByte() { + return GEN_BYTE.next().byteValue(); + } + + public static short nextShort() { + return GEN_SHORT.next().shortValue(); + } + + public static char nextChar() { + return (char)GEN_CHAR.next().intValue(); + } + + public static int nextInt() { + return GEN_INT.next(); + } + + public static long nextLong() { + return GEN_LONG.next(); + } + + public static float nextFloat() { + return GEN_FLOAT.next(); + } + + public static double nextDouble() { + return GEN_DOUBLE.next(); + } + + public static boolean nextBoolean() { + return RANDOM.nextBoolean(); + } + } + """ + )); + return template.asToken(); + }; } diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java new file mode 100644 index 0000000000000..6e11a7050540d --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java @@ -0,0 +1,116 @@ +/* + * 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 + * @summary Demonstrate the use of Expressions from the Template Library. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @compile ../../../compiler/lib/verify/Verify.java + * @run main template_framework.examples.TestExpressions + */ + +package template_framework.examples; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.TemplateToken; +import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.let; +import compiler.lib.template_framework.library.Expression; +import compiler.lib.template_framework.library.Operations; +import compiler.lib.template_framework.library.TestFrameworkClass; + +public class TestExpressions { + public static void main(String[] args) { + // Create a new CompileFramework instance. + CompileFramework comp = new CompileFramework(); + + // Add a java source file. + comp.addJavaSourceCode("p.xyz.InnerTest", generate(comp)); + + // Compile the source file. + comp.compile(); + + // p.xyz.InnterTest.main(new String[] {}); + comp.invoke("p.xyz.InnerTest", "main", new Object[] {new String[] {}}); + } + + // Generate a Java source file as String + public static String generate(CompileFramework comp) { + // Generate a list of test methods. + List tests = new ArrayList<>(); + + // Create a test method that executes the expression, with constant arguments. + var withConstantsTemplate = Template.make("expression", (Expression expression) -> { + // Create a token: fill the expression with a fixed set of constants. + // We then use the same token with the same constants, once compiled and once not compiled. + TemplateToken expressionToken = expression.asToken(expression.argumentTypes.stream().map(t -> t.con()).toList()); + return body( + let("returnType", expression.returnType), + """ + @Test + public static void $primitiveConTest() { + #returnType v0 = ${primitiveConTest}_compiled(); + #returnType v1 = ${primitiveConTest}_reference(); + Verify.checkEQ(v0, v1); + } + + @DontInline + public static #returnType ${primitiveConTest}_compiled() { + """, + "return ", expressionToken, ";\n", + """ + } + + @DontCompile + public static #returnType ${primitiveConTest}_reference() { + """, + "return ", expressionToken, ";\n", + """ + } + """ + ); + }); + + for (Expression operation : Operations.PRIMITIVE_OPERATIONS) { + tests.add(withConstantsTemplate.asToken(operation)); + } + + // Create the test class, which runs all tests. + return TestFrameworkClass.render( + // package and class name. + "p.xyz", "InnerTest", + // Set of imports. + Set.of("compiler.lib.verify.*"), + // classpath, so the Test VM has access to the compiled class files. + comp.getEscapedClassPathOfCompiledClasses(), + // The list of tests. + tests); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java index 5cd3f3c2a226b..a04a5771cb49a 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestPrimitiveTypes.java @@ -169,6 +169,48 @@ public static void test_names() { tests.put("test_names", namesTemplate.asToken()); + // Test runtime random value generation with LibraryRNG + // Runtime random number generation of a given primitive type can be very helpful + // when writing tests that require random inputs. + var libraryRNGWithTypeTemplate = Template.make("type", (PrimitiveType type) -> body( + """ + { + // Fill an array with 1_000 random values. Every type has at least 2 values, + // so the chance that all values are the same is 2^-1_000 < 10^-300. This should + // never happen, even with a relatively weak PRNG. + #type[] a = new #type[1_000]; + for (int i = 0; i < a.length; i++) { + """, + " a[i] = ", type.callLibraryRNG(), ";\n", + """ + } + boolean allSame = true; + for (int i = 0; i < a.length; i++) { + if (a[i] != a[0]) { + allSame = false; + break; + } + } + if (allSame) { throw new RuntimeException("all values were the same for #type"); } + } + """ + )); + + var libraryRNGTemplate = Template.make(() -> body( + // Make sure we instantiate the LibraryRNG class. + PrimitiveType.generateLibraryRNG(), + // Now we can use it inside the test. + """ + public static void test_LibraryRNG() { + """, + CodeGenerationDataNameType.PRIMITIVE_TYPES.stream().map(libraryRNGWithTypeTemplate::asToken).toList(), + """ + } + """ + )); + + tests.put("test_LibraryRNG", libraryRNGTemplate.asToken()); + // Finally, put all the tests together in a class, and invoke all // tests from the main method. var template = Template.make(() -> body( @@ -178,6 +220,11 @@ public static void test_names() { import compiler.lib.verify.*; import java.lang.foreign.MemorySegment; + // Imports for LibraryRNG + import java.util.Random; + import jdk.test.lib.Utils; + import compiler.lib.generators.*; + public class InnerTest { public static void main() { """, diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java new file mode 100644 index 0000000000000..2dac740dd936a --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestExpression.java @@ -0,0 +1,282 @@ +/* + * 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 + * @summary Test template generation with Expressions. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run main template_framework.tests.TestExpression + */ + +package template_framework.tests; + +import java.util.List; +import java.util.Set; + +import compiler.lib.template_framework.DataName; +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.TemplateToken; +import static compiler.lib.template_framework.Template.body; +import compiler.lib.template_framework.library.CodeGenerationDataNameType; +import compiler.lib.template_framework.library.Expression; + +/** + * This tests the use of the {@link Expression} from the template library. This is + * not a tutorial about how to use Expressions, rather we produce deterministic + * output to be able to compare the generated strings to expected strings. + * + * If you are interested in how to use {@link Expression}s, see {@code examples/TestExpressions.java}. + */ +public class TestExpression { + // Interface for failing tests. + interface FailingTest { + void run(); + } + + // We define our own types, so that we can check if subtyping works right. + public record MyType(String name) implements CodeGenerationDataNameType { + @Override + public Object con() { + return "<" + name() + ">"; + } + + @Override + public boolean isSubtypeOf(DataName.Type other) { + return other instanceof MyType(String n) && name().startsWith(n); + } + + @Override + public String toString() { return name(); } + } + private static final MyType myTypeA = new MyType("MyTypeA"); + private static final MyType myTypeA1 = new MyType("MyTypeA1"); + private static final MyType myTypeB = new MyType("MyTypeB"); + + public static void main(String[] args) { + // The following tests all pass, i.e. have no errors during rendering. + testAsToken(); + testNest(); + testNestRandomly(); + testInfo(); + + // The following tests should all fail, with an expected exception and message. + expectIllegalArgumentException(() -> testFailingAsToken1(), "Wrong number of arguments: expected: 2 but got: 1"); + expectIllegalArgumentException(() -> testFailingAsToken2(), "Wrong number of arguments: expected: 2 but got: 3"); + expectIllegalArgumentException(() -> testFailingNest1(), "Cannot nest expressions because of mismatched types."); + } + + public static void testAsToken() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, "]"); + Expression e2 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, "]"); + Expression e3 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, ",", myTypeA1, "]"); + Expression e4 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, ",", myTypeA1, ",", myTypeA, "]"); + + var template = Template.make(() -> body( + "xx", e1.toString(), "yy\n", + "xx", e2.toString(), "yy\n", + "xx", e3.toString(), "yy\n", + "xx", e4.toString(), "yy\n", + "xx", e1.asToken(List.of("a")), "yy\n", + "xx", e2.asToken(List.of("a", "b")), "yy\n", + "xx", e3.asToken(List.of("a", "b", "c")), "yy\n", + "xx", e4.asToken(List.of("a", "b", "c", "d")), "yy\n" + )); + + String expected = + """ + xxExpression["[", MyTypeA, "]"]yy + xxExpression["[", MyTypeA, ",", MyTypeB, "]"]yy + xxExpression["[", MyTypeA, ",", MyTypeB, ",", MyTypeA1, "]"]yy + xxExpression["[", MyTypeA, ",", MyTypeB, ",", MyTypeA1, ",", MyTypeA, "]"]yy + xx[a]yy + xx[a,b]yy + xx[a,b,c]yy + xx[a,b,c,d]yy + """; + String code = template.render(); + checkEQ(code, expected); + } + + public static void testFailingAsToken1() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, "]"); + e1.asToken(List.of("a")); + } + + public static void testFailingAsToken2() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, "]"); + e1.asToken(List.of("a", "b", "c")); + } + + public static void testNest() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, "]"); + Expression e2 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, "]"); + Expression e3 = Expression.make(myTypeA1, "[", myTypeA, "]"); + Expression e4 = Expression.make(myTypeA, "[", myTypeA, "x", myTypeA, "y", myTypeA, "z", myTypeA, "]"); + Expression e5 = Expression.make(myTypeA, "[", myTypeA, "u", myTypeA, "v", myTypeA, "w", myTypeA, "]"); + + Expression e1e1 = e1.nest(0, e1); + Expression e2e1 = e2.nest(0, e1); + Expression e3e1 = e3.nest(0, e1); + Expression e4e5 = e4.nest(1, e5); + + var template = Template.make(() -> body( + "xx", e1e1.toString(), "yy\n", + "xx", e2e1.toString(), "yy\n", + "xx", e3e1.toString(), "yy\n", + "xx", e4e5.toString(), "yy\n", + "xx", e1e1.asToken(List.of("a")), "yy\n", + "xx", e2e1.asToken(List.of("a", "b")), "yy\n", + "xx", e3e1.asToken(List.of("a")), "yy\n", + "xx", e4e5.asToken(List.of("a", "b", "c", "d", "e", "f", "g")), "yy\n" + )); + + String expected = + """ + xxExpression["[[", MyTypeA, "]]"]yy + xxExpression["[[", MyTypeA, "],", MyTypeB, "]"]yy + xxExpression["[[", MyTypeA, "]]"]yy + xxExpression["[", MyTypeA, "x[", MyTypeA, "u", MyTypeA, "v", MyTypeA, "w", MyTypeA, "]y", MyTypeA, "z", MyTypeA, "]"]yy + xx[[a]]yy + xx[[a],b]yy + xx[[a]]yy + xx[ax[bucvdwe]yfzg]yy + """; + String code = template.render(); + checkEQ(code, expected); + } + + public static void testNestRandomly() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, "]"); + Expression e2 = Expression.make(myTypeA, "(", myTypeA, ")"); + Expression e3 = Expression.make(myTypeB, "{", myTypeA, "}"); + Expression e4 = Expression.make(myTypeA1, "<", myTypeA, ">"); + Expression e5 = Expression.make(myTypeA, "[", myTypeB, "]"); + + Expression e1e2 = e1.nestRandomly(List.of(e2)); + Expression e1ex = e1.nestRandomly(List.of(e3, e2, e3)); + Expression e1e4 = e1.nestRandomly(List.of(e3, e4, e3)); + Expression e1ey = e1.nestRandomly(List.of(e3, e3)); + + // 5-deep nesting of e1 + Expression deep1 = Expression.nestRandomly(myTypeA, List.of(e1, e3), 5); + // Alternating pattern + Expression deep2 = Expression.nestRandomly(myTypeA, List.of(e5, e3), 5); + + var template = Template.make(() -> body( + "xx", e1e2.toString(), "yy\n", + "xx", e1ex.toString(), "yy\n", + "xx", e1e4.toString(), "yy\n", + "xx", e1ey.toString(), "yy\n", + "xx", deep1.toString(), "yy\n", + "xx", deep2.toString(), "yy\n", + "xx", e1e2.asToken(List.of("a")), "yy\n", + "xx", e1ex.asToken(List.of("a")), "yy\n", + "xx", e1e4.asToken(List.of("a")), "yy\n", + "xx", e1ey.asToken(List.of("a")), "yy\n", + "xx", deep1.asToken(List.of("a")), "yy\n", + "xx", deep2.asToken(List.of("a")), "yy\n" + )); + + String expected = + """ + xxExpression["[(", MyTypeA, ")]"]yy + xxExpression["[(", MyTypeA, ")]"]yy + xxExpression["[<", MyTypeA, ">]"]yy + xxExpression["[", MyTypeA, "]"]yy + xxExpression["[[[[[", MyTypeA, "]]]]]"]yy + xxExpression["[{[{[", MyTypeB, "]}]}]"]yy + xx[(a)]yy + xx[(a)]yy + xx[]yy + xx[a]yy + xx[[[[[a]]]]]yy + xx[{[{[a]}]}]yy + """; + String code = template.render(); + checkEQ(code, expected); + } + + public static void testFailingNest1() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, "]"); + Expression e2 = Expression.make(myTypeB, "[", myTypeA, "]"); + Expression e1e2 = e1.nest(0, e2); + } + + public static void testInfo() { + Expression e1 = Expression.make(myTypeA, "[", myTypeA, "]"); + Expression e2 = Expression.make(myTypeA, "(", myTypeA, ")"); + Expression e3 = Expression.make(myTypeA, "<", myTypeA, ">", new Expression.Info().withExceptions(Set.of("E1"))); + Expression e4 = Expression.make(myTypeA, "+", myTypeA, "-", new Expression.Info().withExceptions(Set.of("E2"))); + Expression e5 = Expression.make(myTypeA, "x", myTypeA, "y", new Expression.Info().withNondeterministicResult()); + Expression e6 = Expression.make(myTypeA, "u", myTypeA, "v", new Expression.Info().withNondeterministicResult()); + checkInfo(e1, Set.of(), true); + checkInfo(e2, Set.of(), true); + checkInfo(e3, Set.of("E1"), true); + checkInfo(e4, Set.of("E2"), true); + checkInfo(e1.nest(0, e2), Set.of(), true); + checkInfo(e2.nest(0, e1), Set.of(), true); + checkInfo(e1.nest(0, e3), Set.of("E1"), true); + checkInfo(e3.nest(0, e1), Set.of("E1"), true); + checkInfo(e3.nest(0, e4), Set.of("E1", "E2"), true); + checkInfo(e4.nest(0, e3), Set.of("E1", "E2"), true); + checkInfo(e5, Set.of(), false); + checkInfo(e6, Set.of(), false); + checkInfo(e1.nest(0, e5), Set.of(), false); + checkInfo(e5.nest(0, e1), Set.of(), false); + checkInfo(e5.nest(0, e6), Set.of(), false); + checkInfo(e4.nest(0, e3).nest(0, e5), Set.of("E1", "E2"), false); + checkInfo(e5.nest(0, e4).nest(0, e3), Set.of("E1", "E2"), false); + checkInfo(e3.nest(0, e5).nest(0, e4), Set.of("E1", "E2"), false); + } + + public static void checkInfo(Expression e, Set exceptions, boolean isResultDeterministic) { + if (!e.info.exceptions.equals(exceptions) || + e.info.isResultDeterministic != isResultDeterministic) { + throw new RuntimeException("Info not as expected."); + } + } + + public static void checkEQ(String code, String expected) { + if (!code.equals(expected)) { + System.out.println("\"" + code + "\""); + System.out.println("\"" + expected + "\""); + throw new RuntimeException("Template rendering mismatch!"); + } + } + + public static void expectIllegalArgumentException(FailingTest test, String errorPrefix) { + try { + test.run(); + System.out.println("Should have thrown IllegalArgumentException with prefix: " + errorPrefix); + throw new RuntimeException("Should have thrown!"); + } catch(IllegalArgumentException e) { + if (!e.getMessage().startsWith(errorPrefix)) { + System.out.println("Should have thrown with prefix: " + errorPrefix); + System.out.println("got: " + e.getMessage()); + throw new RuntimeException("Prefix mismatch", e); + } + } + } +}