From 248341d372ba9c1031729a65eb10d8def52de641 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 4 Jun 2025 13:16:24 +0000 Subject: [PATCH] 8344942: Template-Based Testing Framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tobias Hartmann Co-authored-by: Tobias Holenstein Co-authored-by: Theo Weidmann Co-authored-by: Roberto Castañeda Lozano Co-authored-by: Christian Hagedorn Co-authored-by: Manuel Hässig Reviewed-by: chagedorn, mhaessig, rcastanedalo --- .../lib/template_framework/AddNameToken.java | 26 + .../compiler/lib/template_framework/Code.java | 51 + .../lib/template_framework/CodeFrame.java | 156 ++ .../lib/template_framework/DataName.java | 227 ++ .../compiler/lib/template_framework/Hook.java | 100 + .../template_framework/HookAnchorToken.java | 28 + .../template_framework/HookInsertToken.java | 26 + .../compiler/lib/template_framework/Name.java | 50 + .../lib/template_framework/NameSet.java | 151 ++ .../lib/template_framework/NothingToken.java | 26 + .../compiler/lib/template_framework/README.md | 12 + .../lib/template_framework/Renderer.java | 437 ++++ .../template_framework/RendererException.java | 35 + .../lib/template_framework/StringToken.java | 26 + .../template_framework/StructuralName.java | 200 ++ .../lib/template_framework/Template.java | 844 ++++++ .../template_framework/TemplateBinding.java | 70 + .../lib/template_framework/TemplateBody.java | 34 + .../lib/template_framework/TemplateFrame.java | 99 + .../lib/template_framework/TemplateToken.java | 169 ++ .../lib/template_framework/Token.java | 78 + .../lib/template_framework/library/Hooks.java | 46 + .../examples/TestAdvanced.java | 162 ++ .../examples/TestSimple.java | 78 + .../examples/TestTutorial.java | 1227 +++++++++ .../template_framework/tests/TestFormat.java | 111 + .../tests/TestTemplate.java | 2253 +++++++++++++++++ 27 files changed, 6722 insertions(+) create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/Code.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/DataName.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/Hook.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/Name.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/README.md create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/RendererException.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/StringToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/Template.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/TemplateBinding.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/Token.java create mode 100644 test/hotspot/jtreg/compiler/lib/template_framework/library/Hooks.java create mode 100644 test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java create mode 100644 test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java create mode 100644 test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java create mode 100644 test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java create mode 100644 test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java new file mode 100644 index 00000000000..4f1f7e569bf --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/AddNameToken.java @@ -0,0 +1,26 @@ +/* + * 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; + +record AddNameToken(Name name) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Code.java b/test/hotspot/jtreg/compiler/lib/template_framework/Code.java new file mode 100644 index 00000000000..5806460ac88 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Code.java @@ -0,0 +1,51 @@ +/* + * 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; + +/** + * This class collects code, i.e. {@link String}s or {@link List}s of {@link String}s. + * All the {@link String}s are later collected in a {@link StringBuilder}. If we used a {@link StringBuilder} + * directly to collect the {@link String}s, we could not as easily insert code at an "earlier" position, i.e. + * reaching out to a {@link Hook#anchor}. + */ +sealed interface Code permits Code.Token, Code.CodeList { + + record Token(String s) implements Code { + @Override + public void renderTo(StringBuilder builder) { + builder.append(s); + } + } + + record CodeList(List list) implements Code { + @Override + public void renderTo(StringBuilder builder) { + list.forEach(code -> code.renderTo(builder)); + } + } + + void renderTo(StringBuilder builder); +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java b/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java new file mode 100644 index 00000000000..5c4ff55614f --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/CodeFrame.java @@ -0,0 +1,156 @@ +/* + * 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.ArrayList; +import java.util.List; + +/** + * The {@link CodeFrame} represents a frame (i.e. scope) of code, appending {@link Code} to the {@code 'codeList'} + * as {@link Token}s are rendered, and adding names to the {@link NameSet}s with {@link Template#addStructuralName}/ + * {@link Template#addDataName}. {@link Hook}s can be added to a frame, which allows code to be inserted at that + * location later. When a {@link Hook} is {@link Hook#anchor}ed, it separates the Template into an outer and inner + * {@link CodeFrame}, ensuring that names that are added inside the inner frame are only available inside that frame. + * + *

+ * On the other hand, each {@link TemplateFrame} represents the frame (or scope) of exactly one use of a + * Template. + * + *

+ * For simple Template nesting, the {@link CodeFrame}s and {@link TemplateFrame}s overlap exactly. + * However, when using {@link Hook#insert}, we simply nest {@link TemplateFrame}s, going further "in", + * but we jump to an outer {@link CodeFrame}, ensuring that we insert {@link Code} at the outer frame, + * and operating on the names of the outer frame. Once the {@link Hook#insert}ion is complete, we jump + * back to the caller {@link TemplateFrame} and {@link CodeFrame}. + */ +class CodeFrame { + public final CodeFrame parent; + private final List codeList = new ArrayList<>(); + private final Map hookCodeLists = new HashMap<>(); + + /** + * The {@link NameSet} is used for variable and fields etc. + */ + private final NameSet names; + + private CodeFrame(CodeFrame parent, boolean isTransparentForNames) { + this.parent = parent; + if (parent == null) { + // NameSet without any parent. + this.names = new NameSet(null); + } else if (isTransparentForNames) { + // We use the same NameSet as the parent - makes it transparent. + this.names = parent.names; + } else { + // New NameSet, to make sure we have a nested scope for the names. + this.names = new NameSet(parent.names); + } + } + + /** + * Creates a base frame, which has no {@link #parent}. + */ + public static CodeFrame makeBase() { + return new CodeFrame(null, false); + } + + /** + * Creates a normal frame, which has a {@link #parent} and which defines an inner + * {@link NameSet}, for the names that are generated inside this frame. Once this + * frame is exited, the name from inside this frame are not available anymore. + */ + public static CodeFrame make(CodeFrame parent) { + return new CodeFrame(parent, false); + } + + /** + * Creates a special frame, which has a {@link #parent} but uses the {@link NameSet} + * from the parent frame, allowing {@link Template#addDataName}/ + * {@link Template#addStructuralName} to persist in the outer frame when the current frame + * is exited. This is necessary for {@link Hook#insert}, where we would possibly want to + * make field or variable definitions during the insertion that are not just local to the + * insertion but affect the {@link CodeFrame} that we {@link Hook#anchor} earlier and are + * now {@link Hook#insert}ing into. + */ + public static CodeFrame makeTransparentForNames(CodeFrame parent) { + return new CodeFrame(parent, true); + } + + void addString(String s) { + codeList.add(new Code.Token(s)); + } + + void addCode(Code code) { + codeList.add(code); + } + + void addHook(Hook hook) { + if (hasHook(hook)) { + // This should never happen, as we add a dedicated CodeFrame for each hook. + throw new RuntimeException("Internal error: Duplicate Hook in CodeFrame: " + hook.name()); + } + hookCodeLists.put(hook, new Code.CodeList(new ArrayList<>())); + } + + private boolean hasHook(Hook hook) { + return hookCodeLists.containsKey(hook); + } + + CodeFrame codeFrameForHook(Hook hook) { + CodeFrame current = this; + while (current != null) { + if (current.hasHook(hook)) { + return current; + } + current = current.parent; + } + return null; + } + + void addName(Name name) { + names.add(name); + } + + Name sampleName(NameSet.Predicate predicate) { + return names.sample(predicate); + } + + int countNames(NameSet.Predicate predicate) { + return names.count(predicate); + } + + boolean hasAnyNames(NameSet.Predicate predicate) { + return names.hasAny(predicate); + } + + List listNames(NameSet.Predicate predicate) { + return names.toList(predicate); + } + + Code getCode() { + return new Code.CodeList(codeList); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java b/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java new file mode 100644 index 00000000000..f45a4db8a1e --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/DataName.java @@ -0,0 +1,227 @@ +/* + * 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; + +/** + * {@link DataName}s represent things like fields and local variables, and can be added to the local + * scope with {@link Template#addDataName}, and accessed with {@link Template#dataNames}, to + * count, list or even sample random {@link DataName}s. Every {@link DataName} has a {@link DataName.Type}, + * so that sampling can be restricted to these types. + * + *

+ * For method and class names and alike, there are the analogous {@link StructuralName}s. + * + * @param name The {@link String} name used in code. + * @param type The type of the {@link DataName}. + * @param mutable Defines if the {@link DataName} is considered mutable or immutable. + * @param weight The weight of the {@link DataName}, it corresponds to the probability of choosing this + * {@link DataName} when sampling later on. + */ +public record DataName(String name, DataName.Type type, boolean mutable, int weight) implements Name { + + /** + * {@link Mutability} defines the possible states of {@link DataName}s, or the + * desired state when filtering. + */ + public enum Mutability { + /** + * Used for mutable fields and variables, i.e. writing is allowed. + */ + MUTABLE, + /** + * Used for immutable fields and variables, i.e. writing is not allowed, + * for example because the field or variable is final. + */ + IMMUTABLE, + /** + * When filtering, we sometimes want to indicate that we accept + * mutable and immutable fields and variables, for example when + * we are only reading, the mutability state does not matter. + */ + MUTABLE_OR_IMMUTABLE + } + + /** + * Creates a new {@link DataName}. + */ + public DataName { + } + + /** + * The interface for the type of a {@link DataName}. + */ + public interface Type extends Name.Type { + /** + * The name of the type, that can be used in code. + * + * @return The {@link String} representation of the type, that can be used in code. + */ + String name(); + + /** + * Defines the subtype relationship with other types, which is used to filter {@link DataName}s + * in {@link FilteredSet#exactOf}, {@link FilteredSet#subtypeOf}, and {@link FilteredSet#supertypeOf}. + * + * @param other The other type, where we check if it is the supertype of {@code 'this'}. + * @return If {@code 'this'} is a subtype of {@code 'other'}. + */ + boolean isSubtypeOf(DataName.Type other); + } + + /** + * The {@link FilteredSet} represents a filtered set of {@link DataName}s in the current scope. + * It can be obtained with {@link Template#dataNames}. It can be used to count the + * available {@link DataName}s, or sample a random {@link DataName} according to the + * weights of the {@link DataName}s in the filtered set. + * Note: The {@link FilteredSet} is only a filtered view on the set of {@link DataName}s, + * and may return different results in different contexts. + */ + public static final class FilteredSet { + private final Mutability mutability; + private final DataName.Type subtype; + private final DataName.Type supertype; + + FilteredSet(Mutability mutability, DataName.Type subtype, DataName.Type supertype) { + this.mutability = mutability; + this.subtype = subtype; + this.supertype = supertype; + } + + FilteredSet(Mutability mutability) { + this(mutability, null, null); + } + + NameSet.Predicate predicate() { + if (subtype == null && supertype == null) { + throw new UnsupportedOperationException("Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); + } + return (Name name) -> { + if (!(name instanceof DataName dataName)) { return false; } + if (mutability == Mutability.MUTABLE && !dataName.mutable()) { return false; } + if (mutability == Mutability.IMMUTABLE && dataName.mutable()) { return false; } + if (subtype != null && !dataName.type().isSubtypeOf(subtype)) { return false; } + if (supertype != null && !supertype.isSubtypeOf(dataName.type())) { return false; } + return true; + }; + } + + /** + * Create a {@link FilteredSet}, where all {@link DataName}s must be subtypes of {@code type}. + * + * @param type The type of which all {@link DataName}s must be subtypes of. + * @return The updated filtered set. + * @throws UnsupportedOperationException If this {@link FilteredSet} was already filtered with + * {@link #subtypeOf} or {@link #exactOf}. + */ + public FilteredSet subtypeOf(DataName.Type type) { + if (subtype != null) { + throw new UnsupportedOperationException("Cannot constrain to subtype " + type + ", is already constrained: " + subtype); + } + return new FilteredSet(mutability, type, supertype); + } + + /** + * Create a {@link FilteredSet}, where all {@link DataName}s must be supertypes of {@code type}. + * + * @param type The type of which all {@link DataName}s must be supertype of. + * @return The updated filtered set. + * @throws UnsupportedOperationException If this {@link FilteredSet} was already filtered with + * {@link #supertypeOf} or {@link #exactOf}. + */ + public FilteredSet supertypeOf(DataName.Type type) { + if (supertype != null) { + throw new UnsupportedOperationException("Cannot constrain to supertype " + type + ", is already constrained: " + supertype); + } + return new FilteredSet(mutability, subtype, type); + } + + /** + * Create a {@link FilteredSet}, where all {@link DataName}s must be of exact {@code type}, + * hence it must be both subtype and supertype thereof. + * + * @param type The type of which all {@link DataName}s must be. + * @return The updated filtered set. + * @throws UnsupportedOperationException If this {@link FilteredSet} was already filtered with + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public FilteredSet exactOf(DataName.Type type) { + return subtypeOf(type).supertypeOf(type); + } + + /** + * Samples a random {@link DataName} from the filtered set, according to the weights + * of the contained {@link DataName}s. + * + * @return The sampled {@link DataName}. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + * @throws RendererException If the set was empty. + */ + public DataName sample() { + DataName n = (DataName)Renderer.getCurrent().sampleName(predicate()); + if (n == null) { + String msg1 = (subtype == null) ? "" : ", subtypeOf(" + subtype + ")"; + String msg2 = (supertype == null) ? "" : ", supertypeOf(" + supertype + ")"; + throw new RendererException("No variable: " + mutability + msg1 + msg2 + "."); + } + return n; + } + + /** + * Counts the number of {@link DataName}s in the filtered set. + * + * @return The number of {@link DataName}s in the filtered set. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public int count() { + return Renderer.getCurrent().countNames(predicate()); + } + + /** + * Checks if there are any {@link DataName}s in the filtered set. + * + * @return Returns {@code true} iff there is at least one {@link DataName} in the filtered set. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public boolean hasAny() { + return Renderer.getCurrent().hasAnyNames(predicate()); + } + + /** + * Collects all {@link DataName}s in the filtered set. + * + * @return A {@link List} of all {@link DataName}s in the filtered set. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public List toList() { + List list = Renderer.getCurrent().listNames(predicate()); + return list.stream().map(n -> (DataName)n).toList(); + } + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java b/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java new file mode 100644 index 00000000000..48f7852d509 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Hook.java @@ -0,0 +1,100 @@ +/* + * 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 Hook}s can be {@link #anchor}ed for a certain scope in a Template, and all nested + * Templates in this scope, and then from within this scope, any Template can + * {@link #insert} code to where the {@link Hook} was {@link #anchor}ed. This can be useful to reach + * "back" or to some outer scope, e.g. while generating code for a method, one can reach out + * to the class scope to insert fields. + * + *

+ * Example: + * {@snippet lang=java : + * var myHook = new Hook("MyHook"); + * + * var template1 = Template.make("name", (String name) -> body( + * """ + * public static int #name = 42; + * """ + * )); + * + * var template2 = Template.make(() -> body( + * """ + * public class Test { + * """, + * // Anchor the hook here. + * myHook.anchor( + * """ + * public static void main(String[] args) { + * System.out.println("$field: " + $field) + * """, + * // Reach out to where the hook was anchored, and insert the code of template1. + * myHook.insert(template1.asToken($("field"))), + * """ + * } + * """ + * ), + * """ + * } + * """ + * )); + * } + * + * @param name The name of the Hook, for debugging purposes only. + */ +public record Hook(String name) { + /** + * Anchor this {@link Hook} for the scope of the provided {@code 'tokens'}. + * From anywhere inside this scope, even in nested Templates, code can be + * {@link #insert}ed back to the location where this {@link Hook} was {@link #anchor}ed. + * + * @param tokens A list of tokens, which have the same restrictions as {@link Template#body}. + * @return A {@link Token} that captures the anchoring of the scope and the list of validated {@link Token}s. + */ + public Token anchor(Object... tokens) { + return new HookAnchorToken(this, Token.parse(tokens)); + } + + /** + * Inserts a {@link TemplateToken} to the innermost location where this {@link Hook} was {@link #anchor}ed. + * This could be in the same Template, or one nested further out. + * + * @param templateToken The Template with applied arguments to be inserted at the {@link Hook}. + * @return The {@link Token} which when used inside a {@link Template#body} performs the code insertion into the {@link Hook}. + */ + public Token insert(TemplateToken templateToken) { + return new HookInsertToken(this, templateToken); + } + + /** + * Checks if the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope. + * + * @return If the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope. + */ + public boolean isAnchored() { + return Renderer.getCurrent().isAnchored(this); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java new file mode 100644 index 00000000000..b025c5ff041 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/HookAnchorToken.java @@ -0,0 +1,28 @@ +/* + * 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; + +record HookAnchorToken(Hook hook, List tokens) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java new file mode 100644 index 00000000000..de8b60bbf24 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/HookInsertToken.java @@ -0,0 +1,26 @@ +/* + * 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; + +record HookInsertToken(Hook hook, TemplateToken templateToken) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Name.java b/test/hotspot/jtreg/compiler/lib/template_framework/Name.java new file mode 100644 index 00000000000..b969ecaa13a --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Name.java @@ -0,0 +1,50 @@ +/* + * 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; + +sealed interface Name permits DataName, StructuralName { + /** + * The name of the name, that can be used in code. + * + * @return The {@link String} name of the name, that can be used in code. + */ + String name(); + + /** + * The type of the name, allowing for filtering by type. + * + * @return The type of the name. + */ + Type type(); + + /** + * The weight of the name, corresponds to the probability of + * choosing this name when sampling. + * + * @return The weight of the name. + */ + int weight(); + + interface Type {} +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java b/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java new file mode 100644 index 00000000000..ef79c33d48a --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NameSet.java @@ -0,0 +1,151 @@ +/* + * 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.List; +import java.util.Random; +import java.util.Optional; + +import jdk.test.lib.Utils; + +/** + * The {@link NameSet} defines a set of {@link Name}s (e.g. fields or variable names). They extend the + * set of the {@code 'parent'} set. + */ +class NameSet { + private static final Random RANDOM = Utils.getRandomInstance(); + + private final NameSet parent; + private final List children = new ArrayList<>(); + private final List names = new ArrayList<>(); + + interface Predicate { + boolean check(Name type); + } + + NameSet(NameSet parent) { + this.parent = parent; + if (parent != null) { parent.registerChild(this); } + } + + void registerChild(NameSet child) { + children.add(child); + } + + private long weight(Predicate predicate) { + long w = names.stream().filter(predicate::check).mapToInt(Name::weight).sum(); + if (parent != null) { w += parent.weight(predicate); } + return w; + } + + public int count(Predicate predicate) { + int c = (int)names.stream().filter(predicate::check).count(); + if (parent != null) { c += parent.count(predicate); } + return c; + } + + public boolean hasAny(Predicate predicate) { + return names.stream().anyMatch(predicate::check) || + (parent != null && parent.hasAny(predicate)); + } + + public List toList(Predicate predicate) { + List list = (parent != null) ? parent.toList(predicate) + : new ArrayList<>(); + list.addAll(names.stream().filter(predicate::check).toList()); + return list; + } + + /** + * Randomly sample a name from this set or a parent set, restricted to the predicate. + */ + public Name sample(Predicate predicate) { + long w = weight(predicate); + if (w <= 0) { + // Negative weight should never happen, as all names have positive weight. + if (w < 0) { + throw new RuntimeException("Negative weight not allowed: " + w); + } + // If the weight is zero, there is no matching Name available. + // Return null, and let the caller handle the situation, e.g. + // throw an exception. + return null; + } + + long r = RANDOM.nextLong(w); + return sample(predicate, r); + } + + private Name sample(Predicate predicate, long r) { + for (var name : names) { + if (predicate.check(name)) { + r -= name.weight(); + if (r < 0) { return name; } + } + } + return parent.sample(predicate, r); + } + + private Name findLocal(String name) { + Optional opt = names.stream().filter(n -> n.name().equals(name)).findAny(); + return opt.orElse(null); + } + + private Name findParents(String name) { + if (parent == null) { return null; } + Name n = parent.findLocal(name); + if (n != null) { return n; } + return parent.findParents(name); + } + + private Name findChildren(String name) { + for (NameSet child : children) { + Name n1 = child.findLocal(name); + if (n1 != null) { return n1; } + Name n2 = child.findChildren(name); + if (n2 != null) { return n2; } + } + return null; + } + + private Name find(String name) { + Name n1 = findLocal(name); + if (n1 != null) { return n1; } + Name n2 = findParents(name); + if (n2 != null) { return n2; } + return findChildren(name); + } + + /** + * Add a variable of a specified type to the set. + */ + public void add(Name name) { + Name other = find(name.name()); + if (other != null) { + throw new RendererException("Duplicate name: " + name + ", previously: " + other); + } + names.add(name); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java new file mode 100644 index 00000000000..540eaf1e14c --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/NothingToken.java @@ -0,0 +1,26 @@ +/* + * 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; + +record NothingToken() implements Token {} 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 00000000000..bc09d34b928 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/README.md @@ -0,0 +1,12 @@ +# 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 template based fuzzing easy to extend. + +We want to make it easy to generate variants of tests. Often, we would like to have a set of tests, corresponding to a set of types, a set of operators, a set of constants, etc. Writing all the tests by hand is cumbersome or even impossible. When generating such tests with scripts, it would be preferable if the code generation happens automatically, and the generator script was checked into the code base. Code generation can go beyond simple regression tests, and one might want to generate random code from a list of possible templates, to fuzz individual Java features and compiler optimizations. + +The Template Framework provides a facility to generate code with Templates. Templates are essentially a list of tokens that are concatenated (i.e. rendered) to a String. The Templates can have "holes", which are filled (replaced) by different values at each Template instantiation. For example, these "holes" can be filled with different types, operators or constants. Templates can also be nested, allowing a modular use of Templates. + +Detailed documentation can be found in [Template.java](./Template.java). + +The Template Framework only generates code in the form of a String. This code can then be compiled and executed, for example with the help of the [Compile Framework](../compile_framework/README.md). + +The basic functionalities of the Template Framework are described in the [Template Interface](./Template.java), together with some examples. More examples can be found in [TestSimple.java](../../../testlibrary_tests/template_framework/examples/TestSimple.java), [TestAdvanced.java](../../../testlibrary_tests/template_framework/examples/TestAdvanced.java) and [TestTutorial.java](../../../testlibrary_tests/template_framework/examples/TestTutorial.java). diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java b/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java new file mode 100644 index 00000000000..14adfc81d3f --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java @@ -0,0 +1,437 @@ +/* + * 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.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The {@link Renderer} class renders a tokenized {@link Template} in the form of a {@link TemplateToken}. + * It also keeps track of the states during a nested Template rendering. There can only be a single + * {@link Renderer} active at any point, since there are static methods that reference + * {@link Renderer#getCurrent}. + * + *

+ * The {@link Renderer} instance keeps track of the current frames. + * + * @see TemplateFrame + * @see CodeFrame + */ +final class Renderer { + private static final String NAME_CHARACTERS = "[a-zA-Z_][a-zA-Z0-9_]*"; + private static final Pattern NAME_PATTERN = Pattern.compile( + // We are parsing patterns: + // #name + // #{name} + // $name + // ${name} + // But the "#" or "$" have already been removed, and the String + // starts at the character after that. + // The pattern must be at the beginning of the String part. + "^" + + // We either have "name" or "{name}" + "(?:" + // non-capturing group for the OR + // capturing group for "name" + "(" + NAME_CHARACTERS + ")" + + "|" + // OR + // We want to trim off the brackets, so have + // another non-capturing group. + "(?:\\{" + + // capturing group for "name" inside "{name}" + "(" + NAME_CHARACTERS + ")" + + "\\})" + + ")"); + private static final Pattern NAME_CHARACTERS_PATTERN = Pattern.compile("^" + NAME_CHARACTERS + "$"); + + static boolean isValidHashtagOrDollarName(String name) { + return NAME_CHARACTERS_PATTERN.matcher(name).find(); + } + + /** + * There can be at most one Renderer instance at any time. + * + *

+ * When using nested templates, the user of the Template Framework may be tempted to first render + * the nested template to a {@link String}, and then use this {@link String} as a token in an outer + * {@link Template#body}. This would be a bad pattern: the outer and nested {@link Template} would + * be rendered separately, and could not interact. For example, the nested {@link Template} would + * not have access to the scopes of the outer {@link Template}. The inner {@link Template} could + * not access {@link Name}s and {@link Hook}s from the outer {@link Template}. The user might assume + * that the inner {@link Template} has access to the outer {@link Template}, but they would actually + * be separated. This could lead to unexpected behavior or even bugs. + * + *

+ * Instead, the user should create a {@link TemplateToken} from the inner {@link Template}, and + * use that {@link TemplateToken} in the {@link Template#body} of the outer {@link Template}. + * This way, the inner and outer {@link Template}s get rendered together, and the inner {@link Template} + * has access to the {@link Name}s and {@link Hook}s of the outer {@link Template}. + * + *

+ * The {@link Renderer} instance exists during the whole rendering process. Should the user ever + * attempt to render a nested {@link Template} to a {@link String}, we would detect that there is + * already a {@link Renderer} instance for the outer {@link Template}, and throw a {@link RendererException}. + */ + private static Renderer renderer = null; + + private int nextTemplateFrameId; + private final TemplateFrame baseTemplateFrame; + private TemplateFrame currentTemplateFrame; + private final CodeFrame baseCodeFrame; + private CodeFrame currentCodeFrame; + + // We do not want any other instances, so we keep it private. + private Renderer(float fuel) { + nextTemplateFrameId = 0; + baseTemplateFrame = TemplateFrame.makeBase(nextTemplateFrameId++, fuel); + currentTemplateFrame = baseTemplateFrame; + baseCodeFrame = CodeFrame.makeBase(); + currentCodeFrame = baseCodeFrame; + } + + static Renderer getCurrent() { + if (renderer == null) { + throw new RendererException("A Template method such as '$', 'let', 'sample', 'count' etc. was called outside a template rendering."); + } + return renderer; + } + + static String render(TemplateToken templateToken) { + return render(templateToken, Template.DEFAULT_FUEL); + } + + static String render(TemplateToken templateToken, float fuel) { + // Check nobody else is using the Renderer. + if (renderer != null) { + throw new RendererException("Nested render not allowed. Please only use 'asToken' inside Templates, and call 'render' only once at the end."); + } + try { + renderer = new Renderer(fuel); + renderer.renderTemplateToken(templateToken); + renderer.checkFrameConsistencyAfterRendering(); + return renderer.collectCode(); + } finally { + // Release the Renderer. + renderer = null; + } + } + + private void checkFrameConsistencyAfterRendering() { + // Ensure CodeFrame consistency. + if (baseCodeFrame != currentCodeFrame) { + throw new RuntimeException("Internal error: Renderer did not end up at base CodeFrame."); + } + // Ensure TemplateFrame consistency. + if (baseTemplateFrame != currentTemplateFrame) { + throw new RuntimeException("Internal error: Renderer did not end up at base TemplateFrame."); + } + } + + private String collectCode() { + StringBuilder builder = new StringBuilder(); + baseCodeFrame.getCode().renderTo(builder); + return builder.toString(); + } + + String $(String name) { + return currentTemplateFrame.$(name); + } + + void addHashtagReplacement(String key, Object value) { + currentTemplateFrame.addHashtagReplacement(key, format(value)); + } + + private String getHashtagReplacement(String key) { + return currentTemplateFrame.getHashtagReplacement(key); + } + + float fuel() { + return currentTemplateFrame.fuel; + } + + void setFuelCost(float fuelCost) { + currentTemplateFrame.setFuelCost(fuelCost); + } + + Name sampleName(NameSet.Predicate predicate) { + return currentCodeFrame.sampleName(predicate); + } + + int countNames(NameSet.Predicate predicate) { + return currentCodeFrame.countNames(predicate); + } + + boolean hasAnyNames(NameSet.Predicate predicate) { + return currentCodeFrame.hasAnyNames(predicate); + } + + List listNames(NameSet.Predicate predicate) { + return currentCodeFrame.listNames(predicate); + } + + /** + * Formats values to {@link String} with the goal of using them in Java code. + * By default, we use the overrides of {@link Object#toString}. + * But for some boxed primitives we need to create a special formatting. + */ + static String format(Object value) { + return switch (value) { + case String s -> s; + case Integer i -> i.toString(); + // We need to append the "L" so that the values are not interpreted as ints, + // and then javac might complain that the values are too large for an int. + case Long l -> l.toString() + "L"; + // Some Float and Double values like Infinity and NaN need a special representation. + case Float f -> formatFloat(f); + case Double d -> formatDouble(d); + default -> value.toString(); + }; + } + + private static String formatFloat(Float f) { + if (Float.isFinite(f)) { + return f.toString() + "f"; + } else if (f.isNaN()) { + return "Float.intBitsToFloat(" + Float.floatToRawIntBits(f) + " /* NaN */)"; + } else if (f.isInfinite()) { + if (f > 0) { + return "Float.POSITIVE_INFINITY"; + } else { + return "Float.NEGATIVE_INFINITY"; + } + } else { + throw new RuntimeException("Not handled: " + f); + } + } + + private static String formatDouble(Double d) { + if (Double.isFinite(d)) { + return d.toString(); + } else if (d.isNaN()) { + return "Double.longBitsToDouble(" + Double.doubleToRawLongBits(d) + "L /* NaN */)"; + } else if (d.isInfinite()) { + if (d > 0) { + return "Double.POSITIVE_INFINITY"; + } else { + return "Double.NEGATIVE_INFINITY"; + } + } else { + throw new RuntimeException("Not handled: " + d); + } + } + + private void renderTemplateToken(TemplateToken templateToken) { + TemplateFrame templateFrame = TemplateFrame.make(currentTemplateFrame, nextTemplateFrameId++); + currentTemplateFrame = templateFrame; + + templateToken.visitArguments((name, value) -> addHashtagReplacement(name, format(value))); + TemplateBody body = templateToken.instantiate(); + renderTokenList(body.tokens()); + + if (currentTemplateFrame != templateFrame) { + throw new RuntimeException("Internal error: TemplateFrame mismatch!"); + } + currentTemplateFrame = currentTemplateFrame.parent; + } + + private void renderToken(Token token) { + switch (token) { + case StringToken(String s) -> { + renderStringWithDollarAndHashtagReplacements(s); + } + case NothingToken() -> { + // Nothing. + } + case HookAnchorToken(Hook hook, List tokens) -> { + CodeFrame outerCodeFrame = currentCodeFrame; + + // We need a CodeFrame to which the hook can insert code. That way, name + // definitions at the hook cannot escape the hookCodeFrame. + CodeFrame hookCodeFrame = CodeFrame.make(outerCodeFrame); + hookCodeFrame.addHook(hook); + + // We need a CodeFrame where the tokens can be rendered. That way, name + // definitions from the tokens cannot escape the innerCodeFrame to the + // hookCodeFrame. + CodeFrame innerCodeFrame = CodeFrame.make(hookCodeFrame); + currentCodeFrame = innerCodeFrame; + + renderTokenList(tokens); + + // Close the hookCodeFrame and innerCodeFrame. hookCodeFrame code comes before the + // innerCodeFrame code from the tokens. + currentCodeFrame = outerCodeFrame; + currentCodeFrame.addCode(hookCodeFrame.getCode()); + currentCodeFrame.addCode(innerCodeFrame.getCode()); + } + case HookInsertToken(Hook hook, TemplateToken templateToken) -> { + // Switch to hook CodeFrame. + CodeFrame callerCodeFrame = currentCodeFrame; + CodeFrame hookCodeFrame = codeFrameForHook(hook); + + // Use a transparent nested CodeFrame. We need a CodeFrame so that the code generated + // by the TemplateToken can be collected, and hook insertions from it can still + // be made to the hookCodeFrame before the code from the TemplateToken is added to + // the hookCodeFrame. + // But the CodeFrame must be transparent, so that its name definitions go out to + // the hookCodeFrame, and are not limited to the CodeFrame for the TemplateToken. + currentCodeFrame = CodeFrame.makeTransparentForNames(hookCodeFrame); + + renderTemplateToken(templateToken); + + hookCodeFrame.addCode(currentCodeFrame.getCode()); + + // Switch back from hook CodeFrame to caller CodeFrame. + currentCodeFrame = callerCodeFrame; + } + case TemplateToken templateToken -> { + // Use a nested CodeFrame. + CodeFrame callerCodeFrame = currentCodeFrame; + currentCodeFrame = CodeFrame.make(currentCodeFrame); + + renderTemplateToken(templateToken); + + callerCodeFrame.addCode(currentCodeFrame.getCode()); + currentCodeFrame = callerCodeFrame; + } + case AddNameToken(Name name) -> { + currentCodeFrame.addName(name); + } + } + } + + private void renderTokenList(List tokens) { + CodeFrame codeFrame = currentCodeFrame; + for (Token t : tokens) { + renderToken(t); + } + if (codeFrame != currentCodeFrame) { + throw new RuntimeException("Internal error: CodeFrame mismatch."); + } + } + + /** + * We split a {@link String} by "#" and "$", and then look at each part. + * Example: + * + * s: "abcdefghijklmnop #name abcdefgh${var_name} 12345#{name2}_con $field_name something" + * parts: --------0-------- ------1------ --------2------- ------3----- ----------4--------- + * start: ^ ^ ^ ^ ^ + * next: ^ ^ ^ ^ ^ + * none hashtag dollar hashtag dollar done + */ + private void renderStringWithDollarAndHashtagReplacements(final String s) { + int count = 0; // First part needs special handling + int start = 0; + boolean startIsAfterDollar = false; + do { + // Find the next "$" or "#", after start. + int dollar = s.indexOf("$", start); + int hashtag = s.indexOf("#", start); + // If the character was not found, we want to have the rest of the + // String s, so instead of "-1" take the end/length of the String. + dollar = (dollar == -1) ? s.length() : dollar; + hashtag = (hashtag == -1) ? s.length() : hashtag; + // Take the first one. + int next = Math.min(dollar, hashtag); + String part = s.substring(start, next); + + if (count == 0) { + // First part has no "#" or "$" before it. + currentCodeFrame.addString(part); + } else { + // All others must do the replacement. + renderStringWithDollarAndHashtagReplacementsPart(s, part, startIsAfterDollar); + } + + if (next == s.length()) { + // No new "#" or "$" was found, we just processed the rest of the String, + // terminate now. + return; + } + start = next + 1; // skip over the "#" or "$" + startIsAfterDollar = next == dollar; // remember which character we just split with + count++; + } while (true); + } + + /** + * We are parsing a part now. Before the part, there was either a "#" or "$": + * isDollar = false: + * "#part" + * "#name abcdefgh" + * ---- + * "#{name2}_con " + * ------- + * + * isDollar = true: + * "$part" + * "${var_name} 12345" + * ---------- + * "$field_name something" + * ---------- + * + * We now want to find the name pattern at the beginning of the part, and replace + * it according to the hashtag or dollar replacement strategy. + */ + private void renderStringWithDollarAndHashtagReplacementsPart(final String s, final String part, final boolean isDollar) { + Matcher matcher = NAME_PATTERN.matcher(part); + // If the string has a "#" or "$" that is not followed by a correct name + // pattern, then the matcher will not match. These can be cases like: + // "##name" -> the first hashtag leads to an empty part, and an empty name. + // "#1name" -> the name pattern does not allow a digit as the first character. + // "anything#" -> a hashtag at the end of the string leads to an empty name. + if (!matcher.find()) { + String replacement = isDollar ? "$" : "#"; + throw new RendererException("Is not a valid '" + replacement + "' replacement pattern: '" + + replacement + part + "' in '" + s + "'."); + } + // We know that there is a correct pattern, and now we replace it. + currentCodeFrame.addString(matcher.replaceFirst( + (MatchResult result) -> { + // There are two groups: (1) for "name" and (2) for "{name}" + String name = result.group(1) != null ? result.group(1) : result.group(2); + if (isDollar) { + return $(name); + } else { + // replaceFirst needs some special escaping of backslashes and ollar signs. + return getHashtagReplacement(name).replace("\\", "\\\\").replace("$", "\\$"); + } + } + )); + } + + boolean isAnchored(Hook hook) { + return currentCodeFrame.codeFrameForHook(hook) != null; + } + + private CodeFrame codeFrameForHook(Hook hook) { + CodeFrame codeFrame = currentCodeFrame.codeFrameForHook(hook); + if (codeFrame == null) { + throw new RendererException("Hook '" + hook.name() + "' was referenced but not found!"); + } + return codeFrame; + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/RendererException.java b/test/hotspot/jtreg/compiler/lib/template_framework/RendererException.java new file mode 100644 index 00000000000..2ab542b6fe8 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/RendererException.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * This exception is thrown when something goes wrong during Template + * rendering, or in the use of any of its static methods. + * It most likely indicates a wrong use of the Templates. + */ +public class RendererException extends RuntimeException { + RendererException(String message) { + super(message); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/StringToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/StringToken.java new file mode 100644 index 00000000000..4926748e51a --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/StringToken.java @@ -0,0 +1,26 @@ +/* + * 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; + +record StringToken(String value) implements Token {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java b/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java new file mode 100644 index 00000000000..866ac6dbfb8 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/StructuralName.java @@ -0,0 +1,200 @@ +/* + * 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; + +/** + * {@link StructuralName}s represent things like method and class names, and can be added to the local + * scope with {@link Template#addStructuralName}, and accessed with {@link Template#structuralNames}, from where + * count, list or even sample random {@link StructuralName}s. Every {@link StructuralName} has a {@link StructuralName.Type}, + * so that sampling can be restricted to these types. + * + *

+ * For field and variable names and alike, there are the analogous {@link DataName}s. + * + * @param name The {@link String} name used in code. + * @param type The type of the {@link StructuralName}. + * @param weight The weight of the {@link StructuralName}, it corresponds to the probability of choosing this + * {@link StructuralName} when sampling later on. + */ +public record StructuralName(String name, StructuralName.Type type, int weight) implements Name { + + /** + * Creates a new {@link StructuralName}. + */ + public StructuralName { + } + + /** + * The interface for the type of a {@link StructuralName}. + */ + public interface Type extends Name.Type { + /** + * The name of the type, that can be used in code. + * + * @return The {@link String} representation of the type, that can be used in code. + */ + String name(); + + /** + * Defines the subtype relationship with other types, which is used to filter {@link StructuralName}s + * in {@link FilteredSet#exactOf}, {@link FilteredSet#subtypeOf}, and {@link FilteredSet#supertypeOf}. + * + * @param other The other type, where we check if it is the supertype of {@code 'this'}. + * @return If {@code 'this'} is a subtype of {@code 'other'}. + */ + boolean isSubtypeOf(StructuralName.Type other); + } + + /** + * The {@link FilteredSet} represents a filtered set of {@link StructuralName}s in the current scope. + * It can be obtained with {@link Template#structuralNames}. It can be used to count the + * available {@link StructuralName}s, or sample a random {@link StructuralName} according to the + * weights of the {@link StructuralName}s in the filtered set. + * Note: The {@link FilteredSet} is only a filtered view on the set of {@link StructuralName}s, + * and may return different results in different contexts. + */ + public static final class FilteredSet { + private final StructuralName.Type subtype; + private final StructuralName.Type supertype; + + FilteredSet(StructuralName.Type subtype, StructuralName.Type supertype) { + this.subtype = subtype; + this.supertype = supertype; + } + + FilteredSet() { + this(null, null); + } + + NameSet.Predicate predicate() { + if (subtype == null && supertype == null) { + throw new UnsupportedOperationException("Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); + } + return (Name name) -> { + if (!(name instanceof StructuralName structuralName)) { return false; } + if (subtype != null && !structuralName.type().isSubtypeOf(subtype)) { return false; } + if (supertype != null && !supertype.isSubtypeOf(structuralName.type())) { return false; } + return true; + }; + } + + /** + * Create a {@link FilteredSet}, where all {@link StructuralName}s must be subtypes of {@code type}. + * + * @param type The type of which all {@link StructuralName}s must be subtypes of. + * @return The updated filtered set. + * @throws UnsupportedOperationException If this {@link FilteredSet} was already filtered with + * {@link #subtypeOf} or {@link #exactOf}. + */ + public FilteredSet subtypeOf(StructuralName.Type type) { + if (subtype != null) { + throw new UnsupportedOperationException("Cannot constrain to subtype " + type + ", is already constrained: " + subtype); + } + return new FilteredSet(type, supertype); + } + + /** + * Create a {@link FilteredSet}, where all {@link StructuralName}s must be supertypes of {@code type}. + * + * @param type The type of which all {@link StructuralName}s must be supertype of. + * @return The updated filtered set. + * @throws UnsupportedOperationException If this {@link FilteredSet} was already filtered with + * {@link #supertypeOf} or {@link #exactOf}. + */ + public FilteredSet supertypeOf(StructuralName.Type type) { + if (supertype != null) { + throw new UnsupportedOperationException("Cannot constrain to supertype " + type + ", is already constrained: " + supertype); + } + return new FilteredSet(subtype, type); + } + + /** + * Create a {@link FilteredSet}, where all {@link StructuralName}s must be of exact {@code type}, + * hence it must be both subtype and supertype thereof. + * + * @param type The type of which all {@link StructuralName}s must be. + * @return The updated filtered set. + * @throws UnsupportedOperationException If this {@link FilteredSet} was already filtered with + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public FilteredSet exactOf(StructuralName.Type type) { + return subtypeOf(type).supertypeOf(type); + } + + /** + * Samples a random {@link StructuralName} from the filtered set, according to the weights + * of the contained {@link StructuralName}s. + * + * @return The sampled {@link StructuralName}. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + * @throws RendererException If the set was empty. + */ + public StructuralName sample() { + StructuralName n = (StructuralName)Renderer.getCurrent().sampleName(predicate()); + if (n == null) { + String msg1 = (subtype == null) ? "" : " subtypeOf(" + subtype + ")"; + String msg2 = (supertype == null) ? "" : " supertypeOf(" + supertype + ")"; + throw new RendererException("No variable:" + msg1 + msg2 + "."); + } + return n; + } + + /** + * Counts the number of {@link StructuralName}s in the filtered set. + * + * @return The number of {@link StructuralName}s in the filtered set. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public int count() { + return Renderer.getCurrent().countNames(predicate()); + } + + /** + * Checks if there are any {@link StructuralName}s in the filtered set. + * + * @return Returns {@code true} iff there is at least one {@link StructuralName} in the filtered set. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public boolean hasAny() { + return Renderer.getCurrent().hasAnyNames(predicate()); + } + + /** + * Collects all {@link StructuralName}s in the filtered set. + * + * @return A {@link List} of all {@link StructuralName}s in the filtered set. + * @throws UnsupportedOperationException If the type was not constrained with either of + * {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}. + */ + public List toList() { + List list = Renderer.getCurrent().listNames(predicate()); + return list.stream().map(n -> (StructuralName)n).toList(); + } + } +} 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 00000000000..f01c5ccffd3 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java @@ -0,0 +1,844 @@ +/* + * 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.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import java.util.List; + +import compiler.lib.compile_framework.CompileFramework; +import compiler.lib.ir_framework.TestFramework; + +/** + * 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 template based fuzzing easy + * to extend. + * + *

+ * Motivation: We want to make it easy to generate variants of tests. Often, we would like to + * have a set of tests, corresponding to a set of types, a set of operators, a set of constants, etc. Writing all + * the tests by hand is cumbersome or even impossible. When generating such tests with scripts, it would be + * preferable if the code generation happens automatically, and the generator script was checked into the code + * base. Code generation can go beyond simple regression tests, and one might want to generate random code from + * a list of possible templates, to fuzz individual Java features and compiler optimizations. + * + *

+ * The Template Framework provides a facility to generate code with Templates. A Template is essentially a list + * of tokens that are concatenated (i.e. rendered) to a {@link String}. The Templates can have "holes", which are + * filled (replaced) by different values at each Template instantiation. For example, these "holes" can + * be filled with different types, operators or constants. Templates can also be nested, allowing a modular + * use of Templates. + * + *

+ * Once we rendered the source code to a {@link String}, we can compile it with the {@link CompileFramework}. + * + *

+ * Example: + * The following snippets are from the example test {@code TestAdvanced.java}. + * First, we define a template that generates a {@code @Test} method for a given type, operator and + * constant generator. We define two constants {@code con1} and {@code con2}, and then use a multiline + * string with hashtags {@code #} (i.e. "holes") that are then replaced by the template arguments and the + * {@link #let} definitions. + * + *

+ * {@snippet lang=java : + * var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> body( + * let("con1", generator.next()), + * let("con2", generator.next()), + * """ + * // #typeName #operator #con1 #con2 + * public static #typeName $GOLD = $test(); + * + * @Test + * public static #typeName $test() { + * return (#typeName)(#con1 #operator #con2); + * } + * + * @Check(test = "$test") + * public static void $check(#typeName result) { + * Verify.checkEQ(result, $GOLD); + * } + * """ + * )); + * } + * + *

+ * To get an executable test, we define a {@link Template} that produces a class body with a main method. The Template + * takes a list of types, and calls the {@code testTemplate} defined above for each type and operator. We use + * the {@link TestFramework} to call our {@code @Test} methods. + * + *

+ * {@snippet lang=java : + * var classTemplate = Template.make("types", (List types) -> body( + * let("classpath", comp.getEscapedClassPathOfCompiledClasses()), + * """ + * package p.xyz; + * + * import compiler.lib.ir_framework.*; + * import compiler.lib.verify.*; + * + * public class InnerTest { + * public static void main() { + * // Set the classpath, so that the TestFramework test VM knows where + * // the CompileFramework put the class files of the compiled source code. + * TestFramework framework = new TestFramework(InnerTest.class); + * framework.addFlags("-classpath", "#classpath"); + * framework.start(); + * } + * + * """, + * // Call the testTemplate for each type and operator, generating a + * // list of lists of TemplateToken: + * types.stream().map((Type type) -> + * type.operators().stream().map((String operator) -> + * testTemplate.asToken(type.name(), operator, type.generator())).toList() + * ).toList(), + * """ + * } + * """ + * )); + * } + * + *

+ * Finally, we generate the list of types, and pass it to the class template: + * + *

+ * {@snippet lang=java : + * List types = List.of( + * new Type("byte", GEN_BYTE::next, List.of("+", "-", "*", "&", "|", "^")), + * new Type("char", GEN_CHAR::next, List.of("+", "-", "*", "&", "|", "^")), + * new Type("short", GEN_SHORT::next, List.of("+", "-", "*", "&", "|", "^")), + * new Type("int", GEN_INT::next, List.of("+", "-", "*", "&", "|", "^")), + * new Type("long", GEN_LONG::next, List.of("+", "-", "*", "&", "|", "^")), + * new Type("float", GEN_FLOAT::next, List.of("+", "-", "*", "/")), + * new Type("double", GEN_DOUBLE::next, List.of("+", "-", "*", "/")) + * ); + * + * // Use the template with one argument, and render it to a String. + * return classTemplate.render(types); + * } + * + *

+ * Details: + *

+ * A {@link Template} can have zero or more arguments. A template can be created with {@code make} methods like + * {@link Template#make(String, Function)}. For each number of arguments there is an implementation + * (e.g. {@link Template.TwoArgs} for two arguments). This allows the use of generics for the + * {@link Template} argument types which enables type checking of the {@link Template} arguments. + * It is currently only allowed to use up to three arguments. + * + *

+ * A {@link Template} can be rendered to a {@link String} (e.g. {@link Template.ZeroArgs#render()}). + * Alternatively, we can generate a {@link Token} (more specifically, a {@link TemplateToken}) with {@code asToken()} + * (e.g. {@link Template.ZeroArgs#asToken()}), and use the {@link Token} inside another {@link Template#body}. + * + *

+ * Ideally, we would have used string templates to inject these Template + * arguments into the strings. But since string templates are not (yet) available, the Templates provide + * hashtag replacements in the {@link String}s: the Template argument names are captured, and + * the argument values automatically replace any {@code "#name"} in the {@link String}s. See the different overloads + * of {@link #make} for examples. Additional hashtag replacements can be defined with {@link #let}. + * + *

+ * When using nested Templates, there can be collisions with identifiers (e.g. variable names and method names). + * For this, Templates provide dollar replacements, which automatically rename any + * {@code "$name"} in the {@link String} with a {@code "name_ID"}, where the {@code "ID"} is unique for every use of + * a Template. The dollar replacement can also be captured with {@link #$}, and passed to nested + * Templates, which allows sharing of these identifier names between Templates. + * + *

+ * The dollar and hashtag names must have at least one character. The first character must be a letter + * or underscore (i.e. {@code a-zA-Z_}), the other characters can also be digits (i.e. {@code a-zA-Z0-9_}). + * One can use them with or without curly braces, e.g. {@code #name}, {@code #{name}}, {@code $name}, or + * {@code #{name}}. + * + *

+ * A {@link TemplateToken} cannot just be used in {@link Template#body}, but it can also be + * {@link Hook#insert}ed to where a {@link Hook} was {@link Hook#anchor}ed earlier (in some outer scope of the code). + * For example, while generating code in a method, one can reach out to the scope of the class, and insert a + * new field, or define a utility method. + * + *

+ * A {@link TemplateBinding} allows the recursive use of Templates. With the indirection of such a binding, + * a Template can reference itself. + * + *

+ * The writer of recursive {@link Template}s must ensure that this recursion terminates. To unify the + * approach across {@link Template}s, we introduce the concept of {@link #fuel}. Templates are rendered starting + * with a limited amount of {@link #fuel} (default: 100, see {@link #DEFAULT_FUEL}), which is decreased at each + * Template nesting by a certain amount (default: 10, see {@link #DEFAULT_FUEL_COST}). The default fuel for a + * template can be changed when we {@code render()} it (e.g. {@link ZeroArgs#render(float)}) and the default + * fuel cost with {@link #setFuelCost}) when defining the {@link #body(Object...)}. Recursive templates are + * supposed to terminate once the {@link #fuel} is depleted (i.e. reaches zero). + * + *

+ * Code generation can involve keeping track of fields and variables, as well as the scopes in which they + * are available, and if they are mutable or immutable. We model fields and variables with {@link DataName}s, + * which we can add to the current scope with {@link #addDataName}. We can access the {@link DataName}s with + * {@link #dataNames}. We can filter for {@link DataName}s of specific {@link DataName.Type}s, and then + * we can call {@link DataName.FilteredSet#count}, {@link DataName.FilteredSet#sample}, + * {@link DataName.FilteredSet#toList}, etc. There are many use-cases for this mechanism, especially + * facilitating communication between the code of outer and inner {@link Template}s. Especially for fuzzing, + * it may be useful to be able to add fields and variables, and sample them randomly, to create a random data + * flow graph. + * + *

+ * Similarly, we may want to model method and class names, and possibly other structural names. We model + * these names with {@link StructuralName}, which works analogously to {@link DataName}, except that they + * are not concerned about mutability. + * + *

+ * When working with {@link DataName}s and {@link StructuralName}s, it is important to be aware of the + * relevant scopes, as well as the execution order of the {@link Template} lambdas and the evaluation + * of the {@link Template#body} tokens. When a {@link Template} is rendered, its lambda is invoked. In the + * lambda, we generate the tokens, and create the {@link Template#body}. Once the lambda returns, the + * tokens are evaluated one by one. While evaluating the tokens, the {@link Renderer} might encounter a nested + * {@link TemplateToken}, which in turn triggers the evaluation of that nested {@link Template}, i.e. + * the evaluation of its lambda and later the evaluation of its tokens. It is important to keep in mind + * that the lambda is always executed first, and the tokens are evaluated afterwards. A method like + * {@code dataNames(MUTABLE).exactOf(type).count()} is a method that is executed during the evaluation + * of the lambda. But a method like {@link #addDataName} returns a token, and does not immediately add + * the {@link DataName}. This ensures that the {@link DataName} is only inserted when the tokens are + * evaluated, so that it is inserted at the exact scope where we would expect it. + * + *

+ * Let us look at the following example to better understand the execution order. + * + *

+ * {@snippet lang=java : + * var testTemplate = Template.make(() -> body( + * // The lambda has just been invoked. + * // We count the DataNames and assign the count to the hashtag replacement "c1". + * let("c1", dataNames(MUTABLE).exactOf(someType).count()), + * // We want to define a DataName "v1", and create a token for it. + * addDataName($("v1"), someType, MUTABLE), + * // We count the DataNames again, but the count does NOT change compared to "c1". + * // This is because the token for "v1" is only evaluated later. + * let("c2", dataNames(MUTABLE).exactOf(someType).count()), + * // Create a nested scope. + * METHOD_HOOK.anchor( + * // We want to define a DataName "v2", which is only valid inside this + * // nested scope. + * addDataName($("v2"), someType, MUTABLE), + * // The count is still not different to "c1". + * let("c3", dataNames(MUTABLE).exactOf(someType).count()), + * // We nest a Template. This creates a TemplateToken, which is later evaluated. + * // By the time the TemplateToken is evaluated, the tokens from above will + * // be already evaluated. Hence, "v1" and "v2" are added by then, and if the + * // "otherTemplate" were to count the DataNames, the count would be increased + * // by 2 compared to "c1". + * otherTemplate.asToken() + * ), + * // After closing the scope, "v2" is no longer available. + * // The count is still the same as "c1", since "v1" is still only a token. + * let("c4", dataNames(MUTABLE).exactOf(someType).count()), + * // We nest another Template. Again, this creates a TemplateToken, which is only + * // evaluated later. By that time, the token for "v1" is evaluated, and so the + * // nested Template would observe an increment in the count. + * anotherTemplate.asToken() + * // By this point, all methods are called, and the tokens generated. + * // The lambda returns the "body", which is all of the tokens that we just + * // generated. After returning from the lambda, the tokens will be evaluated + * // one by one. + * )); + * } + + *

+ * More examples for these functionalities can be found in {@code TestTutorial.java}, {@code TestSimple.java}, + * and {@code TestAdvanced.java}, which all produce compilable Java code. Additional examples can be found in + * the tests, such as {@code TestTemplate.java} and {@code TestFormat.java}, which do not necessarily generate + * valid Java code, but generate deterministic Strings which are easier to verify, and may also serve as a + * reference when learning about these functionalities. + */ +public sealed interface Template permits Template.ZeroArgs, + Template.OneArg, + Template.TwoArgs, + Template.ThreeArgs { + + /** + * A {@link Template} with no arguments. + * + * @param function The {@link Supplier} that creates the {@link TemplateBody}. + */ + record ZeroArgs(Supplier function) implements Template { + TemplateBody instantiate() { + return function.get(); + } + + /** + * Creates a {@link TemplateToken} which can be used as a {@link Token} inside + * a {@link Template} for nested code generation. + * + * @return The {@link TemplateToken} to use the {@link Template} inside another + * {@link Template}. + */ + public TemplateToken asToken() { + return new TemplateToken.ZeroArgs(this); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render() { + return new TemplateToken.ZeroArgs(this).render(); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param fuel The amount of fuel provided for recursive Template instantiations. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(float fuel) { + return new TemplateToken.ZeroArgs(this).render(fuel); + } + } + + /** + * A {@link Template} with one argument. + * + * @param arg1Name The name of the (first) argument, used for hashtag replacements in the {@link Template}. + * @param The type of the (first) argument. + * @param function The {@link Function} that creates the {@link TemplateBody} given the template argument. + */ + record OneArg(String arg1Name, Function function) implements Template { + TemplateBody instantiate(T1 arg1) { + return function.apply(arg1); + } + + /** + * Creates a {@link TemplateToken} which can be used as a {@link Token} inside + * a {@link Template} for nested code generation. + * + * @param arg1 The value for the (first) argument. + * @return The {@link TemplateToken} to use the {@link Template} inside another + * {@link Template}. + */ + public TemplateToken asToken(T1 arg1) { + return new TemplateToken.OneArg<>(this, arg1); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param arg1 The value for the first argument. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(T1 arg1) { + return new TemplateToken.OneArg<>(this, arg1).render(); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param arg1 The value for the first argument. + * @param fuel The amount of fuel provided for recursive Template instantiations. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(float fuel, T1 arg1) { + return new TemplateToken.OneArg<>(this, arg1).render(fuel); + } + } + + /** + * A {@link Template} with two arguments. + * + * @param arg1Name The name of the first argument, used for hashtag replacements in the {@link Template}. + * @param arg2Name The name of the second argument, used for hashtag replacements in the {@link Template}. + * @param The type of the first argument. + * @param The type of the second argument. + * @param function The {@link BiFunction} that creates the {@link TemplateBody} given the template arguments. + */ + record TwoArgs(String arg1Name, String arg2Name, BiFunction function) implements Template { + TemplateBody instantiate(T1 arg1, T2 arg2) { + return function.apply(arg1, arg2); + } + + /** + * Creates a {@link TemplateToken} which can be used as a {@link Token} inside + * a {@link Template} for nested code generation. + * + * @param arg1 The value for the first argument. + * @param arg2 The value for the second argument. + * @return The {@link TemplateToken} to use the {@link Template} inside another + * {@link Template}. + */ + public TemplateToken asToken(T1 arg1, T2 arg2) { + return new TemplateToken.TwoArgs<>(this, arg1, arg2); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param arg1 The value for the first argument. + * @param arg2 The value for the second argument. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(T1 arg1, T2 arg2) { + return new TemplateToken.TwoArgs<>(this, arg1, arg2).render(); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param arg1 The value for the first argument. + * @param arg2 The value for the second argument. + * @param fuel The amount of fuel provided for recursive Template instantiations. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(float fuel, T1 arg1, T2 arg2) { + return new TemplateToken.TwoArgs<>(this, arg1, arg2).render(fuel); + } + } + + /** + * Interface for function with three arguments. + * + * @param Type of the first argument. + * @param Type of the second argument. + * @param Type of the third argument. + * @param Type of the return value. + */ + @FunctionalInterface + interface TriFunction { + + /** + * Function definition for the three argument functions. + * + * @param t The first argument. + * @param u The second argument. + * @param v The third argument. + * @return Return value of the three argument function. + */ + R apply(T t, U u, V v); + } + + /** + * A {@link Template} with three arguments. + * + * @param arg1Name The name of the first argument, used for hashtag replacements in the {@link Template}. + * @param arg2Name The name of the second argument, used for hashtag replacements in the {@link Template}. + * @param arg3Name The name of the third argument, used for hashtag replacements in the {@link Template}. + * @param The type of the first argument. + * @param The type of the second argument. + * @param The type of the third argument. + * @param function The function with three arguments that creates the {@link TemplateBody} given the template arguments. + */ + record ThreeArgs(String arg1Name, String arg2Name, String arg3Name, TriFunction function) implements Template { + TemplateBody instantiate(T1 arg1, T2 arg2, T3 arg3) { + return function.apply(arg1, arg2, arg3); + } + + /** + * Creates a {@link TemplateToken} which can be used as a {@link Token} inside + * a {@link Template} for nested code generation. + * + * @param arg1 The value for the first argument. + * @param arg2 The value for the second argument. + * @param arg3 The value for the third argument. + * @return The {@link TemplateToken} to use the {@link Template} inside another + * {@link Template}. + */ + public TemplateToken asToken(T1 arg1, T2 arg2, T3 arg3) { + return new TemplateToken.ThreeArgs<>(this, arg1, arg2, arg3); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param arg1 The value for the first argument. + * @param arg2 The value for the second argument. + * @param arg3 The value for the third argument. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(T1 arg1, T2 arg2, T3 arg3) { + return new TemplateToken.ThreeArgs<>(this, arg1, arg2, arg3).render(); + } + + /** + * Renders the {@link Template} to a {@link String}. + * + * @param arg1 The value for the first argument. + * @param arg2 The value for the second argument. + * @param arg3 The value for the third argument. + * @param fuel The amount of fuel provided for recursive Template instantiations. + * @return The {@link String}, resulting from rendering the {@link Template}. + */ + public String render(float fuel, T1 arg1, T2 arg2, T3 arg3) { + return new TemplateToken.ThreeArgs<>(this, arg1, arg2, arg3).render(fuel); + } + } + + /** + * Creates a {@link Template} with no arguments. + * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * + *

+ * Example: + * {@snippet lang=java : + * var template = Template.make(() -> body( + * """ + * Multi-line string or other tokens. + * """ + * )); + * } + * + * @param body The {@link TemplateBody} created by {@link Template#body}. + * @return A {@link Template} with zero arguments. + */ + static Template.ZeroArgs make(Supplier body) { + return new Template.ZeroArgs(body); + } + + /** + * Creates a {@link Template} with one argument. + * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * Good practice but not enforced but not enforced: {@code arg1Name} should match the lambda argument name. + * + *

+ * Here is an example with template argument {@code 'a'}, captured once as string name + * for use in hashtag replacements, and captured once as lambda argument with the corresponding type + * of the generic argument. + * {@snippet lang=java : + * var template = Template.make("a", (Integer a) -> body( + * """ + * Multi-line string or other tokens. + * We can use the hashtag replacement #a to directly insert the String value of a. + * """, + * "We can also use the captured parameter of a: " + a + * )); + * } + * + * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param Type of the (first) argument. + * @param arg1Name The name of the (first) argument for hashtag replacement. + * @return A {@link Template} with one argument. + */ + static Template.OneArg make(String arg1Name, Function body) { + return new Template.OneArg<>(arg1Name, body); + } + + /** + * Creates a {@link Template} with two arguments. + * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * Good practice but not enforced: {@code arg1Name} and {@code arg2Name} should match the lambda argument names. + * + *

+ * Here is an example with template arguments {@code 'a'} and {@code 'b'}, captured once as string names + * for use in hashtag replacements, and captured once as lambda arguments with the corresponding types + * of the generic arguments. + * {@snippet lang=java : + * var template = Template.make("a", "b", (Integer a, String b) -> body( + * """ + * Multi-line string or other tokens. + * We can use the hashtag replacement #a and #b to directly insert the String value of a and b. + * """, + * "We can also use the captured parameter of a and b: " + a + " and " + b + * )); + * } + * + * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param Type of the first argument. + * @param arg1Name The name of the first argument for hashtag replacement. + * @param Type of the second argument. + * @param arg2Name The name of the second argument for hashtag replacement. + * @return A {@link Template} with two arguments. + */ + static Template.TwoArgs make(String arg1Name, String arg2Name, BiFunction body) { + return new Template.TwoArgs<>(arg1Name, arg2Name, body); + } + + /** + * Creates a {@link Template} with three arguments. + * See {@link #body} for more details about how to construct a Template with {@link Token}s. + * Good practice but not enforced: {@code arg1Name}, {@code arg2Name}, and {@code arg3Name} should match the lambda argument names. + * + * @param body The {@link TemplateBody} created by {@link Template#body}. + * @param Type of the first argument. + * @param arg1Name The name of the first argument for hashtag replacement. + * @param Type of the second argument. + * @param arg2Name The name of the second argument for hashtag replacement. + * @param Type of the third argument. + * @param arg3Name The name of the third argument for hashtag replacement. + * @return A {@link Template} with three arguments. + */ + static Template.ThreeArgs make(String arg1Name, String arg2Name, String arg3Name, Template.TriFunction body) { + return new Template.ThreeArgs<>(arg1Name, arg2Name, arg3Name, body); + } + + /** + * Creates a {@link TemplateBody} from a list of tokens, which can be {@link String}s, + * boxed primitive types (for example {@link Integer} or auto-boxed {@code int}), any {@link Token}, + * or {@link List}s of any of these. + * + *

+ * {@snippet lang=java : + * var template = Template.make(() -> body( + * """ + * Multi-line string + * """, + * "normal string ", Integer.valueOf(3), 3, Float.valueOf(1.5f), 1.5f, + * List.of("abc", "def"), + * nestedTemplate.asToken(42) + * )); + * } + * + * @param tokens A list of tokens, which can be {@link String}s, boxed primitive types + * (for example {@link Integer}), any {@link Token}, or {@link List}s + * of any of these. + * @return The {@link TemplateBody} which captures the list of validated {@link Token}s. + * @throws IllegalArgumentException if the list of tokens contains an unexpected object. + */ + static TemplateBody body(Object... tokens) { + return new TemplateBody(Token.parse(tokens)); + } + + /** + * Retrieves the dollar replacement of the {@code 'name'} for the + * current Template that is being instantiated. It returns the same + * dollar replacement as the string use {@code "$name"}. + * + *

+ * Here is an example where a Template creates a local variable {@code 'var'}, + * with an implicit dollar replacement, and then captures that dollar replacement + * using {@link #$} for the use inside a nested template. + * {@snippet lang=java : + * var template = Template.make(() -> body( + * """ + * int $var = 42; + * """, + * otherTemplate.asToken($("var")) + * )); + * } + * + * @param name The {@link String} name of the name. + * @return The dollar replacement for the {@code 'name'}. + */ + static String $(String name) { + return Renderer.getCurrent().$(name); + } + + /** + * Define a hashtag replacement for {@code "#key"}, with a specific value. + * + *

+ * {@snippet lang=java : + * var template = Template.make("a", (Integer a) -> body( + * let("b", a * 5), + * """ + * System.out.println("Use a and b with hashtag replacement: #a and #b"); + * """ + * )); + * } + * + * @param key Name for the hashtag replacement. + * @param value The value that the hashtag is replaced with. + * @return A token that does nothing, so that the {@link #let} can easily be put in a list of tokens + * inside a {@link Template#body}. + * @throws RendererException if there is a duplicate hashtag {@code key}. + */ + static Token let(String key, Object value) { + Renderer.getCurrent().addHashtagReplacement(key, value); + return new NothingToken(); + } + + /** + * Define a hashtag replacement for {@code "#key"}, with a specific value, which is also captured + * by the provided {@code function} with type {@code }. + * + *

+ * {@snippet lang=java : + * var template = Template.make("a", (Integer a) -> let("b", a * 2, (Integer b) -> body( + * """ + * System.out.println("Use a and b with hashtag replacement: #a and #b"); + * """, + * "System.out.println(\"Use a and b as capture variables:\"" + a + " and " + b + ");\n" + * ))); + * } + * + * @param key Name for the hashtag replacement. + * @param value The value that the hashtag is replaced with. + * @param The type of the value. + * @param function The function that is applied with the provided {@code value}. + * @return A {@link TemplateBody}. + * @throws RendererException if there is a duplicate hashtag {@code key}. + */ + static TemplateBody let(String key, T value, Function function) { + Renderer.getCurrent().addHashtagReplacement(key, value); + return function.apply(value); + } + + /** + * Default amount of fuel for Template rendering. It guides the nesting depth of Templates. Can be changed when + * rendering a template with {@code render(fuel)} (e.g. {@link ZeroArgs#render(float)}). + */ + float DEFAULT_FUEL = 100.0f; + + /** + * The default amount of fuel spent per Template. It is subtracted from the current {@link #fuel} at every + * nesting level, and once the {@link #fuel} reaches zero, the nesting is supposed to terminate. Can be changed + * with {@link #setFuelCost(float)} inside {@link #body(Object...)}. + */ + float DEFAULT_FUEL_COST = 10.0f; + + /** + * The current remaining fuel for nested Templates. Every level of Template nesting + * subtracts a certain amount of fuel, and when it reaches zero, Templates are supposed to + * stop nesting, if possible. This is not a hard rule, but a guide, and a mechanism to ensure + * termination in recursive Template instantiations. + * + *

+ * Example of a recursive Template, which checks the remaining {@link #fuel} at every level, + * and terminates if it reaches zero. It also demonstrates the use of {@link TemplateBinding} for + * the recursive use of Templates. We {@link Template.OneArg#render} with {@code 30} total fuel, + * and spend {@code 5} fuel at each recursion level. + * + *

+ * {@snippet lang=java : + * var binding = new TemplateBinding>(); + * var template = Template.make("depth", (Integer depth) -> body( + * setFuelCost(5.0f), + * let("fuel", fuel()), + * """ + * System.out.println("Currently at depth #depth with fuel #fuel"); + * """, + * (fuel() > 0) ? binding.get().asToken(depth + 1) : + * "// terminate\n" + * )); + * binding.bind(template); + * String code = template.render(30.0f, 0); + * } + * + * @return The amount of fuel left for nested Template use. + */ + static float fuel() { + return Renderer.getCurrent().fuel(); + } + + /** + * Changes the amount of fuel used for the current Template, where the default is + * {@link Template#DEFAULT_FUEL_COST}. + * + * @param fuelCost The amount of fuel used for the current Template. + * @return A token for convenient use in {@link Template#body}. + */ + static Token setFuelCost(float fuelCost) { + Renderer.getCurrent().setFuelCost(fuelCost); + return new NothingToken(); + } + + /** + * Add a {@link DataName} in the current scope, that is the innermost of either + * {@link Template#body} or {@link Hook#anchor}. + * + * @param name The name of the {@link DataName}, i.e. the {@link String} used in code. + * @param type The type of the {@link DataName}. + * @param mutability Indicates if the {@link DataName} is to be mutable or immutable, + * i.e. if we intend to use the {@link DataName} only for reading + * or if we also allow it to be mutated. + * @param weight The weight of the {@link DataName}, which correlates to the probability + * of this {@link DataName} being chosen when we sample. + * Must be a value from 1 to 1000. + * @return The token that performs the defining action. + */ + static Token addDataName(String name, DataName.Type type, DataName.Mutability mutability, int weight) { + if (mutability != DataName.Mutability.MUTABLE && + mutability != DataName.Mutability.IMMUTABLE) { + throw new IllegalArgumentException("Unexpected mutability: " + mutability); + } + boolean mutable = mutability == DataName.Mutability.MUTABLE; + if (weight <= 0 || 1000 < weight) { + throw new IllegalArgumentException("Unexpected weight: " + weight); + } + return new AddNameToken(new DataName(name, type, mutable, weight)); + } + + /** + * Add a {@link DataName} in the current scope, that is the innermost of either + * {@link Template#body} or {@link Hook#anchor}, with a {@code weight} of 1. + * + * @param name The name of the {@link DataName}, i.e. the {@link String} used in code. + * @param type The type of the {@link DataName}. + * @param mutability Indicates if the {@link DataName} is to be mutable or immutable, + * i.e. if we intend to use the {@link DataName} only for reading + * or if we also allow it to be mutated. + * @return The token that performs the defining action. + */ + static Token addDataName(String name, DataName.Type type, DataName.Mutability mutability) { + return addDataName(name, type, mutability, 1); + } + + /** + * Access the set of {@link DataName}s, for sampling, counting, etc. + * + * @param mutability Indicates if we only sample from mutable, immutable or either {@link DataName}s. + * @return A view on the {@link DataName}s, on which we can sample, count, etc. + */ + static DataName.FilteredSet dataNames(DataName.Mutability mutability) { + return new DataName.FilteredSet(mutability); + } + + /** + * Add a {@link StructuralName} in the current scope, that is the innermost of either + * {@link Template#body} or {@link Hook#anchor}. + * + * @param name The name of the {@link StructuralName}, i.e. the {@link String} used in code. + * @param type The type of the {@link StructuralName}. + * @param weight The weight of the {@link StructuralName}, which correlates to the probability + * of this {@link StructuralName} being chosen when we sample. + * Must be a value from 1 to 1000. + * @return The token that performs the defining action. + */ + static Token addStructuralName(String name, StructuralName.Type type, int weight) { + if (weight <= 0 || 1000 < weight) { + throw new IllegalArgumentException("Unexpected weight: " + weight); + } + return new AddNameToken(new StructuralName(name, type, weight)); + } + + /** + * Add a {@link StructuralName} in the current scope, that is the innermost of either + * {@link Template#body} or {@link Hook#anchor}, with a {@code weight} of 1. + * + * @param name The name of the {@link StructuralName}, i.e. the {@link String} used in code. + * @param type The type of the {@link StructuralName}. + * @return The token that performs the defining action. + */ + static Token addStructuralName(String name, StructuralName.Type type) { + return addStructuralName(name, type, 1); + } + + /** + * Access the set of {@link StructuralName}s, for sampling, counting, etc. + * + * @return A view on the {@link StructuralName}s, on which we can sample, count, etc. + */ + static StructuralName.FilteredSet structuralNames() { + return new StructuralName.FilteredSet(); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBinding.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBinding.java new file mode 100644 index 00000000000..2073b788e71 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBinding.java @@ -0,0 +1,70 @@ +/* + * 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; + +/** + * To facilitate recursive uses of Templates, for example where a template uses + * itself and needs to be referenced before it is fully defined, + * one can use the indirection of a {@link TemplateBinding}. The {@link TemplateBinding} + * is allocated first without any Template bound to it yet. At this stage, + * it can be used with {@link #get} inside a Template. Later, we can {@link #bind} + * a Template to the binding, such that {@link #get} returns that bound + * Template. + * + * @param Type of the template. + */ +public class TemplateBinding { + private T template = null; + + /** + * Creates a new {@link TemplateBinding} that has no Template bound to it yet. + */ + public TemplateBinding() {} + + /** + * Retrieve the Template that was previously bound to the binding. + * + * @return The Template that was previously bound with {@link #bind}. + * @throws RendererException if no Template was bound yet. + */ + public T get() { + if (template == null) { + throw new RendererException("Cannot 'get' before 'bind'."); + } + return template; + } + + /** + * Binds a Template for future reference using {@link #get}. + * + * @param template The Template to be bound. + * @throws RendererException if a Template was already bound. + */ + public void bind(T template) { + if (this.template != null) { + throw new RendererException("Duplicate 'bind' not allowed."); + } + this.template = template; + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java new file mode 100644 index 00000000000..440766b3f79 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateBody.java @@ -0,0 +1,34 @@ +/* + * 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; + +/** + * A Template generates a {@link TemplateBody}, which is a list of {@link Token}s, + * which are then later rendered to {@link String}s. + * + * @param tokens The list of {@link Token}s that are later rendered to {@link String}s. + */ +public record TemplateBody(List tokens) {} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.java new file mode 100644 index 00000000000..cf8c4afb321 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateFrame.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. + */ + +package compiler.lib.template_framework; + +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link TemplateFrame} is the frame for a {@link Template}, i.e. the corresponding + * {@link TemplateToken}. It ensures that each template use has its own unique {@link #id} + * used to deconflict names using {@link Template#$}. It also has a set of hashtag + * replacements, which combine the key-value pairs from the template argument and the + * {@link Template#let} definitions. The {@link #parent} relationship provides a trace + * for the use chain of templates. The {@link #fuel} is reduced over this chain, to give + * a heuristic on how much time is spent on the code from the template corresponding to + * the frame, and to give a termination criterion to avoid nesting templates too deeply. + * + *

+ * See also {@link CodeFrame} for more explanations about the frames. + */ +class TemplateFrame { + final TemplateFrame parent; + private final int id; + private final Map hashtagReplacements = new HashMap<>(); + final float fuel; + private float fuelCost; + + public static TemplateFrame makeBase(int id, float fuel) { + return new TemplateFrame(null, id, fuel, 0.0f); + } + + public static TemplateFrame make(TemplateFrame parent, int id) { + return new TemplateFrame(parent, id, parent.fuel - parent.fuelCost, Template.DEFAULT_FUEL_COST); + } + + private TemplateFrame(TemplateFrame parent, int id, float fuel, float fuelCost) { + this.parent = parent; + this.id = id; + this.fuel = fuel; + this.fuelCost = fuelCost; + } + + public String $(String name) { + if (name == null) { + throw new RendererException("A '$' name should not be null."); + } + if (!Renderer.isValidHashtagOrDollarName(name)) { + throw new RendererException("Is not a valid '$' name: '" + name + "'."); + } + return name + "_" + id; + } + + void addHashtagReplacement(String key, String value) { + if (key == null) { + throw new RendererException("A hashtag replacement should not be null."); + } + if (!Renderer.isValidHashtagOrDollarName(key)) { + throw new RendererException("Is not a valid hashtag replacement name: '" + key + "'."); + } + if (hashtagReplacements.putIfAbsent(key, value) != null) { + throw new RendererException("Duplicate hashtag replacement for #" + key); + } + } + + String getHashtagReplacement(String key) { + if (!Renderer.isValidHashtagOrDollarName(key)) { + throw new RendererException("Is not a valid hashtag replacement name: '" + key + "'."); + } + if (hashtagReplacements.containsKey(key)) { + return hashtagReplacements.get(key); + } + throw new RendererException("Missing hashtag replacement for #" + key); + } + + void setFuelCost(float fuelCost) { + this.fuelCost = fuelCost; + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java new file mode 100644 index 00000000000..47262f152d4 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/TemplateToken.java @@ -0,0 +1,169 @@ +/* + * 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; + +/** + * Represents a tokenized {@link Template} (after calling {@code asToken()}) ready for + * instantiation either as a {@link Token} inside another {@link Template} or as + * a {@link String} with {@link #render}. + */ +public sealed abstract class TemplateToken implements Token + permits TemplateToken.ZeroArgs, + TemplateToken.OneArg, + TemplateToken.TwoArgs, + TemplateToken.ThreeArgs +{ + private TemplateToken() {} + + /** + * Represents a tokenized zero-argument {@link Template} ready for instantiation + * either as a {@link Token} inside another {@link Template} or as a {@link String} + * with {@link #render}. + */ + static final class ZeroArgs extends TemplateToken implements Token { + private final Template.ZeroArgs zeroArgs; + + ZeroArgs(Template.ZeroArgs zeroArgs) { + this.zeroArgs = zeroArgs; + } + + @Override + public TemplateBody instantiate() { + return zeroArgs.instantiate(); + } + + @Override + public void visitArguments(ArgumentVisitor visitor) {} + } + + /** + * Represents a tokenized one-argument {@link Template}, already filled with arguments, ready for + * instantiation either as a {@link Token} inside another {@link Template} or as a {@link String} + * with {@link #render}. + * + * @param The type of the (first) argument. + */ + static final class OneArg extends TemplateToken implements Token { + private final Template.OneArg oneArgs; + private final T1 arg1; + + OneArg(Template.OneArg oneArgs, T1 arg1) { + this.oneArgs = oneArgs; + this.arg1 = arg1; + } + + @Override + public TemplateBody instantiate() { + return oneArgs.instantiate(arg1); + } + + @Override + public void visitArguments(ArgumentVisitor visitor) { + visitor.visit(oneArgs.arg1Name(), arg1); + } + } + + /** + * Represents a tokenized two-argument {@link Template}, already filled with arguments, ready for + * instantiation either as a {@link Token} inside another {@link Template} or as a {@link String} + * with {@link #render}. + * + * @param The type of the first argument. + * @param The type of the second argument. + */ + static final class TwoArgs extends TemplateToken implements Token { + private final Template.TwoArgs twoArgs; + private final T1 arg1; + private final T2 arg2; + + TwoArgs(Template.TwoArgs twoArgs, T1 arg1, T2 arg2) { + this.twoArgs = twoArgs; + this.arg1 = arg1; + this.arg2 = arg2; + } + + @Override + public TemplateBody instantiate() { + return twoArgs.instantiate(arg1, arg2); + } + + @Override + public void visitArguments(ArgumentVisitor visitor) { + visitor.visit(twoArgs.arg1Name(), arg1); + visitor.visit(twoArgs.arg2Name(), arg2); + } + } + + /** + * Represents a tokenized three-argument {@link TemplateToken}, already filled with arguments, ready for + * instantiation either as a {@link Token} inside another {@link Template} or as a {@link String} + * with {@link #render}. + * + * @param The type of the first argument. + * @param The type of the second argument. + * @param The type of the second argument. + */ + static final class ThreeArgs extends TemplateToken implements Token { + private final Template.ThreeArgs threeArgs; + private final T1 arg1; + private final T2 arg2; + private final T3 arg3; + + ThreeArgs(Template.ThreeArgs threeArgs, T1 arg1, T2 arg2, T3 arg3) { + this.threeArgs = threeArgs; + this.arg1 = arg1; + this.arg2 = arg2; + this.arg3 = arg3; + } + + @Override + public TemplateBody instantiate() { + return threeArgs.instantiate(arg1, arg2, arg3); + } + + @Override + public void visitArguments(ArgumentVisitor visitor) { + visitor.visit(threeArgs.arg1Name(), arg1); + visitor.visit(threeArgs.arg2Name(), arg2); + visitor.visit(threeArgs.arg3Name(), arg3); + } + } + + abstract TemplateBody instantiate(); + + @FunctionalInterface + interface ArgumentVisitor { + void visit(String name, Object value); + } + + abstract void visitArguments(ArgumentVisitor visitor); + + final String render() { + return Renderer.render(this); + } + + final String render(float fuel) { + return Renderer.render(this, fuel); + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Token.java b/test/hotspot/jtreg/compiler/lib/template_framework/Token.java new file mode 100644 index 00000000000..dc750c7f79f --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Token.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.Arrays; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link Template#body} and {@link Hook#anchor} are given a list of tokens, which are either + * {@link Token}s or {@link String}s or some permitted boxed primitives. These are then parsed + * and all non-{@link Token}s are converted to {@link StringToken}s. The parsing also flattens + * {@link List}s. + */ +sealed interface Token permits StringToken, + TemplateToken, + TemplateToken.ZeroArgs, + TemplateToken.OneArg, + TemplateToken.TwoArgs, + TemplateToken.ThreeArgs, + HookAnchorToken, + HookInsertToken, + AddNameToken, + NothingToken +{ + static List parse(Object[] objects) { + if (objects == null) { + throw new IllegalArgumentException("Unexpected tokens: null"); + } + List outputList = new ArrayList<>(); + parseToken(Arrays.asList(objects), outputList); + return outputList; + } + + private static void parseList(List inputList, List outputList) { + for (Object o : inputList) { + parseToken(o, outputList); + } + } + + private static void parseToken(Object o, List outputList) { + if (o == null) { + throw new IllegalArgumentException("Unexpected token: null"); + } + switch (o) { + case Token t -> outputList.add(t); + case String s -> outputList.add(new StringToken(Renderer.format(s))); + case Integer s -> outputList.add(new StringToken(Renderer.format(s))); + case Long s -> outputList.add(new StringToken(Renderer.format(s))); + case Double s -> outputList.add(new StringToken(Renderer.format(s))); + case Float s -> outputList.add(new StringToken(Renderer.format(s))); + case Boolean s -> outputList.add(new StringToken(Renderer.format(s))); + case List l -> parseList(l, outputList); + default -> throw new IllegalArgumentException("Unexpected token: " + o); + } + } +} diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/library/Hooks.java b/test/hotspot/jtreg/compiler/lib/template_framework/library/Hooks.java new file mode 100644 index 00000000000..410b790e3b5 --- /dev/null +++ b/test/hotspot/jtreg/compiler/lib/template_framework/library/Hooks.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package compiler.lib.template_framework.library; + +import compiler.lib.template_framework.Hook; + +/** + * Provides a hook for class and method scopes, to be used in Templates. + */ +public final class Hooks { + private Hooks() {} // Avoid instantiation and need for documentation. + + /** + * Template {@link Hook} used by the Template Library for class scopes, to insert + * fields and methods. + */ + public static final Hook CLASS_HOOK = new Hook("Class"); + + /** + * Template {@link Hook} used by the Template Library for method scopes, to insert + * local variables, and computations for local variables at the beginning of a + * method. + */ + public static final Hook METHOD_HOOK = new Hook("Method"); +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java new file mode 100644 index 00000000000..c5a4528f63d --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestAdvanced.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8344942 + * @summary Test simple use of Templates with the Compile Framework. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @compile ../../../compiler/lib/ir_framework/TestFramework.java + * @compile ../../../compiler/lib/verify/Verify.java + * @run main template_framework.examples.TestAdvanced + */ + +package template_framework.examples; + +import java.util.List; +import jdk.test.lib.Utils; + +import compiler.lib.generators.Generator; +import compiler.lib.generators.Generators; +import compiler.lib.generators.RestrictableGenerator; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.Template; +import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.let; + +/** + * This is a basic example for Templates, using them to cover a list of test variants. + *

+ * 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 TestAdvanced { + public static final RestrictableGenerator GEN_BYTE = Generators.G.safeRestrict(Generators.G.ints(), Byte.MIN_VALUE, Byte.MAX_VALUE); + public static final RestrictableGenerator GEN_CHAR = Generators.G.safeRestrict(Generators.G.ints(), Character.MIN_VALUE, Character.MAX_VALUE); + public static final RestrictableGenerator GEN_SHORT = Generators.G.safeRestrict(Generators.G.ints(), Short.MIN_VALUE, Short.MAX_VALUE); + public static final RestrictableGenerator GEN_INT = Generators.G.ints(); + public static final RestrictableGenerator GEN_LONG = Generators.G.longs(); + public static final Generator GEN_FLOAT = Generators.G.floats(); + public static final Generator GEN_DOUBLE = Generators.G.doubles(); + + 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(); + comp.invoke("p.xyz.InnerTest", "main", new Object[] {}); + } + + interface MyGenerator { + Object next(); + } + + record Type(String name, MyGenerator generator, List operators) {} + + // Generate a source Java file as String + public static String generate(CompileFramework comp) { + + // The test template: + // - For a chosen type, operator, and generator. + // - The variable name "GOLD" and the test name "test" would get conflicts + // if we instantiate the template multiple times. Thus, we use the $ prefix + // so that the Template Framework can replace the names and make them unique + // for each Template instantiation. + // - The GOLD value is computed at the beginning, hopefully by the interpreter. + // - The test method is eventually compiled, and the values are verified by the + // check method. + var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> body( + let("con1", generator.next()), + let("con2", generator.next()), + """ + // #typeName #operator #con1 #con2 + public static #typeName $GOLD = $test(); + + @Test + public static #typeName $test() { + return (#typeName)(#con1 #operator #con2); + } + + @Check(test = "$test") + public static void $check(#typeName result) { + Verify.checkEQ(result, $GOLD); + } + """ + )); + + // Template for the Class. + var classTemplate = Template.make("types", (List types) -> body( + let("classpath", comp.getEscapedClassPathOfCompiledClasses()), + """ + package p.xyz; + + import compiler.lib.ir_framework.*; + import compiler.lib.verify.*; + + public class InnerTest { + public static void main() { + TestFramework framework = new TestFramework(InnerTest.class); + // Set the classpath, so that the TestFramework test VM knows where + // the CompileFramework put the class files of the compiled source code. + framework.addFlags("-classpath", "#classpath"); + framework.start(); + } + + """, + // Call the testTemplate for each type and operator, generating a + // list of lists of TemplateToken: + types.stream().map((Type type) -> + type.operators().stream().map((String operator) -> + testTemplate.asToken(type.name(), operator, type.generator())).toList() + ).toList(), + """ + } + """ + )); + + // For each type, we choose a list of operators that do not throw exceptions. + List types = List.of( + new Type("byte", GEN_BYTE::next, List.of("+", "-", "*", "&", "|", "^")), + new Type("char", GEN_CHAR::next, List.of("+", "-", "*", "&", "|", "^")), + new Type("short", GEN_SHORT::next, List.of("+", "-", "*", "&", "|", "^")), + new Type("int", GEN_INT::next, List.of("+", "-", "*", "&", "|", "^")), + new Type("long", GEN_LONG::next, List.of("+", "-", "*", "&", "|", "^")), + new Type("float", GEN_FLOAT::next, List.of("+", "-", "*", "/")), + new Type("double", GEN_DOUBLE::next, List.of("+", "-", "*", "/")) + ); + + // Use the template with one argument and render it to a String. + return classTemplate.render(types); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.java new file mode 100644 index 00000000000..e06671ca951 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestSimple.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. + */ + +/* + * @test + * @bug 8344942 + * @summary Test simple use of Templates with the Compile Framework. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run main template_framework.examples.TestSimple + */ + +package template_framework.examples; + +import compiler.lib.compile_framework.*; +import compiler.lib.template_framework.Template; +import static compiler.lib.template_framework.Template.body; + +public class TestSimple { + + 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.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 arguments. + 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 arguments. + var template = Template.make("arg1", "arg2", (Integer arg1, String arg2) -> body( + """ + package p.xyz; + public class InnerTest { + public static int test() { + return #arg1 + #arg2; + } + } + """ + )); + + // Use the template with two arguments, and render it to a String. + return template.render(42, "7"); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java new file mode 100644 index 00000000000..faa05b29d82 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java @@ -0,0 +1,1227 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8344942 + * @summary Demonstrate the use of Templates with the Compile Framework. + * It displays the use of most features in the Template Framework. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run main template_framework.examples.TestTutorial + */ + +package template_framework.examples; + +import java.util.Collections; +import java.util.List; + +import compiler.lib.compile_framework.*; + +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.Hook; +import compiler.lib.template_framework.TemplateBinding; +import compiler.lib.template_framework.DataName; +import compiler.lib.template_framework.StructuralName; +import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.let; +import static compiler.lib.template_framework.Template.$; +import static compiler.lib.template_framework.Template.fuel; +import static compiler.lib.template_framework.Template.addDataName; +import static compiler.lib.template_framework.Template.dataNames; +import static compiler.lib.template_framework.Template.addStructuralName; +import static compiler.lib.template_framework.Template.structuralNames; +import static compiler.lib.template_framework.DataName.Mutability.MUTABLE; +import static compiler.lib.template_framework.DataName.Mutability.IMMUTABLE; +import static compiler.lib.template_framework.DataName.Mutability.MUTABLE_OR_IMMUTABLE; + +import compiler.lib.template_framework.library.Hooks; + +public class TestTutorial { + + public static void main(String[] args) { + // Create a new CompileFramework instance. + CompileFramework comp = new CompileFramework(); + + // Add Java source files. + comp.addJavaSourceCode("p.xyz.InnerTest1", generateWithListOfTokens()); + comp.addJavaSourceCode("p.xyz.InnerTest2", generateWithTemplateArguments()); + comp.addJavaSourceCode("p.xyz.InnerTest3", generateWithHashtagAndDollarReplacements()); + comp.addJavaSourceCode("p.xyz.InnerTest3b", generateWithHashtagAndDollarReplacements2()); + comp.addJavaSourceCode("p.xyz.InnerTest4", generateWithCustomHooks()); + comp.addJavaSourceCode("p.xyz.InnerTest5", generateWithLibraryHooks()); + comp.addJavaSourceCode("p.xyz.InnerTest6", generateWithRecursionAndBindingsAndFuel()); + comp.addJavaSourceCode("p.xyz.InnerTest7", generateWithDataNamesSimple()); + comp.addJavaSourceCode("p.xyz.InnerTest8", generateWithDataNamesForFieldsAndVariables()); + comp.addJavaSourceCode("p.xyz.InnerTest9a", generateWithDataNamesAndScopes1()); + comp.addJavaSourceCode("p.xyz.InnerTest9b", generateWithDataNamesAndScopes2()); + comp.addJavaSourceCode("p.xyz.InnerTest10", generateWithDataNamesForFuzzing()); + comp.addJavaSourceCode("p.xyz.InnerTest11", generateWithStructuralNamesForMethods()); + + // Compile the source files. + // Hint: if you want to see the generated source code, you can enable + // printing of the source code that the CompileFramework receives, + // with -DCompileFrameworkVerbose=true + // The code may not be nicely formatted, especially regarding + // indentation. You might consider dumping the generated code + // into an IDE or other auto-formatting tool. + comp.compile(); + + comp.invoke("p.xyz.InnerTest1", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest2", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest3", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest3b", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest4", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest5", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest6", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest7", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest8", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest9a", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest9b", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest10", "main", new Object[] {}); + comp.invoke("p.xyz.InnerTest11", "main", new Object[] {}); + } + + // This example shows the use of various Tokens. + public static String generateWithListOfTokens() { + // A Template is essentially a function / lambda that produces a + // token body, which is a list of Tokens that are concatenated. + var templateClass = Template.make(() -> body( + // The "body" method is filled by a sequence of "Tokens". + // These can be Strings and multi-line Strings, but also + // boxed primitives. + """ + package p.xyz; + + public class InnerTest1 { + public static void main() { + System.out.println("Hello World!"); + """, + "int a = ", 1, ";\n", + "float b = ", 1.5f, ";\n", + // Special Float values are "smartly" formatted! + "float nan = ", Float.POSITIVE_INFINITY, ";\n", + "boolean c = ", true, ";\n", + // Lists of Tokens are also allowed: + List.of("int ", "d = 5", ";\n"), + // We can also stream / map over an existing list, or one created on + // the fly: + List.of(3, 5, 7, 11).stream().map(i -> "System.out.println(" + i + ");\n").toList(), + """ + System.out.println(a + " " + b + " " + nan + " " + c + " " + d); + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // This example shows the use of Templates, with and without arguments. + public static String generateWithTemplateArguments() { + // A Template with no arguments. + var templateHello = Template.make(() -> body( + """ + System.out.println("Hello"); + """ + )); + + // A Template with a single Integer argument. + var templateCompare = Template.make("arg", (Integer arg) -> body( + "System.out.println(", arg, ");\n", // capture arg via lambda argument + "System.out.println(#arg);\n", // capture arg via hashtag replacement + "System.out.println(#{arg});\n", // capture arg via hashtag replacement with brackets + // It would have been optimal to use Java String Templates to format + // argument values into Strings. However, since these are not (yet) + // available, the Template Framework provides two alternative ways of + // formatting Strings: + // 1) By appending to the comma-separated list of Tokens passed to body(). + // Appending as a Token works whenever one has a reference to the Object + // in Java code. But often, this is rather cumbersome and looks awkward, + // given all the additional quotes and commands required. Hence, it + // is encouraged to only use this method when necessary. + // 2) By hashtag replacements inside a single string. One can either + // use "#arg" directly, or use brackets "#{arg}". When possible, one + // should prefer avoiding the brackets, as they create additional + // noise. However, there are cases where they are useful, for + // example "#TYPE_CON" would be parsed as a hashtag replacement + // for the hashtag name "TYPE_CON", whereas "#{TYPE}_CON" is + // parsed as hashtag name "TYPE", followed by literal string "_CON". + // See also: generateWithHashtagAndDollarReplacements2 + // There are two ways to define the value of a hashtag replacement: + // a) Capturing Template arguments as Strings. + // b) Using a "let" definition (see examples further down). + // Which one should be preferred is a code style question. Generally, we + // prefer the use of hashtag replacements because that allows easy use of + // multiline strings (i.e. text blocks). + "if (#arg != ", arg, ") { throw new RuntimeException(\"mismatch\"); }\n" + )); + + // A Template that creates the body of the Class and main method, and then + // uses the two Templates above inside it. + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest2 { + public static void main() { + """, + templateHello.asToken(), + templateCompare.asToken(7), + templateCompare.asToken(42), + """ + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // Example with hashtag replacements (arguments and let), and $-name renamings. + // Note: hashtag replacements are a workaround for the missing string templates. + // If we had string templates, we could just capture the typed lambda + // arguments, and use them directly in the String via string templating. + public static String generateWithHashtagAndDollarReplacements() { + var template1 = Template.make("x", (Integer x) -> body( + // We have the "#x" hashtag replacement from the argument capture above. + // Additionally, we can define "#con" as a hashtag replacement from let: + let("con", 3 * x), + // In the code below, we use "var" as a local variable. But if we were + // to instantiate this template twice, the names could conflict. Hence, + // we automatically rename the names that have a $ prepended with + // var_1, var_2, etc. + """ + int $var = #con; + System.out.println("T1: #x, #con, " + $var); + """ + )); + + var template2 = Template.make("x", (Integer x) -> + // Sometimes it can be helpful to not just create a hashtag replacement + // with let, but also to capture the variable to use it as lambda parameter. + let("y", 11 * x, y -> + body( + """ + System.out.println("T2: #x, #y"); + """, + template1.asToken(y) + ) + ) + ); + + // This template generates an int variable and assigns it a value. + // Together with template4, we see that each template has a unique renaming + // for a $-name replacement. + var template3 = Template.make("name", "value", (String name, Integer value) -> body( + """ + int #name = #value; // Note: $var is not #name + """ + )); + + var template4 = Template.make(() -> body( + """ + // We will define the variable $var: + """, + // We can capture the $-name programmatically, and pass it to other templates: + template3.asToken($("var"), 42), + """ + if ($var != 42) { throw new RuntimeException("Wrong value!"); } + """ + )); + + var templateClass = Template.make(() -> body( + // The Template Framework API only guarantees that every Template use + // has a unique ID. When using the Templates, all we need is that + // variables from different Template uses do not conflict. But it can + // be helpful to understand how the IDs are produced. The implementation + // simply gives the first Template use the ID=1, and increments from there. + // + // In this example, the templateClass is the first Template use, and + // has ID=1. We never use a dollar replacement here, so the code will + // not show any "_1". + """ + package p.xyz; + + public class InnerTest3 { + public static void main() { + """, + // Second Template use: ID=2 -> var_2 + template1.asToken(1), + // Third Template use: ID=3 -> var_3 + template1.asToken(7), + // Fourth Template use with template2, no use of dollar, so + // no "_4" shows up in the generated code. Internally, it + // calls template1, which is the fifth Template use, with + // ID = 5 -> var_5 + template2.asToken(2), + // Sixth and Seventh Template use -> var_7 + template2.asToken(5), + // Eighth Template use with template4 -> var_8. + // Ninth Template use with internal call to template3, + // The local "$var" turns to "var_9", but the Template + // argument captured value = "var_8" from the outer + // template use of $("var"). + template4.asToken(), + """ + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // In some cases, you may want to transform string arguments. You may + // be working with types "int" and "long", and want to create names like + // "INT_CON" and "LONG_CON". + public static String generateWithHashtagAndDollarReplacements2() { + // Let us define some final static variables of a specific type. + var template1 = Template.make("type", (String type) -> body( + // The type (e.g. "int") is lower case, let us create the upper case "INT_CON" from it. + let("TYPE", type.toUpperCase()), + """ + static final #type #{TYPE}_CON = 42; + """ + )); + + // Let's write a simple class to demonstrate that this works, i.e. produces compilable code. + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest3b { + """, + template1.asToken("int"), + template1.asToken("long"), + """ + public static void main() { + if (INT_CON != 42 || LONG_CON != 42) { + throw new RuntimeException("Wrong result!"); + } + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // In this example, we look at the use of Hooks. They allow us to reach back, to outer + // scopes. For example, we can reach out from inside a method body to a hook anchored at + // the top of the class, and insert a field. + public static String generateWithCustomHooks() { + // We can define a custom hook. + // Note: generally we prefer using the pre-defined CLASS_HOOK and METHOD_HOOK from the library, + // whenever possible. See also the example after this one. + var myHook = new Hook("MyHook"); + + var template1 = Template.make("name", "value", (String name, Integer value) -> body( + """ + public static int #name = #value; + """ + )); + + var template2 = Template.make("x", (Integer x) -> body( + """ + // Let us go back to where we anchored the hook with anchor() and define a field named $field there. + // Note that in the Java code we have not defined anchor() on the hook, yet. But since it's a lambda + // expression, it is not evaluated, yet! Eventually, anchor() will be evaluated before insert() in + // this example. + """, + myHook.insert(template1.asToken($("field"), x)), + """ + System.out.println("$field: " + $field); + if ($field != #x) { throw new RuntimeException("Wrong value!"); } + """ + )); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest4 { + """, + // We anchor a Hook outside the main method, but inside the Class. + // Anchoring a Hook creates a scope, spanning the braces of the + // "anchor" call. Any Hook.insert that happens inside this scope + // goes to the top of that scope. + myHook.anchor( + // Any Hook.insert goes here. + // + // <-------- field_X = 5 ------------------+ + // <-------- field_Y = 7 -------------+ | + // | | + """ + public static void main() { + """, // ^ ^ + template2.asToken(5), // -------- | ---+ + template2.asToken(7), // ---------+ + """ + } + """ + ), // The Hook scope ends here. + """ + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // We saw the use of custom hooks above, but now we look at the use of CLASS_HOOK and METHOD_HOOK. + // By convention, we use the CLASS_HOOK for class scopes, and METHOD_HOOK for method scopes. + // Whenever we open a class scope, we should anchor a CLASS_HOOK for that scope, and whenever we + // open a method, we should anchor a METHOD_HOOK. Conversely, this allows us to check if we are + // inside a class or method scope by querying "isAnchored". This convention helps us when building + // a large library of Templates. But if you are writing your own self-contained set of Templates, + // you do not have to follow this convention. + // + // Hooks are "re-entrant", that is we can anchor the same hook inside a scope that we already + // anchored it previously. The "Hook.insert" always goes to the innermost anchoring of that + // hook. There are cases where "re-entrant" Hooks are helpful such as nested classes, where + // there is a class scope inside another class scope. Similarly, we can nest lambda bodies + // inside method bodies, so also METHOD_HOOK can be used in such a "re-entrant" way. + public static String generateWithLibraryHooks() { + var templateStaticField = Template.make("name", "value", (String name, Integer value) -> body( + """ + static { System.out.println("Defining static field #name"); } + public static int #name = #value; + """ + )); + + var templateLocalVariable = Template.make("name", "value", (String name, Integer value) -> body( + """ + System.out.println("Defining local variable #name"); + int #name = #value; + """ + )); + + var templateMethodBody = Template.make(() -> body( + """ + // Let's define a local variable $var and a static field $field. + """, + Hooks.CLASS_HOOK.insert(templateStaticField.asToken($("field"), 5)), + Hooks.METHOD_HOOK.insert(templateLocalVariable.asToken($("var"), 11)), + """ + System.out.println("$field: " + $field); + System.out.println("$var: " + $var); + if ($field * $var != 55) { throw new RuntimeException("Wrong value!"); } + """ + )); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest5 { + """, + // Class Hook for fields. + Hooks.CLASS_HOOK.anchor( + """ + public static void main() { + """, + // Method Hook for local variables, and earlier computations. + Hooks.METHOD_HOOK.anchor( + """ + // This is the beginning of the "main" method body. + System.out.println("Welcome to main!"); + """, + templateMethodBody.asToken(), + """ + System.out.println("Going to call other..."); + other(); + """ + ), + """ + } + + private static void other() { + """, + // Have a separate method hook for other, so that it can insert + // its own local variables. + Hooks.METHOD_HOOK.anchor( + """ + System.out.println("Welcome to other!"); + """, + templateMethodBody.asToken(), + """ + System.out.println("Done with other."); + """ + ), + """ + } + """ + ), + """ + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // This example shows the use of bindings to allow cyclic references of Templates, + // allowing recursive template generation. We also show the use of fuel to limit + // recursion. + public static String generateWithRecursionAndBindingsAndFuel() { + // Binding allows the use of template1 inside of template1, via the binding indirection. + var binding1 = new TemplateBinding>(); + var template1 = Template.make("depth", (Integer depth) -> body( + let("fuel", fuel()), + """ + System.out.println("At depth #depth with fuel #fuel."); + """, + // We cannot yet use template1 directly, as it is being defined. + // So we use binding1 instead. + // For every recursion depth, some fuel is automatically subtracted + // so that the fuel slowly depletes with the depth. + // We keep the recursion going until the fuel is depleted. + // + // Note: if we forget to check the fuel(), the renderer causes a + // StackOverflowException, because the recursion never ends. + (fuel() > 0) ? binding1.get().asToken(depth + 1) + : "System.out.println(\"Fuel depleted.\");\n", + """ + System.out.println("Exit depth #depth."); + """ + )); + binding1.bind(template1); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest6 { + public static void main() { + System.out.println("Welcome to main!"); + """, + template1.asToken(0), + """ + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // Below, we introduce the concept of "DataNames". Code generation often involves defining + // fields and variables, which are then available inside a defined scope. "DataNames" can + // be registered at a certain scope with addDataName. This "DataName" is then available + // in this scope, and in any nested scope, including nested Templates. This allows us to + // add some fields and variables in one Template, and later on, in another Template, we + // can access these fields and variables again with "dataNames()". + // + // Here are a few use-cases: + // - You are writing some inner Template, and would like to access a random field or + // variable from an outer Template. Luckily, the outer Templates have added their + // fields and variables, and you can now access them with "dataNames()". You can + // count them, get a list of them, or sample a random one. + // - You are writing some outer Template, and would like to generate a variable that + // an inner Template could read from or even write to. You can "addDataName" the + // variable, and the inner Template can then find that variable in "dataNames()". + // If the inner Template wants to find a random field or variable, it may sample + // from "dataNodes()", and with some probability, it would sample your variable. + // + // A "DataName" captures the name of the field or variable in a String. It also + // stores the type of the field or variable, as well as its "mutability", i.e. + // an indication if the field or variable is only for reading, or if writing to + // it is also allowed. If a field or variable is final, we must make sure that the + // "DataName" is immutable, otherwise we risk that some Template attempts to generate + // code that writes to the final field or variable, and then we get a compilation + // error from "javac" later on. + // + // To get started, we show an example where all DataNames have the same type, and where + // all Names are mutable. For simplicity, our type represents the primitive int type. + private record MySimpleInt() implements DataName.Type { + // The type is only subtype of itself. This is relevant when sampling or weighing + // DataNames, because we do not just sample from the given type, but also its subtypes. + @Override + public boolean isSubtypeOf(DataName.Type other) { + return other instanceof MySimpleInt(); + } + + // The name of the type can later be accessed, and used in code. We are working + // with ints, so that is what we return. + @Override + public String name() { return "int"; } + } + private static final MySimpleInt mySimpleInt = new MySimpleInt(); + + // In this example, we generate 3 fields, and add their names to the + // current scope. In a nested Template, we can then sample one of these + // DataNames, which gives us one of the fields. We increment that randomly + // chosen field. At the end, we print all three fields. + public static String generateWithDataNamesSimple() { + var templateMain = Template.make(() -> body( + // Sample a random DataName, i.e. field, and assign its name to + // the hashtag replacement "#f". + // We are picking a mutable DataName, because we are not just + // reading but also writing to the field. + let("f", dataNames(MUTABLE).exactOf(mySimpleInt).sample().name()), + """ + // Let us now sample a random field #f, and increment it. + #f += 42; + """ + )); + + var templateClass = Template.make(() -> body( + // Let us define the names for the three fields. + // We can then sample from these names in a nested Template. + // We make all DataNames mutable, and with the same weight of 1, + // so that they have equal probability of being sampled. + // Note: the default weight is 1, so we can also omit the weight. + addDataName($("f1"), mySimpleInt, MUTABLE, 1), + addDataName($("f2"), mySimpleInt, MUTABLE, 1), + addDataName($("f3"), mySimpleInt, MUTABLE), // omit weight, default is 1. + """ + package p.xyz; + + public class InnerTest7 { + // Let us define some fields. + public static int $f1 = 0; + public static int $f2 = 0; + public static int $f3 = 0; + + public static void main() { + // Let us now call the nested template that samples + // a random field and increments it. + """, + templateMain.asToken(), + """ + // Now, we can print all three fields, and see which + // one was incremented. + System.out.println("f1: " + $f1); + System.out.println("f2: " + $f2); + System.out.println("f3: " + $f3); + // We have two zeros, and one 42. + if ($f1 + $f2 + $f3 != 42) { throw new RuntimeException("wrong result!"); } + } + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // In the example above, we could have easily kept track of the three fields ourselves, + // and would not have had to rely on the Template Framework's DataNames for this. However, + // with more complicated examples, this gets more and more difficult, if not impossible. + // + // In the example below, we make the scenario a little more realistic. We work with an + // int and a long type. In the main method, we add some fields and local variables, and + // register their DataNames. When sampling from the main method, we should be able to see + // both fields and variables that we just registered. But from another method, we should + // only see the fields, but the local variables from main should not be sampled. + // + // Let us now define the wrapper for primitive types such as int and long. + private record MyPrimitive(String name) implements DataName.Type { + @Override + public boolean isSubtypeOf(DataName.Type other) { + return other instanceof MyPrimitive(String n) && n.equals(name()); + } + + // Note: the name method is automatically overridden by the record + // field accessor. + // But we would like to also directly use the type in the templates, + // hence we let "toString" return "int" or "long". + @Override + public String toString() { return name(); } + } + private static final MyPrimitive myInt = new MyPrimitive("int"); + private static final MyPrimitive myLong = new MyPrimitive("long"); + + public static String generateWithDataNamesForFieldsAndVariables() { + // Define a static field. + var templateStaticField = Template.make("type", (DataName.Type type) -> body( + addDataName($("field"), type, MUTABLE), + // Note: since we have overridden MyPrimitive::toString, we can use + // the type directly as "#type" in the template, which then + // gets hashtag replaced with "int" or "long". + """ + public static #type $field = 0; + """ + )); + + // Define a local variable. + var templateLocalVariable = Template.make("type", (DataName.Type type) -> body( + addDataName($("var"), type, MUTABLE), + """ + #type $var = 0; + """ + )); + + // Sample a random field or variable, from those that are available at + // the current scope. + var templateSample = Template.make("type", (DataName.Type type) -> body( + let("name", dataNames(MUTABLE).exactOf(type).sample().name()), + // Note: we could also sample from MUTABLE_OR_IMMUTABLE, we will + // cover the concept of mutability in an example further down. + """ + System.out.println("Sampling type #type: #name = " + #name); + """ + )); + + // Check how many fields and variables are available at the current scope. + var templateStatus = Template.make(() -> body( + let("ints", dataNames(MUTABLE).exactOf(myInt).count()), + let("longs", dataNames(MUTABLE).exactOf(myLong).count()), + // Note: we could also count the MUTABLE_OR_IMMUTABLE, we will + // cover the concept of mutability in an example further down. + """ + System.out.println("Status: #ints ints, #longs longs."); + """ + )); + + // Definition of the main method body. + var templateMain = Template.make(() -> body( + """ + System.out.println("Starting inside main..."); + """, + // Check the initial status, there should be nothing available. + templateStatus.asToken(), + // Define some local variables. We place them at the beginning of + // the method, by using the METHOD_HOOK. + Hooks.METHOD_HOOK.insert(templateLocalVariable.asToken(myInt)), + Hooks.METHOD_HOOK.insert(templateLocalVariable.asToken(myLong)), + // Define some static fields. We place them at the top of the class, + // by using the CLASS_HOOK. + Hooks.CLASS_HOOK.insert(templateStaticField.asToken(myInt)), + Hooks.CLASS_HOOK.insert(templateStaticField.asToken(myLong)), + // If we check the status now, we should see two int and two + // long names, corresponding to our two fields and variables. + templateStatus.asToken(), + // Now, we sample 5 int and 5 long names. We should get a mix + // of fields and variables. We have access to the fields because + // we inserted them to the class scope. We have access to the + // variables because we inserted them to the current method + // body. + Collections.nCopies(5, templateSample.asToken(myInt)), + Collections.nCopies(5, templateSample.asToken(myLong)), + // The status should not have changed since we last checked. + templateStatus.asToken(), + """ + System.out.println("Finishing inside main."); + """ + )); + + // Definition of another method's body. It is in the same class + // as the main method, so it has access to the same static fields. + var templateOther = Template.make(() -> body( + """ + System.out.println("Starting inside other..."); + """, + // We should see the fields defined in the main body, + // one int and one long field. + templateStatus.asToken(), + // Sampling 5 random int and 5 random long DataNames. We should + // only see the fields, and not the local variables from main. + Collections.nCopies(5, templateSample.asToken(myInt)), + Collections.nCopies(5, templateSample.asToken(myLong)), + // The status should not have changed since we last checked. + templateStatus.asToken(), + """ + System.out.println("Finishing inside other."); + """ + )); + + // Finally, we put it all together in a class. + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest8 { + """, + // Class Hook for fields. + Hooks.CLASS_HOOK.anchor( + """ + public static void main() { + """, + // Method Hook for local variables. + Hooks.METHOD_HOOK.anchor( + """ + // This is the beginning of the "main" method body. + System.out.println("Welcome to main!"); + """, + templateMain.asToken(), + """ + System.out.println("Going to call other..."); + other(); + """ + ), + """ + } + + private static void other() { + """, + // Have a separate method hook for other, where it could insert + // its own local variables (but happens not to). + Hooks.METHOD_HOOK.anchor( + """ + System.out.println("Welcome to other!"); + """, + templateOther.asToken(), + """ + System.out.println("Done with other."); + """ + ), + """ + } + """ + ), + """ + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // Let us have a closer look at how DataNames interact with scopes created by + // Templates and Hooks. Additionally, we see how the execution order of the + // lambdas and token evaluation affects the availability of DataNames. + // + // We inject the results directly into verification inside the code, so it + // is relatively simple to see what the expected results are. + // + // For simplicity, we define a simple "list" function. It collects all + // field and variable names, and immediately returns the comma separated + // list of the names. We can use that to visualize the available names + // at any point. + public static String listNames() { + return "{" + String.join(", ", dataNames(MUTABLE).exactOf(myInt).toList() + .stream().map(DataName::name).toList()) + "}"; + } + + // Even simpler: count the available variables and return the count immediately. + public static int countNames() { + return dataNames(MUTABLE).exactOf(myInt).count(); + } + + // Having defined these helper methods, let us start with the first example. + // You should start reading this example bottom-up, starting at + // templateClass, then going to templateMain and last to templateInner. + public static String generateWithDataNamesAndScopes1() { + + var templateInner = Template.make(() -> body( + // We just got called from the templateMain. All tokens from there + // are already evaluated, so "v1" is now available: + let("l1", listNames()), + """ + if (!"{v1}".equals("#l1")) { throw new RuntimeException("l1 should have been '{v1}' but was '#l1'"); } + """ + )); + + var templateMain = Template.make(() -> body( + // So far, no names were defined. We expect "c1" to be zero. + let("c1", countNames()), + """ + if (#c1 != 0) { throw new RuntimeException("c1 was not zero but #c1"); } + """, + // We now add a local variable "v1" to the scope of this templateMain. + // This only generates a token, and does not immediately add the name. + // The name is only added once we evaluate the tokens, and arrive at + // this particular token. + addDataName("v1", myInt, MUTABLE), + // We count again with "c2". The variable "v1" is at this point still + // in token form, hence it is not yet made available while executing + // the template lambda of templateMain. + let("c2", countNames()), + """ + if (#c2 != 0) { throw new RuntimeException("c2 was not zero but #c2"); } + """, + // But now we call an inner Template. This is added as a TemplateToken. + // This means it is not evaluated immediately, but only once we evaluate + // the tokens. By that time, all tokens from above are already evaluated + // and we see that "v1" is available. + templateInner.asToken() + )); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest9a { + """, + Hooks.CLASS_HOOK.anchor( + """ + public static void main() { + """, + Hooks.METHOD_HOOK.anchor( + templateMain.asToken() + ), + """ + } + """ + ), + """ + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + // Now that we understand this simple example, we go to a more complicated one + // where we use Hook.insert. Just as above, you should read this example + // bottom-up, starting at templateClass. + public static String generateWithDataNamesAndScopes2() { + + var templateFields = Template.make(() -> body( + // We were just called from templateMain. But the code is not + // generated into the main scope, rather into the class scope + // out in templateClass. + // Let us now add a field "f1". + addDataName("f1", myInt, MUTABLE), + // And let's also generate the code for it. + """ + public static int f1 = 42; + """, + // But why is this DataName now available inside the scope of + // templateInner? Does that not mean that "f1" escapes this + // templateFields here? Yes it does! + // For normal template nesting, the names do not escape the + // scope of the nested template. But this here is no normal + // template nesting, rather it is an insertion into a Hook, + // and we treat those differently. We make the scope of the + // inserted templateFields transparent, so that any added + // DataNames are added to the scope of the Hook we just + // inserted into, i.e. the CLASS_HOOK. This is very important, + // if we did not make that scope transparent, we could not + // add any DataNames to the class scope anymore, and we could + // not add any fields that would be available in the class + // scope. + Hooks.METHOD_HOOK.anchor( + // We now create a separate scope. This one is not the + // template scope from above, and it is not transparent. + // Hence, "f2" will not be available outside of this + // scope. + addDataName("f2", myInt, MUTABLE), + // And let's also generate the code for it. + """ + public static int f2 = 666; + """ + // Similarly, if we called any nested Template here, + // and added DataNames inside, this would happen inside + // nested scopes that are not transparent. If one wanted + // to add names to the CLASS_HOOK from there, one would + // have to do another Hook.insert, and make sure that + // the names are added from the outermost scope of that + // inserted Template, because only that outermost scope + // is transparent to the CLASS_HOOK. + ) + )); + + var templateInner = Template.make(() -> body( + // We just got called from the templateMain. All tokens from there + // are already evaluated, so there should be some fields available. + // We can see field "f1". + let("l1", listNames()), + """ + if (!"{f1}".equals("#l1")) { throw new RuntimeException("l1 should have been '{f1}' but was '#l1'"); } + """ + // Now go and have a look at templateFields, to understand how that + // field was added, and why not any others. + )); + + var templateMain = Template.make(() -> body( + // So far, no names were defined. We expect "c1" to be zero. + let("c1", countNames()), + """ + if (#c1 != 0) { throw new RuntimeException("c1 was not zero but #c1"); } + """, + // We would now like to add some fields to the class scope, out in the + // templateClass. This creates a token, which is only evaluated after + // the completion of the templateMain lambda. Before you go and look + // at templateFields, just assume that it does add some fields, and + // continue reading in templateMain. + Hooks.CLASS_HOOK.insert(templateFields.asToken()), + // We count again with "c2". The fields we wanted to add above are not + // yet available, because the token is not yet evaluated. Hence, we + // still only count zero names. + let("c2", countNames()), + """ + if (#c2 != 0) { throw new RuntimeException("c2 was not zero but #c2"); } + """, + // Now we call an inner Template. This also creates a token, and so it + // is not evaluated immediately. And by the time this token is evaluated + // the tokens from above are already evaluated, and so the fields should + // be available. Go have a look at templateInner now. + templateInner.asToken() + )); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest9b { + """, + Hooks.CLASS_HOOK.anchor( + """ + public static void main() { + """, + Hooks.METHOD_HOOK.anchor( + templateMain.asToken() + ), + """ + } + """ + ), + """ + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + } + + + // There are two more concepts to understand more deeply with DataNames. + // + // One is the use of mutable and immutable DataNames. + // In some cases, we only want to sample DataNames that are mutable, because + // we want to store to a field or variable. We have to make sure that we + // do not generate code that tries to store to a final field or variable. + // In other cases, we only want to load, and we do not care if the + // fields or variables are final or non-final. + // + // Another concept is subtyping of DataName Types. With primitive types, this + // is irrelevant, but with instances of Objects, this becomes relevant. + // We may want to load an object of any field or variable of a certain + // class, or any subclass. When a value is of a given class, we can only + // store it to fields and variables of that class or any superclass. + // + // Let us look at an example that demonstrates these two concepts. + // + // First, we define a DataName Type that represents different classes, that + // may or may not be in a subtype relation. Subtypes start with the name + // of the super type. + private record MyClass(String name) implements DataName.Type { + @Override + public boolean isSubtypeOf(DataName.Type other) { + return other instanceof MyClass(String n) && name().startsWith(n); + } + + @Override + public String toString() { return name(); } + } + private static final MyClass myClassA = new MyClass("MyClassA"); + private static final MyClass myClassA1 = new MyClass("MyClassA1"); + private static final MyClass myClassA2 = new MyClass("MyClassA2"); + private static final MyClass myClassA11 = new MyClass("MyClassA11"); + private static final MyClass myClassB = new MyClass("MyClassB"); + private static final List myClassList = List.of(myClassA, myClassA1, myClassA2, myClassA11, myClassB); + + public static String generateWithDataNamesForFuzzing() { + var templateStaticField = Template.make("type", "mutable", (DataName.Type type, Boolean mutable) -> body( + addDataName($("field"), type, mutable ? MUTABLE : IMMUTABLE), + let("isFinal", mutable ? "" : "final"), + """ + public static #isFinal #type $field = new #type(); + """ + )); + + var templateLoad = Template.make("type", (DataName.Type type) -> body( + // We only load from the field, so we do not need a mutable one, + // we can load from final and non-final fields. + // We want to find any field from which we can read the value and store + // it in our variable v of our given type. Hence, we can take a field + // of the given type or any subtype thereof. + let("field", dataNames(MUTABLE_OR_IMMUTABLE).subtypeOf(type).sample().name()), + """ + #type $v = #field; + System.out.println("#field: " + $v); + """ + )); + + var templateStore = Template.make("type", (DataName.Type type) -> body( + // We are storing to a field, so it better be non-final, i.e. mutable. + // We want to store a new instance of our given type to a field. This + // field must be of the given type or any supertype. + let("field", dataNames(MUTABLE).supertypeOf(type).sample().name()), + """ + #field = new #type(); + """ + )); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest10 { + // First, we define our classes. + public static class MyClassA {} + public static class MyClassA1 extends MyClassA {} + public static class MyClassA2 extends MyClassA {} + public static class MyClassA11 extends MyClassA1 {} + public static class MyClassB {} + + // Now, we define a list of static fields. Some of them are final, others not. + """, + // We must create a CLASS_HOOK and insert the fields to it. Otherwise, + // addDataName is restricted to the scope of the templateStaticField. But + // with the insertion to CLASS_HOOK, the addDataName goes through the scope + // of the templateStaticField out to the scope of the CLASS_HOOK. + Hooks.CLASS_HOOK.anchor( + myClassList.stream().map(c -> + (Object)Hooks.CLASS_HOOK.insert(templateStaticField.asToken(c, true)) + ).toList(), + myClassList.stream().map(c -> + (Object)Hooks.CLASS_HOOK.insert(templateStaticField.asToken(c, false)) + ).toList(), + """ + + public static void main() { + // All fields are still in their initial state. + """, + myClassList.stream().map(templateLoad::asToken).toList(), + """ + // Now let us mutate some fields. + """, + myClassList.stream().map(templateStore::asToken).toList(), + """ + // And now some fields are different than before. + """, + myClassList.stream().map(templateLoad::asToken).toList(), + """ + } + """ + ), + """ + } + """ + )); + + // Render templateClass to String. + return templateClass.render(); + + } + + // "DataNames" are useful for modeling fields and variables. They hold data, + // and we can read and write to them, they may be mutable or immutable. + // We now introduce another set of "Names", the "StructuralNames". They are + // useful for modeling method names and class names, and possibly more. Anything + // that has a fixed name in the Java code, for which mutability is inapplicable. + // Some use-cases for "StructuralNames": + // - Method names. The Type could represent the signature of the static method + // or the class of the non-static method. + // - Class names. Type could represent the signature of the constructor, so + // that we could instantiate random instances. + // - try/catch blocks. If a specific Exception is caught in the scope, we could + // register that Exception, and in the inner scope we can + // check if there is any "StructuralName" for an Exception + // and its subtypes - if so, we know the exception would be + // caught. + // + // Let us look at an example with Method names. But for simplicity, we assume they + // all have the same signature: they take two int arguments and return an int. + // + // Should you ever work on a test where there are methods with different signatures, + // then you would have to very carefully study and design the subtype relation between + // methods. You may want to read up about covariance and contravariance. This + // example ignores all of that, because we only have "(II)I" methods. + private record MyMethodType() implements StructuralName.Type { + @Override + public boolean isSubtypeOf(StructuralName.Type other) { + return other instanceof MyMethodType(); + } + + @Override + public String name() { return ""; } + } + private static final MyMethodType myMethodType = new MyMethodType(); + + public static String generateWithStructuralNamesForMethods() { + // Define a method, which takes two ints, returns the result of op. + var templateMethod = Template.make("op", (String op) -> body( + // Register the method name, so we can later sample. + addStructuralName($("methodName"), myMethodType), + """ + public static int $methodName(int a, int b) { + return a #op b; + } + """ + )); + + var templateSample = Template.make(() -> body( + // Sample a random method, and retrieve its name. + let("methodName", structuralNames().exactOf(myMethodType).sample().name()), + """ + System.out.println("Calling #methodName with inputs 7 and 11"); + System.out.println(" result: " + #methodName(7, 11)); + """ + )); + + var templateClass = Template.make(() -> body( + """ + package p.xyz; + + public class InnerTest11 { + // Let us define some methods that we can sample from later. + """, + // We must anchor a CLASS_HOOK here, and insert the method definitions to that hook. + Hooks.CLASS_HOOK.anchor( + // If we directly nest the templateMethod, then the addStructuralName goes to the nested + // scope, and is not available at the class scope, i.e. it is not visible + // for sampleStructuralName outside of the templateMethod. + // DO NOT DO THIS, the nested addStructuralName will not be visible: + "// We cannot sample from the following methods:\n", + templateMethod.asToken("+"), + templateMethod.asToken("-"), + // However, if we insert to the CLASS_HOOK, then the Renderer makes the + // scope of the inserted templateMethod transparent, and the addStructuralName + // goes out to the scope of the CLASS_HOOK (but no further than that). + // RATHER, DO THIS to ensure the addStructuralName is visible: + Hooks.CLASS_HOOK.insert(templateMethod.asToken("*")), + Hooks.CLASS_HOOK.insert(templateMethod.asToken("|")), + Hooks.CLASS_HOOK.insert(templateMethod.asToken("&")), + """ + + public static void main() { + // Now, we call some random methods, but only those that were inserted + // to the CLASS_HOOK. + """, + Collections.nCopies(10, templateSample.asToken()), + """ + } + } + """ + ) + )); + + // Render templateClass to String. + return templateClass.render(); + } +} diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java new file mode 100644 index 00000000000..fe267a3ff63 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestFormat.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8344942 + * @summary Test formatting of Integer, Long, Float and Double. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run main template_framework.tests.TestFormat + */ + +package template_framework.tests; + +import java.util.List; +import java.util.ArrayList; + +import compiler.lib.compile_framework.*; +import compiler.lib.generators.*; +import compiler.lib.verify.*; +import compiler.lib.template_framework.Template; +import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.let; + +public class TestFormat { + record FormatInfo(int id, String type, Object value) {} + + public static void main(String[] args) { + List list = new ArrayList<>(); + + for (int i = 0; i < 1000; i++) { + int v = Generators.G.ints().next(); + list.add(new FormatInfo(i, "int", v)); + } + + for (int i = 1000; i < 2000; i++) { + long v = Generators.G.longs().next(); + list.add(new FormatInfo(i, "long", v)); + } + + for (int i = 2000; i < 3000; i++) { + float v = Generators.G.floats().next(); + list.add(new FormatInfo(i, "float", v)); + } + + for (int i = 3000; i < 4000; i++) { + double v = Generators.G.doubles().next(); + list.add(new FormatInfo(i, "double", v)); + } + + CompileFramework comp = new CompileFramework(); + comp.addJavaSourceCode("p.xyz.InnerTest", generate(list)); + comp.compile(); + + // Run each of the "get" methods, and check the result. + for (FormatInfo info : list) { + Object ret1 = comp.invoke("p.xyz.InnerTest", "get_let_" + info.id, new Object[] {}); + Object ret2 = comp.invoke("p.xyz.InnerTest", "get_token_" + info.id, new Object[] {}); + System.out.println("id: " + info.id + " -> " + info.value + " == " + ret1 + " == " + ret2); + Verify.checkEQ(ret1, info.value); + Verify.checkEQ(ret2, info.value); + } + } + + private static String generate(List list) { + // Generate 2 "get" methods, one that formats via "let" (hashtag), the other via direct token. + var template1 = Template.make("info", (FormatInfo info) -> body( + let("id", info.id()), + let("type", info.type()), + let("value", info.value()), + """ + public static #type get_let_#id() { return #value; } + """, + "public static #type get_token_#id() { return ", info.value(), "; }\n" + )); + + // For each FormatInfo in list, generate the "get" methods inside InnerTest class. + var template2 = Template.make(() -> body( + """ + package p.xyz; + public class InnerTest { + """, + list.stream().map(info -> template1.asToken(info)).toList(), + """ + } + """ + )); + + return template2.render(); + } +} 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 00000000000..35d020b6080 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java @@ -0,0 +1,2253 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8344942 + * @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 main template_framework.tests.TestTemplate + */ + +package template_framework.tests; + +import java.util.Arrays; +import java.util.List; +import java.util.HashSet; + +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.DataName; +import compiler.lib.template_framework.StructuralName; +import compiler.lib.template_framework.Hook; +import compiler.lib.template_framework.TemplateBinding; +import compiler.lib.template_framework.RendererException; +import static compiler.lib.template_framework.Template.body; +import static compiler.lib.template_framework.Template.$; +import static compiler.lib.template_framework.Template.let; +import static compiler.lib.template_framework.Template.fuel; +import static compiler.lib.template_framework.Template.setFuelCost; +import static compiler.lib.template_framework.Template.addDataName; +import static compiler.lib.template_framework.Template.dataNames; +import static compiler.lib.template_framework.Template.addStructuralName; +import static compiler.lib.template_framework.Template.structuralNames; +import static compiler.lib.template_framework.DataName.Mutability.MUTABLE; +import static compiler.lib.template_framework.DataName.Mutability.IMMUTABLE; +import static compiler.lib.template_framework.DataName.Mutability.MUTABLE_OR_IMMUTABLE; + +/** + * The tests in this file are mostly there to ensure that the Template Rendering + * works correctly, and not that we produce compilable Java code. Rather, we + * produce deterministic output, and compare it to the expected String. + * Still, this file may be helpful for learning more about how Templates + * work and can be used. + * + * We assume that you have already studied {@code TestTutorial.java}. + */ +public class TestTemplate { + // Interface for failing tests. + interface FailingTest { + void run(); + } + + // Define a simple type to model primitive types. + private record MyPrimitive(String name) implements DataName.Type { + @Override + public boolean isSubtypeOf(DataName.Type other) { + return other instanceof MyPrimitive(String n) && n.equals(name()); + } + + @Override + public String toString() { return name(); } + } + private static final MyPrimitive myInt = new MyPrimitive("int"); + private static final MyPrimitive myLong = new MyPrimitive("long"); + + // Simulate classes. Subtypes start with the name of the super type. + private record MyClass(String name) implements DataName.Type { + @Override + public boolean isSubtypeOf(DataName.Type other) { + return other instanceof MyClass(String n) && name().startsWith(n); + } + + @Override + public String toString() { return name(); } + } + private static final MyClass myClassA = new MyClass("myClassA"); + private static final MyClass myClassA1 = new MyClass("myClassA1"); + private static final MyClass myClassA2 = new MyClass("myClassA2"); + private static final MyClass myClassA11 = new MyClass("myClassA11"); + private static final MyClass myClassB = new MyClass("myClassB"); + + // Simulate some structural types. + private record MyStructuralType(String name) implements StructuralName.Type { + @Override + public boolean isSubtypeOf(StructuralName.Type other) { + return other instanceof MyStructuralType(String n) && name().startsWith(n); + } + + @Override + public String toString() { return name(); } + } + private static final MyStructuralType myStructuralTypeA = new MyStructuralType("StructuralA"); + private static final MyStructuralType myStructuralTypeA1 = new MyStructuralType("StructuralA1"); + private static final MyStructuralType myStructuralTypeA2 = new MyStructuralType("StructuralA2"); + private static final MyStructuralType myStructuralTypeA11 = new MyStructuralType("StructuralA11"); + private static final MyStructuralType myStructuralTypeB = new MyStructuralType("StructuralB"); + + public static void main(String[] args) { + // The following tests all pass, i.e. have no errors during rendering. + testSingleLine(); + testMultiLine(); + testBodyTokens(); + testWithOneArgument(); + testWithTwoArguments(); + testWithThreeArguments(); + testNested(); + testHookSimple(); + testHookIsAnchored(); + testHookNested(); + testHookWithNestedTemplates(); + testHookRecursion(); + testDollar(); + testLet(); + testDollarAndHashtagBrackets(); + testSelector(); + testRecursion(); + testFuel(); + testFuelCustom(); + testDataNames1(); + testDataNames2(); + testDataNames3(); + testDataNames4(); + testDataNames5(); + testStructuralNames1(); + testStructuralNames2(); + testListArgument(); + + // The following tests should all fail, with an expected exception and message. + expectRendererException(() -> testFailingNestedRendering(), "Nested render not allowed."); + expectRendererException(() -> $("name"), "A Template method such as"); + expectRendererException(() -> let("x","y"), "A Template method such as"); + expectRendererException(() -> fuel(), "A Template method such as"); + expectRendererException(() -> setFuelCost(1.0f), "A Template method such as"); + expectRendererException(() -> dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).count(), "A Template method such as"); + expectRendererException(() -> dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).sample(), "A Template method such as"); + expectRendererException(() -> (new Hook("abc")).isAnchored(), "A Template method such as"); + expectRendererException(() -> testFailingDollarName1(), "Is not a valid '$' name: ''."); + expectRendererException(() -> testFailingDollarName2(), "Is not a valid '$' name: '#abc'."); + expectRendererException(() -> testFailingDollarName3(), "Is not a valid '$' name: 'abc#'."); + expectRendererException(() -> testFailingDollarName4(), "A '$' name should not be null."); + expectRendererException(() -> testFailingDollarName5(), "Is not a valid '$' replacement pattern: '$' in '$'."); + expectRendererException(() -> testFailingDollarName6(), "Is not a valid '$' replacement pattern: '$' in 'asdf$'."); + expectRendererException(() -> testFailingDollarName7(), "Is not a valid '$' replacement pattern: '$1' in 'asdf$1'."); + expectRendererException(() -> testFailingDollarName8(), "Is not a valid '$' replacement pattern: '$' in 'abc$$abc'."); + expectRendererException(() -> testFailingLetName1(), "A hashtag replacement should not be null."); + expectRendererException(() -> testFailingHashtagName1(), "Is not a valid hashtag replacement name: ''."); + expectRendererException(() -> testFailingHashtagName2(), "Is not a valid hashtag replacement name: 'abc#abc'."); + expectRendererException(() -> testFailingHashtagName3(), "Is not a valid hashtag replacement name: ''."); + expectRendererException(() -> testFailingHashtagName4(), "Is not a valid hashtag replacement name: 'xyz#xyz'."); + expectRendererException(() -> testFailingHashtagName5(), "Is not a valid '#' replacement pattern: '#' in '#'."); + expectRendererException(() -> testFailingHashtagName6(), "Is not a valid '#' replacement pattern: '#' in 'asdf#'."); + expectRendererException(() -> testFailingHashtagName7(), "Is not a valid '#' replacement pattern: '#1' in 'asdf#1'."); + expectRendererException(() -> testFailingHashtagName8(), "Is not a valid '#' replacement pattern: '#' in 'abc##abc'."); + expectRendererException(() -> testFailingDollarHashtagName1(), "Is not a valid '#' replacement pattern: '#' in '#$'."); + expectRendererException(() -> testFailingDollarHashtagName2(), "Is not a valid '$' replacement pattern: '$' in '$#'."); + expectRendererException(() -> testFailingDollarHashtagName3(), "Is not a valid '#' replacement pattern: '#' in '#$name'."); + expectRendererException(() -> testFailingDollarHashtagName4(), "Is not a valid '$' replacement pattern: '$' in '$#name'."); + expectRendererException(() -> testFailingHook(), "Hook 'Hook1' was referenced but not found!"); + expectRendererException(() -> testFailingSample1(), "No variable: MUTABLE, subtypeOf(int), supertypeOf(int)."); + expectRendererException(() -> testFailingHashtag1(), "Duplicate hashtag replacement for #a"); + expectRendererException(() -> testFailingHashtag2(), "Duplicate hashtag replacement for #a"); + expectRendererException(() -> testFailingHashtag3(), "Duplicate hashtag replacement for #a"); + expectRendererException(() -> testFailingHashtag4(), "Missing hashtag replacement for #a"); + expectRendererException(() -> testFailingBinding1(), "Duplicate 'bind' not allowed."); + expectRendererException(() -> testFailingBinding2(), "Cannot 'get' before 'bind'."); + expectIllegalArgumentException(() -> body(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> body("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> body(new Hook("Hook1")), "Unexpected token:"); + Hook hook1 = new Hook("Hook1"); + expectIllegalArgumentException(() -> hook1.anchor(null), "Unexpected tokens: null"); + expectIllegalArgumentException(() -> hook1.anchor("x", null), "Unexpected token: null"); + expectIllegalArgumentException(() -> hook1.anchor(hook1), "Unexpected token:"); + expectIllegalArgumentException(() -> testFailingAddDataName1(), "Unexpected mutability: MUTABLE_OR_IMMUTABLE"); + expectIllegalArgumentException(() -> testFailingAddDataName2(), "Unexpected weight: "); + expectIllegalArgumentException(() -> testFailingAddDataName3(), "Unexpected weight: "); + expectIllegalArgumentException(() -> testFailingAddDataName4(), "Unexpected weight: "); + expectIllegalArgumentException(() -> testFailingAddStructuralName1(), "Unexpected weight: "); + expectIllegalArgumentException(() -> testFailingAddStructuralName2(), "Unexpected weight: "); + expectIllegalArgumentException(() -> testFailingAddStructuralName3(), "Unexpected weight: "); + expectUnsupportedOperationException(() -> testFailingSample2(), "Must first call 'subtypeOf', 'supertypeOf', or 'exactOf'."); + expectRendererException(() -> testFailingAddNameDuplication1(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication2(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication3(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication4(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication5(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication6(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication7(), "Duplicate name:"); + expectRendererException(() -> testFailingAddNameDuplication8(), "Duplicate name:"); + } + + public static void testSingleLine() { + var template = Template.make(() -> body("Hello World!")); + String code = template.render(); + checkEQ(code, "Hello World!"); + } + + public static void testMultiLine() { + var template = Template.make(() -> body( + """ + Code on more + than a single line + """ + )); + String code = template.render(); + String expected = + """ + Code on more + than a single line + """; + checkEQ(code, expected); + } + + public static void testBodyTokens() { + // We can fill the body with Objects of different types, and they get concatenated. + // Lists get flattened into the body. + var template = Template.make(() -> body( + "start ", + Integer.valueOf(1), 1, + Long.valueOf(2), 2L, + Double.valueOf(3.4), 3.4, + Float.valueOf(5.6f), 5.6f, + List.of(" ", 1, " and ", 2), + " end" + )); + String code = template.render(); + checkEQ(code, "start 112L2L3.43.45.6f5.6f 1 and 2 end"); + } + + public static void testWithOneArgument() { + // Capture String argument via String name. + var template1 = Template.make("a", (String a) -> body("start #a end")); + checkEQ(template1.render("x"), "start x end"); + checkEQ(template1.render("a"), "start a end"); + checkEQ(template1.render("" ), "start end"); + + // Capture String argument via typed lambda argument. + var template2 = Template.make("a", (String a) -> body("start ", a, " end")); + checkEQ(template2.render("x"), "start x end"); + checkEQ(template2.render("a"), "start a end"); + checkEQ(template2.render("" ), "start end"); + + // Capture Integer argument via String name. + var template3 = Template.make("a", (Integer a) -> body("start #a end")); + checkEQ(template3.render(0 ), "start 0 end"); + checkEQ(template3.render(22 ), "start 22 end"); + checkEQ(template3.render(444), "start 444 end"); + + // Capture Integer argument via templated lambda argument. + var template4 = Template.make("a", (Integer a) -> body("start ", a, " end")); + checkEQ(template4.render(0 ), "start 0 end"); + checkEQ(template4.render(22 ), "start 22 end"); + checkEQ(template4.render(444), "start 444 end"); + + // Test Strings with backslashes: + var template5 = Template.make("a", (String a) -> body("start #a " + a + " end")); + checkEQ(template5.render("/"), "start / / end"); + checkEQ(template5.render("\\"), "start \\ \\ end"); + checkEQ(template5.render("\\\\"), "start \\\\ \\\\ end"); + } + + public static void testWithTwoArguments() { + // Capture 2 String arguments via String names. + var template1 = Template.make("a1", "a2", (String a1, String a2) -> body("start #a1 #a2 end")); + checkEQ(template1.render("x", "y"), "start x y end"); + checkEQ(template1.render("a", "b"), "start a b end"); + checkEQ(template1.render("", "" ), "start end"); + + // Capture 2 String arguments via typed lambda arguments. + var template2 = Template.make("a1", "a2", (String a1, String a2) -> body("start ", a1, " ", a2, " end")); + checkEQ(template2.render("x", "y"), "start x y end"); + checkEQ(template2.render("a", "b"), "start a b end"); + checkEQ(template2.render("", "" ), "start end"); + + // Capture 2 Integer arguments via String names. + var template3 = Template.make("a1", "a2", (Integer a1, Integer a2) -> body("start #a1 #a2 end")); + checkEQ(template3.render(0, 1 ), "start 0 1 end"); + checkEQ(template3.render(22, 33 ), "start 22 33 end"); + checkEQ(template3.render(444, 555), "start 444 555 end"); + + // Capture 2 Integer arguments via templated lambda arguments. + var template4 = Template.make("a1", "a2", (Integer a1, Integer a2) -> body("start ", a1, " ", a2, " end")); + checkEQ(template4.render(0, 1 ), "start 0 1 end"); + checkEQ(template4.render(22, 33 ), "start 22 33 end"); + checkEQ(template4.render(444, 555), "start 444 555 end"); + } + + public static void testWithThreeArguments() { + // Capture 3 String arguments via String names. + var template1 = Template.make("a1", "a2", "a3", (String a1, String a2, String a3) -> body("start #a1 #a2 #a3 end")); + checkEQ(template1.render("x", "y", "z"), "start x y z end"); + checkEQ(template1.render("a", "b", "c"), "start a b c end"); + checkEQ(template1.render("", "", "" ), "start end"); + + // Capture 3 String arguments via typed lambda arguments. + var template2 = Template.make("a1", "a2", "a3", (String a1, String a2, String a3) -> body("start ", a1, " ", a2, " ", a3, " end")); + checkEQ(template1.render("x", "y", "z"), "start x y z end"); + checkEQ(template1.render("a", "b", "c"), "start a b c end"); + checkEQ(template1.render("", "", "" ), "start end"); + + // Capture 3 Integer arguments via String names. + var template3 = Template.make("a1", "a2", "a3", (Integer a1, Integer a2, Integer a3) -> body("start #a1 #a2 #a3 end")); + checkEQ(template3.render(0, 1 , 2 ), "start 0 1 2 end"); + checkEQ(template3.render(22, 33 , 44 ), "start 22 33 44 end"); + checkEQ(template3.render(444, 555, 666), "start 444 555 666 end"); + + // Capture 2 Integer arguments via templated lambda arguments. + var template4 = Template.make("a1", "a2", "a3", (Integer a1, Integer a2, Integer a3) -> body("start ", a1, " ", a2, " ", a3, " end")); + checkEQ(template3.render(0, 1 , 2 ), "start 0 1 2 end"); + checkEQ(template3.render(22, 33 , 44 ), "start 22 33 44 end"); + checkEQ(template3.render(444, 555, 666), "start 444 555 666 end"); + } + + public static void testNested() { + var template1 = Template.make(() -> body("proton")); + + var template2 = Template.make("a1", "a2", (String a1, String a2) -> body( + "electron #a1\n", + "neutron #a2\n" + )); + + var template3 = Template.make("a1", "a2", (String a1, String a2) -> body( + "Universe ", template1.asToken(), " {\n", + template2.asToken("up", "down"), + template2.asToken(a1, a2), + "}\n" + )); + + var template4 = Template.make(() -> body( + template3.asToken("low", "high"), + "{\n", + template3.asToken("42", "24"), + "}" + )); + + String code = template4.render(); + 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 testHookSimple() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make(() -> body("Hello\n")); + + var template2 = Template.make(() -> body( + "{\n", + hook1.anchor( + "World\n", + // Note: "Hello" from the template below will be inserted + // above "World" above. + hook1.insert(template1.asToken()) + ), + "}" + )); + + String code = template2.render(); + String expected = + """ + { + Hello + World + }"""; + checkEQ(code, expected); + } + + public static void testHookIsAnchored() { + var hook1 = new Hook("Hook1"); + + var template0 = Template.make(() -> body("isAnchored: ", hook1.isAnchored(), "\n")); + + var template1 = Template.make(() -> body("Hello\n", template0.asToken())); + + var template2 = Template.make(() -> body( + "{\n", + template0.asToken(), + hook1.anchor( + "World\n", + template0.asToken(), + hook1.insert(template1.asToken()) + ), + template0.asToken(), + "}" + )); + + String code = template2.render(); + String expected = + """ + { + isAnchored: false + Hello + isAnchored: true + World + isAnchored: true + isAnchored: false + }"""; + checkEQ(code, expected); + } + + public static void testHookNested() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("a", (String a) -> body("x #a x\n")); + + // Test nested use of hooks in the same template. + var template2 = Template.make(() -> body( + "{\n", + hook1.anchor(), // empty + "zero\n", + hook1.anchor( + template1.asToken("one"), + template1.asToken("two"), + hook1.insert(template1.asToken("intoHook1a")), + hook1.insert(template1.asToken("intoHook1b")), + template1.asToken("three"), + hook1.anchor( + template1.asToken("four"), + hook1.insert(template1.asToken("intoHook1c")), + template1.asToken("five") + ), + template1.asToken("six"), + hook1.anchor(), // empty + template1.asToken("seven"), + hook1.insert(template1.asToken("intoHook1d")), + template1.asToken("eight"), + hook1.anchor( + template1.asToken("nine"), + hook1.insert(template1.asToken("intoHook1e")), + template1.asToken("ten") + ), + template1.asToken("eleven") + ), + "}" + )); + + String code = template2.render(); + String expected = + """ + { + zero + x intoHook1a x + x intoHook1b x + x intoHook1d x + x one x + x two x + x three x + x intoHook1c x + x four x + x five x + x six x + x seven x + x eight x + x intoHook1e x + x nine x + x ten x + x eleven x + }"""; + checkEQ(code, expected); + } + + public static void testHookWithNestedTemplates() { + var hook1 = new Hook("Hook1"); + var hook2 = new Hook("Hook2"); + + var template1 = Template.make("a", (String a) -> body("x #a x\n")); + + var template2 = Template.make("b", (String b) -> body( + "{\n", + template1.asToken(b + "A"), + hook1.insert(template1.asToken(b + "B")), + hook2.insert(template1.asToken(b + "C")), + template1.asToken(b + "D"), + hook1.anchor( + template1.asToken(b + "E"), + hook1.insert(template1.asToken(b + "F")), + hook2.insert(template1.asToken(b + "G")), + template1.asToken(b + "H"), + hook2.anchor( + template1.asToken(b + "I"), + hook1.insert(template1.asToken(b + "J")), + hook2.insert(template1.asToken(b + "K")), + template1.asToken(b + "L") + ), + template1.asToken(b + "M"), + hook1.insert(template1.asToken(b + "N")), + hook2.insert(template1.asToken(b + "O")), + template1.asToken(b + "O") + ), + template1.asToken(b + "P"), + hook1.insert(template1.asToken(b + "Q")), + hook2.insert(template1.asToken(b + "R")), + template1.asToken(b + "S"), + "}\n" + )); + + // Test use of hooks across templates. + var template3 = Template.make(() -> body( + "{\n", + "base-A\n", + hook1.anchor( + "base-B\n", + hook2.anchor( + "base-C\n", + template2.asToken("sub-"), + "base-D\n" + ), + "base-E\n" + ), + "base-F\n", + "}\n" + )); + + String code = template3.render(); + String expected = + """ + { + base-A + x sub-B x + x sub-Q x + base-B + x sub-C x + x sub-G x + x sub-O x + x sub-R x + base-C + { + x sub-A x + x sub-D x + x sub-F x + x sub-J x + x sub-N x + x sub-E x + x sub-H x + x sub-K x + x sub-I x + x sub-L x + x sub-M x + x sub-O x + x sub-P x + x sub-S x + } + base-D + base-E + base-F + } + """; + checkEQ(code, expected); + } + + public static void testHookRecursion() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("a", (String a) -> body("x #a x\n")); + + var template2 = Template.make("b", (String b) -> body( + "<\n", + template1.asToken(b + "A"), + hook1.insert(template1.asToken(b + "B")), // sub-B is rendered before template2. + template1.asToken(b + "C"), + "inner-hook-start\n", + hook1.anchor( + "inner-hook-end\n", + template1.asToken(b + "E"), + hook1.insert(template1.asToken(b + "E")), + template1.asToken(b + "F") + ), + ">\n" + )); + + // Test use of hooks across templates. + var template3 = Template.make(() -> body( + "{\n", + "hook-start\n", + hook1.anchor( + "hook-end\n", + hook1.insert(template2.asToken("sub-")), + "base-C\n" + ), + "base-D\n", + "}\n" + )); + + String code = template3.render(); + String expected = + """ + { + hook-start + x sub-B x + < + x sub-A x + x sub-C x + inner-hook-start + x sub-E x + inner-hook-end + x sub-E x + x sub-F x + > + hook-end + base-C + base-D + } + """; + checkEQ(code, expected); + } + + public static void testDollar() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("a", (String a) -> body("x $name #a x\n")); + + var template2 = Template.make("a", (String a) -> body( + "{\n", + "y $name #a y\n", + template1.asToken($("name")), + "}\n" + )); + + var template3 = Template.make(() -> body( + "{\n", + "$name\n", + "$name", "\n", + "z $name z\n", + "z$name z\n", + template1.asToken("name"), // does not capture -> literal "$name" + template1.asToken("$name"), // does not capture -> literal "$name" + template1.asToken($("name")), // capture replacement name "name_1" + hook1.anchor( + "$name\n" + ), + "break\n", + hook1.anchor( + "one\n", + hook1.insert(template1.asToken($("name"))), + "two\n", + template1.asToken($("name")), + "three\n", + hook1.insert(template2.asToken($("name"))), + "four\n" + ), + "}\n" + )); + + String code = template3.render(); + String expected = + """ + { + name_1 + name_1 + z name_1 z + zname_1 z + x name_2 name x + x name_3 $name x + x name_4 name_1 x + name_1 + break + x name_5 name_1 x + { + y name_7 name_1 y + x name_8 name_7 x + } + one + two + x name_6 name_1 x + three + four + } + """; + checkEQ(code, expected); + } + + public static void testLet() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("a", (String a) -> body( + "{\n", + "y #a y\n", + let("b", "<" + a + ">"), + "y #b y\n", + "}\n" + )); + + var template2 = Template.make("a", (Integer a) -> let("b", a * 10, b -> + body( + let("c", b * 3), + "abc = #a #b #c\n" + ) + )); + + var template3 = Template.make(() -> body( + "{\n", + let("x", "abc"), + template1.asToken("alpha"), + "break\n", + "x1 = #x\n", + hook1.anchor( + "x2 = #x\n", // leaks inside + template1.asToken("beta"), + let("y", "one"), + "y1 = #y\n" + ), + "break\n", + "y2 = #y\n", // leaks outside + "break\n", + template2.asToken(5), + "}\n" + )); + + String code = template3.render(); + String expected = + """ + { + { + y alpha y + y y + } + break + x1 = abc + x2 = abc + { + y beta y + y y + } + y1 = one + break + y2 = one + break + abc = 5 50 150 + } + """; + checkEQ(code, expected); + } + + public static void testDollarAndHashtagBrackets() { + var template1 = Template.make(() -> body( + let("xyz", "abc"), + let("xyz_", "def"), + let("xyz_klm", "ghi"), + let("klm", "jkl"), + """ + no bracket: #xyz #xyz_klm #xyz_#klm + no bracket: $var $var_two $var_$two + with bracket: #{xyz} #{xyz_klm} #{xyz}_#{klm} + with bracket: ${var} ${var_two} ${var}_${two} + """ + )); + + String code = template1.render(); + String expected = + """ + no bracket: abc ghi defjkl + no bracket: var_1 var_two_1 var__1two_1 + with bracket: abc ghi abc_jkl + with bracket: var_1 var_two_1 var_1_two_1 + """; + checkEQ(code, expected); + } + + public static void testSelector() { + var template1 = Template.make("a", (String a) -> body( + "<\n", + "x #a x\n", + ">\n" + )); + + var template2 = Template.make("a", (String a) -> body( + "<\n", + "y #a y\n", + ">\n" + )); + + var template3 = Template.make("a", (Integer a) -> body( + "[\n", + "z #a z\n", + // Select which template should be used: + a > 0 ? template1.asToken("A_" + a) + : template2.asToken("B_" + a), + "]\n" + )); + + var template4 = Template.make(() -> body( + "{\n", + template3.asToken(-1), + "break\n", + template3.asToken(0), + "break\n", + template3.asToken(1), + "break\n", + template3.asToken(2), + "}\n" + )); + + String code = template4.render(); + String expected = + """ + { + [ + z -1 z + < + y B_-1 y + > + ] + break + [ + z 0 z + < + y B_0 y + > + ] + break + [ + z 1 z + < + x A_1 x + > + ] + break + [ + z 2 z + < + x A_2 x + > + ] + } + """; + checkEQ(code, expected); + } + + public static void testRecursion() { + // Binding allows use of template1 inside template1, via the Binding indirection. + var binding1 = new TemplateBinding>(); + + var template1 = Template.make("i", (Integer i) -> body( + "[ #i\n", + // We cannot yet use the template1 directly, as it is being defined. + // So we use binding1 instead. + i < 0 ? "done\n" : binding1.get().asToken(i - 1), + "] #i\n" + )); + binding1.bind(template1); + + var template2 = Template.make(() -> body( + "{\n", + // Now, we can use template1 normally, as it is already defined. + template1.asToken(3), + "}\n" + )); + + String code = template2.render(); + String expected = + """ + { + [ 3 + [ 2 + [ 1 + [ 0 + [ -1 + done + ] -1 + ] 0 + ] 1 + ] 2 + ] 3 + } + """; + checkEQ(code, expected); + } + + public static void testFuel() { + var template1 = Template.make(() -> body( + let("f", fuel()), + + "<#f>\n" + )); + + // Binding allows use of template2 inside template2, via the Binding indirection. + var binding2 = new TemplateBinding>(); + var template2 = Template.make("i", (Integer i) -> body( + let("f", fuel()), + + "[ #i #f\n", + template1.asToken(), + fuel() <= 60.f ? "done" : binding2.get().asToken(i - 1), + "] #i #f\n" + )); + binding2.bind(template2); + + var template3 = Template.make(() -> body( + "{\n", + template2.asToken(3), + "}\n" + )); + + String code = template3.render(); + String expected = + """ + { + [ 3 90.0f + <80.0f> + [ 2 80.0f + <70.0f> + [ 1 70.0f + <60.0f> + [ 0 60.0f + <50.0f> + done] 0 60.0f + ] 1 70.0f + ] 2 80.0f + ] 3 90.0f + } + """; + checkEQ(code, expected); + } + + public static void testFuelCustom() { + var template1 = Template.make(() -> body( + setFuelCost(2.0f), + let("f", fuel()), + + "<#f>\n" + )); + + // Binding allows use of template2 inside template2, via the Binding indirection. + var binding2 = new TemplateBinding>(); + var template2 = Template.make("i", (Integer i) -> body( + setFuelCost(3.0f), + let("f", fuel()), + + "[ #i #f\n", + template1.asToken(), + fuel() <= 5.f ? "done\n" : binding2.get().asToken(i - 1), + "] #i #f\n" + )); + binding2.bind(template2); + + var template3 = Template.make(() -> body( + setFuelCost(5.0f), + let("f", fuel()), + + "{ #f\n", + template2.asToken(3), + "} #f\n" + )); + + String code = template3.render(20.0f); + String expected = + """ + { 20.0f + [ 3 15.0f + <12.0f> + [ 2 12.0f + <9.0f> + [ 1 9.0f + <6.0f> + [ 0 6.0f + <3.0f> + [ -1 3.0f + <0.0f> + done + ] -1 3.0f + ] 0 6.0f + ] 1 9.0f + ] 2 12.0f + ] 3 15.0f + } 20.0f + """; + checkEQ(code, expected); + } + + public static void testDataNames1() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make(() -> body( + "[", + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).hasAny(), + ", ", + dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).count(), + ", names: {", + String.join(", ", dataNames(MUTABLE_OR_IMMUTABLE).exactOf(myInt).toList().stream().map(DataName::name).toList()), + "}]\n" + )); + + var template2 = Template.make("name", "type", (String name, DataName.Type type) -> body( + addDataName(name, type, MUTABLE), + "define #type #name\n", + template1.asToken() + )); + + var template3 = Template.make(() -> body( + "<\n", + hook1.insert(template2.asToken($("name"), myInt)), + "$name = 5\n", + ">\n" + )); + + var template4 = Template.make(() -> body( + "{\n", + template1.asToken(), + hook1.anchor( + template1.asToken(), + "something\n", + template3.asToken(), + "more\n", + template1.asToken(), + "more\n", + template2.asToken($("name"), myInt), + "more\n", + template1.asToken() + ), + template1.asToken(), + "}\n" + )); + + String code = template4.render(); + String expected = + """ + { + [false, 0, names: {}] + define int name_4 + [true, 1, names: {name_4}] + [false, 0, names: {}] + something + < + name_4 = 5 + > + more + [true, 1, names: {name_4}] + more + define int name_1 + [true, 2, names: {name_4, name_1}] + more + [true, 1, names: {name_4}] + [false, 0, names: {}] + } + """; + checkEQ(code, expected); + } + + public static void testDataNames2() { + var hook1 = new Hook("Hook1"); + + var template0 = Template.make("type", "mutability", (DataName.Type type, DataName.Mutability mutability) -> body( + " #mutability: [", + dataNames(mutability).exactOf(myInt).hasAny(), + ", ", + dataNames(mutability).exactOf(myInt).count(), + ", names: {", + String.join(", ", dataNames(mutability).exactOf(myInt).toList().stream().map(DataName::name).toList()), + "}]\n" + )); + + var template1 = Template.make("type", (DataName.Type type) -> body( + "[#type:\n", + template0.asToken(type, MUTABLE), + template0.asToken(type, IMMUTABLE), + template0.asToken(type, MUTABLE_OR_IMMUTABLE), + "]\n" + )); + + var template2 = Template.make("name", "type", (String name, DataName.Type type) -> body( + addDataName(name, type, MUTABLE), + "define mutable #type #name\n", + template1.asToken(type) + )); + + var template3 = Template.make("name", "type", (String name, DataName.Type type) -> body( + addDataName(name, type, IMMUTABLE), + "define immutable #type #name\n", + template1.asToken(type) + )); + + var template4 = Template.make("type", (DataName.Type type) -> body( + "{ $store\n", + hook1.insert(template2.asToken($("name"), type)), + "$name = 5\n", + "} $store\n" + )); + + var template5 = Template.make("type", (DataName.Type type) -> body( + "{ $load\n", + hook1.insert(template3.asToken($("name"), type)), + "blackhole($name)\n", + "} $load\n" + )); + + var template6 = Template.make("type", (DataName.Type type) -> body( + let("v", dataNames(MUTABLE).exactOf(type).sample().name()), + "{ $sample\n", + "#v = 7\n", + "} $sample\n" + )); + + var template7 = Template.make("type", (DataName.Type type) -> body( + let("v", dataNames(MUTABLE_OR_IMMUTABLE).exactOf(type).sample().name()), + "{ $sample\n", + "blackhole(#v)\n", + "} $sample\n" + )); + + var template8 = Template.make(() -> body( + "class $X {\n", + template1.asToken(myInt), + hook1.anchor( + "begin $body\n", + template1.asToken(myInt), + "start with immutable\n", + template5.asToken(myInt), + "then load from it\n", + template7.asToken(myInt), + template1.asToken(myInt), + "now make something mutable\n", + template4.asToken(myInt), + "then store to it\n", + template6.asToken(myInt), + template1.asToken(myInt) + ), + template1.asToken(myInt), + "}\n" + )); + + String code = template8.render(); + String expected = + """ + class X_1 { + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] + ] + define immutable int name_10 + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [true, 1, names: {name_10}] + MUTABLE_OR_IMMUTABLE: [true, 1, names: {name_10}] + ] + define mutable int name_21 + [int: + MUTABLE: [true, 1, names: {name_21}] + IMMUTABLE: [true, 1, names: {name_10}] + MUTABLE_OR_IMMUTABLE: [true, 2, names: {name_10, name_21}] + ] + begin body_1 + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] + ] + start with immutable + { load_10 + blackhole(name_10) + } load_10 + then load from it + { sample_16 + blackhole(name_10) + } sample_16 + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [true, 1, names: {name_10}] + MUTABLE_OR_IMMUTABLE: [true, 1, names: {name_10}] + ] + now make something mutable + { store_21 + name_21 = 5 + } store_21 + then store to it + { sample_27 + name_21 = 7 + } sample_27 + [int: + MUTABLE: [true, 1, names: {name_21}] + IMMUTABLE: [true, 1, names: {name_10}] + MUTABLE_OR_IMMUTABLE: [true, 2, names: {name_10, name_21}] + ] + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] + ] + } + """; + checkEQ(code, expected); + } + + public static void testDataNames3() { + var hook1 = new Hook("Hook1"); + + var template0 = Template.make("type", "mutability", (DataName.Type type, DataName.Mutability mutability) -> body( + " #mutability: [", + dataNames(mutability).exactOf(myInt).hasAny(), + ", ", + dataNames(mutability).exactOf(myInt).count(), + ", names: {", + String.join(", ", dataNames(mutability).exactOf(myInt).toList().stream().map(DataName::name).toList()), + "}]\n" + )); + + var template1 = Template.make("type", (DataName.Type type) -> body( + "[#type:\n", + template0.asToken(type, MUTABLE), + template0.asToken(type, IMMUTABLE), + template0.asToken(type, MUTABLE_OR_IMMUTABLE), + "]\n" + )); + + var template2 = Template.make(() -> body( + "class $Y {\n", + template1.asToken(myInt), + hook1.anchor( + "begin $body\n", + template1.asToken(myInt), + "define mutable $v1\n", + addDataName($("v1"), myInt, MUTABLE), + template1.asToken(myInt), + "define immutable $v2\n", + addDataName($("v2"), myInt, IMMUTABLE), + template1.asToken(myInt) + ), + template1.asToken(myInt), + "}\n" + )); + + String code = template2.render(); + String expected = + """ + class Y_1 { + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] + ] + begin body_1 + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] + ] + define mutable v1_1 + [int: + MUTABLE: [true, 1, names: {v1_1}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [true, 1, names: {v1_1}] + ] + define immutable v2_1 + [int: + MUTABLE: [true, 1, names: {v1_1}] + IMMUTABLE: [true, 1, names: {v2_1}] + MUTABLE_OR_IMMUTABLE: [true, 2, names: {v1_1, v2_1}] + ] + [int: + MUTABLE: [false, 0, names: {}] + IMMUTABLE: [false, 0, names: {}] + MUTABLE_OR_IMMUTABLE: [false, 0, names: {}] + ] + } + """; + checkEQ(code, expected); + } + + public static void testDataNames4() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("type", (DataName.Type type) -> body( + "[#type:\n", + " exact: ", + dataNames(MUTABLE).exactOf(type).hasAny(), + ", ", + dataNames(MUTABLE).exactOf(type).count(), + ", {", + String.join(", ", dataNames(MUTABLE).exactOf(type).toList().stream().map(DataName::name).toList()), + "}\n", + " subtype: ", + dataNames(MUTABLE).subtypeOf(type).hasAny(), + ", ", + dataNames(MUTABLE).subtypeOf(type).count(), + ", {", + String.join(", ", dataNames(MUTABLE).subtypeOf(type).toList().stream().map(DataName::name).toList()), + "}\n", + " supertype: ", + dataNames(MUTABLE).supertypeOf(type).hasAny(), + ", ", + dataNames(MUTABLE).supertypeOf(type).count(), + ", {", + String.join(", ", dataNames(MUTABLE).supertypeOf(type).toList().stream().map(DataName::name).toList()), + "}\n", + "]\n" + )); + + List types = List.of(myClassA, myClassA1, myClassA2, myClassA11, myClassB); + var template2 = Template.make(() -> body( + "DataNames:\n", + types.stream().map(t -> template1.asToken(t)).toList() + )); + + var template3 = Template.make("type", (DataName.Type type) -> body( + let("name", dataNames(MUTABLE).subtypeOf(type).sample()), + "Sample #type: #name\n" + )); + + var template4 = Template.make(() -> body( + "class $W {\n", + template2.asToken(), + hook1.anchor( + "Create name for myClassA11, should be visible for the super classes\n", + addDataName($("v1"), myClassA11, MUTABLE), + template3.asToken(myClassA11), + template3.asToken(myClassA1), + template3.asToken(myClassA), + "Create name for myClassA, should never be visible for the sub classes\n", + addDataName($("v2"), myClassA, MUTABLE), + template3.asToken(myClassA11), + template3.asToken(myClassA1), + template2.asToken() + ), + template2.asToken(), + "}\n" + )); + + String code = template4.render(); + String expected = + """ + class W_1 { + DataNames: + [myClassA: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassA1: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassA2: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassA11: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassB: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + Create name for myClassA11, should be visible for the super classes + Sample myClassA11: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA1: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Create name for myClassA, should never be visible for the sub classes + Sample myClassA11: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + Sample myClassA1: DataName[name=v1_1, type=myClassA11, mutable=true, weight=1] + DataNames: + [myClassA: + exact: true, 1, {v2_1} + subtype: true, 2, {v1_1, v2_1} + supertype: true, 1, {v2_1} + ] + [myClassA1: + exact: false, 0, {} + subtype: true, 1, {v1_1} + supertype: true, 1, {v2_1} + ] + [myClassA2: + exact: false, 0, {} + subtype: false, 0, {} + supertype: true, 1, {v2_1} + ] + [myClassA11: + exact: true, 1, {v1_1} + subtype: true, 1, {v1_1} + supertype: true, 2, {v1_1, v2_1} + ] + [myClassB: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + DataNames: + [myClassA: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassA1: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassA2: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassA11: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [myClassB: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + } + """; + checkEQ(code, expected); + } + + // Test duplicate names in safe cases. + public static void testDataNames5() { + var hook1 = new Hook("Hook1"); + var hook2 = new Hook("Hook2"); + + // It is safe in separate Hook scopes. + var template1 = Template.make(() -> body( + hook1.anchor( + addDataName("name1", myInt, MUTABLE) + ), + hook1.anchor( + addDataName("name1", myInt, MUTABLE) + ) + )); + + // It is safe in separate Template scopes. + var template2 = Template.make(() -> body( + addDataName("name2", myInt, MUTABLE) + )); + var template3 = Template.make(() -> body( + template2.asToken(), + template2.asToken() + )); + + var template4 = Template.make(() -> body( + // The following is not safe, it would collide + // with (1), because it would be inserted to the + // hook1.anchor in template5, and hence be available + // inside the scope where (1) is available. + // See: testFailingAddNameDuplication8 + // addDataName("name", myInt, MUTABLE), + hook2.anchor( + // (2) This one is added second. Since it is + // inside the hook2.anchor, it does not go + // out to the hook1.anchor, and is not + // available inside the scope of (1). + addDataName("name3", myInt, MUTABLE) + ) + )); + var template5 = Template.make(() -> body( + hook1.anchor( + // (1) this is the first one we add. + addDataName("name3", myInt, MUTABLE) + ) + )); + + // Put it all together into a single test. + var template6 = Template.make(() -> body( + template1.asToken(), + template3.asToken(), + template5.asToken() + )); + + String code = template1.render(); + String expected = ""; + checkEQ(code, expected); + } + + public static void testStructuralNames1() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("type", (StructuralName.Type type) -> body( + "[#type:\n", + " exact: ", + structuralNames().exactOf(type).hasAny(), + ", ", + structuralNames().exactOf(type).count(), + ", {", + String.join(", ", structuralNames().exactOf(type).toList().stream().map(StructuralName::name).toList()), + "}\n", + " subtype: ", + structuralNames().subtypeOf(type).hasAny(), + ", ", + structuralNames().subtypeOf(type).count(), + ", {", + String.join(", ", structuralNames().subtypeOf(type).toList().stream().map(StructuralName::name).toList()), + "}\n", + " supertype: ", + structuralNames().supertypeOf(type).hasAny(), + ", ", + structuralNames().supertypeOf(type).count(), + ", {", + String.join(", ", structuralNames().supertypeOf(type).toList().stream().map(StructuralName::name).toList()), + "}\n", + "]\n" + )); + + List types = List.of(myStructuralTypeA, + myStructuralTypeA1, + myStructuralTypeA2, + myStructuralTypeA11, + myStructuralTypeB); + var template2 = Template.make(() -> body( + "StructuralNames:\n", + types.stream().map(t -> template1.asToken(t)).toList() + )); + + var template3 = Template.make("type", (StructuralName.Type type) -> body( + let("name", structuralNames().subtypeOf(type).sample()), + "Sample #type: #name\n" + )); + + var template4 = Template.make(() -> body( + "class $Q {\n", + template2.asToken(), + hook1.anchor( + "Create name for myStructuralTypeA11, should be visible for the supertypes\n", + addStructuralName($("v1"), myStructuralTypeA11), + template3.asToken(myStructuralTypeA11), + template3.asToken(myStructuralTypeA1), + template3.asToken(myStructuralTypeA), + "Create name for myStructuralTypeA, should never be visible for the subtypes\n", + addStructuralName($("v2"), myStructuralTypeA), + template3.asToken(myStructuralTypeA11), + template3.asToken(myStructuralTypeA1), + template2.asToken() + ), + template2.asToken(), + "}\n" + )); + + String code = template4.render(); + String expected = + """ + class Q_1 { + StructuralNames: + [StructuralA: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralA1: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralA2: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralA11: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralB: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + Create name for myStructuralTypeA11, should be visible for the supertypes + Sample StructuralA11: StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA1: StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA: StructuralName[name=v1_1, type=StructuralA11, weight=1] + Create name for myStructuralTypeA, should never be visible for the subtypes + Sample StructuralA11: StructuralName[name=v1_1, type=StructuralA11, weight=1] + Sample StructuralA1: StructuralName[name=v1_1, type=StructuralA11, weight=1] + StructuralNames: + [StructuralA: + exact: true, 1, {v2_1} + subtype: true, 2, {v1_1, v2_1} + supertype: true, 1, {v2_1} + ] + [StructuralA1: + exact: false, 0, {} + subtype: true, 1, {v1_1} + supertype: true, 1, {v2_1} + ] + [StructuralA2: + exact: false, 0, {} + subtype: false, 0, {} + supertype: true, 1, {v2_1} + ] + [StructuralA11: + exact: true, 1, {v1_1} + subtype: true, 1, {v1_1} + supertype: true, 2, {v1_1, v2_1} + ] + [StructuralB: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + StructuralNames: + [StructuralA: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralA1: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralA2: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralA11: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + [StructuralB: + exact: false, 0, {} + subtype: false, 0, {} + supertype: false, 0, {} + ] + } + """; + checkEQ(code, expected); + } + + public static void testStructuralNames2() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make("type", (StructuralName.Type type) -> body( + "[#type: ", + structuralNames().exactOf(type).hasAny(), + ", ", + structuralNames().exactOf(type).count(), + ", names: {", + String.join(", ", structuralNames().exactOf(type).toList().stream().map(StructuralName::name).toList()), + "}]\n" + )); + + var template2 = Template.make("name", "type", (String name, StructuralName.Type type) -> body( + addStructuralName(name, type), + "define #type #name\n" + )); + + var template3 = Template.make("type", (StructuralName.Type type) -> body( + "{ $access\n", + hook1.insert(template2.asToken($("name"), type)), + "$name = 5\n", + "} $access\n" + )); + + var template4 = Template.make("type", (StructuralName.Type type) -> body( + let("v", structuralNames().exactOf(type).sample().name()), + "{ $sample\n", + "blackhole(#v)\n", + "} $sample\n" + )); + + var template8 = Template.make(() -> body( + "class $X {\n", + template1.asToken(myStructuralTypeA), + template1.asToken(myStructuralTypeB), + hook1.anchor( + "begin $body\n", + template1.asToken(myStructuralTypeA), + template1.asToken(myStructuralTypeB), + "start with A\n", + template3.asToken(myStructuralTypeA), + "then access it\n", + template4.asToken(myStructuralTypeA), + template1.asToken(myStructuralTypeA), + template1.asToken(myStructuralTypeB), + "now make a B\n", + template3.asToken(myStructuralTypeB), + "then access to it\n", + template4.asToken(myStructuralTypeB), + template1.asToken(myStructuralTypeA), + template1.asToken(myStructuralTypeB) + ), + template1.asToken(myStructuralTypeA), + template1.asToken(myStructuralTypeB), + "}\n" + )); + + String code = template8.render(); + String expected = + """ + class X_1 { + [StructuralA: false, 0, names: {}] + [StructuralB: false, 0, names: {}] + define StructuralA name_6 + define StructuralB name_11 + begin body_1 + [StructuralA: false, 0, names: {}] + [StructuralB: false, 0, names: {}] + start with A + { access_6 + name_6 = 5 + } access_6 + then access it + { sample_8 + blackhole(name_6) + } sample_8 + [StructuralA: true, 1, names: {name_6}] + [StructuralB: false, 0, names: {}] + now make a B + { access_11 + name_11 = 5 + } access_11 + then access to it + { sample_13 + blackhole(name_11) + } sample_13 + [StructuralA: true, 1, names: {name_6}] + [StructuralB: true, 1, names: {name_11}] + [StructuralA: false, 0, names: {}] + [StructuralB: false, 0, names: {}] + } + """; + checkEQ(code, expected); + } + + record MyItem(DataName.Type type, String op) {} + + public static void testListArgument() { + var template1 = Template.make("item", (MyItem item) -> body( + let("type", item.type()), + let("op", item.op()), + "#type apply #op\n" + )); + + var template2 = Template.make("list", (List list) -> body( + "class $Z {\n", + // Use template1 for every item in the list. + list.stream().map(item -> template1.asToken(item)).toList(), + "}\n" + )); + + List list = List.of(new MyItem(myInt, "+"), + new MyItem(myInt, "-"), + new MyItem(myInt, "*"), + new MyItem(myInt, "/"), + new MyItem(myLong, "+"), + new MyItem(myLong, "-"), + new MyItem(myLong, "*"), + new MyItem(myLong, "/")); + + String code = template2.render(list); + String expected = + """ + class Z_1 { + int apply + + int apply - + int apply * + int apply / + long apply + + long apply - + long apply * + long apply / + } + """; + checkEQ(code, expected); + } + + public static void testFailingNestedRendering() { + var template1 = Template.make(() -> body( + "alpha\n" + )); + + var template2 = Template.make(() -> body( + "beta\n", + // Nested "render" call not allowed! + template1.render(), + "gamma\n" + )); + + String code = template2.render(); + } + + public static void testFailingDollarName1() { + var template1 = Template.make(() -> body( + let("x", $("")) // empty string not allowed + )); + String code = template1.render(); + } + + public static void testFailingDollarName2() { + var template1 = Template.make(() -> body( + let("x", $("#abc")) // "#" character not allowed + )); + String code = template1.render(); + } + + public static void testFailingDollarName3() { + var template1 = Template.make(() -> body( + let("x", $("abc#")) // "#" character not allowed + )); + String code = template1.render(); + } + + public static void testFailingDollarName4() { + var template1 = Template.make(() -> body( + let("x", $(null)) // Null input to dollar + )); + String code = template1.render(); + } + + public static void testFailingDollarName5() { + var template1 = Template.make(() -> body( + "$" // empty dollar name + )); + String code = template1.render(); + } + + public static void testFailingDollarName6() { + var template1 = Template.make(() -> body( + "asdf$" // empty dollar name + )); + String code = template1.render(); + } + + public static void testFailingDollarName7() { + var template1 = Template.make(() -> body( + "asdf$1" // Bad pattern after dollar + )); + String code = template1.render(); + } + + public static void testFailingDollarName8() { + var template1 = Template.make(() -> body( + "abc$$abc" // empty dollar name + )); + String code = template1.render(); + } + + public static void testFailingLetName1() { + var template1 = Template.make(() -> body( + let(null, $("abc")) // Null input for hashtag name + )); + String code = template1.render(); + } + + public static void testFailingHashtagName1() { + // Empty Template argument + var template1 = Template.make("", (String x) -> body( + )); + String code = template1.render("abc"); + } + + public static void testFailingHashtagName2() { + // "#" character not allowed in template argument + var template1 = Template.make("abc#abc", (String x) -> body( + )); + String code = template1.render("abc"); + } + + public static void testFailingHashtagName3() { + var template1 = Template.make(() -> body( + // Empty let hashtag name not allowed + let("", "abc") + )); + String code = template1.render(); + } + + public static void testFailingHashtagName4() { + var template1 = Template.make(() -> body( + // "#" character not allowed in let hashtag name + let("xyz#xyz", "abc") + )); + String code = template1.render(); + } + + public static void testFailingHashtagName5() { + var template1 = Template.make(() -> body( + "#" // empty hashtag name + )); + String code = template1.render(); + } + + public static void testFailingHashtagName6() { + var template1 = Template.make(() -> body( + "asdf#" // empty hashtag name + )); + String code = template1.render(); + } + + public static void testFailingHashtagName7() { + var template1 = Template.make(() -> body( + "asdf#1" // Bad pattern after hashtag + )); + String code = template1.render(); + } + + public static void testFailingHashtagName8() { + var template1 = Template.make(() -> body( + "abc##abc" // empty hashtag name + )); + String code = template1.render(); + } + + public static void testFailingDollarHashtagName1() { + var template1 = Template.make(() -> body( + "#$" // empty hashtag name + )); + String code = template1.render(); + } + + public static void testFailingDollarHashtagName2() { + var template1 = Template.make(() -> body( + "$#" // empty dollar name + )); + String code = template1.render(); + } + + public static void testFailingDollarHashtagName3() { + var template1 = Template.make(() -> body( + "#$name" // empty hashtag name + )); + String code = template1.render(); + } + + public static void testFailingDollarHashtagName4() { + var template1 = Template.make(() -> body( + "$#name" // empty dollar name + )); + String code = template1.render(); + } + + public static void testFailingHook() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make(() -> body( + "alpha\n" + )); + + var template2 = Template.make(() -> body( + "beta\n", + // Use hook without hook1.anchor + hook1.insert(template1.asToken()), + "gamma\n" + )); + + String code = template2.render(); + } + + public static void testFailingSample1() { + var template1 = Template.make(() -> body( + // No variable added yet. + let("v", dataNames(MUTABLE).exactOf(myInt).sample().name()), + "v is #v\n" + )); + + String code = template1.render(); + } + + public static void testFailingSample2() { + var template1 = Template.make(() -> body( + // no type restriction + let("v", dataNames(MUTABLE).sample().name()), + "v is #v\n" + )); + + String code = template1.render(); + } + + public static void testFailingHashtag1() { + // Duplicate hashtag definition from arguments. + var template1 = Template.make("a", "a", (String _, String _) -> body( + "nothing\n" + )); + + String code = template1.render("x", "y"); + } + + public static void testFailingHashtag2() { + var template1 = Template.make("a", (String _) -> body( + // Duplicate hashtag name + let("a", "x"), + "nothing\n" + )); + + String code = template1.render("y"); + } + + public static void testFailingHashtag3() { + var template1 = Template.make(() -> body( + let("a", "x"), + // Duplicate hashtag name + let("a", "y"), + "nothing\n" + )); + + String code = template1.render(); + } + + public static void testFailingHashtag4() { + var template1 = Template.make(() -> body( + // Missing hashtag name definition + "#a\n" + )); + + String code = template1.render(); + } + + public static void testFailingBinding1() { + var binding = new TemplateBinding(); + var template1 = Template.make(() -> body( + "nothing\n" + )); + binding.bind(template1); + // Duplicate bind + binding.bind(template1); + } + + public static void testFailingBinding2() { + var binding = new TemplateBinding(); + var template1 = Template.make(() -> body( + "nothing\n", + // binding was never bound. + binding.get() + )); + // Should have bound the binding here. + String code = template1.render(); + } + + public static void testFailingAddDataName1() { + var template1 = Template.make(() -> body( + // Must pick either MUTABLE or IMMUTABLE. + addDataName("name", myInt, MUTABLE_OR_IMMUTABLE) + )); + String code = template1.render(); + } + + public static void testFailingAddDataName2() { + var template1 = Template.make(() -> body( + // weight out of bounds [0..1000] + addDataName("name", myInt, MUTABLE, 0) + )); + String code = template1.render(); + } + + public static void testFailingAddDataName3() { + var template1 = Template.make(() -> body( + // weight out of bounds [0..1000] + addDataName("name", myInt, MUTABLE, -1) + )); + String code = template1.render(); + } + + public static void testFailingAddDataName4() { + var template1 = Template.make(() -> body( + // weight out of bounds [0..1000] + addDataName("name", myInt, MUTABLE, 1001) + )); + String code = template1.render(); + } + + public static void testFailingAddStructuralName1() { + var template1 = Template.make(() -> body( + // weight out of bounds [0..1000] + addStructuralName("name", myStructuralTypeA, 0) + )); + String code = template1.render(); + } + + public static void testFailingAddStructuralName2() { + var template1 = Template.make(() -> body( + // weight out of bounds [0..1000] + addStructuralName("name", myStructuralTypeA, -1) + )); + String code = template1.render(); + } + + public static void testFailingAddStructuralName3() { + var template1 = Template.make(() -> body( + // weight out of bounds [0..1000] + addStructuralName("name", myStructuralTypeA, 1001) + )); + String code = template1.render(); + } + + // Duplicate name in the same scope, name identical -> expect RendererException. + public static void testFailingAddNameDuplication1() { + var template1 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE), + addDataName("name", myInt, MUTABLE) + )); + String code = template1.render(); + } + + // Duplicate name in the same scope, names have different mutability -> expect RendererException. + public static void testFailingAddNameDuplication2() { + var template1 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE), + addDataName("name", myInt, IMMUTABLE) + )); + String code = template1.render(); + } + + // Duplicate name in the same scope, names have different type -> expect RendererException. + public static void testFailingAddNameDuplication3() { + var template1 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE), + addDataName("name", myLong, MUTABLE) + )); + String code = template1.render(); + } + + // Duplicate name in the same scope, name identical -> expect RendererException. + public static void testFailingAddNameDuplication4() { + var template1 = Template.make(() -> body( + addStructuralName("name", myStructuralTypeA), + addStructuralName("name", myStructuralTypeA) + )); + String code = template1.render(); + } + + // Duplicate name in the same scope, names have different type -> expect RendererException. + public static void testFailingAddNameDuplication5() { + var template1 = Template.make(() -> body( + addStructuralName("name", myStructuralTypeA), + addStructuralName("name", myStructuralTypeB) + )); + String code = template1.render(); + } + + // Duplicate name in inner Template, name identical -> expect RendererException. + public static void testFailingAddNameDuplication6() { + var template1 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE) + )); + var template2 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE), + template1.asToken() + )); + String code = template2.render(); + } + + // Duplicate name in Hook scope, name identical -> expect RendererException. + public static void testFailingAddNameDuplication7() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE), + hook1.anchor( + addDataName("name", myInt, MUTABLE) + ) + )); + String code = template1.render(); + } + + // Duplicate name in Hook.insert, name identical -> expect RendererException. + public static void testFailingAddNameDuplication8() { + var hook1 = new Hook("Hook1"); + + var template1 = Template.make(() -> body( + addDataName("name", myInt, MUTABLE) + )); + + var template2 = Template.make(() -> body( + hook1.anchor( + addDataName("name", myInt, MUTABLE), + hook1.insert(template1.asToken()) + ) + )); + String code = template2.render(); + } + + public static void expectRendererException(FailingTest test, String errorPrefix) { + try { + test.run(); + System.out.println("Should have thrown RendererException with prefix: " + errorPrefix); + throw new RuntimeException("Should have thrown!"); + } catch(RendererException e) { + if (!e.getMessage().startsWith(errorPrefix)) { + System.out.println("Should have thrown with prefix: " + errorPrefix); + System.out.println("got: " + e.getMessage()); + throw new RuntimeException("Prefix mismatch", e); + } + } + } + + public static void expectIllegalArgumentException(FailingTest test, String errorPrefix) { + try { + test.run(); + System.out.println("Should have thrown IllegalArgumentException with prefix: " + errorPrefix); + throw new RuntimeException("Should have thrown!"); + } catch(IllegalArgumentException e) { + if (!e.getMessage().startsWith(errorPrefix)) { + System.out.println("Should have thrown with prefix: " + errorPrefix); + System.out.println("got: " + e.getMessage()); + throw new RuntimeException("Prefix mismatch", e); + } + } + } + + public static void expectUnsupportedOperationException(FailingTest test, String errorPrefix) { + try { + test.run(); + System.out.println("Should have thrown UnsupportedOperationException with prefix: " + errorPrefix); + throw new RuntimeException("Should have thrown!"); + } catch(UnsupportedOperationException e) { + if (!e.getMessage().startsWith(errorPrefix)) { + System.out.println("Should have thrown with prefix: " + errorPrefix); + System.out.println("got: " + e.getMessage()); + throw new RuntimeException("Prefix mismatch", e); + } + } + } + + public static void checkEQ(String code, String expected) { + if (!code.equals(expected)) { + System.out.println("\"" + code + "\""); + System.out.println("\"" + expected + "\""); + throw new RuntimeException("Template rendering mismatch!"); + } + } +}