diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/BaseScope.java b/test/hotspot/jtreg/compiler/lib/template_framework/BaseScope.java new file mode 100644 index 0000000000000..47a717df73408 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/BaseScope.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * {link BaseScope} is the outer-most scope, and determines the {@link CodeGeneratorLibrary} which is used. + */ +final class BaseScope extends Scope { + public static final int DEFAULT_FUEL = 50; + private final CodeGeneratorLibrary codeGeneratorLibrary; + + public BaseScope(long fuel, CodeGeneratorLibrary codeGeneratorLibrary) { + super(null, fuel); + this.codeGeneratorLibrary = (codeGeneratorLibrary != null) ? codeGeneratorLibrary + : CodeGeneratorLibrary.standard(); + } + + public BaseScope(CodeGeneratorLibrary codeGeneratorLibrary) { + this(DEFAULT_FUEL, codeGeneratorLibrary); + }; + + @Override + public CodeGeneratorLibrary library() { + return codeGeneratorLibrary; + } + + /** + * Collect all the generated code and return it as a String. + */ + public String toString() { + return stream.toString(); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/ClassScope.java b/test/hotspot/jtreg/compiler/lib/template_framework/ClassScope.java new file mode 100644 index 0000000000000..8b02d39ec4764 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/ClassScope.java @@ -0,0 +1,43 @@ +/* + * 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; + +/** + * The {@link ClassScope} sets the scope of a class body, i.e. after its opening bracked and before its + * closing bracket. This allows dispatching {@link CodeGenerator}s to the top of the class body, for + * example for field declarations, see {@link DispatchScope#dispatch}. In {@link Template}s, a + * {@link ClassScope} is opened with {@code #open(class)} and closed with {@code #close(class)}. + */ +final class ClassScope extends DispatchScope { + + /** + * Create a new {@link MethodScope}. + * + * @param parent Parent scope or null if the new scope is an outermost scope. + * @param fuel Remaining fuel for recursive {@link CodeGenerator} instantiations. + */ + public ClassScope(Scope parent, long fuel) { + super(parent, fuel); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/CodeGenerator.java b/test/hotspot/jtreg/compiler/lib/template_framework/CodeGenerator.java new file mode 100644 index 0000000000000..2bf40d137ac0b --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/CodeGenerator.java @@ -0,0 +1,209 @@ +/* + * 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; + +import java.util.Map; + +/** + * A {@link CodeGenerator} is used to instantiate code into a scope. This instantiated code may be genrated recursively + * i.e. the {@link CodeGenerator} instantiation may recursively instantiate other {@link CodeGenerator}s. + */ +public sealed abstract class CodeGenerator permits Template, ProgrammaticCodeGenerator, SelectorCodeGenerator { + + /** + * Every {@link CodeGenerator} must have an unique name. The name is used to find {@link CodeGenerator}s in the + * {@link CodeGeneratorLibrary}, and also in the {@link Scope} trace printed if the Template Framework encounters + * an error. + */ + public final String name; + + + /** + * The fuel cost indicates the approximate logarithmic execution cost of the code generated by the + * {@link CodeGenerator}. When CodeGenerators are called recursively, the fuel cost is subtracted from the + * remaining fuel of the {@link Scope}. Once the scope runs out of fuel, the recursion is supposed to stop. + */ + public final int fuelCost; + + /** + * Create a new {@link CodeGenerator}. + * + * @param name Name of the generator, can be used for lookup in the + * {@link CodeGeneratorLibrary} if the {@link Template} + * is added to a library. + * @param fuelCost The {@link fuelCost} for the {@link CodeGenerator}. + */ + CodeGenerator(String name, int fuelCost) { + this.name = name; + this.fuelCost = fuelCost; + } + + /** + * Instantiate the CodeGenerator. + * + * @param scope Scope into which the code is generated. + * @param parameters Provides the parameters for the instantiation, as well as a unique ID for identifier + * name generation (e.g. variable of method names). + */ + public abstract void instantiate(Scope scope, Parameters parameters); + + /** + * Helper facility to instantiate CodeGenerators more easily. It creates a unique Parameter set which can + * be further populated with {@link where}. + */ + public final class Instantiator { + private final CodeGenerator codeGenerator; + private final Parameters parameters; + private boolean isUsed; + private CodeGeneratorLibrary library; + + Instantiator(CodeGenerator codeGenerator) { + this.codeGenerator = codeGenerator; + this.parameters = new Parameters(); + this.library = null; + } + + /** + * Add a parameter key-value pair. + * + * @param paramKey The name of the parameter. + * @param paramValue The value to be set. + * @return The Instantiator for chaining. + */ + public Instantiator where(String paramKey, String paramValue) { + parameters.add(paramKey, paramValue); + return this; + } + + /** + * Add all parameter key-value pairs. + * + * @param parameterMap Map containing the key-value pairs. + * @return The Instantiator for chaining. + */ + public Instantiator where(Map parameterMap) { + parameters.add(parameterMap); + return this; + } + + /** + * Set a custom library for the instantiation. + * + * @param library The custom library. + * @return The Instantiator for chaining. + */ + public Instantiator with(CodeGeneratorLibrary library) { + if (this.library != null) { + throw new TemplateFrameworkException("Cannot set custom library twice."); + } + this.library = library; + return this; + } + + /** + * Instantiate the CodeGenerator to a String, using the prepared parameters. + * This is useful for the outer-most instantiation, as it directly results in a String. + * + * @return The String resulting from the instantiation. + */ + public String instantiate() { + if (isUsed) { + throw new TemplateFrameworkException("Repeated use of Instantiator not allowed."); + } + isUsed = true; + BaseScope scope = new BaseScope(library); + codeGenerator.instantiate(scope, parameters); + scope.close(); + return scope.toString(); + } + + /** + * Instantiate the CodeGenerator into a specified scope, using the prepared parameters. + * This is useful for recursive instantiations. + * + * @param scope Scope into which the code is generated. + */ + public void instantiate(Scope scope) { + if (isUsed) { + throw new TemplateFrameworkException("Repeated use of Instantiator not allowed."); + } + isUsed = true; + Scope nestedScope = new Scope(scope, scope.fuel); + codeGenerator.instantiate(nestedScope, parameters); + nestedScope.close(); + scope.stream.addCodeStream(nestedScope.stream); + } + } + + /** + * Create an {@link Instantiator}, which already has a first parameter key-value pair. + * + * @param paramKey The name of the parameter. + * @param paramValue The value to be set. + * @return The Instantiator. + */ + public final Instantiator where(String paramKey, String paramValue) { + return new Instantiator(this).where(paramKey, paramValue); + } + + /** + * Create an {@link Instantiator}, which already has all parameter key-value pairs from the provided map. + * + * @param parameterMap Map containing the key-value pairs. + * @return The Instantiator. + */ + public final Instantiator where(Map parameterMap) { + return new Instantiator(this).where(parameterMap); + } + + /** + * Create an {@link Instantiator}, which already has set a custom library. + * + * @param library The custom library. + * @return The Instantiator for chaining. + */ + public Instantiator with(CodeGeneratorLibrary library) { + return new Instantiator(this).with(library); + } + + /** + * Instantiate the CodeGenerator to a String, using the prepared parameters. + * This is useful for the outer-most instantiation, as it directly results in a String. + * + * @return The String resulting from the instantiation. + */ + public final String instantiate() { + return new Instantiator(this).instantiate(); + } + + /** + * Instantiate the CodeGenerator into a specified scope, using the prepared parameters. + * This is useful for recursive instantiations. + * + * @param scope Scope into which the code is generated. + */ + public final void instantiate(Scope scope) { + new Instantiator(this).instantiate(scope); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/CodeGeneratorLibrary.java b/test/hotspot/jtreg/compiler/lib/template_framework/CodeGeneratorLibrary.java new file mode 100644 index 0000000000000..3f9abd26cef8e --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/CodeGeneratorLibrary.java @@ -0,0 +1,402 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Map; +import java.util.HashSet; +import java.util.Random; +import java.util.function.Consumer; + +import jdk.test.lib.Utils; + +import compiler.lib.generators.*; + +/** + * The {@link CodeGeneratorLibrary} provides a way to map {@link CodeGenerator} names to {@link CodeGenerator}, + * and provides the lookup facility required for recursive instantiation calls. + */ +public final class CodeGeneratorLibrary { + private static final Random RANDOM = Utils.getRandomInstance(); + private static final RestrictableGenerator intGenerator = Generators.G.ints(); + private static final RestrictableGenerator longGenerator = Generators.G.longs(); + private static final Generator floatGenerator = Generators.G.floats(); + private static final Generator doubleGenerator = Generators.G.doubles(); + + private CodeGeneratorLibrary parent; + private HashMap library; + + /** + * Create a new {@link CodeGeneratorLibrary}. + * + * @param parent The parent library, or null. If a parent library is provided, that library is extended with + * the content of this library. + * @param generators The set of generators for this library. + */ + public CodeGeneratorLibrary(CodeGeneratorLibrary parent, HashSet generators) { + this.parent = parent; + this.library = new HashMap(); + for (CodeGenerator generator : generators) { + if (findOrNull(generator.name) != null) { + throw new TemplateFrameworkException("Code library already has a generator for name " + generator.name); + } + this.library.put(generator.name, generator); + } + } + + /** + * Recursively find CodeGenerator with given name in this library or a parent library. + * + * @param name Name of the generator to find. + * @param errorMessage Error message added in the exception if no generator is found for the name. + * @return The generator from the library with the specified name. + * @throws TemplateFrameworkException If no generator is found for the name. + */ + public CodeGenerator find(String name, String errorMessage) { + CodeGenerator codeGenerator = findOrNull(name); + if (codeGenerator == null) { + print(); + throw new TemplateFrameworkException("Code generator '" + name + "' not found" + errorMessage); + } + return codeGenerator; + } + + /** + * Recursively find CodeGenerator with given name in this library or a parent library. + * + * @param name Name of the generator to find. + * @return The generator from the library with the specified name, or null if not found. + */ + public CodeGenerator findOrNull(String name) { + CodeGenerator codeGenerator = library.get(name); + if (codeGenerator != null) { + return codeGenerator; + } else if (parent != null){ + return parent.findOrNull(name); + } else { + return null; + } + } + + /** + * Print all generator names in the library. + */ + public void print() { + System.out.println("Library"); + for (Map.Entry e : library.entrySet()) { + System.out.println(" " + e.getKey() + ": fuelCost=" + e.getValue().fuelCost); + } + if (parent != null) { + parent.print(); + } + } + + /** + * {@code mutable_var} samples a random mutable variable. + * {@code var} samples a random variable, including mutable and immutable. + * + * @param type Name of the type of the variable. + * @return Name of the variable. + */ + private static CodeGenerator factorySampleVariable(boolean mutable) { + String generatorName = mutable ? "mutable_var" : "var"; + return new ProgrammaticCodeGenerator(generatorName, (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "type"); + String type = parameters.get("type", scope); + String name = scope.sampleVariable(type, mutable); + if (name == null) { + scope.print(); + throw new TemplateFrameworkException("Cannot find variable of type: " + type); + } + scope.stream.addCodeToLine(String.valueOf(name)); + }, 0); + } + + private static CodeGenerator factoryDispatch() { + return new ProgrammaticCodeGenerator("dispatch", (Scope scope, Parameters parameters) -> { + String scopeKind = parameters.get("scope", scope); + String generatorName = parameters.get("call", scope); + CodeGenerator generator = scope.library().find(generatorName, " for dispatch in " + scopeKind + " scope"); + + // Copy arguments, and remove the 2 args we just used. Forward the other args to the dispatch. + HashMap parameterMap = new HashMap(parameters.getParameterMap()); + parameterMap.remove("scope"); + parameterMap.remove("call"); + + switch(scopeKind) { + case "class" -> { + ClassScope classScope = scope.classScope(" in dispatch for " + generatorName); + classScope.dispatch(generator, parameterMap); + } + case "method" -> { + MethodScope methodScope = scope.methodScope(" in dispatch for " + generatorName); + methodScope.dispatch(generator, parameterMap); + } + default -> { + scope.print(); + throw new TemplateFrameworkException("Generator dispatch got: scope=" + scopeKind + + "but should be scope=class or scope=method"); + } + } + }, 0); + } + + /** + * {@code add_var} adds a variable. + * + * @param scope Either {@code class} or {@code method}, to add to class or method scope. + * @param name Name of the variable. + * @param type Name of the type of the variable. + * @param mutable Optional (default true), if set to true: the variable is mutable, and + * can be chosen both via {@code var} and {@code mutable_var} sampling, + * if false: it can only be sampled via {@code var}. + */ + private static CodeGenerator factoryAddVariable() { + return new ProgrammaticCodeGenerator("add_var", (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "scope", "name", "type", "mutable"); + String scopeKind = parameters.get("scope", scope); + String name = parameters.get("name", scope); + String type = parameters.get("type", scope); + String isMutable = parameters.getOrNull("mutable"); + + if (isMutable != null && !isMutable.equals("true") && !isMutable.equals("false")) { + scope.print(); + throw new TemplateFrameworkException("Generator 'add_var' got: mutable=" + isMutable + + "but should be mutable=true or mutable=false " + + "or unset with default false."); + } + boolean mutable = (isMutable == null) || isMutable.equals("true"); + + switch(scopeKind) { + case "class" -> { + ClassScope classScope = scope.classScope(" in 'add_var' for " + name); + classScope.addVariable(name, type, mutable); + } + case "method" -> { + MethodScope methodScope = scope.methodScope(" in 'add_var' for " + name); + methodScope.addVariable(name, type, mutable); + } + default -> { + scope.print(); + throw new TemplateFrameworkException("Generator dispatch got: scope=" + scopeKind + + "but should be scope=class or scope=method"); + } + } + }, 0); + } + + private static CodeGenerator factoryRepeat() { + return new ProgrammaticCodeGenerator("repeat", (Scope scope, Parameters parameters) -> { + String generatorName = parameters.get("call", scope); + int repeat = parameters.getInt("repeat", scope); + + if (repeat > 1000) { + scope.print(); + throw new TemplateFrameworkException("Generator repeat should have repeat <= 1000, got: " + repeat); + } + + CodeGenerator generator = scope.library().find(generatorName, " for repeat"); + + // Copy arguments, and remove the 2 args we just used. Forward the other args to the repeat. + HashMap parameterMap = new HashMap(parameters.getParameterMap()); + parameterMap.remove("call"); + parameterMap.remove("repeat"); + + for (int i = 0; i < repeat; i++) { + generator.where(parameterMap).instantiate(scope); + } + }, 0); + } + + /** + * The standard library populated with a large set of {@link CodeGenerator}s. + * + * @return The standard library. + */ + public static CodeGeneratorLibrary standard() { + HashSet codeGenerators = new HashSet(); + + addConstants(codeGenerators); + + codeGenerators.add(factoryDispatch()); + codeGenerators.add(factoryRepeat()); + + addBasicOperators(codeGenerators); + addVariableCodeGenerators(codeGenerators); + return new CodeGeneratorLibrary(null, codeGenerators); + } + + private static void addConstants(HashSet codeGenerators) { + /** + * {@code int_con} returns a random int. + * + * @param lo Optional: lower inclusive bound of the range, default min_int. + * @param hi Optional: upper inclusive bound of the range, default max_int. + * @return Value in the range [lo,hi]. + */ + codeGenerators.add(new ProgrammaticCodeGenerator("int_con", + (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "lo", "hi"); + int lo = parameters.getIntOrDefault("lo", Integer.MIN_VALUE, scope); + int hi = parameters.getIntOrDefault("hi", Integer.MAX_VALUE, scope); + + int v = Generators.G.safeRestrict(intGenerator, lo, hi).next(); + scope.stream.addCodeToLine(String.valueOf(v)); + }, 0)); + + /** + * {@code long_con} returns a random long. + * + * @param lo Optional: lower inclusive bound of the range, default min_long. + * @param hi Optional: upper inclusive bound of the range, default max_long. + * @return Value in the range [lo,hi]. + */ + codeGenerators.add(new ProgrammaticCodeGenerator("long_con", + (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "lo", "hi"); + long lo = parameters.getLongOrDefault("lo", Long.MIN_VALUE, scope); + long hi = parameters.getLongOrDefault("hi", Long.MAX_VALUE, scope); + + long v = Generators.G.safeRestrict(longGenerator, lo, hi).next(); + scope.stream.addCodeToLine(String.valueOf(v) + "L"); + }, 0)); + + /** + * {@code boolean_con} returns a random boolean. + * + * @return Random boolean: true or false. + */ + codeGenerators.add(new ProgrammaticCodeGenerator("boolean_con", + (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope); + int v = Generators.G.safeRestrict(intGenerator, 0, 1).next(); + String bool = (v == 0) ? "false" : "true"; + scope.stream.addCodeToLine(bool); + }, 0)); + + /** + * {@code con} returns a random constant of a specified type. Internally, the + * {@link CodeGenerator} "_con" is called. If a user-defined type has a + * correpsonding "_con" generator added to the library, one can call it + * via "con(type=)". + * + * @param type The type from which the value is to be sampled. + * @return Random value from the specified type. + */ + codeGenerators.add(new ProgrammaticCodeGenerator("con", + (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "type"); + String type = parameters.get("type", scope); + String generatorName = type + "_con"; + CodeGenerator generator = scope.library().find(generatorName, " for 'con'"); + generator.instantiate(scope); + }, 0)); + } + + private static void addBasicOperators(HashSet codeGenerators) { + /** + * {@code choose} picks a random choice from a list like "aaa|bbb|ccc", with separator "|". + * + * @param from List of strings, separated by "|". + * @return One element from the list, picked uniformly at random. + */ + codeGenerators.add(new ProgrammaticCodeGenerator("choose", + (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "from"); + String list = parameters.get("from", scope); + String[] elements = list.split("\\|"); + int r = RANDOM.nextInt(elements.length); + scope.stream.addCodeToLine(elements[r]); + }, 0)); + } + + private static void addVariableCodeGenerators(HashSet codeGenerators) { + // Add variable to ClassScope or MethodScope. + codeGenerators.add(factoryAddVariable()); + + // Sample random variable, mutable or immutable. + codeGenerators.add(factorySampleVariable(false)); + codeGenerators.add(factorySampleVariable(true)); + + // Internal: Define variable + codeGenerators.add(new Template("_internal_def_var", + "#{prefix} #{name} = #{value};#{:add_var(scope=method,name=#name,type=#type,mutable=#mutable)}" + )); + + // Internal: Define field + codeGenerators.add(new Template("_internal_def_field", + "#{prefix} #{name} = #{value};#{:add_var(scope=class,name=#name,type=#type,mutable=#mutable)}" + )); + + /** + * {@code def_var} defines a variable in the current method scope. + * + * @param name Name of the variable. + * @param prefix Access qualifier and type. + * @param value Value assigned to the variable on definition. + * @param type Type for which the variable can be sampled with {@code var} or {@code mutable_var}. + */ + codeGenerators.add(new Template("def_var", + "#{:dispatch(scope=method,call=_internal_def_var,name=#name,prefix=#prefix,value=#value,type=#type,mutable=true)}" + )); + + /** + * {@code def_immutable_var} defines a variable in the current method scope, and the + * variable is to be considered immutable. + * + * @param name Name of the variable. + * @param prefix Access qualifier and type. + * @param value Value assigned to the variable on definition. + * @param type Type for which the variable can be sampled with {@code var}. + */ + codeGenerators.add(new Template("def_immutable_var", + "#{:dispatch(scope=method,call=_internal_def_var,name=#name,prefix=#prefix,value=#value,type=#type,mutable=false)}" + )); + + /** + * {@code def_field} defines a field in the current class scope. + * + * @param name Name of the field. + * @param prefix Access qualifier and type. + * @param value Value assigned to the field on definition. + * @param type Type for which the field can be sampled with {@code var} or {@code mutable_var}. + */ + codeGenerators.add(new Template("def_field", + "#{:dispatch(scope=class,call=_internal_def_field,name=#name,prefix=#prefix,value=#value,type=#type,mutable=true)}" + )); + + /** + * {@code def_immutable_field} defines a field in the current class scope, and the field + * is to be considered immutable. + * + * @param name Name of the field. + * @param prefix Access qualifier and type. + * @param value Value assigned to the field on definition. + * @param type Type for which the field can be sampled with {@code var}. + */ + codeGenerators.add(new Template("def_immutable_field", + "#{:dispatch(scope=class,call=_internal_def_field,name=#name,prefix=#prefix,value=#value,type=#type,mutable=false)}" + )); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/CodeStream.java b/test/hotspot/jtreg/compiler/lib/template_framework/CodeStream.java new file mode 100644 index 0000000000000..44a5c5e1724a1 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/CodeStream.java @@ -0,0 +1,266 @@ +/* + * 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; + +import java.util.List; +import java.util.ArrayList; + +/** + * The {@link CodeStream} collects code {@link Token}s that are generated during {@link CodeGenerator} instantiation. + * It allows addition of plane {@link String}s, newlines, indentation, and even adding the whole content of another + * {@link CodeStream}. Once a {@link CodeStream} is closed, its {@link String} representation can be requested with + * {@link toString}. + */ +public final class CodeStream { + + // Token definition. + private sealed interface Token permits CodeSegment, Newline, Indent, Outdent, MultiIndent, NestedCodeStream {} + private record CodeSegment(String code) implements Token {} + private record Newline() implements Token {} + private record Indent() implements Token {} + private record Outdent() implements Token {} + private record MultiIndent(int difference) implements Token {} + private record NestedCodeStream(CodeStream stream) implements Token {} + + // To avoid allocating repeatedly. + private static final Newline NEWLINE = new Newline(); + private static final Indent INDENT = new Indent(); + private static final Outdent OUTDENT = new Outdent(); + + private List stream; + private boolean closed; + private int indentation; + + /** + * Create a new empty {@link CodeStream}. + */ + public CodeStream() { + this.stream = new ArrayList(); + this.closed = false; + this.indentation = 0; + checkOpen(); + } + + /** + * Check if the {@link CodeStream} is still open. + * + * @throws TemplateFrameworkException If the stream is already closed. + */ + public void checkOpen() { + if (closed) { + throw new TemplateFrameworkException("Stream is already closed."); + } + } + + /** + * Check if the {@link CodeStream} is already closed. + * + * @throws TemplateFrameworkException If the stream is still open. + */ + public void checkClosed() { + if (!closed) { + throw new TemplateFrameworkException("Stream is still open."); + } + } + + /** + * Add code to the current line, no newline allowed. + * + * @param code The code to be added to the current line. + */ + public void addCodeToLine(String code) { + checkOpen(); + if (code.contains("\n")) { + throw new TemplateFrameworkException("No newline allowed. Got: " + code); + } + if (!code.equals("")) { + stream.add(new CodeSegment(code)); + } + } + + /** + * Add a newline to the stream. + */ + public void addNewline() { + checkOpen(); + stream.add(NEWLINE); + } + + /** + * Get the (local) indentation of the stream. + * + * @return The indentation of the stream. + */ + public int getIndentation() { + return indentation; + } + + /** + * Increase the indentation of the next line. + */ + public void indent() { + checkOpen(); + stream.add(INDENT); + indentation++; + if (indentation > 100) { + throw new TemplateFrameworkException("Indentation should not be too deep, is " + indentation); + } + } + + /** + * Decrease the indentation of the next line. + */ + public void outdent() { + checkOpen(); + stream.add(OUTDENT); + indentation--; + if (indentation < 0) { + throw new TemplateFrameworkException("Indentation should not become negative."); + } + } + + /** + * Set the indentation to a specific depth. + * + * @param indentation The requested indentation depth. + */ + public void setIndentation(int indentation) { + checkOpen(); + if (indentation < 0 || indentation > 100) { + throw new TemplateFrameworkException("Indentation unreasonable: " + indentation); + } + if (indentation == this.indentation) { + // Nothing + } else if (indentation == this.indentation + 1) { + stream.add(INDENT); + this.indentation++; + } else if (indentation == this.indentation - 1) { + stream.add(OUTDENT); + this.indentation--; + } else { + stream.add(new MultiIndent(indentation - this.indentation)); + this.indentation = indentation; + } + } + + /** + * Add the content of a {@link CodeStream} to the current {@link CodeStream}. + * + * @param other The stream to be added to this stream. + */ + public void addCodeStream(CodeStream other) { + checkOpen(); + other.checkClosed(); + stream.add(new NestedCodeStream(other)); + } + + /** + * Prepend the content of a {@link CodeStream} to the current {@link CodeStream}. + * + * @param other The stream to be prepended to this stream. + */ + public void prependCodeStream(CodeStream other) { + checkOpen(); + other.checkClosed(); + stream.addFirst(new NestedCodeStream(other)); + } + + /** + * Close the stream, after which no more code can be added, but after which this stream can be added to + * other streams or converted to a String with {@link toString}. + */ + public void close() { + checkOpen(); + if (indentation != 0) { + throw new TemplateFrameworkException("Indentation must be zero when closing, is " + indentation); + } + closed = true; + checkClosed(); + } + + /** + * Helper class for state for {@link String} generation in {@link toString}. + */ + private final class CollectionState { + private StringBuilder stringBuilder = new StringBuilder(); + private boolean lastWasNewline = false; + private int indentation = 0; + + public CollectionState() {} + + public void addCodeToLine(String code) { + // If we just had a newline, and we are now pushing code, + // then we have to set the correct indentation. + if (lastWasNewline) { + stringBuilder.append(" ".repeat(4 * indentation)); + } + stringBuilder.append(code); + lastWasNewline = false; + } + + public void addNewline() { + stringBuilder.append("\n"); + lastWasNewline = true; + } + + public void indent() { + indentation++; + } + + public void outdent() { + indentation--; + } + + public void multiIndent(int difference) { + indentation += difference; + } + + public String toString() { + return stringBuilder.toString(); + } + } + + /** + * Collect a closed {@link CodeStream} to a {@link String}. + */ + public String toString() { + checkClosed(); + CollectionState state = new CollectionState(); + collect(state); + return state.toString(); + } + + private void collect(CollectionState state) { + for (Token t : stream) { + switch (t) { + case CodeSegment(String code) -> { state.addCodeToLine(code); } + case Newline() -> { state.addNewline(); } + case Indent() -> { state.indent(); } + case Outdent() -> { state.outdent(); } + case MultiIndent(int difference) -> { state.multiIndent(difference); } + case NestedCodeStream(CodeStream stream) -> { stream.collect(state); } + } + } + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/DispatchScope.java b/test/hotspot/jtreg/compiler/lib/template_framework/DispatchScope.java new file mode 100644 index 0000000000000..22ca441111783 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/DispatchScope.java @@ -0,0 +1,78 @@ +/* + * 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; + +import java.util.HashMap; + +/** + * A {@link DispatchScope} allows the use of {@link dispatch} for dispatching {@link CodeGenerator}s + * to the top of a {@link ClassScope} or {@link MethodScope}. + */ +public abstract sealed class DispatchScope extends Scope permits ClassScope, MethodScope{ + + /** + * The {@link CodeStream} used for dispatched code at the top of the scope. + */ + public final CodeStream dispatchStream; + + /** + * Create a new {@link DispatchScope}. + * + * @param parent Parent scope or null if the new scope is an outermost scope. + * @param fuel Remaining fuel for recursive {@link CodeGenerator} instantiations. + */ + public DispatchScope(Scope parent, long fuel) { + super(parent, fuel); + this.dispatchStream = new CodeStream(); + } + + /** + * Dispatch a {@link CodeGenerator} to generate code at the top of the scope. + * + * @param generator The generator that is dispatched to the top of the scope. + * @param parameterMap The parameter key-value pairs for the generator instantiation. + */ + public void dispatch(CodeGenerator generator, HashMap parameterMap) { + Scope dispatchScope = new Scope(this, this.fuel - generator.fuelCost); + Parameters parameters = new Parameters(parameterMap); + generator.instantiate(dispatchScope, parameters); + dispatchScope.stream.addNewline(); + dispatchScope.close(); + dispatchStream.addCodeStream(dispatchScope.stream); + } + + /** + * Close the {@link DispatchScope}. + *

+ * Close the {@link dispatchStream} for all the dispatched code at the top of the scope, and prepend + * the code to the scope's {@link CodeStream}, then close that stream after which no more code can be + * generated into the scope. + */ + @Override + public void close() { + dispatchStream.close(); + stream.prependCodeStream(dispatchStream); + super.close(); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/MethodScope.java b/test/hotspot/jtreg/compiler/lib/template_framework/MethodScope.java new file mode 100644 index 0000000000000..ddf5b23a6d890 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/MethodScope.java @@ -0,0 +1,43 @@ +/* + * 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; + +/** + * The {@link MethodScope} sets the scope of a method body, i.e. after its opening bracked and before its + * closing bracket. This allows dispatching {@link CodeGenerator}s to the top of the method body, for + * example for variable declarations, see {@link DispatchScope#dispatch}. In {@link Template}s, a + * {@link MethodScope} is opened with {@code #open(method)} and closed with {@code #close(method)}. + */ +final class MethodScope extends DispatchScope { + + /** + * Create a new {@link MethodScope}. + * + * @param parent Parent scope or null if the new scope is an outermost scope. + * @param fuel Remaining fuel for recursive {@link CodeGenerator} instantiations. + */ + public MethodScope(Scope parent, long fuel) { + super(parent, fuel); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Parameters.java b/test/hotspot/jtreg/compiler/lib/template_framework/Parameters.java new file mode 100644 index 0000000000000..72b20d58d7aa8 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Parameters.java @@ -0,0 +1,236 @@ +/* + * 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; + +import java.util.Map; +import java.util.HashMap; +import java.util.Set; + +/** + * Parameters is required to instantiate a {@link CodeGenerator} (e.g. {@link Template}). + *

+ * It has a set of parameter key-value pairs. In {@link Template}s, these are used to fill + * parameter holes specified by {@code #{param}}. + *

+ * The {@link instantiationID} is unique for an instantiation, and allows the variable renaming + * to avoid variable name collisions when multiple {@link Template}s use the same variable name. + */ +public final class Parameters { + private static int instantiationIDCounter = 0; + + private HashMap parameterMap; + + /** + * Unique ID used for variable renaming, to avoid name collisions between {@link Template}s. + */ + public final int instantiationID; + + /** + * Create an empty Parameters set, then add parameter key-value pairs later. + */ + public Parameters() { + this(new HashMap()); + } + + /** + * Create a Parameters set that already has some parameter key-value pairs. + * + * @param parameterMap A list of parameter key-value pairs. + */ + public Parameters(Map parameterMap) { + this.parameterMap = new HashMap(parameterMap); + this.instantiationID = instantiationIDCounter++; + } + + /** + * Add a parameter key-value pair to the parameter set. + * + * @param name Name of the parameter. + * @param value Value to be set for the parameter. + */ + public void add(String name, String value) { + if (parameterMap.containsKey(name)) { + throw new TemplateFrameworkException("Parameter " + name + " cannot be added as " + value + + ", is already added as " + parameterMap.get(name)); + } + parameterMap.put(name, value); + } + + /** + * Add a set of parameter key-value pairs. + * + * @param parameterMap Map that contains all the parameter key-value pairs to be added. + */ + void add(Map parameterMap) { + for (Map.Entry e : parameterMap.entrySet()) { + add(e.getKey(), e.getValue()); + } + } + + /** + * Get the parameter value for a specified parameter name, or {@code null} if there is no parameter + * key-value pair for this parameter name. + * + * @param name The name of the parameter. + * @return Parameter value, or {@code null} if there is no parameter key-value pair for the name. + */ + public String getOrNull(String name) { + return parameterMap.get(name); + } + + /** + * Get the parameter value for a specified parameter name. + * + * @param name The name of the parameter. + * @param scope For debug printing the "scope-trace". + * @return Parameter value. + * @throws TemplateFrameworkException If the parameter for the name does not exist. + */ + public String get(String name, Scope scope) { + String param = getOrNull(name); + if (param == null) { + scope.print(); + throw new TemplateFrameworkException("Missing parameter '" + name + "'."); + } + return param; + } + + /** + * Get the parameter value as an int for a specified parameter name. + * + * @param name The name of the parameter. + * @param scope For debug printing the "scope-trace". + * @return Parameter int value. + * @throws TemplateFrameworkException If the parameter for the name does not exist, or cannot be + * parsed as an int. + */ + public int getInt(String name, Scope scope) { + String param = get(name, scope); + return parseInt(param, scope, "for parameter '" + name + "'."); + } + + /** + * Get the parameter value as an int for a specified parameter name, + * or the default value if the parameter is not available. + * + * @param name The name of the parameter. + * @param defaultValue Default value if the parameter name is not available. + * @param scope For debug printing the "scope-trace". + * @return Parameter int value if the parameter name is present, else the default value. + * @throws TemplateFrameworkException If the parameter for the name exists but cannot be + * parsed as an int. + */ + public int getIntOrDefault(String name, int defaultValue, Scope scope) { + String param = getOrNull(name); + if (param == null) { + return defaultValue; + } + return parseInt(param, scope, "for parameter '" + name + "'."); + } + + /** + * Get the parameter value as an long for a specified parameter name, + * or the default value if the parameter is not available. + * + * @param name The name of the parameter. + * @param defaultValue Default value if the parameter name is not available. + * @param scope For debug printing the "scope-trace". + * @return Parameter long value if the parameter name is present, else the default value. + * @throws TemplateFrameworkException If the parameter for the name exists but cannot be + * parsed as a long. + */ + public long getLongOrDefault(String name, long defaultValue, Scope scope) { + String param = getOrNull(name); + if (param == null) { + return defaultValue; + } + return parseLong(param, scope, "for parameter '" + name + "'."); + } + + private static int parseInt(String string, Scope scope, String errorMessage) { + switch (string) { + case "min_int" -> { return Integer.MIN_VALUE; } + case "max_int" -> { return Integer.MAX_VALUE; } + } + try { + return Integer.valueOf(string); + } catch (NumberFormatException e) { + scope.print(); + throw new TemplateFrameworkException("Could not parse int from string '" + string + "' " + errorMessage); + } + } + + private static long parseLong(String string, Scope scope, String errorMessage) { + switch (string) { + case "min_int" -> { return Integer.MIN_VALUE; } + case "max_int" -> { return Integer.MAX_VALUE; } + case "min_long" -> { return Long.MIN_VALUE; } + case "max_long" -> { return Long.MAX_VALUE; } + } + try { + return Long.valueOf(string); + } catch (NumberFormatException e) { + scope.print(); + throw new TemplateFrameworkException("Could not parse long from string '" + string + "' " + errorMessage); + } + } + + /** + * Get the parameter map with all parameter key-value pairs. + * + * @return Parameter map with all key-value pairs. + */ + HashMap getParameterMap() { + return parameterMap; + } + + /** + * Print all parameter key-value pairs for debugging. + */ + public void print() { + System.out.println(" Parameters ID=" + instantiationID); + for (Map.Entry e : parameterMap.entrySet()) { + System.out.println(" " + e.getKey() + "=" + e.getValue()); + } + } + + + /** + * Check that only parameter with parameter names from {@code names} are in the parameter set. + * Useful for parameter verification in {@link ProgrammaticCodeGenerator}s. + * + * @param scope For debug printing the "scope-trace". + * @param names List of allowed parameter names. + * @throws TemplateFrameworkException If parameter names are used that are not in {@code names}. + */ + public void checkOnlyHas(Scope scope, String... names) { + Set set = Set.of(names); + for (String key : parameterMap.keySet()) { + if (!set.contains(key)) { + scope.print(); + throw new TemplateFrameworkException("Parameters have unexpected entry: " + key); + } + } + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/ProgrammaticCodeGenerator.java b/test/hotspot/jtreg/compiler/lib/template_framework/ProgrammaticCodeGenerator.java new file mode 100644 index 0000000000000..aaa0790496bfd --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/ProgrammaticCodeGenerator.java @@ -0,0 +1,76 @@ +/* + * 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; + +/** + * The {@link ProgrammaticCodeGenerator} is to be used when a {@link Template} is not enough powerful, and + * one instead needs to programmaticall generate code. The user provided lambda {@link Instantiator} + * function gets called during the instantiation, and has direct access to the {@link Scope} and + * {@link Parameters}, and has to directly generate code into the {@link CodeStream} of the {@link Scope}. + */ +public final class ProgrammaticCodeGenerator extends CodeGenerator { + + /** + * Interface definition for instantiator lambda functions. + */ + public interface Instantiator { + /** + * The provided lambda function is called during instantiation. + * + * @param scope Scope into which the code is generated. + * @param parameters Provides the parameters for the instantiation, as well as a unique ID for identifier + * name generation (e.g. variable of method names). + */ + public void call(Scope scope, Parameters parameters); + } + + private final Instantiator instantiator; + + /** + * Create a new {@link ProgrammaticCodeGenerator}. + * + * @param generatorName Name of the generator, can be used for lookup in the + * {@link CodeGeneratorLibrary} if the {@link Template} + * is added to a library. + * @param instantiator Lambda function provided for instantiation. + * @param fuelCost The {@link fuelCost} for the {@link CodeGenerator}. + */ + public ProgrammaticCodeGenerator(String generatorName, Instantiator instantiator, int fuelCost) { + super(generatorName, fuelCost); + this.instantiator = instantiator; + } + + /** + * Instantiate the {@link ProgrammaticCodeGenerator}. + * + * @param scope Scope into which the code is generated. + * @param parameters Provides the parameters for the instantiation, as well as a unique ID for identifier + * name generation (e.g. variable of method names). + */ + @Override + public void instantiate(Scope scope, Parameters parameters) { + scope.setDebugContext(name, parameters); + instantiator.call(scope, parameters); + }; +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/README.md b/test/hotspot/jtreg/compiler/lib/template_framework/README.md new file mode 100644 index 0000000000000..ab4c5bfed8698 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/README.md @@ -0,0 +1,88 @@ +# Template Framework +The Template Framework allows the generation of code with Templates. The goal is that these Templates are easy to write, and allow regression tests to cover a larger scope, and to make temlate based fuzzing easy to extend. + +## Motivation +Often one has to write a large set of tests over many types and code shapes and constant values to more adequately cover compiler features, bugs and optimizations. This is tedious work, and often one writes fewer tests than could have been or should have been written because of time limitations. + +With the [Compile Framework](../compile_framework/README.md), we can easily compile and invoke runtime-generated Java code. The Template Framework now provides an easy way to generate such Java code. Templates can be specified with Strings. A simple syntax allows defining holes in these Templates, that can be filled with parameter values or recursive Templates. + +This allows the test writer to specify just one or a few Templates, possibly with some parameter holes and a list of parameter values, or with Template holes for recursive Template instantiation. The Template Framework then takes care of generating code for each parameter value, and for filling in recursive Template instantiations, possibly with random code, code shapes and constant values. + +## How to use the Template Framework +Please reference the examples found in [examples](../../../testlibrary_tests/template_framework/examples/). Some basic tests can be found in [tests](../../../testlibrary_tests/template_framework/tests/). + +### Instantiating a single Template + +One can instantiate a single Template directly using `template.instantiate()`, if it has no parameter holes. If there are template holes, these can be filled with `template.where` chaining calls, and after filling all the template holes, the template can be instantiated. Here a simple example: + + // Defiie a Template with name "my_example" that has two parameter holes + // "param1" and "param2", and a recursive call to a "int_con" CodeGenerator + // which takes parameters "lo" with value 0 and "hi" with value 100. + Template template = new Template("my_example", + """ + package p.xyz; + + public class InnerTest { + public static int test() { + return #{param1} + #{param2} + #{:int_con(lo=0,hi=100)}; + } + } + """ + ); + + // The template is instantiated, and the two parameters are replaced with the + // provided values, and the recursive CodeGenerator call replaces the corresponding + // Template hole with a random number from 0 to 100. The resulting code is returned + // as a String that could be passed on to the Compile Framework. + String code = template.where("param1", "42").where("param2", "7").instantiate(); + +### Conveniently instantiating multiple tests into a single class with the TestClassInstantiator + +The [TestClassInstantiator](./TestClassInstantiator.java) is a convenient facility that allows the instantiation of multiple tests into a single test class. Please refer to the example [TestInstantiationOfManyTests.java](../../../testlibrary_tests/template_framework/examples/TestInstantiationOfManyTests.java) for various ways of using this utility. + +TODO example with IR framework??? + +## Use case: Regression Fest +Hand-written regression tests are very time consuming, and often fewere tests are written than desired due to time constraints of the developer. With Templates, the developer can simply take the reduced regression test, and turn some constants into Template holes that can then be replaced by random constants, or other interesting code shapes that may trigger special cases of the bug or feature under test. The standard [CodeGeneratorLibrary](./CodeGeneratorLibrary.java) can be used for recursive CodeGenerator instantiations, for random constant generation, random variable sampling, and inserting specific or random intersting code shapes. + +## Use case: Extensive feature testing (targetted Fuzzer) +When working on a new feature, or working on a bigger bug, it may be helpful to generate tests for a vast set of parameters, code shapes, operators, class hierarchies, types, constant values, etc. The difference to a simple regression test is simply in the complexity in combinations of templates, parameters and recursive CodeGenerator instantiations. One might want to extend the `CodeGeneratorLibrary` with more options, or implement custom `ProgrammaticCodeGenerators` (see below) to unlock more powerful features. + +## Use case: General purpose Template based Fuzzer. +The goal is to test code patterns, language features, libraries and the related compiler optimizations not just in isolation, but in combination, to test their correctness under more circumstances. Existing fuzzers are difficult to extend for new features. The Template Framework is the basis for a Template based Fuzzer, which combines many templates through Template nesting. + +TODO decide if this is part of this patch, or a remark for future work. + +## The Standard CodeGeneratorLibrary +The standard library is defined in [CodeGeneratorLibrary](./CodeGeneratorLibrary.java), and contains a number of helpful CodeGenerators. + +TODO: list all CodeGenerators + +## Custom CodeGenerator Library +The standard library can be extended with more functionality. Here a simple example: + + // Create a set of CodeGenerators (e.g. Templates, ProgrammaticCodeGenerators, SelectorCodeGenerators): + HashSet set = new HashSet(); + + set.add(new Template("my_template_1", ...)); + set.add(new Template("my_template_2", ...)); + ... + + // Create an extension library using the set of just defined generators to extend the standard + // library. + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), set); + + // This library can now be passed to a TestClassInstantiator, which then has access to the + // extended library. + +TODO add example with Template and that new test, and "with" + +Please refer to the example [TestCustomLibraryForClassFuzzing](../../../testlibrary_tests/template_framework/examples/TestCustomLibraryForClassFuzzing.java). + +## Implementing Custom ProgrammaticCodeGenerators +It is good practice to use Templates where ever possible. But in some cases, it is required to implement more powerful functionality, that can query some custom state during the code generation. For this, one can use the [ProgrammaticCodeGenerator](./ProgrammaticCodeGenerator.java), which allows the user to specify a lambda function that is called during instantiation and has direct access to the internals of the Template Framework, and can generate code or make recursive CodeGenerator instantiations. + +Please refer to the example [TestCustomLibraryForClassFuzzing](../../../testlibrary_tests/template_framework/examples/TestCustomLibraryForClassFuzzing.java) and the definition of standard library functions in [CodeGeneratorLibrary](./CodeGeneratorLibrary.java) for further examples. + +## Implementing Custom SelectorCodeGenerators +TODO - first need a better example! diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Scope.java b/test/hotspot/jtreg/compiler/lib/template_framework/Scope.java new file mode 100644 index 0000000000000..bf3b0f4ad7bb0 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Scope.java @@ -0,0 +1,324 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import jdk.test.lib.Utils; + +/** + * The {@link Scope} defines the the relative location within a recursive {@link CodeGenerator} + * instantiation, and provides access to the available variables, and the output {@link CodeStream} + * for the current scope of code generation. + */ +public sealed class Scope permits BaseScope, DispatchScope { + private static final Random RANDOM = Utils.getRandomInstance(); + + /** + * Parent {@link Scope}, which transitively gives access to all outer scopes. + */ + public final Scope parent; + + /** + * Remaining fuel in the current scope for recursive {@link CodeGenerator} instantiations, + * used as a guide to limit the recursion depth. + */ + public final long fuel; + + /** + * Output {@link CodeStream} into which all code of this scope and its nested scopes + * are generated into. + */ + public final CodeStream stream; + + /** + * Helper class to keep track of the local variables defined inside the scope. + */ + private final class VariableSet { + public final VariableSet parent; + public final HashMap> variables; + + public VariableSet(VariableSet parent) { + this.parent = parent; + this.variables = new HashMap>(); + } + + public int countLocal(String type) { + ArrayList locals = variables.get(type); + return (locals == null) ? 0 : locals.size(); + } + + public int count(String type) { + int c = countLocal(type); + if (parent != null) { + return c + parent.count(type); + } + return c; + } + + /** + * Randomly sample a variable from this scope or a parent scope, restricted to the specified type. + */ + public String sample(String type) { + int c = count(type); + if (c == 0) { + // No variable of this type + return null; + } + + // Maybe sample from parent. + if (parent != null) { + int pc = parent.count(type); + int r = RANDOM.nextInt(c); + if (r < pc) { + return parent.sample(type); + } + } + + ArrayList locals = variables.get(type); + int r = RANDOM.nextInt(locals.size()); + return locals.get(r); + } + + /** + * Add a variable of a specified type to the scope. + */ + public void add(String name, String type) { + // Fetch list of variables - if non-existant create a new one. + ArrayList variablesWithType = variables.get(type); + if (variablesWithType == null) { + variablesWithType = new ArrayList(); + variables.put(type, variablesWithType); + } + variablesWithType.add(name); + } + + public void printLocals() { + for (Map.Entry> e : variables.entrySet()) { + String type = e.getKey(); + ArrayList locals = e.getValue(); + System.out.println(" type: " + type + " - " + count(type)); + for (String v : locals) { + System.out.println(" " + v); + } + } + if (variables.isEmpty()) { + System.out.println(" empty"); + } + } + + public void print(int i) { + System.out.println("print " + i); + printLocals(); + if (parent != null) { + parent.print(i+1); + } + } + } + + /** + * We have two sets of variables: mutable and immutable (read-only) variables. + */ + private final VariableSet allVariables; + private final VariableSet mutableVariables; + + /** + * Helper record, providing debugging information for the scope, which can be printed when + * an error is encountered in the Template Framework, providing a "scope-trace" with helpful + * information for users of the framework. + */ + record DebugContext(String description, Parameters parameters) { + public void print() { + System.out.println(" " + description); + if (parameters == null) { + System.out.println(" No parameters."); + } else { + parameters.print(); + } + } + } + + DebugContext debugContext; + + /** + * Create a new {@link Scope}. + * + * @param parent Parent scope or null if the new scope is an outermost scope. + * @param fuel Remaining fuel for recursive {@link CodeGenerator} instantiations. + */ + public Scope(Scope parent, long fuel) { + this.parent = parent; + this.fuel = fuel; + this.stream = new CodeStream(); + + this.allVariables = new VariableSet(this.parent != null ? this.parent.allVariables : null); + this.mutableVariables = new VariableSet(this.parent != null ? this.parent.mutableVariables : null); + } + + /** + * Add debugging information to the scope, when using it in an instantiation. + * + * @param description Description, which contains information about how this scope is used. + * @param parameters The {@link Parameters} used in the instantiantion, or null if not relevant. + */ + public void setDebugContext(String description, Parameters parameters) { + DebugContext newDebugContext = new DebugContext(description, parameters); + if (this.debugContext != null) { + System.out.println("Setting debug context a second time. New context"); + newDebugContext.print(); + System.out.println("Old trace:"); + print(); + throw new TemplateFrameworkException("Duplicate setting debug context not allowed."); + } + this.debugContext = newDebugContext; + } + + /** + * Access the {@link CodeGeneratorLibrary} associated with the {@link BaseScope}. + * + * @return The library associated with the {@link BaseScope}. + */ + public CodeGeneratorLibrary library() { + return this.parent.library(); + } + + /** + * Close the scope, together with its {@link CodeStream}, after which no more code can be generated + * into the scope. + */ + public void close() { + if (this.debugContext == null) { + print(); + throw new TemplateFrameworkException("No debug context set until end of scope."); + } + stream.close(); + } + + /** + * Add a variable to the scope. + * + * @param name Name of the variable. + * @param type Type of the variable. + * @param mutable Indicates if the variable is to be mutated or used for read-only purposes. + */ + public void addVariable(String name, String type, boolean mutable) { + allVariables.add(name, type); + if (mutable) { + mutableVariables.add(name, type); + } + } + + /** + * Sample a random variable from the set of variables defined in the scope or outer scopes. + * @param type Type of the variable. + * @param mutable Indicates if the variable is to be mutated or used for read-only purposes. + * @return Name of the sampled variable. + */ + public String sampleVariable(String type, boolean mutable) { + return mutable ? mutableVariables.sample(type) : allVariables.sample(type); + } + + /** + * Next outer {@link Scope} (not this) that is a {@link ClassScope}. + * + * @param errorMessage Error message added to the exception if no such {@link ClassScope} is found. + * @return The outer {@link ClassScope}. + * @throws TemplateFrameworkException If no such outer {@link ClassScope} is found. + */ + public final ClassScope classScope(String errorMessage) { + Scope current = this; + while (current.parent != null) { + if (current.parent instanceof ClassScope s) { + return s; + } + current = current.parent; + } + print(); + throw new TemplateFrameworkException("Could not find ClassScope / '#open(class)' " + errorMessage); + } + + /** + * Next outer {@link Scope} (not this) that is a {@link MethodScope}. + * + * @param errorMessage Error message added to the exception if no such {@link MethodScope} is found. + * @return The outer {@link MethodScope}. + * @throws TemplateFrameworkException If no such outer {@link MethodScope} is found. + */ + public final MethodScope methodScope(String errorMessage) { + Scope current = this; + while (current.parent != null) { + if (current.parent instanceof MethodScope s) { + return s; + } + current = current.parent; + } + print(); + throw new TemplateFrameworkException("Could not find MethodScope / '#open(method)' " + errorMessage); + } + + /** + * Compute the relative indentation to an outer (recursive parent) scope. + * + * @param outer Some (recursive parent) outer scope. + * @return Indentation relative to some outer scope. + */ + public final int indentationFrom(Scope outer) { + int difference = 0; + Scope current = this; + while (current != null && current != outer) { + current = current.parent; + difference += current.stream.getIndentation(); + } + if (current == null) { + System.out.println("This scope:"); + print(); + System.out.println("Outer scope:"); + outer.print(); + throw new TemplateFrameworkException("Outer scope not found."); + } + return difference; + } + + /** + * Printing the "scope-trace" for debbuging. + */ + public final void print() { + System.out.println(this.getClass().getSimpleName() + ":"); + if (debugContext != null) { + debugContext.print(); + } else { + System.out.println(" No debug context set yet."); + } + System.out.println(" mutable variables:"); + mutableVariables.printLocals(); + System.out.println(" all variables:"); + allVariables.printLocals(); + if (parent != null) { + parent.print(); + } + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/SelectorCodeGenerator.java b/test/hotspot/jtreg/compiler/lib/template_framework/SelectorCodeGenerator.java new file mode 100644 index 0000000000000..63b56cc2acc74 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/SelectorCodeGenerator.java @@ -0,0 +1,171 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import jdk.test.lib.Utils; + +/** + * The {@link SelectorCodeGenerator} randomly selects one {@link CodeGenerator} from + * a list, according to the weights assigned to each {@link CodeGenerator}. However, + * we first filter the list for {link CodeGenerator}s that have low enough + * {@link CodeGenerator#fuelCost} for the remaining {@link Scope#fuel}, and if + * none of them have sufficient fuel, the we take the generator with the + * {@link defaultGeneratorName}. Optionally, one can also provide {@link Predicate}s + * which filter which generators are available, based on the {@link Scope} and + * {@link Parameters}. + */ +public final class SelectorCodeGenerator extends CodeGenerator { + private static final Random RANDOM = Utils.getRandomInstance(); + + /** + * {@link Predicate}s are used to enable / disable choices based on the + * state of the {@link Scope} and {@link Parameters}. + */ + public interface Predicate { + /** + * Checks if the corresponding choice should be available. + * + * @param scope Scope of the {@link SelectorCodeGenerator}. + * @param parameters Parameters that would be passed to the choice's generator. + * @return A boolean indicating if the choice is to be available. + */ + public boolean check(Scope scope, Parameters parameters); + } + + private HashMap choiceWeights; + private HashMap choicePredicates; + private String defaultGeneratorName; + + /** + * Create a new {@link SelectorCodeGenerator}. + * + * @param selectorName Name of the selector, can be used for lookup in the + * {@link CodeGeneratorLibrary} if the {@link Template} + * is added to a library. + * @param defaultGeneratorName Name of the default generator if none of the generators + * in the list have low enough fuel cost. + */ + public SelectorCodeGenerator(String selectorName, String defaultGeneratorName) { + super(selectorName, 0); + this.defaultGeneratorName = defaultGeneratorName; + this.choiceWeights = new HashMap(); + this.choicePredicates = new HashMap(); + } + + /** + * Add another {@link CodeGenerator} name to the list. + * + * @param name Name of the additional {@link CodeGenerator}. + * @param weight Weight of the generator, used in random sampling. + * @param predicate Predicate that indicates if the choice is to be available. + */ + public void add(String name, float weight, Predicate predicate) { + if (!(0.1 < weight && weight <= 10_000)) { + throw new TemplateFrameworkException("Unreasonable weight " + weight + " for " + name); + } + if (choiceWeights.containsKey(name)) { + throw new TemplateFrameworkException("Already added before: " + name); + } + choiceWeights.put(name, weight); + choicePredicates.put(name, predicate); + } + + /** + * Add another {@link CodeGenerator} name to the list, with a {@link Predicate} that + * always returns true, i.e. makes the choice always available. + * + * @param name Name of the additional {@link CodeGenerator}. + * @param weight Weight of the generator, used in random sampling. + */ + public void add(String name, float weight) { + add(name, weight, (_, _) -> { return true; }); + } + + /** + * Randomly sample one of the generators from the list, according to filter and weight. + */ + private String choose(Scope scope, Parameters parameters) { + // Total weight of allowed choices + double total = 0; + for (Map.Entry entry : choiceWeights.entrySet()) { + String name = entry.getKey(); + float weight = entry.getValue().floatValue(); + Predicate predicate = choicePredicates.get(name); + CodeGenerator codeGenerator = scope.library().find(name, " in selector"); + if (scope.fuel < codeGenerator.fuelCost) { continue; } + if (!predicate.check(scope, parameters)) { continue; } + total += weight; + } + + if (total == 0) { + // No generator in the list had low enough cost. + return defaultGeneratorName; + } + + double r = RANDOM.nextDouble() * total; + + double total2 = 0; + for (Map.Entry entry : choiceWeights.entrySet()) { + String name = entry.getKey(); + float weight = entry.getValue().floatValue(); + Predicate predicate = choicePredicates.get(name); + CodeGenerator codeGenerator = scope.library().find(name, " in selector"); + if (scope.fuel < codeGenerator.fuelCost) { continue; } + if (!predicate.check(scope, parameters)) { continue; } + total2 += weight; + if (r <= total2) { + return name; + } + } + scope.print(); + throw new TemplateFrameworkException("Failed to select total=" + total + ", r=" + r); + } + + /** + * Instantiate the {@link SelectorCodeGenerator}, which randomly samples one of the generators + * from the list according to remaining fuel cost and weights. + * + * @param scope Scope into which the code is generated. + * @param parameters Provides the parameters for the instantiation. + */ + @Override + public void instantiate(Scope scope, Parameters parameters) { + scope.setDebugContext(name, parameters); + // Sample a generator. + String generatorName = choose(scope, parameters); + CodeGenerator generator = scope.library().find(generatorName, " in selector"); + + // Dispatch via with new scope, but the same parameters. + Scope nestedScope = new Scope(scope, scope.fuel - generator.fuelCost); + generator.instantiate(nestedScope, parameters); + nestedScope.close(); + + // Add all generated code to the outer scope's stream. + scope.stream.addCodeStream(nestedScope.stream); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Template.java b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java new file mode 100644 index 0000000000000..4260632aec626 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java @@ -0,0 +1,647 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Templates are {@link CodeGenerator}s, which can be conveniently specified as template Strings. + * The template Strings can just be plain Strings, but they can also use additional + * functionalities. + *

+ * When using multiple templates, or the same template multiple times, the same variable + * name may be used in different template instantiations, which can lead to variable name + * collisions. For this, one can use the Template's variable renaming functionality, by + * prepending the dollar sign to a variable name, e.g. {@code $name}. In the instantiation + * the we append the unique {@link Parameters#instantiationID} to the variable name, which + * ensures that the resulting variable names are distinct. + *

+ * The String template may also contain template holes, either to be replaced with a + * parameter value or with a recursive {@link CodeGenerator} instantiation. The syntax + * for the parameter holes is {@code #{param}}, which is replaced with the String value + * of the parameter {@code param}. + *

+ * Recursive {@link CodeGenerator} instantiations can be specified with + * {@code #{replacement_name:generator_name}}. The {@link CodeGenerator} with the name + * {@code generator_name} is looked up in the {@link CodeGeneratorLibrary}, and is then + * instantiated. The resulting string is inserted into the hole. One can then repeat + * the same exact string with {@code #{replacement_name}}. Since repetition is not always + * used, one can omit the {@code replacement_name}, and simply write + * {@code #{:generator_name}}. + *

+ * Some {@link CodeGenerator}s require parameters, which can be specified in this form: + * {@code #{:generator_name(param1=value1,param2=value2)}}, where the String "param1" + * is passed for parameter "param1" and the String "value2" is passed for parameter + * "param2". One can also pass renamed variable names, or the replacement string of + * an earlier recursive {@link CodeGenerator} instantiation, or a parameter value: + * {@code #{:generator_name(param1=my_string,param2=$my_var,param3=#my_param,param4=#my_replacement)}}, + * where "my_string" is simply passed as this literal string, and for "$my_var" the + * renamed variable name (e.g. "my_var_42") is passed, and for "#param" the parameter + * value for the parameter "my_param" is passed, and for "#my_replacement" the + * replacement string for the earlier instantiation with the name "my_replacement" + * is passed. + *

+ * Variables from the {@link Template} can be passed to the recursive instantiations, + * by specifying them in a comma separated list: + * {@code #{:generator_name:var1,var2,var3}}, where the renamed variables "var1", + * "var2" and "var3" are passed to the recursive instantiation scope, where they + * can then be sampled via {@link Scope#sampleVariable}, or with recursive calls + * to {@link CodeGenerator}s that wrap this functionality, such as + * {@code load(type=type_name)} or {@code store(type=type_name)}. + *

+ * One can specify the beginning and end of class bodies and method bodies with + * {@code #open(class)}, + * {@code #close(class)}, + * {@code #open(method)}, and + * {@code #close(method)}. + * This is useful for recursive {@link CodeGenerator}s which may want to add + * additional class fields or method variables to the respecitive scopes. + */ +public final class Template extends CodeGenerator { + + /** + * The default {@link fuelCost} for a {@link Template}. Fuel cost are used to guide + * the choice of recursive {@link CodeGenerator} instantiation, and ensure eventual + * termination at a recursive depth where the remaining fuel reaches zero. + */ + public static final int DEFAULT_FUEL_COST = 10; + + /** + * Match local variables: + * $name + */ + private static final String VARIABLE_PATTERN = "(\\$\\w+)"; + + /** + * Match local variable with type declaration: + * ${name:type} + * ${name:type:immutable} + */ + private static final String VARIABLE_WITH_TYPE_CHARS = "\\w:"; + private static final String VARIABLE_WITH_TYPE_PATTERN = "(\\$\\{[" + VARIABLE_WITH_TYPE_CHARS + "]+\\})"; + + /** + * Match replacements: + * #{name} + * #{name:generator} + * #{name:generator(arg1=v1,arg2=v2)} + * #{name:generator(arg1=$var,arg2=v2)} + * #{name:generator(arg1=#param,arg2=v2)} + * #{:generator} + * #{:generator:var1,var2,var3} + */ + private static final String KEY_VALUE_PATTERN = "\\w+=[\\$#]?[\\w\\|\\s-]*"; + private static final String KEY_VALUE_LIST_PATTERN = "(?:" + // open non-capturing group 1 + KEY_VALUE_PATTERN + + "(?:" + // open non-capturing group 1 + "," + KEY_VALUE_PATTERN + + ")*" + // Do 0.. times + ")?"; // Do 0 or 1 times. + private static final String GENERATOR_PATTERN = "(?:" + // open non-capturing group 1 + "\\w+" + // generator name + "(?:" + // open non-capturing group 2: "(args)" + "\\(" + + KEY_VALUE_LIST_PATTERN + + "\\)" + + ")?" + + ")?"; // Do 0 or 1 times. + private static final String VARIABLE_LIST_PATTERN = "(?:" + // open non-capturing group 1 + "\\$\\w+" + + "(?:" + // open non-capturing group 1 + ",\\$\\w+" + + ")*" + // Do 0.. times + ")?"; // Do 0 or 1 times. + private static final String REPLACEMENT_PATTERN = "(" + // capturing group + "#\\{" + + "\\w*" + + "(?:" + + ":" + + GENERATOR_PATTERN + + "(?:" + + ":" + + VARIABLE_LIST_PATTERN + + ")?" + + ")?" + + "\\}" + + ")"; + + /** + *Match newline + indentation: + */ + private static final String NEWLINE_AND_INDENTATION_PATTERN = "(\\n *)"; + + /** + * Scopes: + * #open(class) + * #close(class) + * #open(method) + * #close(method) + */ + private static final String SCOPE_PATTERN = "(#\\w+\\(\\w+\\))"; + + /** + * Match either variable or replacement or newline or scopes. + */ + private static final String ALL_PATTERNS = "" + + VARIABLE_PATTERN + + "|" + + VARIABLE_WITH_TYPE_PATTERN + + "|" + + REPLACEMENT_PATTERN + + "|" + + NEWLINE_AND_INDENTATION_PATTERN + + "|" + + SCOPE_PATTERN + + ""; + private static final Pattern PATTERNS = Pattern.compile(ALL_PATTERNS); + + private final String templateString; + + /** + * Create a new {@link Template}, with custom {@link fuelCost}. + * + * @param templateName Name of the template, can be used for lookup in the + * {@link CodeGeneratorLibrary} if the {@link Template} + * is added to a library. + * @param templateString The String specifying the content of the {@link Template}. + * @param fuelCost The {@link fuelCost} for the {@link Template}. + */ + public Template(String templateName, String templateString, int fuelCost) { + super(templateName, fuelCost); + this.templateString = templateString; + } + + /** + * Create a new {@link Template}, with default {@link fuelCost}. + * + * @param templateName Name of the template, can be used for lookup in the + * {@link CodeGeneratorLibrary} if the {@link Template} + * is added to a library. + * @param templateString The String specifying the content of the {@link Template}. + */ + public Template(String templateName, String templateString) { + this(templateName, templateString, DEFAULT_FUEL_COST); + } + + /** + * Package-private helper class, used to remember replacement strings from + * recursive {@link CodeGenerator}. In most cases, there is one such + * {@link ReplacementState} per instantiation. However, one can also share + * this state across the instantiation of multiple {@link Template}s, which + * means that a replacement in an earlier instantiation can be remembered + * in a later instantiation, see {@link TestClassInstantiator#Instantiator#generate}. + */ + static final class ReplacementState { + private HashMap replacementsMap; + + public ReplacementState() { + this.replacementsMap = new HashMap(); + } + + public void checkHasNot(String name, Scope scope, String templated) { + if (!name.equals("") && replacementsMap.containsKey(name)) { + scope.print(); + throw new TemplateFrameworkException("Template generator call is not the first use of " + name + + ". Got " + templated); + } + } + + public void put(String name, CodeStream stream) { + if (!name.equals("")) { + replacementsMap.put(name, stream); + } + } + + public CodeStream get(String name, Scope scope, String templated) { + if (name.equals("")) { + scope.print(); + throw new TemplateFrameworkException("Template syntax error. Got: " + templated); + } + if (!replacementsMap.containsKey(name)) { + scope.print(); + throw new TemplateFrameworkException("Template replacement error. '" + name + "' " + + "was neither parameter nor " + + "repeat of previous generator call: " + templated); + } + + return replacementsMap.get(name); + } + } + + /** + * Helper class for all states in an instantiation, including {@link Parameters}, + * {@link ReplacementState}, local variable names with their renamings, and the + * recursive {@link Scope}s inside the {@link Template}. + */ + private final class InstantiationState { + public final Scope templateScope; + public Scope currentScope; + public final Parameters parameters; + + /** + * Map local variable types, so we know them after their declaration. + */ + record TypeAndMutability(String type, boolean mutable) {} + private HashMap localVariables; + + public final ReplacementState replacementState; + + public InstantiationState(Scope scope, Parameters parameters, ReplacementState replacementState) { + this.templateScope = scope; + this.currentScope = scope; + this.parameters = parameters; + this.localVariables = new HashMap(); + this.replacementState = replacementState; + } + + /** + * Rename the variable names by appending the unique {@link Parameters#instantiationID}, + * to avoid variable name collisions. + */ + public String wrapVariable(String name, String templated) { + if (name.equals("")) { + currentScope.print(); + throw new TemplateFrameworkException("Template local variable cannot be empty string. Got: " + templated); + } + int id = parameters.instantiationID; + return name + "_" + id; + } + + public void registerVariable(String name, String type, boolean mutable) { + if (localVariables.containsKey(name)) { + currentScope.print(); + throw new TemplateFrameworkException("Template local variable with type declaration " + + "${" + name + ":" + type + "} was not the first use of the variable."); + } + localVariables.put(name, new TypeAndMutability(type, mutable)); + } + + public void registerVariable(String name) { + if (!localVariables.containsKey(name)) { + localVariables.put(name, null); + } + } + + public TypeAndMutability getVariable(String name) { + return localVariables.get(name); + } + + public void handleGeneratorCall(String name, + String generatorName, + Map parameterMap, + List variableList, + String templated) { + replacementState.checkHasNot(name, currentScope, templated); + + CodeGenerator generator = templateScope.library().find(generatorName, ", got " + templated); + + // Create nested scope, and add the new variables to it. + Scope nestedScope = new Scope(currentScope, currentScope.fuel - generator.fuelCost); + for (String variable : variableList) { + variable = wrapVariable(variable, templated); + TypeAndMutability typeAndMutability = getVariable(variable); + if (typeAndMutability == null) { + nestedScope.print(); + throw new TemplateFrameworkException("Template generator call error. Variable type declaration not found" + + " for variable " + variable + + ". For generator call " + templated); + } + nestedScope.addVariable(variable, typeAndMutability.type, typeAndMutability.mutable); + } + + Parameters parameters = new Parameters(parameterMap); + generator.instantiate(nestedScope, parameters); + nestedScope.close(); + + // Map replacement for later repeats. + replacementState.put(name, nestedScope.stream); + + // Add all generated code to the outer scope's stream. + currentScope.stream.addCodeStream(nestedScope.stream); + } + + /** + * Fetch earlier stream generated with the generator, and push it again. + */ + public void repeatReplacement(String name, String templated) { + CodeStream stream = replacementState.get(name, currentScope, templated); + currentScope.stream.addCodeStream(stream); + } + + public void openClassScope() { + ClassScope classScope = new ClassScope(currentScope, currentScope.fuel); + classScope.setDebugContext("inside template", null); + currentScope = classScope; + } + + public void closeClassScope() { + if (!(currentScope instanceof ClassScope)) { + currentScope.print(); + throw new TemplateFrameworkException("Template scope mismatch."); + } + Scope classScope = currentScope; + currentScope = classScope.parent; + classScope.stream.setIndentation(0); + classScope.close(); + currentScope.stream.addCodeStream(classScope.stream); + } + + public void openMethodScope() { + MethodScope methodScope = new MethodScope(currentScope, currentScope.fuel); + methodScope.setDebugContext("inside template", null); + currentScope = methodScope; + } + + public void closeMethodScope() { + if (!(currentScope instanceof MethodScope)) { + currentScope.print(); + throw new TemplateFrameworkException("Template scope mismatch."); + } + Scope methodScope = currentScope; + currentScope = methodScope.parent; + methodScope.stream.setIndentation(0); + methodScope.close(); + currentScope.stream.addCodeStream(methodScope.stream); + } + } + + /** + * Instantiate the {@link Template}. + * + * @param scope Scope into which the code is generated. + * @param parameters Provides the parameters for the instantiation, as well as a unique ID + * for identifier name generation (e.g. variable of method names). + */ + @Override + public void instantiate(Scope scope, Parameters parameters) { + ReplacementState replacementState = new ReplacementState(); + instantiate(scope, parameters, replacementState); + } + + /** + * Instantiate the {@link Template}, with a possibly shared {@link ReplacementState} to + * share replacements from recursive {@link CodeGenerator} calls between the instantiation + * of multiple {@link Template}s. + * + * @param scope Scope into which the code is generated. + * @param parameters Provides the parameters for the instantiation, as well as a unique ID$ + * for identifier name generation (e.g. variable of method names). + * @param replacementState Possibly shared replacement state. + */ + public void instantiate(Scope scope, Parameters parameters, ReplacementState replacementState) { + scope.setDebugContext(name, parameters); + InstantiationState state = new InstantiationState(scope, parameters, replacementState); + + // Parse the templateString, detect templated and nonTemplated segments. + Matcher matcher = PATTERNS.matcher(templateString); + int pos = 0; + while (matcher.find()) { + int start = matcher.start(); + int end = matcher.end(); + + // We have segments: [pos...start] [start...end] + // nonTemplated templated + String nonTemplated = templateString.substring(pos, start); + String templated = templateString.substring(start, end); + pos = end; + + // The nonTemplated code segment can simply be added. + state.currentScope.stream.addCodeToLine(nonTemplated); + + if (templated.startsWith("\n")) { + // Newline with indentation + int spaces = templated.length() - 1; + if (spaces % 4 != 0) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template non factor-of-4 indentation: " + spaces); + } + // Compute indentation relative to templateScope from spaces. + int indentation = spaces / 4 - state.currentScope.indentationFrom(state.templateScope); + // Empty lines can have zero spaces -> would lead to negative local indentation. + indentation = Math.max(0, indentation); + state.currentScope.stream.setIndentation(indentation); + state.currentScope.stream.addNewline(); + } else { + // The templated code needs to be analyzed and transformed or recursively generated. + handleTemplated(state, templated); + } + } + + // Cleanup: part after the last templated segments. + String nonTemplated = templateString.substring(pos); + state.currentScope.stream.addCodeToLine(nonTemplated); + + // Cleanup: revert any indentation + state.currentScope.stream.setIndentation(0); + } + + /** + * Helper method used during template parsing for instantiation, which handles all the + * template variable, parameter and replacement patterns. + */ + private void handleTemplated(InstantiationState state, String templated) { + if (templated.startsWith("${")) { + // Local variable with type declaration: ${name} or ${name:type} or ${name:type:immutable} + String[] parts = templated.substring(2, templated.length() - 1).split(":"); + if (parts.length > 3 || (parts.length == 3 && !parts[2].equals("immutable"))) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template local variable with type declaration should have format " + + "$name or ${name} or ${name:type} or ${name:type:immutable}, but got " + templated); + } + String name = state.wrapVariable(parts[0], templated); + if (parts.length == 1) { + state.registerVariable(name); + state.currentScope.stream.addCodeToLine(name); + return; + } + String type = parts[1]; + boolean mutable = parts.length == 2; // third position is "immutable" qualifier. + state.registerVariable(name, type, mutable); + state.currentScope.stream.addCodeToLine(name); + } else if (templated.startsWith("$")) { + // Local variable: $name + String name = state.wrapVariable(templated.substring(1), templated); + state.registerVariable(name); + state.currentScope.stream.addCodeToLine(name); + } else if (templated.startsWith("#{")) { + // Replacement: #{name:generator:variables} + String replacement = templated.substring(2, templated.length() - 1); + String[] parts = replacement.split(":"); + if (parts.length > 3) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template replacement syntax error. Should be " + + "#{name:generator:variables}, but got " + templated); + } + String name = parts[0]; + String generator = (parts.length > 1) ? parts[1] : ""; + String variables = (parts.length > 2) ? parts[2] : ""; + + // Can only have variables if there is a generator. + if (generator.equals("") && !variables.equals("")) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template replacement syntax error. Cannot have variables " + + "without generator. Usage: ${name:generator:variables}. Got " + + templated); + } + + // Check if it is a parameter. + if (!name.equals("")) { + String parameterValue = state.parameters.getOrNull(name); + if (parameterValue != null) { + // It is a parameter value. + if (!generator.equals("") || !variables.equals("")) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template replacement error: " + name + "is given as " + + "parameter with value " + parameterValue + ", so we " + + "cannot also define a generator. Got " + templated); + } + state.currentScope.stream.addCodeToLine(parameterValue); + return; + } + } + + // Recursive generator call. + if (!generator.equals("")) { + // Parse generator string. + int openPos = generator.indexOf('('); + int closePos = generator.indexOf(')'); + if ((openPos == -1) != (closePos == -1) || (closePos != -1 && closePos != generator.length() - 1)) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template replacement syntax error (generator brackets). " + + "Got: " + templated); + } + String generatorName = null; + String generatorArguments = null; + if (openPos == -1) { + generatorName = generator; + generatorArguments = ""; + } else { + generatorName = generator.substring(0, openPos); + generatorArguments = generator.substring(openPos + 1, generator.length() - 1); + } + if (generatorName.contains("(") || + generatorName.contains(")") || + generatorArguments.contains("(") || + generatorArguments.contains(")")) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template replacement syntax error (generator brackets). " + + "Generator name: " + generatorName + ". " + + "Generator arguments: " + generatorArguments + ". " + + "Found in: " + templated); + } + + // Arguments: + // arg=text + // arg=$var + // arg=#param + // arg=#repeat_replacement + Map parameterMap = parseKeyValuePairs(generatorArguments, state); + parameterMap = parameterMap.entrySet().stream().collect(Collectors.toMap( + e -> e.getKey(), + e -> { + String val = e.getValue(); + if (val.startsWith("$")) { + return state.wrapVariable(val.substring(1), e.getKey() + "=$" + val + " in " + templated); + } else if (val.startsWith("#")) { + String n = val.substring(1); + // Try parameter + String parameterValue = state.parameters.getOrNull(n); + if (parameterValue != null) { + return parameterValue; + } + // Else replacement + CodeStream repeatReplacement = state.replacementState.get(n, state.currentScope, templated); + return repeatReplacement.toString(); + } + return val; + } + )); + + // Pattern: "$v1,$v2,$v3" -> v1 v2 v3 + String[] variableArray = variables.equals("") ? new String[0] : variables.split(","); + List variableList = Arrays.stream(variableArray).map(s -> s.substring(1)).toList(); + state.handleGeneratorCall(name, generatorName, parameterMap, variableList, templated); + return; + } + + // Default case: Repeat an earlier replacement. + state.repeatReplacement(name, templated); + } else if (templated.startsWith("#")) { + // Scope: #open(method) + String[] parts = templated.substring(1,templated.length()-1).split("\\("); + String scopeAction = parts[0]; + String scopeKind = parts[1]; + + if (scopeAction.equals("open") && scopeKind.equals("class")) { + state.openClassScope(); + } else if (scopeAction.equals("close") && scopeKind.equals("class")) { + state.closeClassScope(); + } else if (scopeAction.equals("open") && scopeKind.equals("method")) { + state.openMethodScope(); + } else if (scopeAction.equals("close") && scopeKind.equals("method")) { + state.closeMethodScope(); + } else { + state.currentScope.print(); + throw new TemplateFrameworkException("Template scope syntax error. Got: " + templated); + } + } else { + state.currentScope.print(); + throw new TemplateFrameworkException("Template pattern not handled: " + templated); + } + } + + /** + * Helper method to parse a comma separated list of key-value pairs. + */ + private static HashMap parseKeyValuePairs(String pairs, InstantiationState state) { + HashMap map = new HashMap(); + if (!pairs.equals("")) { + for (String pair : pairs.split(",")) { + String[] parts = pair.split("=", -1); + if (parts.length != 2) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template syntax error in key value pairs. " + + "Got: " + pairs); + } + String key = parts[0]; + String val = parts[1]; + String oldVal = map.put(key, val); + if (oldVal != null) { + state.currentScope.print(); + throw new TemplateFrameworkException("Template syntax error in key value pairs. " + + "Duplicate of key " + key + ". " + + "Got: " + pairs); + } + } + } + return map; + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrameworkException.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrameworkException.java new file mode 100644 index 0000000000000..26c3a7409e8ea --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrameworkException.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * Exception thrown in the Template Framework. Most likely, the user is responsible for the failure. + */ +public final class TemplateFrameworkException extends RuntimeException { + + /** + * Exception thrown by the Template Framework. + * + * @param message Exception message, speficying where something went wrong. + */ + public TemplateFrameworkException(String message) { + super("Exception in Template Framework:" + System.lineSeparator() + message); + } + + /** + * Exception thrown by the Template Framework. + * + * @param message Exception message, speficying where something went wrong. + * @param e Nexted exception: the reason why this exception was thrown. + */ + public TemplateFrameworkException(String message, Throwable e) { + super("Exception in Template Framework:" + System.lineSeparator() + message, e); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TestClassInstantiator.java b/test/hotspot/jtreg/compiler/lib/template_framework/TestClassInstantiator.java new file mode 100644 index 0000000000000..4d415ee353650 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TestClassInstantiator.java @@ -0,0 +1,355 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.HashSet; + +/** + * The {@link TestClassInstantiator} is a utility class, which generates a class, and allows instantiating + * multiple {@link Template}s, for example in the static block, the main method, or further below in the + * test section. + * + * First create a {@link TestClassInstantiator}, then {@link add} one or multiple {@link Template}s to it + * and finally {@link instantiate} the class which generates a {@link String} from all generated code. + */ +public final class TestClassInstantiator { + private boolean isUsed = false; + private final BaseScope baseScope; + private final ClassScope classScope; + private final Scope staticsScope; + private final Scope mainScope; + private final MethodScope mainMethodScope; + + /** + * Create a new {@link TestClassInstantiator} for a specific class, using the {@link CodeGeneratorLibrary#standard}. + * + * @param packageName Name of the package for the class. + * @param className Name of the class. + */ + public TestClassInstantiator(String packageName, String className) { + this(packageName, className, null); + } + + /** + * Create a new {@link TestClassInstantiator} for a specific class, using the specified library. + * + * @param packageName Name of the package for the class. + * @param className Name of the class. + * @param codeGeneratorLibrary The library to be used for finding CodeGenerators in recursive instantiations. + */ + public TestClassInstantiator(String packageName, String className, CodeGeneratorLibrary codeGeneratorLibrary) { + this(packageName, className, codeGeneratorLibrary, new HashSet()); + } + + /** + * Create a new {@link TestClassInstantiator} for a specific class, using the specified library and + * with a set of classes to import. + * + * @param packageName Name of the package for the class. + * @param className Name of the class. + * @param codeGeneratorLibrary The library to be used for finding CodeGenerators in recursive instantiations. + * @param imports Set of imported classes. + */ + public TestClassInstantiator(String packageName, String className, CodeGeneratorLibrary codeGeneratorLibrary, HashSet imports) { + // Open the base scope. + baseScope = new BaseScope(codeGeneratorLibrary); + baseScope.setDebugContext("for TestClassInstantiator", null); + + // package + new Template("test_class_instantiator_package", + """ + package #{packageName}; + """ + ).where("packageName", packageName).instantiate(baseScope); + + // import + Template importTemplate = new Template("test_class_instantiator_import", + """ + import #{name}; + """ + ); + for (String name : imports) { + importTemplate.where("name", name).instantiate(baseScope); + } + + // Open class + new Template("test_class_instantiator_open", + """ + public class #{className} { + """ + ).where("className", className).instantiate(baseScope); + baseScope.stream.indent(); + + // Open the class scope + // Inside we have: + // - statics scope: add all the statics templates + // - main scope: add all the main templates + // - and all the testTemplates go directly into the classScope + classScope = new ClassScope(baseScope, baseScope.fuel); + classScope.setDebugContext("for TestClassInstantiator", null); + + staticsScope = new Scope(classScope, classScope.fuel); + staticsScope.setDebugContext("for TestClassInstantiator statics", null); + + mainScope = new Scope(classScope, classScope.fuel); + mainScope.setDebugContext("for TestClassInstantiator main", null); + new Template("test_class_instantiator_open_main", + """ + public static void main(String[] args) { + """ + ).instantiate(mainScope); + mainScope.stream.indent(); + + mainMethodScope = new MethodScope(mainScope, mainScope.fuel); + mainMethodScope.setDebugContext("inside main for TestClassInstantiator", null); + } + + /** + * Helper class for adding templates into the static, main or test block. + */ + public final class Instantiator { + private boolean isUsed = false; + private TestClassInstantiator parent; + private final Template staticTemplate = null; + private final Template mainTemplate = null; + private final Template testTemplate = null; + private final HashMap> parameterMap = new HashMap>(); + private int repeatCount = 1; + + Instantiator(TestClassInstantiator parent) { + this.parent = parent; + } + + /** + * Add templates to the static, main and test block of the {@link TestClassInstantiator} class. + * One can selectively provide a {@link Template} if one is to be added, or {@code null} if not. + * + * @param staticTemplate Template for static block, or {@code null} if not to be added. + * @param mainTemplate Template for main block, or {@code null} if not to be added. + * @param testTemplate Template for test block, or {@code null} if not to be added. + */ + public void add(Template staticTemplate, Template mainTemplate, Template testTemplate) { + if (isUsed) { + throw new TemplateFrameworkException("Repeated use of Instantiator not allowed."); + } + isUsed = true; + + ArrayList setOfParameters = parametersCrossProduct(); + for (Parameters p : setOfParameters) { + for (int i = 0; i < repeatCount; i++) { + // If we have more than 1 set, we must clone the parameters, so that we get a unique ID. + p = (i == 0) ? p : new Parameters(p.getParameterMap()); + generate(staticTemplate, mainTemplate, testTemplate, p); + } + } + } + + private ArrayList parametersCrossProduct() { + String[] keys = parameterMap.keySet().toArray(new String[0]); + ArrayList setOfParameters = new ArrayList(); + setOfParameters.add(new Parameters()); + return parametersCrossProduct(keys, 0, setOfParameters); + } + + private ArrayList parametersCrossProduct(String[] keys, int keysPos, ArrayList setOfParameters) { + if (keysPos == keys.length) { + return setOfParameters; + } + ArrayList newSet = new ArrayList(); + String key = keys[keysPos]; + List values = parameterMap.get(key); + for (Parameters pOld : setOfParameters) { + for (String v : values) { + Parameters p = new Parameters(pOld.getParameterMap()); + p.add(key, v); + newSet.add(p); + } + } + return parametersCrossProduct(keys, keysPos + 1, newSet); + } + + private void generate(Template staticTemplate, Template mainTemplate, Template testTemplate, + Parameters parameters) { + // The 3 instantiations share the same parameters, and the ReplacementState. This ensures + // that the variable names and replacements are shared among the 3 instantiations. + Template.ReplacementState replacementState = new Template.ReplacementState(); + if (staticTemplate != null) { + Scope staticsSubScope = new Scope(staticsScope, staticsScope.fuel); + staticTemplate.instantiate(staticsSubScope, parameters, replacementState); + staticsSubScope.stream.addNewline(); + staticsSubScope.close(); + staticsScope.stream.addCodeStream(staticsSubScope.stream); + } + + if (mainTemplate != null) { + Scope mainSubScope = new Scope(mainMethodScope, mainMethodScope.fuel); + mainTemplate.instantiate(mainSubScope, parameters, replacementState); + mainSubScope.close(); + mainMethodScope.stream.addCodeStream(mainSubScope.stream); + } + + if (testTemplate != null) { + Scope testScope = new Scope(classScope, classScope.fuel); + testTemplate.instantiate(testScope, parameters, replacementState); + testScope.stream.addNewline(); + testScope.close(); + classScope.stream.addCodeStream(testScope.stream); + } + } + + /** + * Add a parameter key-value pair. + * + * @param paramKey The name of the parameter. + * @param paramValue The value to be set. + * @return The Instantiator for chaining. + */ + public Instantiator where(String paramKey, String paramValue) { + if (parameterMap.containsKey(paramKey)) { + throw new TemplateFrameworkException("Duplicate parameter key: " + paramKey); + } + parameterMap.put(paramKey, Arrays.asList(paramValue)); + return this; + } + + /** + * Add a list of values for a given parameter name, creating an instantiation for every value. + * Note: if multiple {@link where} specify a list, then the cross-product of all these parameter + * sets is generated, and an instantiation is created for each. + * + * @param paramKey The name of the parameter. + * @param paramValues List of parameter values. + * @return The Instantiator for chaining. + */ + public Instantiator where(String paramKey, List paramValues) { + if (parameterMap.containsKey(paramKey)) { + throw new TemplateFrameworkException("Duplicate parameter key: " + paramKey); + } + parameterMap.put(paramKey, paramValues); + return this; + } + + /** + * Repeat every instantiation {@code repeatCount} times, which is useful to instantiate {@link Template}s + * with random components multiple times. + * + * @param repeatCount Number of times every instantiation is repeated. + * @return The Instantiator for chaining. + */ + public Instantiator repeat(int repeatCount) { + if (repeatCount < 2 || repeatCount > 1000) { + throw new TemplateFrameworkException("Bad repeat count: " + repeatCount + " should be 2..1000"); + } + if (this.repeatCount > 1) { + throw new TemplateFrameworkException("Repeat count already set."); + } + this.repeatCount = repeatCount; + return this; + } + } + + /** + * Add templates to the static, main and test block of the {@link TestClassInstantiator} class. + * One can selectively provide a {@link Template} if one is to be added, or {@code null} if not. + * + * @param staticTemplate Template for static block, or {@code null} if not to be added. + * @param mainTemplate Template for main block, or {@code null} if not to be added. + * @param testTemplate Template for test block, or {@code null} if not to be added. + */ + public void add(Template staticTemplate, Template mainTemplate, Template testTemplate) { + new Instantiator(this).add(staticTemplate, mainTemplate, testTemplate); + } + + /** + * Create an {@link Instantiator}, which already has a first parameter key-value pair. + * + * @param paramKey The name of the parameter. + * @param paramValue The value to be set. + * @return The Instantiator. + */ + public Instantiator where(String paramKey, String paramValue) { + return new Instantiator(this).where(paramKey, paramValue); + } + + /** + * Create an {@link Instantiator}, which already has a list of values for a parameter name. + * Note: if multiple {@link where} specify a list, then the cross-product of all these parameter + * sets is generated, and an instantiation is created for each. + * + * @param paramKey The name of the parameter. + * @param paramValues List of parameter values. + * @return The Instantiator for chaining. + */ + public Instantiator where(String paramKey, List paramValues) { + return new Instantiator(this).where(paramKey, paramValues); + } + + /** + * Repeat every instantiation {@code repeatCount} times, which is useful to instantiate {@link Template}s + * with random components multiple times. + * + * @param repeatCount Number of times every instantiation is repeated. + * @return The Instantiator for chaining. + */ + public Instantiator repeat(int repeatCount) { + return new Instantiator(this).repeat(repeatCount); + } + + /** + * Instantiate the class with all the added templates, and return all the generated code in a String. + * + * @return The {@link String} of generated code. + */ + public String instantiate() { + if (isUsed) { + throw new TemplateFrameworkException("Repeated use of Instantiator not allowed."); + } + + mainMethodScope.close(); + mainScope.stream.addCodeStream(mainMethodScope.stream); + + mainScope.stream.outdent(); + mainScope.stream.addNewline(); + mainScope.stream.addCodeToLine("}"); + mainScope.stream.addNewline(); + mainScope.stream.addNewline(); + mainScope.close(); + classScope.stream.prependCodeStream(mainScope.stream); + + staticsScope.close(); + classScope.stream.prependCodeStream(staticsScope.stream); + + classScope.close(); + baseScope.stream.addCodeStream(classScope.stream); + + baseScope.stream.outdent(); + baseScope.stream.addCodeToLine("}"); + baseScope.close(); + return baseScope.toString(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestCustomLibraryForClassFuzzing.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestCustomLibraryForClassFuzzing.java new file mode 100644 index 0000000000000..93d7bab6bdacd --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestCustomLibraryForClassFuzzing.java @@ -0,0 +1,267 @@ +/* + * 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 + * @summary Example with custom Library. We generate some random classes and use them. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.examples.TestCustomLibraryForClassFuzzing + */ + +package template_framework.examples; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Random; + +import jdk.test.lib.Utils; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +public class TestCustomLibraryForClassFuzzing { + private static final Random RANDOM = Utils.getRandomInstance(); + + 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()); + + // Compile the source file. + comp.compile(); + + // Object ret = p.xyz.InnerTest.main(); + Object ret = comp.invoke("p.xyz.InnerTest", "main", new Object[] {new String[0]}); + System.out.println("res: " + ret); + } + + public static record KField (String name, String type) {} + + public static class Klass { + public final String name; + public final Klass superKlass; + public final HashSet subKlasses; + public final ArrayList fields; + + public Klass(String name, Klass superKlass) { + this.name = name; + this.superKlass = superKlass; + this.subKlasses = new HashSet(); + if (superKlass != null) { + superKlass.subKlasses.add(this); + } + this.fields = new ArrayList(); + } + + private int countFields() { + int count = fields.size(); + if (superKlass != null) { + count += superKlass.countFields(); + } + return count; + } + + private KField pickField(int r) { + if (r < fields.size()) { + return fields.get(r); + } + return superKlass.pickField(r - fields.size()); + } + + public KField randomField() { + int r = RANDOM.nextInt(countFields()); + return pickField(r); + } + } + + static class KlassHierarchy { + private static int ID = 0; + + private final HashSet rootKlasses; + private final HashMap klasses; // for finding + private final ArrayList klassesList; // for sampling + + public KlassHierarchy() { + this.rootKlasses = new HashSet(); + this.klasses = new HashMap(); + this.klassesList = new ArrayList(); + } + + public Klass makeKlass(String superKlassName, Scope scope) { + if (superKlassName == null) { + Klass klass = new Klass("K" + (ID++) + "K", null); + rootKlasses.add(klass); + klasses.put(klass.name, klass); + klassesList.add(klass); + return klass; + } else { + Klass superKlass = find(superKlassName, scope); + Klass klass = new Klass(superKlassName + "_" + (ID++) + "K", superKlass); + klasses.put(klass.name, klass); + klassesList.add(klass); + return klass; + } + } + + public Klass find(String name, Scope scope) { + Klass klass = klasses.get(name); + if (klass == null) { + scope.print(); + throw new RuntimeException("Could not find klass " + name); + } + return klass; + } + + public String makeField(String klassName, String type, Scope scope) { + Klass klass = find(klassName, scope); + String name = "field" + (ID++); + klass.fields.add(new KField(name, type)); + return name; + } + + public Klass randomKlass() { + int r = RANDOM.nextInt(klassesList.size()); + return klassesList.get(r); + } + + public String randomField(String klassName, Scope scope) { + Klass klass = find(klassName, scope); + KField field = klass.randomField(); + return field.name; + } + } + + // Generate a source Java file as String + public static String generate() { + // Generate classes + KlassHierarchy hierarchy = new KlassHierarchy(); + + // Create a new instantiator with the custom Library. + CodeGeneratorLibrary customLibrary = createCustomLibrary(hierarchy); + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest", customLibrary); + + // Generate code for classes + // Note: we get a random num_klasses 1..9, and call my_base_klass that many times. + Template klassTemplate = new Template("my_klass", + """ + // KlassHierarchy with #{num_klasses:int_con(lo=1,hi=10)} base classes. + #{:repeat(call=my_base_klass,repeat=#num_klasses)} + """ + ); + instantiator.add(klassTemplate, null, null); + + // Generate tests + Template mainTemplate = new Template("my_main", + """ + $test(); + """ + ); + Template testTemplate = new Template("my_test", + """ + public static Object $test() { + // Allocate Object of klass #{klass:my_random_klass}. + #{klass} k = new #{klass}(); + // Set random field #{field:my_random_field(klass=#klass)} + k.#{field} = #{:int_con}; + return k; + } + """ + ); + instantiator.repeat(10).add(null, mainTemplate, testTemplate); + + // Collect everything into a String. + return instantiator.instantiate(); + } + + // We take the standard library, and add some more generators to it. + public static CodeGeneratorLibrary createCustomLibrary(KlassHierarchy hierarchy) { + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_base_klass", + """ + // $my_base_klass with #{num_sub_klasses:int_con(lo=0,hi=5)} sub classes. + public static class #{klass:my_new_klass} { + // Generate #{num_fields:int_con(lo=1,hi=4)} fields: + #{:repeat(call=my_define_new_field,repeat=#num_fields,klass=#klass)} + } + + #{:repeat(call=my_sub_klass,repeat=#num_sub_klasses,super=#klass)} + """ + )); + + codeGenerators.add(new Template("my_sub_klass", + """ + // $my_sub_klass + public static class #{klass:my_new_klass(super=#super)} extends #{super} { + // Generate #{num_fields:int_con(lo=0,hi=2)} fields: + #{:repeat(call=my_define_new_field,repeat=#num_fields,klass=#klass)} + } + """ + )); + + codeGenerators.add(new Template("my_define_new_field", + """ + // $my_define_new_field for #{klass} of type #{type:choose(from=int|long)}. + public #{type} #{:my_new_field(klass=#klass,type=#type)} = 0; + """ + )); + + // Creates a new klass. If "super" provided, it is a subklass of "super". Returns the name of the klass. + codeGenerators.add(new ProgrammaticCodeGenerator("my_new_klass", (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "super"); + String superKlassName = parameters.getOrNull("super"); + Klass klass = hierarchy.makeKlass(superKlassName, scope); + scope.stream.addCodeToLine(klass.name); + }, 0)); + + // Creates a new field in a klass, with the specified type. Returns the name of the field. + codeGenerators.add(new ProgrammaticCodeGenerator("my_new_field", (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "klass", "type"); + String klassName = parameters.get("klass", scope); + String type = parameters.get("type", scope); + String fieldName = hierarchy.makeField(klassName, type, scope); + scope.stream.addCodeToLine(fieldName); + }, 0)); + + codeGenerators.add(new ProgrammaticCodeGenerator("my_random_klass", (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope); // no agrs + Klass klass = hierarchy.randomKlass(); + scope.stream.addCodeToLine(klass.name); + }, 0)); + + codeGenerators.add(new ProgrammaticCodeGenerator("my_random_field", (Scope scope, Parameters parameters) -> { + parameters.checkOnlyHas(scope, "klass"); + String klassName = parameters.get("klass", scope); + String fieldName = hierarchy.randomField(klassName, scope); + scope.stream.addCodeToLine(fieldName); + }, 0)); + + return new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestInstantiationOfManyTests.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestInstantiationOfManyTests.java new file mode 100644 index 0000000000000..cd43f08feef6e --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestInstantiationOfManyTests.java @@ -0,0 +1,168 @@ +/* + * 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 + * @summary Example to demonstrate basic usage of TestClassInstantiator. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.examples.TestInstantiationOfManyTests + */ + +package template_framework.examples; + +import java.util.Arrays; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +public class TestInstantiationOfManyTests { + + 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()); + + // Compile the source file. + comp.compile(); + + // Object ret = p.xyz.InnerTest.main(); + Object ret = comp.invoke("p.xyz.InnerTest", "main", new Object[] {new String[0]}); + System.out.println("res: " + ret); + } + + // Generate a source Java file as String + public static String generate() { + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest"); + + Template staticsTemplate = new Template("my_example_statics", + """ + // $statics + private static String $PARAM1 = "#{param1}"; + """ + ); + Template mainTemplate = new Template("my_example_main", + """ + // $main + $test("#{param2}"); + """ + ); + Template testTemplate = new Template("my_example_test", + """ + // $test + public static void $test(String param2) { + System.out.println("$test #{param1} #{param2}"); + System.out.println("$test " + $PARAM1 + " " + param2); + if (!$PARAM1.equals("#{param1}") || !param2.equals("#{param2}")) { + throw new RuntimeException("Strings mismatched"); + } + } + """ + ); + + // 2 individual instantiations: + instantiator.where("param1", "abc") + .where("param2", "xyz") + .add(staticsTemplate, mainTemplate, testTemplate); + instantiator.where("param1", "def") + .where("param2", "pqr") + .add(staticsTemplate, mainTemplate, testTemplate); + + // Cross product with parameters, produces 9 individual instantiations: + instantiator.where("param1", Arrays.asList("aaa", "bbb", "ccc")) + .where("param2", Arrays.asList("xxx", "yyy", "zzz")) + .add(staticsTemplate, mainTemplate, testTemplate); + + // But we do not have to provide all 3 templates, we can also provide them + // selectively. + Template helloStaticTemplate = new Template("my_example_hello_static", + """ + // $hello_static + static { + System.out.println("$hello_static"); + } + """ + ); + instantiator.add(helloStaticTemplate, null, null); + + Template helloMainTemplate = new Template("my_example_hello_main", + """ + // $hello_main + System.out.println("$hello_main"); + """ + ); + instantiator.add(null, helloMainTemplate, null); + + // Define some method/function we would like to call from a few templates later: + Template helloFunctionTemplate = new Template("my_example_hello_function", + """ + public static void helloFunction(String txt) { + System.out.println("helloFunction: " + txt); + } + """ + ); + instantiator.add(null, null, helloFunctionTemplate); + + // Invoke that function with 3 different arguments. + Template callFunctionTemplate = new Template("my_example_call_function", + """ + // $call_function + helloFunction("#{param}"); + """ + ); + instantiator.where("param", Arrays.asList("hello A", "hello B", "hello C")) + .add(null, callFunctionTemplate, null); + + // Replacements are shared between templates of the same instantiation: + Template staticsTemplate2 = new Template("my_example_statics_2", + """ + // $statics2 + private static int $MY_CON = #{my_con:int_con}; + """ + ); + Template mainTemplate2 = new Template("my_example_main_2", + """ + // $main2 + $test(#{my_con}); + """ + ); + Template testTemplate2 = new Template("my_example_test_2", + """ + // $test2 + public static void $test(int myCon) { + if (myCon != #{my_con}) { + throw new RuntimeException("Replacements mismatch."); + } + } + """ + ); + // We instantiate these templates in 3 sets. Every set internally shares the + // replacements and variables. + instantiator.repeat(3).add(staticsTemplate2, mainTemplate2, testTemplate2); + + // Collect everything into a String. + return instantiator.instantiate(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestListOfOperators.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestListOfOperators.java new file mode 100644 index 0000000000000..9dc0478bc1752 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestListOfOperators.java @@ -0,0 +1,99 @@ +/* + * 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 + * @summary Example to test lists of operators. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.examples.TestListOfOperators + */ + +package template_framework.examples; + +import java.util.Arrays; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +public class TestListOfOperators { + + 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()); + + // Compile the source file. + comp.compile(); + + // Object ret = p.xyz.InnerTest.main(); + Object ret = comp.invoke("p.xyz.InnerTest", "main", new Object[] {new String[0]}); + System.out.println("res: " + ret); + } + + // Generate a source Java file as String + public static String generate() { + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest"); + + Template staticsTemplate = new Template("my_example_statics", + """ + // $statics + private static int $GOLD1 = $test(#{in1:int_con}); + private static int $GOLD2 = $test(#{in2:int_con}); + private static int $GOLD3 = $test(#{in3:int_con}); + """ + ); + Template mainTemplate = new Template("my_example_main", + """ + // $main + for (int i = 0; i < 10_000; i++) { + int $res1 = $test(#{in1}); + int $res2 = $test(#{in2}); + int $res3 = $test(#{in3}); + if ($res1 != $GOLD1 || + $res2 != $GOLD2 || + $res3 != $GOLD3) { + throw new RuntimeException("wrong result for $test"); + } + } + """ + ); + Template testTemplate = new Template("my_example_test", + """ + // $test + public static int $test(int in) { + return in #{OP} #{:int_con}; + } + """ + ); + + // Instantiate a set of the 3 templates for every operator. + instantiator.where("OP", Arrays.asList("+", "-", "*", "&", "|")) + .add(staticsTemplate, mainTemplate, testTemplate); + + // Collect everything into a String. + return instantiator.instantiate(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestParameters.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestParameters.java new file mode 100644 index 0000000000000..beb5269ade7f4 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestParameters.java @@ -0,0 +1,77 @@ +/* + * 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 + * @summary Example test with parameters. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.examples.TestParameters + */ + +package template_framework.examples; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +public class TestParameters { + + 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()); + + // Compile the source file. + comp.compile(); + + // Object ret = p.xyz.InnterTest.test(); + Object ret = comp.invoke("p.xyz.InnerTest", "test", new Object[] {}); + System.out.println("res: " + ret); + + // Check that the return value is the sum of the two parameters. + if ((42 + 7) != (int)ret) { + throw new RuntimeException("Unexpected result"); + } + } + + // Generate a source Java file as String + public static String generate() { + // Create a Template with two parameter holes. + Template template = new Template("my_example", + """ + package p.xyz; + + public class InnerTest { + public static int test() { + return #{param1} + #{param2}; + } + } + """ + ); + + // The two parameter holes are to be replaced with the provided values. + return template.where("param1", "42").where("param2", "7").instantiate(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestRandomCode.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestRandomCode.java new file mode 100644 index 0000000000000..d8fb01c1d4ff9 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestRandomCode.java @@ -0,0 +1,171 @@ +/* + * 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 + * @summary Generate random code with the library, demonstrating a simple Fuzzer. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.examples.TestRandomCode + */ + +package template_framework.examples; + +import java.util.HashSet; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +public class TestRandomCode { + + 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()); + + // Compile the source file. + comp.compile(); + + // InnerTest.main(); + int res = (int)comp.invoke("p.xyz.InnerTest", "main", new Object[] {}); + System.out.println("res: " + res); + } + + // Generate a source Java file as String + public static String generate() { + // Add some extra methods to the library. + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_empty","/* empty */", 0)); + + codeGenerators.add(new Template("my_split", + """ + #{:my_code} + #{:my_code}""", 10 + )); + + codeGenerators.add(new Template("my_int_loop", + """ + for (int ${i:int:immutable} = 0; $i < 100; $i++) { + #{:my_code:$i} + }""", 10 + )); + + codeGenerators.add(new Template("my_if", + """ + if (#{:my_expr(type=boolean)}) { + #{:my_code} + } else { + #{:my_code} + }""", 10 + )); + + codeGenerators.add(new Template("my_var", "#{:var(type=#type)}", 5)); + codeGenerators.add(new Template("my_add", "(#{:my_expr(type=#type)} + #{:my_expr(type=#type)})", 5)); + codeGenerators.add(new Template("my_sub", "(#{:my_expr(type=#type)} - #{:my_expr(type=#type)})", 5)); + codeGenerators.add(new Template("my_mul", "(#{:my_expr(type=#type)} * #{:my_expr(type=#type)})", 5)); + codeGenerators.add(new Template("my_and", "(#{:my_expr(type=#type)} & #{:my_expr(type=#type)})", 5)); + codeGenerators.add(new Template("my_or", "(#{:my_expr(type=#type)} | #{:my_expr(type=#type)})", 5)); + codeGenerators.add(new Template("my_xor", "(#{:my_expr(type=#type)} ^ #{:my_expr(type=#type)})", 5)); + + SelectorCodeGenerator.Predicate isNumber = (Scope scope, Parameters parameters) -> { + String type = parameters.get("type", scope); + return type.equals("int") || type.equals("long"); + }; + + SelectorCodeGenerator expression = new SelectorCodeGenerator("my_expr", "con"); + expression.add("con", 10); + expression.add("my_var", 20); + expression.add("my_add", 20, isNumber); + expression.add("my_sub", 20, isNumber); + expression.add("my_mul", 20, isNumber); + expression.add("my_and", 20); + expression.add("my_or", 20); + expression.add("my_xor", 20); + codeGenerators.add(expression); + + codeGenerators.add(new Template("my_assign", + """ + // Assignment with type #{type:choose(from=int|long|boolean)} + #{:mutable_var(type=#type)} = #{:my_expr(type=#type)}; + #{:my_code}""", 2 + )); + + codeGenerators.add(new Template("my_def_var", + """ + // def_var $var with type #{type:choose(from=int|long|boolean)} + // value #{value:con(type=#type)} + // #{:def_var(name=$var,prefix=#type,value=#value,type=#type)} dispatched + $var = #{:my_expr(type=#type)}; + #{:my_code}""", 2 + )); + + codeGenerators.add(new Template("my_static_prefix", "static #{type}")); + codeGenerators.add(new Template("my_def_field", + """ + // def_field $field with type #{type:choose(from=int|long|boolean)} + // value #{value:con(type=#type)} + // prefix #{prefix:my_static_prefix(type=#type)} + // #{:def_field(name=$field,prefix=#prefix,value=#value,type=#type)} dispatched + $field = #{:my_expr(type=#type)}; + #{:my_code}""", 2 + )); + + // This is the core of the random code generator: the selector picks a random template from above, + // and then those templates may call back recursively to this selector. + SelectorCodeGenerator selectorForCode = new SelectorCodeGenerator("my_code", "my_empty"); + selectorForCode.add("my_split", 10); + selectorForCode.add("my_int_loop", 10); + selectorForCode.add("my_if", 10); + selectorForCode.add("my_assign", 100); + selectorForCode.add("my_def_var", 50); + selectorForCode.add("my_def_field", 50); + codeGenerators.add(selectorForCode); + + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + + Template template = new Template("my_example", + """ + package p.xyz; + + public class InnerTest { + #open(class) + public static int main() { + #open(method) + // make sure we have at least 1 mutable variable per type. + int ${xi:int} = 0; + long ${xl:long} = 0; + boolean ${xb:boolean} = false; + // Add that variable to available variables, and call my_code. + #{:my_code:$xi,$xl,$xb} + return #{:my_expr(type=int):$xi,$xl,$xb}; + #close(method) + } + #close(class) + }""" + ); + return template.with(library).instantiate(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestRandomConstants.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestRandomConstants.java new file mode 100644 index 0000000000000..eeaf3b215ca75 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestRandomConstants.java @@ -0,0 +1,147 @@ +/* + * 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 + * @summary Example test with random constants in Template. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.examples.TestRandomConstants + */ + +package template_framework.examples; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +public class TestRandomConstants { + + 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()); + + // Compile the source file. + comp.compile(); + + // Object retI = p.xyz.InnterTest.testInt(); + Object retI = comp.invoke("p.xyz.InnerTest", "testInt", new Object[] {}); + System.out.println("retI: " + retI); + + // Object retL = p.xyz.InnterTest.testLong(); + Object retL = comp.invoke("p.xyz.InnerTest", "testLong", new Object[] {}); + System.out.println("retL: " + retL); + + // Object retBool = p.xyz.InnterTest.testBool(); + Object retBool = comp.invoke("p.xyz.InnerTest", "testBool", new Object[] {}); + System.out.println("retBool: " + retBool); + } + + // Generate a source Java file as String + public static String generate() { + Template template = new Template("my_example", + """ + package p.xyz; + + public class InnerTest { + public static int testInt() { + int $con0 = 123; + int $con1 = #{:int_con}; + int $con2 = #{:int_con(lo=0,hi=100)}; + int $con3 = #{:int_con(lo=0)}; + int $con4 = #{:int_con(hi=0)}; + + int $con5 = #{:int_con(lo=min_int)}; + int $con6 = #{:int_con(lo=max_int)}; + int $con7 = #{:int_con(hi=max_int)}; + + int $con8 = #{:con(type=int)}; + + if ($con0 != 123) { + throw new RuntimeException("$con0 was not 123"); + } + if ($con2 < 0 || -$con2 >= 100) { + throw new RuntimeException("$con2 was out of range"); + } + if ($con3 < 0) { + throw new RuntimeException("$con3 was not positive"); + } + if ($con4 >= 0) { + throw new RuntimeException("$con4 was not negative"); + } + + return $con0 + $con1 + $con2 + $con3 + $con4; + } + + public static long testLong() { + long $con0 = 123; + long $con1 = #{:long_con}; + long $con2 = #{:long_con(lo=0,hi=100)}; + long $con3 = #{:long_con(lo=0)}; + long $con4 = #{:long_con(hi=0)}; + + long $con5 = #{:long_con(lo=min_int)}; + long $con6 = #{:long_con(lo=max_int)}; + long $con7 = #{:long_con(hi=max_int)}; + + long $con8 = #{:long_con(lo=min_long)}; + long $con9 = #{:long_con(lo=max_long)}; + long $con10 = #{:long_con(hi=max_long)}; + + long $con11 = #{:con(type=long)}; + + if ($con0 != 123) { + throw new RuntimeException("$con0 was not 123"); + } + if ($con2 < 0 || -$con2 >= 100) { + throw new RuntimeException("$con2 was out of range"); + } + if ($con3 < 0) { + throw new RuntimeException("$con3 was not positive"); + } + if ($con4 >= 0) { + throw new RuntimeException("$con4 was not negative"); + } + + return $con0 + $con1 + $con2 + $con3 + $con4 + $con11; + } + + public static boolean testBool() { + boolean $con0 = true; + boolean $con1 = #{:boolean_con}; + boolean $con2 = #{:con(type=boolean)}; + + if ($con0 != true) { + throw new RuntimeException("$con0 was not true"); + } + + return $con0 && $con1 && $con2; + } + } + """ + ); + return template.instantiate(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithGeneratorsIRAndVerify.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithGeneratorsIRAndVerify.java new file mode 100644 index 0000000000000..55913738d547a --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestWithGeneratorsIRAndVerify.java @@ -0,0 +1,152 @@ +/* + * 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 + * @summary Example of a "best practice" test, using Generators, the IR Framework and Verify. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @compile ../../../compiler/lib/ir_framework/TestFramework.java + * @compile ../../../compiler/lib/generators/Generators.java + * @compile ../../../compiler/lib/verify/Verify.java + * @run driver template_framework.examples.TestWithGeneratorsIRAndVerify + */ + +package template_framework.examples; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.*; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * This is a basic IR verification test, in combination with Generators for random input generation + * and Verify for output verification. + *

+ * The "@compile" command for JTREG is required so that the frameworks used in the Template code + * are compiled and available for the Test-VM. + *

+ * Additionally, we must set the classpath for the Test-VM, so that it has access to all compiled + * classes (see {@link CompileFramework#getEscapedClassPathOfCompiledClasses}). + */ +public class TestWithGeneratorsIRAndVerify { + + 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(); + + // Object ret = p.xyz.InnerTest.main(); + Object ret = comp.invoke("p.xyz.InnerTest", "main", new Object[] {new String[0]}); + System.out.println("res: " + ret); + } + + // Generate a source Java file as String + public static String generate(CompileFramework comp) { + // We need to import all the used classes. + HashSet imports = new HashSet(); + imports.add("compiler.lib.ir_framework.*"); + imports.add("compiler.lib.generators.*"); + imports.add("compiler.lib.verify.*"); + CodeGeneratorLibrary library = CodeGeneratorLibrary.standard(); + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest", library, imports); + + // Definie the main method body, where we laungh the IR tests from. It is imporant + // that we pass the classpath to the Test-VM, so that it has access to all compiled + // classes. + Template mainTemplate = new Template("my_example_main", + """ + TestFramework framework = new TestFramework(InnerTest.class); + framework.addFlags("-classpath", "#{classpath}"); + framework.start(); + """ + ); + instantiator.where("classpath", comp.getEscapedClassPathOfCompiledClasses()) + .add(null, mainTemplate, null); + + // We define a Test-Template: + // - static fields for inputs: INPUT_A and INPUT_B + // - Data generated with Generators and template holes (int_con). + // - GOLD value precomputed with dedicated call to test. + // - This ensures that the GOLD value is computed in the interpreter + // most likely, since the test method is not yet compiled. + // This allows us later to compare to the results of the compiled + // code. + // The input data is cloned, so that the original INPUT_A is never + // modified and can serve as identical input in later calls to test. + // - In the Setup method, we clone the input data, since the input data + // could be modified inside the test method. + // - The test method can further make use of template holes, e.g. + // recursive calls to other generators (e.g. int_con) or holes that + // are filled with parameter values (e.g. OP). + // - The Check method verifies the results of the test method with the + // GOLD value. + Template testTemplate = new Template("my_example_test", + """ + // $test with length #{size:int_con(lo=10000,hi=20000)} + private static int[] $INPUT_A = new int[#{size}]; + static { + Generators.G.fill(Generators.G.ints(), $INPUT_A); + } + + private static int $INPUT_B = #{:int_con}; + private static Object $GOLD = $test($INPUT_A.clone(), $INPUT_B); + + @Setup + public static Object[] $setup() { + // Must make sure to clone input arrays, if it is mutated in the test. + return new Object[] {$INPUT_A.clone(), $INPUT_B}; + } + + @Test + @Arguments(setup = "$setup") + public static Object $test(int[] a, int b) { + for (int i = 0; i < a.length; i++) { + int con = #{:int_con(lo=1,hi=max_int)}; + a[i] = (a[i] * con) #{OP} b; + } + return a; + } + + @Check(test = "$test") + public static void $check(Object result) { + Verify.checkEQ(result, $GOLD); + } + """ + ); + + // Instantiate the test twice (repeat 2) for each operator OP. + instantiator.where("OP", Arrays.asList("+", "-", "*", "&", "|")) + .repeat(2) + .add(null, null, testTemplate); + + // Collect everything into a String. + return instantiator.instantiate(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java new file mode 100644 index 0000000000000..4665c5ddc36b7 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java @@ -0,0 +1,1036 @@ +/* + * 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 + * @summary Test some basic Template instantiations. We do not necessarily generate correct + * java code, we just test that the code generation deterministically creates the + * expected String. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run driver template_framework.tests.TestTemplate + */ + +package template_framework.tests; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; + +import jdk.test.lib.Utils; + +import compiler.lib.template_framework.*; +import compiler.lib.compile_framework.*; + +public class TestTemplate { + private static final Random RANDOM = Utils.getRandomInstance(); + + public static void main(String[] args) { + testSingleLine(); + testMultiLine(); + testMultiLineWithParameters(); + testCustomLibrary(); + testClassInstantiator(); + testRepeat(); + testDispatch(); + testClassInstantiatorAndDispatch(); + testChoose(); + testFieldsAndVariables(); + testFieldsAndVariablesDispatch(); + testIntCon(); + testLongCon(); + testFuel(); + testRecursiveCalls(); + } + + public static void testSingleLine() { + Template template = new Template("my_template","Hello World!"); + String code = template.instantiate(); + checkEQ(code, "Hello World!"); + } + + public static void testMultiLine() { + Template template = new Template("my_template", + """ + Code on more + than a single line + """ + ); + String code = template.instantiate(); + String expected = + """ + Code on more + than a single line + """; + checkEQ(code, expected); + } + + public static void testMultiLineWithParameters() { + Template template = new Template("my_template","start #{p1} #{p2} end"); + checkEQ(template.where("p1", "x").where("p2", "y").instantiate(), "start x y end"); + checkEQ(template.where("p1", "a").where("p2", "b").instantiate(), "start a b end"); + checkEQ(template.where("p1", "").where("p2", "").instantiate(), "start end"); + } + + public static void testCustomLibrary() { + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_generator_1", "proton")); + + codeGenerators.add(new Template("my_generator_2", + """ + electron #{param1} + neutron #{param2} + """ + )); + + codeGenerators.add(new Template("my_generator_3", + """ + Universe #{:my_generator_1} { + #{:my_generator_2(param1=up,param2=down)} + #{:my_generator_2(param1=#param1,param2=#param2)} + } + """ + )); + CodeGeneratorLibrary library = new CodeGeneratorLibrary(null, codeGenerators); + + Template template = new Template("my_template", + """ + #{:my_generator_3(param1=low,param2=high)} + { + #{:my_generator_3(param1=42,param2=24)} + } + """ + ); + + String code = template.with(library).instantiate(); + String expected = + """ + Universe proton { + electron up + neutron down + + electron low + neutron high + + } + + { + Universe proton { + electron up + neutron down + + electron 42 + neutron 24 + + } + + } + """; + checkEQ(code, expected); + } + + public static void testClassInstantiator() { + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest"); + + Template staticsTemplate = new Template("my_example_statics", + """ + static #{param1} #{param2} + """ + ); + Template mainTemplate = new Template("my_example_main", + """ + main #{param1} #{param2} + """ + ); + Template testTemplate = new Template("my_example_test", + """ + test #{param1} #{param2} + """ + ); + + instantiator.where("param1", "abc") + .where("param2", "xyz") + .add(staticsTemplate, mainTemplate, testTemplate); + + instantiator.where("param1", Arrays.asList("aaa", "bbb")) + .where("param2", Arrays.asList("xxx", "yyy")) + .add(staticsTemplate, mainTemplate, testTemplate); + + Template test2Template = new Template("my_example_test2", + """ + test2 #{param1} #{param2} + """ + ); + + instantiator.where("param1", "alice") + .where("param2", "bob") + .repeat(3) + .add(null, null, test2Template); + + String code = instantiator.instantiate(); + String expected = + """ + package p.xyz; + public class InnerTest { + static abc xyz + + static aaa xxx + + static aaa yyy + + static bbb xxx + + static bbb yyy + + public static void main(String[] args) { + main abc xyz + main aaa xxx + main aaa yyy + main bbb xxx + main bbb yyy + + } + + test abc xyz + + test aaa xxx + + test aaa yyy + + test bbb xxx + + test bbb yyy + + test2 alice bob + + test2 alice bob + + test2 alice bob + + }"""; + checkEQ(code, expected); + } + + public static void testRepeat() { + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_generator_1", + """ + test1 #{param1} #{param2} + """ + )); + + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + + Template template = new Template("my_template", + """ + #{:repeat(call=my_generator_1,repeat=5,param1=alpha,param2=beta)} + { + #{:repeat(call=my_generator_1,repeat=5,param1=gamma,param2=delta)} + } + """ + ); + + String code = template.with(library).instantiate(); + String expected = + """ + test1 alpha beta + test1 alpha beta + test1 alpha beta + test1 alpha beta + test1 alpha beta + + { + test1 gamma delta + test1 gamma delta + test1 gamma delta + test1 gamma delta + test1 gamma delta + + } + """; + checkEQ(code, expected); + } + + public static void testDispatch() { + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_generator", + """ + test1a #{param1} + test1b #{param2} + """ + )); + + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + + Template template = new Template("my_template", + """ + alpha + { + #open(class) + beta + gamma + { + eins + #open(method) + zwei + x#{:dispatch(scope=class,call=my_generator,param1=abc,param2=bcd)}x + y#{:dispatch(scope=class,call=my_generator,param1=cde,param2=def)}y + z#{:dispatch(scope=method,call=my_generator,param1=efg,param2=gfh)}z + w#{:dispatch(scope=method,call=my_generator,param1=fhi,param2=hij)}w + drei + #close(method) + } + delta + { + un + #open(method) + deux + x#{:dispatch(scope=class,call=my_generator,param1=123,param2=234)}x + y#{:dispatch(scope=class,call=my_generator,param1=345,param2=456)}y + z#{:dispatch(scope=method,call=my_generator,param1=567,param2=678)}z + w#{:dispatch(scope=method,call=my_generator,param1=789,param2=890)}w + troi + #close(method) + } + epsilon + x#{:dispatch(scope=class,call=my_generator,param1=xxx,param2=yyy)}x + y#{:dispatch(scope=class,call=my_generator,param1=zzz,param2=www)}y + { + monday + #open(class) + tuesday + x#{:dispatch(scope=class,call=my_generator,param1=bar,param2=foo)}x + y#{:dispatch(scope=class,call=my_generator,param1=alice,param2=bob)}y + { + uno + #open(method) + due + x#{:dispatch(scope=class,call=my_generator,param1=alef,param2=bet)}x + y#{:dispatch(scope=class,call=my_generator,param1=vet,param2=gimel)}y + z#{:dispatch(scope=method,call=my_generator,param1=dalet,param2=he)}z + w#{:dispatch(scope=method,call=my_generator,param1=vav,param2=zayin)}w + tre + #close(method) + quatro + } + wednesday + #close(class) + thursday + x#{:dispatch(scope=class,call=my_generator,param1=aaa,param2=sss)}x + y#{:dispatch(scope=class,call=my_generator,param1=ddd,param2=fff)}y + friday + } + zeta + eta + x#{:dispatch(scope=class,call=my_generator,param1=up,param2=down)}x + y#{:dispatch(scope=class,call=my_generator,param1=left,param2=right)}y + theta + #close(class) + } + """ + ); + + String code = template.with(library).instantiate(); + String expected = + """ + alpha + { + test1a abc + test1b bcd + + test1a cde + test1b def + + test1a 123 + test1b 234 + + test1a 345 + test1b 456 + + test1a xxx + test1b yyy + + test1a zzz + test1b www + + test1a aaa + test1b sss + + test1a ddd + test1b fff + + test1a up + test1b down + + test1a left + test1b right + + + beta + gamma + { + eins + test1a efg + test1b gfh + + test1a fhi + test1b hij + + + zwei + xx + yy + zz + ww + drei + + } + delta + { + un + test1a 567 + test1b 678 + + test1a 789 + test1b 890 + + + deux + xx + yy + zz + ww + troi + + } + epsilon + xx + yy + { + monday + test1a bar + test1b foo + + test1a alice + test1b bob + + test1a alef + test1b bet + + test1a vet + test1b gimel + + + tuesday + xx + yy + { + uno + test1a dalet + test1b he + + test1a vav + test1b zayin + + + due + xx + yy + zz + ww + tre + + quatro + } + wednesday + + thursday + xx + yy + friday + } + zeta + eta + xx + yy + theta + + } + """; + checkEQ(code, expected); + } + + public static void testClassInstantiatorAndDispatch() { + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_generator", + """ + gen1 #{param1} + gen2 #{param2} + """ + )); + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest", library); + + Template staticsTemplate = new Template("my_example_statics", + """ + static #{param1} #{param2} + x#{:dispatch(scope=class,call=my_generator,param1=#param1,param2=123)}x + """ + ); + Template mainTemplate = new Template("my_example_main", + """ + main #{param1} #{param2} + x#{:dispatch(scope=class,call=my_generator,param1=234,param2=#param2)}x + y#{:dispatch(scope=method,call=my_generator,param1=foo,param2=bar)}y + """ + ); + Template testTemplate = new Template("my_example_test", + """ + test #{param1} #{param2} + x#{:dispatch(scope=class,call=my_generator,param1=uno,param2=#param1)}x + { + #open(method) + hello + x#{:dispatch(scope=class,call=my_generator,param1=due,param2=tre)}x + y#{:dispatch(scope=method,call=my_generator,param1=quatro,param2=chinque)}y + world + #close(method) + } + """ + ); + + instantiator.where("param1", "abc") + .where("param2", "xyz") + .add(staticsTemplate, mainTemplate, testTemplate); + + String code = instantiator.instantiate(); + String expected = + """ + package p.xyz; + public class InnerTest { + gen1 abc + gen2 123 + + gen1 234 + gen2 xyz + + gen1 uno + gen2 abc + + gen1 due + gen2 tre + + static abc xyz + xx + + public static void main(String[] args) { + gen1 foo + gen2 bar + + main abc xyz + xx + yy + + } + + test abc xyz + xx + { + gen1 quatro + gen2 chinque + + + hello + xx + yy + world + + } + + }"""; + checkEQ(code, expected); + } + + public static void testChoose() { + Template template = new Template("my_template", + """ + x#{v1:choose(from=11|11|11)}x#{v1}x + x#{v2:choose(from=)}x#{v2}x + x#{v3:choose(from=abc)}x#{v3}x + """ + ); + String code = template.instantiate(); + String expected = + """ + x11x11x + xxx + xabcxabcx + """; + checkEQ(code, expected); + } + + public static void testFieldsAndVariables() { + // We use dummy types like "my_int_1" etc. to make sure we have exactly 1 valid + // option, so that we get a deterministic output string from the instantiation. + // + // We generate 2 variables for "my_int_2", but only "hardCoded2" is mutable. + // Same for "my_int_4". + // + // "hardCoded7" is added to "my_int_4", which has also variables declared in the + // method test, but should not be available outside method test. + Template template = new Template("my_template", + """ + public class XYZ { + #open(class) + public static int hardCoded1 = 1; + #{:add_var(scope=class,name=hardCoded1,type=my_int_1)} + public static int hardCoded2 = 1; + public static final int hardCoded3 = 1; + #{:add_var(scope=class,name=hardCoded2,type=my_int_2)} + #{:add_var(scope=class,name=hardCoded3,type=my_int_2,mutable=false)} + + static void test() { + #open(method) + int hardCoded4 = 1; + #{:add_var(scope=method,name=hardCoded4,type=my_int_3)} + int hardCoded5 = 1; + final int hardCoded6 = 1; + #{:add_var(scope=method,name=hardCoded5,type=my_int_4)} + #{:add_var(scope=method,name=hardCoded6,type=my_int_4,mutable=false)} + + #{:mutable_var(type=my_int_1)} = #{:var(type=my_int_1)} + 1; + #{:mutable_var(type=my_int_2)} = 5; + + #{:mutable_var(type=my_int_3)} = #{:var(type=my_int_3)} + 1; + #{:mutable_var(type=my_int_4)} = 5; + + #close(method) + } + + public static int hardCoded0 = 1; + #{:add_var(scope=class,name=hardCoded0,type=my_int_4)} + + static void foo() { + #open(method) + int hardCoded7 = 1; + #{:add_var(scope=method,name=hardCoded7,type=my_int_5)} + int hardCoded8 = 1; + final int hardCoded9 = 1; + #{:add_var(scope=method,name=hardCoded8,type=my_int_6)} + #{:add_var(scope=method,name=hardCoded9,type=my_int_6,mutable=false)} + + #{:mutable_var(type=my_int_5)} = #{:var(type=my_int_5)} + 1; + #{:mutable_var(type=my_int_6)} = 5; + + #{:mutable_var(type=my_int_4)} = #{:var(type=my_int_4)} + 1; + #close(method) + } + #close(class) + } + """ + ); + String code = template.instantiate(); + String expected = + """ + public class XYZ { + + public static int hardCoded1 = 1; + + public static int hardCoded2 = 1; + public static final int hardCoded3 = 1; + + + + static void test() { + + int hardCoded4 = 1; + + int hardCoded5 = 1; + final int hardCoded6 = 1; + + + + hardCoded1 = hardCoded1 + 1; + hardCoded2 = 5; + + hardCoded4 = hardCoded4 + 1; + hardCoded5 = 5; + + + } + + public static int hardCoded0 = 1; + + + static void foo() { + + int hardCoded7 = 1; + + int hardCoded8 = 1; + final int hardCoded9 = 1; + + + + hardCoded7 = hardCoded7 + 1; + hardCoded8 = 5; + + hardCoded0 = hardCoded0 + 1; + + } + + } + """; + checkEQ(code, expected); + } + + public static void testFieldsAndVariablesDispatch() { + Template template = new Template("my_template", + """ + public class XYZ { + #open(class) + // class body + + static void test() { + #open(method) + // method body + #{:_internal_def_var(name=var1,prefix=final int,value=1,type=my_int_1,mutable=false)} + int x1 = #{:var(type=my_int_1)} + 1; + #{:_internal_def_var(name=var2,prefix=int,value=2,type=my_int_1,mutable=true)} + #{:mutable_var(type=my_int_1)} += 2; + #{:def_immutable_var(name=var3,prefix=final int,value=3,type=my_int_2)} + int x2 = #{:var(type=my_int_2)} + 1; + #{:def_var(name=var4,prefix=int,value=4,type=my_int_2)} + #{:mutable_var(type=my_int_2)} += 2; + #{:def_immutable_field(name=field1,prefix=public static final int,value=5,type=my_int_3)} + int x3 = #{:var(type=my_int_3)} + 1; + #{:def_field(name=field2,prefix=public static int,value=6,type=my_int_3)} + #{:mutable_var(type=my_int_3)} += 2; + #close(method) + } + #close(class) + } + """ + ); + String code = template.instantiate(); + String expected = + """ + public class XYZ { + public static final int field1 = 5; + public static int field2 = 6; + + // class body + + static void test() { + final int var3 = 3; + int var4 = 4; + + // method body + final int var1 = 1; + int x1 = var1 + 1; + int var2 = 2; + var2 += 2; + + int x2 = var3 + 1; + + var4 += 2; + + int x3 = field1 + 1; + + field2 += 2; + + } + + } + """; + checkEQ(code, expected); + } + + public static void testIntCon() { + // To keep the result deterministic, we have to always keep hi=lo. + Template template = new Template("my_template", + """ + x#{c1:int_con(lo=min_int,hi=min_int)}x + x#{c2:int_con(lo=max_int,hi=max_int)}x + x#{c3:int_con(lo=42,hi=42)}x + x#{c4:int_con(lo=-2147483648,hi=-2147483648)}x + x#{c5:int_con(lo=2147483647,hi=2147483647)}x + y#{c3}y + x#{c6:int_con(lo=#param,hi=#param)}x + y#{param}y + """ + ); + String param = String.valueOf(RANDOM.nextInt()); + String code = template.where("param", param).instantiate(); + String expected = + """ + x-2147483648x + x2147483647x + x42x + x-2147483648x + x2147483647x + y42y + x""" + + param + + """ + x + y""" + + param + + """ + y + """; + checkEQ(code, expected); + } + + public static void testLongCon() { + // To keep the result deterministic, we have to always keep hi=lo. + Template template = new Template("my_template", + """ + x#{c1:long_con(lo=min_int,hi=min_int)}x + x#{c2:long_con(lo=max_int,hi=max_int)}x + x#{c3:long_con(lo=42,hi=42)}x + x#{c4:long_con(lo=-2147483648,hi=-2147483648)}x + x#{c5:long_con(lo=2147483647,hi=2147483647)}x + y#{c3}y + x#{c6:long_con(lo=#param1,hi=#param1)}x + y#{param1}y + x#{l1:long_con(lo=min_long,hi=min_long)}x + x#{l2:long_con(lo=max_long,hi=max_long)}x + x#{l4:long_con(lo=-9223372036854775808,hi=-9223372036854775808)}x + x#{l5:long_con(lo=9223372036854775807,hi=9223372036854775807)}x + x#{l6:long_con(lo=#param2,hi=#param2)}x + y#{param2}y + """ + ); + String param1 = String.valueOf(RANDOM.nextInt()); + String param2 = String.valueOf(RANDOM.nextLong()); + String code = template.where("param1", param1).where("param2", param2).instantiate(); + String expected = + """ + x-2147483648Lx + x2147483647Lx + x42Lx + x-2147483648Lx + x2147483647Lx + y42Ly + x""" + + param1 + + """ + Lx + y""" + + param1 + + """ + y + x-9223372036854775808Lx + x9223372036854775807Lx + x-9223372036854775808Lx + x9223372036854775807Lx + x""" + + param2 + + """ + Lx + y""" + + param2 + + """ + y + """; + checkEQ(code, expected); + } + + public static void testFuel() { + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_leaf", + "leaf" + )); + + // Default fuel is 50, the cost of this is set to 25 so we get 2 recursion levels. + codeGenerators.add(new Template("my_split", + """ + begin + #{:my_code} + mid + #{:my_code} + end""", 25 + )); + + // The selector picks my_split as long as there is enough fuel, and once there is no fuel left, it picks + // my_leaf. + SelectorCodeGenerator selector = new SelectorCodeGenerator("my_code", "my_leaf"); + selector.add("my_split", 100); + codeGenerators.add(selector); + + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + + Template template = new Template("my_template", + """ + #{:my_code} + """ + ); + + String code = template.with(library).instantiate(); + String expected = + """ + begin + begin + leaf + mid + leaf + end + mid + begin + leaf + mid + leaf + end + end + """; + checkEQ(code, expected); + } + + public static void testRecursiveCalls() { + // Create a new CompileFramework instance. + CompileFramework comp = new CompileFramework(); + + // Add a java source file. + comp.addJavaSourceCode("p.xyz.InnerTest", generateRecursiveCalls()); + + // Compile the source file. + comp.compile(); + + // int ret1 = InnerTest.test1(5); + int ret1 = (int)comp.invoke("p.xyz.InnerTest", "test1", new Object[] {5}); + if (ret1 != 5 * 3) { + throw new RuntimeException("Unexpected result: " + ret1); + } + + // int ret2 = InnerTest.test2(5); + int ret2 = (int)comp.invoke("p.xyz.InnerTest", "test2", new Object[] {5}); + if (ret2 != 5 * 7) { + throw new RuntimeException("Unexpected result: " + ret2); + } + + // int ret3 = InnerTest.test3(5); + int ret3 = (int)comp.invoke("p.xyz.InnerTest", "test3", new Object[] {5}); + if (ret3 != 5 + 11) { + throw new RuntimeException("Unexpected result: " + ret3); + } + + // int ret4 = InnerTest.test3(5); + int ret4 = (int)comp.invoke("p.xyz.InnerTest", "test4", new Object[] {5}); + if (ret4 != 5 - 13) { + throw new RuntimeException("Unexpected result: " + ret4); + } + } + + public static String generateRecursiveCalls() { + // First, we set up some additional code generators for the library. + HashSet codeGenerators = new HashSet(); + + codeGenerators.add(new Template("my_param_op_var", + "#{param} #{op} #{:var(type=#type)}" + )); + + codeGenerators.add(new Template("my_assign", + "#{:mutable_var(type=#type_dst)} = #{:var(type=#type_src)};" + )); + + CodeGeneratorLibrary library = new CodeGeneratorLibrary(CodeGeneratorLibrary.standard(), codeGenerators); + + // Now, we start generating code. + TestClassInstantiator instantiator = new TestClassInstantiator("p.xyz", "InnerTest", library); + + // test1(in) -> in * 3 + Template test1Template = new Template("my_test1", + """ + public static int test1(int in) { + return in * #{param}; + } + """ + ); + instantiator.where("param", "3").add(null, null, test1Template); + + // test2(in) -> in * 7 + // Register $in as a local variable, then pass it on with ":$in", + // and sample it as the only option inside my_param_op_var. + Template test2Template = new Template("my_test2", + """ + public static int test2(int ${in:my_int_2:immutable}) { + return #{:my_param_op_var(param=#param,op=#op,type=my_int_2):$in}; + } + """ + ); + instantiator.where("param", "7") + .where("op", "*") + .add(null, null, test2Template); + + // test3(in) -> in + 11 + // Register $in as local variable but don't pass it, so that only the field + // can be sampled inside my_param_op_var. + Template test3Template = new Template("my_test3", + """ + public static int test3(int ${in:my_int_3:immutable}) { + #{:def_immutable_field(name=$field3,prefix=public static int,value=#param,type=my_int_3)} + return #{:my_param_op_var(param=$in,op=#op,type=my_int_3)}; + } + """ + ); + instantiator.where("param", "11") + .where("op", "+") + .add(null, null, test3Template); + + // test4(in) -> in - 13 + // garbage4 should not be written to, but has the same type as field4a. + // field4a = 0 + // field4b = param + // field4a = field4b + Template test4Template = new Template("my_test4", + """ + public static int test4(int $in) { + #{:def_immutable_field(name=$garbage4,prefix=public static final int,value=0,type=my_int_4a)} + #{:def_field(name=$field4a,prefix=public static int,value=0,type=my_int_4a)} + #{:def_immutable_field(name=$field4b,prefix=public static final int,value=#param,type=my_int_4b)} + #{:my_assign(type_src=my_int_4b,type_dst=my_int_4a)} + return $in #{op} $field4a; + } + """ + ); + instantiator.where("param", "13") + .where("op", "-") + .add(null, null, test4Template); + + return instantiator.instantiate(); + } + + public static void checkEQ(String code, String expected) { + if (!code.equals(expected)) { + System.out.println("\"" + code + "\""); + System.out.println("\"" + expected + "\""); + throw new RuntimeException("Template instantiation mismatch!"); + } + } +}