8367531: Template Framework: use scopes and tokens instead of misbehaving immediate-return-queries

Co-authored-by: Christian Hagedorn <chagedorn@openjdk.org>
Reviewed-by: rcastanedalo, mhaessig, chagedorn
This commit is contained in:
Emanuel Peter 2025-11-20 09:32:57 +00:00
parent 6fc8e49980
commit b41146cd1e
39 changed files with 3988 additions and 1019 deletions

View File

@ -60,7 +60,7 @@ public class TestMethodArguments {
: IntStream.range(0, numberOfArguments)
.mapToObj(i -> "x" + i)
.collect(Collectors.joining(" + "));
return Template.make(() -> Template.body(
return Template.make(() -> Template.scope(
Template.let("type", type.name()),
Template.let("boxedType", type.boxedTypeName()),
Template.let("arguments", arguments),
@ -115,7 +115,7 @@ public class TestMethodArguments {
tests.add(generateTest(CodeGenerationDataNameType.longs(), i / 2).asToken());
tests.add(generateTest(CodeGenerationDataNameType.doubles(), i / 2).asToken());
}
return Template.make(() -> Template.body(
return Template.make(() -> Template.scope(
Template.let("classpath", comp.getEscapedClassPathOfCompiledClasses()),
"""
import java.util.Arrays;

View File

@ -45,7 +45,7 @@ import java.util.stream.IntStream;
import compiler.lib.compile_framework.*;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import static compiler.lib.template_framework.Template.let;
import static compiler.lib.template_framework.Template.$;
import compiler.lib.template_framework.library.CodeGenerationDataNameType;
@ -99,7 +99,7 @@ public class ExpressionFuzzer {
// Create the body for the test. We use it twice: compiled and reference.
// Execute the expression and catch expected Exceptions.
var bodyTemplate = Template.make("expression", "arguments", "checksum", (Expression expression, List<Object> arguments, String checksum) -> body(
var bodyTemplate = Template.make("expression", "arguments", "checksum", (Expression expression, List<Object> arguments, String checksum) -> scope(
"""
try {
""",
@ -167,14 +167,14 @@ public class ExpressionFuzzer {
default -> throw new RuntimeException("not handled: " + type.name());
};
StringPair cmp = cmps.get(RANDOM.nextInt(cmps.size()));
return body(
return scope(
", ", cmp.s0(), type.con(), cmp.s1()
);
});
// Checksum method: returns not just the value, but also does some range / bit checks.
// This gives us enhanced verification on the range / bits of the result type.
var checksumTemplate = Template.make("expression", "checksum", (Expression expression, String checksum) -> body(
var checksumTemplate = Template.make("expression", "checksum", (Expression expression, String checksum) -> scope(
let("returnType", expression.returnType),
"""
@ForceInline
@ -201,7 +201,7 @@ public class ExpressionFuzzer {
// We need to prepare some random values to pass into the test method. We generate the values
// once, and pass the same values into both the compiled and reference method.
var valueTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> body(
var valueTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> scope(
"#type #name = ",
(type instanceof PrimitiveType pt) ? pt.callLibraryRNG() : type.con(),
";\n"
@ -213,7 +213,7 @@ public class ExpressionFuzzer {
//
// To ensure that both the compiled and reference method use the same constraint, we put
// the computation in a ForceInline method.
var constrainArgumentMethodTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> body(
var constrainArgumentMethodTemplate = Template.make("name", "type", (String name, CodeGenerationDataNameType type) -> scope(
"""
@ForceInline
public static #type constrain_#name(#type v) {
@ -247,7 +247,7 @@ public class ExpressionFuzzer {
"""
));
var constrainArgumentTemplate = Template.make("name", (String name) -> body(
var constrainArgumentTemplate = Template.make("name", (String name) -> scope(
"""
#name = constrain_#name(#name);
"""
@ -279,7 +279,7 @@ public class ExpressionFuzzer {
}
}
}
return body(
return scope(
let("methodArguments",
methodArguments.stream().map(ma -> ma.name).collect(Collectors.joining(", "))),
let("methodArgumentsWithTypes",

View File

@ -23,4 +23,8 @@
package compiler.lib.template_framework;
/**
* Represents the addition of the specified {@link Name} to the current scope,
* or an outer scope if the inner scope is transparent to {@link Name}s.
*/
record AddNameToken(Name name) implements Token {}

View File

@ -29,22 +29,96 @@ 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'}
* The {@link CodeFrame} represents a frame (i.e. scope) of generated code by appending {@link Code} to the {@link #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.
* {@link Template#addDataName}. {@link Hook}s can be added to a code frame, which allows code to be inserted at that
* location later.
*
* <p>
* On the other hand, each {@link TemplateFrame} represents the frame (or scope) of exactly one use of a
* Template.
* The {@link CodeFrame} thus implements the {@link Name} non-transparency aspect of {@link ScopeToken}.
*
* <p>
* 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}.
* The {@link CodeFrame}s are nested relative to the order of the final rendered code. This can
* diverge from the nesting order of the {@link Template} when using {@link Hook#insert}, where
* the execution jumps from the current (caller) {@link CodeFrame} scope to the scope of the
* {@link Hook#anchor}. This ensures that the {@link Name}s of the anchor scope are accessed,
* and not the ones from the caller scope. Once the {@link Hook#insert}ion is complete, we
* jump back to the caller {@link CodeFrame}.
*
* <p>
* Note, that {@link CodeFrame}s and {@link TemplateFrame}s often go together. But they do diverge when
* we call {@link Hook#insert}. On the {@link CodeFrame} side, the inserted scope is nested in the anchoring
* scope, so that the inserted scope has access to the Names of the anchoring scope, and not the caller
* scope. But the {@link TemplateFrame} of the inserted scope is nested in the caller scope, so
* that the inserted scope has access to hashtag replacements of the caller scope, and not the
* anchoring scope.
*/
/*
* Below, we look at an example, and show the use of CodeFrames (c) and TemplateFrames (t).
*
* Explanations:
* - Generally, every scope has a CodeFrame and a TemplateFrame. There can be multiple
* scopes inside a Template, and so there can be multiple CodeFrames and TemplateFrames.
* In the drawing below, we draw the frames vertically, and give each a unique id.
* - When we nest scopes inside scopes, we create a new CodeFrame and a new TemplateFrame,
* and so they grow the same nested structure. Example: t3 is nested inside t2 and
* c3 is nested inside c2b.
* - The exception to this:
* - At a hook.anchor, there are two CodeFrames. The first one (e.g. c2a) we call the
* hook CodeFrame, it is kept empty until we insert code to the hook. The second
* (e.g. c2b) we call the inner CodeFrame of the anchoring, into which we keep
* generating the code that is inside the scope of the hook.anchor.
* - At a hook.insert, the TemplateFrame (e.g. t4) is nested into the caller (e.g. t3),
* while the CodeFrame (e.g. c4) is nested into the anchoring CodeFrame (e.g. c2a).
*
* Template(
* t1 c1
* t1 c1
* t1 c1 Anchoring Scope
* t1 c1 hook.anchor(scope(
* t1 c1 t2 c2a
* t1 c1 t2 c2a <------ CodeFrame nesting--------+
* t1 c1 t2 c2a with generated code |
* t1 c1 t2 and Names |
* t1 c1 t2 ^ |
* t1 c1 t2 +- Two CodeFramees |
* t1 c1 t2 v |
* t1 c1 t2 |
* t1 c1 t2 c2b |
* t1 c1 t2 c2b |
* t1 c1 t2 c2b Caller Scope |
* t1 c1 t2 c2b ... scope( |
* t1 c1 t2 c2b ... t3 c3 | Insertion Scope
* t1 c1 t2 c2b ... t3 c3 | hook.insert(transparentScope(
* t1 c1 t2 c2b ... t3 c3 | t4 c4
* t1 c1 t2 c2b ... t3 c3 +---- t4 ----c4
* t1 c1 t2 c2b ... t3 c3 t4 c4
* t1 c1 t2 c2b ... t3 c3 <-- TemplateFrame nesting ---t4 c4
* t1 c1 t2 c2b ... t3 c3 with hashtag t4 c4 // t: Concerns Template Frame
* t1 c1 t2 c2b ... t3 c3 and setFuelCost t4 c4 // c: Concerns Code Frame
* t1 c1 t2 c2b ... t3 c3 t4 c4 "use hashtag #x" -> t: hashtag queried in Insertion (t4) and Caller Scope (t3)
* t1 c1 t2 c2b ... t3 c3 t4 c4 c: code added to Anchoring Scope (c2a)
* t1 c1 t2 c2b ... t3 c3 t4 c4
* t1 c1 t2 c2b ... t3 c3 t4 c4 let("x", 42) -> t: hashtag definition escapes to Caller Scope (t3) because
* t1 c1 t2 c2b ... t3 c3 t4 c4 Insertion Scope is transparent
* t1 c1 t2 c2b ... t3 c3 t4 c4
* t1 c1 t2 c2b ... t3 c3 t4 c4 dataNames(...)...sample() -> c: sample from Insertion (c4) and Anchoring Scope (c2a)
* t1 c1 t2 c2b ... t3 c3 t4 c4 (CodeFrame nesting: c2a -> c4)
* t1 c1 t2 c2b ... t3 c3 t4 c4 addDataName(...) -> c: names escape to the Caller Scope (c3) because
* t1 c1 t2 c2b ... t3 c3 t4 c4 Insertion Scope is transparent
* t1 c1 t2 c2b ... t3 c3 t4 c4
* t1 c1 t2 c2b ... t3 c3 ))
* t1 c1 t2 c2b ... t3 c3
* t1 c1 t2 c2b ... t3 c3
* t1 c1 t2 c2b ... )
* t1 c1 t2 c2b
* t1 c1 t2 c2b
* t1 c1 ))
* t1 c1
* t1 c1
* )
*
*/
class CodeFrame {
public final CodeFrame parent;
@ -78,25 +152,16 @@ class CodeFrame {
}
/**
* 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.
* Creates a normal frame, which has a {@link #parent}. It can either be
* transparent for names, meaning that names are added and accessed to and
* from an outer frame. Names that are added in a transparent frame are
* still available in the outer frames, as far out as the next non-transparent
* frame. If a frame is non-transparent, this frame defines an inner
* {@link NameSet}, for the names that are generated inside this frame. Once
* this frame is exited, the names from inside this frame are not available.
*/
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);
public static CodeFrame make(CodeFrame parent, boolean isTransparentForNames) {
return new CodeFrame(parent, isTransparentForNames);
}
void addString(String s) {

View File

@ -24,6 +24,7 @@
package compiler.lib.template_framework;
import java.util.List;
import java.util.function.Function;
/**
* {@link DataName}s represent things like fields and local variables, and can be added to the local
@ -114,18 +115,36 @@ public record DataName(String name, DataName.Type type, boolean mutable, int wei
this(mutability, null, null);
}
// Wrap the FilteredSet as a Predicate.
private record DataNamePredicate(FilteredSet fs) implements NameSet.Predicate {
public boolean check(Name type) {
return fs.check(type);
}
public String toString() {
return fs.toString();
}
}
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;
};
return new DataNamePredicate(this);
}
boolean check(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;
}
public String toString() {
String msg1 = (subtype == null) ? "" : ", subtypeOf(" + subtype + ")";
String msg2 = (supertype == null) ? "" : ", supertypeOf(" + supertype + ")";
return "DataName.FilterdSet(" + mutability + msg1 + msg2 + ")";
}
/**
@ -173,55 +192,179 @@ public record DataName(String name, DataName.Type type, boolean mutable, int wei
/**
* Samples a random {@link DataName} from the filtered set, according to the weights
* of the contained {@link DataName}s.
* of the contained {@link DataName}s, making the sampled {@link DataName}
* available to an inner scope.
*
* @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.
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the sampled {@link DataName}.
* @return a token that represents the sampling and inner scope.
* @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());
public Token sample(Function<DataName, ScopeToken> function) {
return new NameSampleToken<>(predicate(), null, null, function);
}
/**
* Checks if there are any {@link DataName}s in the filtered set.
* Samples a random {@link DataName} from the filtered set, according to the weights
* of the contained {@link DataName}s, and makes a hashtag replacement for both
* the name and type of the {@link DataName}, in the current scope.
*
* @return Returns {@code true} iff there is at least one {@link DataName} in the filtered set.
* <p>
* Note, that the following two do the equivalent:
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* dataNames(MUTABLE).subtypeOf(type).sampleAndLetAs("name", "type"),
* """
* #name #type
* """
* ));
* }
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* dataNames(MUTABLE).subtypeOf(type).sample((DataName dn) -> transparentScope(
* // The "let" hashtag definitions escape the "transparentScope".
* let("name", dn.name()),
* let("type", dn.type())
* )),
* """
* #name #type
* """
* ));
* }
*
* @param name the key of the hashtag replacement for the {@link DataName} name.
* @param type the key of the hashtag replacement for the {@link DataName} type.
* @return a token that represents the sampling and hashtag replacement definition.
* @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());
public Token sampleAndLetAs(String name, String type) {
return new NameSampleToken<DataName>(predicate(), name, type, n -> Template.transparentScope());
}
/**
* Collects all {@link DataName}s in the filtered set.
* Samples a random {@link DataName} from the filtered set, according to the weights
* of the contained {@link DataName}s, and makes a hashtag replacement for the
* name of the {@link DataName}, in the current scope.
*
* <p>
* Note, that the following two do the equivalent:
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* dataNames(MUTABLE).subtypeOf(type).sampleAndLetAs("name"),
* """
* #name
* """
* ));
* }
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* dataNames(MUTABLE).subtypeOf(type).sample((DataName dn) -> transparentScope(
* // The "let" hashtag definition escape the "transparentScope".
* let("name", dn.name())
* )),
* """
* #name
* """
* ));
* }
*
* @param name the key of the hashtag replacement for the {@link DataName} name.
* @return a token that represents the sampling and hashtag replacement definition.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token sampleAndLetAs(String name) {
return new NameSampleToken<DataName>(predicate(), name, null, n -> Template.transparentScope());
}
/**
* Counts the number of {@link DataName}s in the filtered set, making the count
* available to an inner scope.
*
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the count.
* @return a token that represents the counting and inner scope.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token count(Function<Integer, ScopeToken> function) {
return new NameCountToken(predicate(), function);
}
/**
* Checks if there are any {@link DataName}s in the filtered set, making the resulting boolean
* available to an inner scope.
*
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the boolean indicating iff there are any {@link DataName}s in the filtered set.
* @return a token that represents the checking and inner scope.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token hasAny(Function<Boolean, ScopeToken> function) {
return new NameHasAnyToken(predicate(), function);
}
/**
* Collects all {@link DataName}s in the filtered set, making the collected list
* available to an inner scope.
*
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the list of {@link DataName}.
* @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<DataName> toList() {
List<Name> list = Renderer.getCurrent().listNames(predicate());
return list.stream().map(n -> (DataName)n).toList();
public Token toList(Function<List<DataName>, ScopeToken> function) {
return new NamesToListToken<>(predicate(), function);
}
/**
* Calls the provided {@code function} for each {@link DataName}s in the filtered set,
* making each of these {@link DataName}s available to a separate inner scope.
*
* @param function The {@link Function} that is called to create the inner {@link ScopeToken}s
* for each of the {@link DataName}s in the filtered set.
* @return The token representing the for-each execution and the respective inner scopes.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token forEach(Function<DataName, ScopeToken> function) {
return new NameForEachToken<>(predicate(), null, null, function);
}
/**
* Calls the provided {@code function} for each {@link DataName}s in the filtered set,
* making each of these {@link DataName}s available to a separate inner scope, and additionally
* setting hashtag replacements for the {@code name} and {@code type} of the respective
* {@link DataName}s.
*
* <p>
* Note, to avoid duplication of the {@code name} and {@code type}
* hashtag replacements, the scope created by the provided {@code function} should be
* non-transparent to hashtag replacements, for example {@link Template#scope} or
* {@link Template#hashtagScope}.
*
* @param name the key of the hashtag replacement for each individual {@link DataName} name.
* @param type the key of the hashtag replacement for each individual {@link DataName} type.
* @param function The {@link Function} that is called to create the inner {@link ScopeToken}s
* for each of the {@link DataName}s in the filtereds set.
* @return The token representing the for-each execution and the respective inner scopes.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token forEach(String name, String type, Function<DataName, ScopeToken> function) {
return new NameForEachToken<>(predicate(), name, type, function);
}
}
}

View File

@ -23,59 +23,84 @@
package compiler.lib.template_framework;
import java.util.function.Function;
/**
* {@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.
* A {@link Hook} can be {@link #anchor}ed for a certain scope ({@link ScopeToken}), and that
* anchoring stays active for any nested scope or nested {@link Template}. With {@link #insert},
* one can insert a template ({@link TemplateToken}) or scope ({@link ScopeToken}) to where the
* {@link Hook} was {@link #anchor}'ed. If the hook was anchored for multiple outer scopes, the
* innermost is chosen for insertion.
*
* <p>
* 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. Or one may want to reach back
* to the beginning of a method to insert local variables that should be live for the whole method.
*
* <p>
* The choice of {@link ScopeToken} is very important and powerful.
* For example, if you want to insert a {@link DataName} to the scope of an anchor,
* it is important that the scope of the insertion is transparent for {@link DataName}s,
* e.g. using {@link Template#transparentScope}. In most cases, we want {@link DataName}s to escape
* the inserted scope but not the anchor scope, so the anchor scope should be
* non-transparent for {@link DataName}s, e.g. using {@link Template#scope}.
* Example:
*
* <p>
* {@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(
* var template = Template.make(() -> scope(
* """
* public class Test {
* """,
* // Anchor the hook here.
* myHook.anchor(
* myHook.anchor(scope(
* """
* 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"))),
* // Reach out to where the hook was anchored, and insert some code.
* myHook.insert(transparentScope(
* // The field (DataName) escapes because the inserted scope is "transparentScope"
* addDataName($("field"), Primitives.INTS, MUTABLE),
* """
* public static int $field = 42;
* """
* )),
* """
* }
* """
* ),
* )),
* """
* }
* """
* ));
* }
*
* <p>
* Note that if we use {@link #insert} with {@link Template#transparentScope}, then
* {@link DataName}s and {@link StructuralName}s escape from the inserted scope to the
* anchor scope, but hashtag replacements and {@link Template#setFuelCost} escape to
* the caller, i.e. from where we inserted the scope. This makes sense if we consider
* {@link DataName}s belonging to the structure of the generated code and the inserted
* scope belonging to the anchor scope. On the other hand, hashtag replacements and
* {@link Template#setFuelCost} rather belong to the code generation that happens
* within the context of a template.
*
* @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'}.
* Anchor this {@link Hook} for the provided inner scope.
* 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.
* @param innerScope An inner scope, for which the {@link Hook} is anchored.
* @return A {@link Token} that captures the anchoring and the inner scope.
*/
public Token anchor(Object... tokens) {
return new HookAnchorToken(this, TokenParser.parse(tokens));
public Token anchor(ScopeToken innerScope) {
return new HookAnchorToken(this, innerScope);
}
/**
@ -83,18 +108,31 @@ public record Hook(String name) {
* 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}.
* @return The {@link Token} which represents the code insertion into the {@link Hook}.
*/
public Token insert(TemplateToken templateToken) {
return new HookInsertToken(this, templateToken);
return new HookInsertToken(this, Template.transparentScope(templateToken));
}
/**
* Checks if the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope.
* Inserts a scope ({@link ScopeToken}) to the innermost location where this {@link Hook} was {@link #anchor}ed.
* This could be in the same Template, or one nested further out.
*
* @return If the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope.
* @param scopeToken The scope to be inserted at the {@link Hook}.
* @return The {@link Token} which represents the code insertion into the {@link Hook}.
*/
public boolean isAnchored() {
return Renderer.getCurrent().isAnchored(this);
public Token insert(ScopeToken scopeToken) {
return new HookInsertToken(this, scopeToken);
}
/**
* Checks if the {@link Hook} was {@link Hook#anchor}ed for the current scope or an outer scope,
* and makes the boolean result available to an inner scope.
*
* @param function the function that generates the inner scope given the boolean result.
* @return the token that represents the check and inner scope.
*/
public Token isAnchored(Function<Boolean, ScopeToken> function) {
return new HookIsAnchoredToken(this, function);
}
}

View File

@ -25,4 +25,7 @@ package compiler.lib.template_framework;
import java.util.List;
record HookAnchorToken(Hook hook, List<Token> tokens) implements Token {}
/**
* Represents the {@link Hook#anchor} with its inner scope.
*/
record HookAnchorToken(Hook hook, ScopeToken innerScope) implements Token {}

View File

@ -23,4 +23,8 @@
package compiler.lib.template_framework;
record HookInsertToken(Hook hook, TemplateToken templateToken) implements Token {}
/**
* Represents the {@link Hook#insert} with the {@link ScopeToken} of the
* scope that is to be inserted.
*/
record HookInsertToken(Hook hook, ScopeToken scopeToken) implements Token {}

View File

@ -0,0 +1,37 @@
/*
* 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.Function;
/**
* Represents an {@link Hook#isAnchored} query with the function that creates an inner scope
* given the boolean answer.
*/
record HookIsAnchoredToken(Hook hook, Function<Boolean, ScopeToken> function) implements Token {
ScopeToken getScopeToken(boolean isAnchored) {
return function().apply(isAnchored);
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.Function;
/**
* Represents a let (aka hashtag) definition. The hashtag replacement is active for the
* scope ({@link ScopeToken}) that the {@code function} creates, but can escape that
* scope if it is transparent to hashtags.
*/
record LetToken<T>(String key, T value, Function<T, ScopeToken> function) implements Token {
ScopeToken getScopeToken() {
return function().apply(value);
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.Function;
/**
* Represents the counting of {@link Name}s, and the function that is called
* to create an inner scope given the count.
*/
record NameCountToken(
NameSet.Predicate predicate,
Function<Integer, ScopeToken> function) implements Token {
ScopeToken getScopeToken(int count) {
return function().apply(count);
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.Function;
/**
* Represents the for-each execution of the provided function and (optional) hashtag replacement
* keys for name and type of each name.
*/
record NameForEachToken<N>(
NameSet.Predicate predicate,
String name,
String type,
Function<N, ScopeToken> function) implements Token {
ScopeToken getScopeToken(Name n) {
return function().apply((N)n);
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.Function;
/**
* Represents the check if there is any name and the function that is to
* be called given the boolean value (true iff there are any names).
*/
record NameHasAnyToken(
NameSet.Predicate predicate,
Function<Boolean, ScopeToken> function) implements Token {
ScopeToken getScopeToken(boolean hasAny) {
return function().apply(hasAny);
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package compiler.lib.template_framework;
import java.util.function.Function;
/**
* Represents the sampling of {@link Name}s, and the function that is called given
* the sampled name, as well as the (optional) hashtag replacement keys for the
* name and type of the sampled name, which are then available in the inner scope
* created by the provided function.
*/
record NameSampleToken<N>(
NameSet.Predicate predicate,
String name,
String type,
Function<N, ScopeToken> function) implements Token {
ScopeToken getScopeToken(Name n) {
return function().apply((N)n);
}
}

View File

@ -43,6 +43,7 @@ class NameSet {
interface Predicate {
boolean check(Name type);
String toString(); // used when sampling fails.
}
NameSet(NameSet parent) {

View File

@ -0,0 +1,41 @@
/*
* 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.Function;
import java.util.List;
/**
* Represents the {@code toList} on a filtered name set, including the collection of the
* names and the creation of the inner scope with the function.
*/
record NamesToListToken<N>(
NameSet.Predicate predicate,
Function<List<N>, ScopeToken> function) implements Token {
ScopeToken getScopeToken(List<Name> names) {
List<N> castNames = names.stream().map(n -> (N)n).toList();
return function().apply(castNames);
}
}

View File

@ -76,7 +76,7 @@ final class Renderer {
* <p>
* 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
* {@link Template#scope}. 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
@ -84,8 +84,8 @@ final class Renderer {
* be separated. This could lead to unexpected behavior or even bugs.
*
* <p>
* 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}.
* Instead, the user must create a {@link TemplateToken} from the inner {@link Template}, and
* use that {@link TemplateToken} in the {@link Template#scope} 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}.
*
@ -113,7 +113,7 @@ final class Renderer {
static Renderer getCurrent() {
if (renderer == null) {
throw new RendererException("A Template method such as '$', 'let', 'sample', 'count' etc. was called outside a template rendering.");
throw new RendererException("A Template method such as '$', 'fuel', etc. was called outside a template rendering call.");
}
return renderer;
}
@ -171,26 +171,6 @@ final class Renderer {
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<Name> 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}.
@ -243,12 +223,16 @@ final class Renderer {
}
private void renderTemplateToken(TemplateToken templateToken) {
// We need a TemplateFrame in all cases, this ensures that the outermost scope of the template
// is not transparent for hashtags and setFuelCost, and also that the id of the template is
// unique.
TemplateFrame templateFrame = TemplateFrame.make(currentTemplateFrame, nextTemplateFrameId++);
currentTemplateFrame = templateFrame;
templateToken.visitArguments((name, value) -> addHashtagReplacement(name, format(value)));
TemplateBody body = templateToken.instantiate();
renderTokenList(body.tokens());
// If the ScopeToken is transparent to Names, then the Template is transparent to names.
renderScopeToken(templateToken.instantiate());
if (currentTemplateFrame != templateFrame) {
throw new RuntimeException("Internal error: TemplateFrame mismatch!");
@ -256,29 +240,76 @@ final class Renderer {
currentTemplateFrame = currentTemplateFrame.parent;
}
private void renderScopeToken(ScopeToken st) {
renderScopeToken(st, () -> {});
}
private void renderScopeToken(ScopeToken st, Runnable preamble) {
if (!(st instanceof ScopeTokenImpl(List<Token> tokens,
boolean isTransparentForNames,
boolean isTransparentForHashtags,
boolean isTransparentForSetFuelCost))) {
throw new RuntimeException("Internal error: could not unpack ScopeTokenImpl.");
}
// We need the CodeFrame for local names.
CodeFrame outerCodeFrame = currentCodeFrame;
if (!isTransparentForNames) {
currentCodeFrame = CodeFrame.make(currentCodeFrame, false);
}
// We need to be able to define local hashtag replacements, but still
// see the outer ones. We also need to have the same id for dollar
// replacement as the outer frame. And we need to be able to allow
// local setFuelCost definitions.
TemplateFrame innerTemplateFrame = null;
if (!isTransparentForHashtags || !isTransparentForSetFuelCost) {
innerTemplateFrame = TemplateFrame.makeInnerScope(currentTemplateFrame,
isTransparentForHashtags,
isTransparentForSetFuelCost);
currentTemplateFrame = innerTemplateFrame;
}
// Allow definition of hashtags and variables to be placed in the nested frames.
preamble.run();
// Now render the nested code.
renderTokenList(tokens);
if (!isTransparentForHashtags || !isTransparentForSetFuelCost) {
if (currentTemplateFrame != innerTemplateFrame) {
throw new RuntimeException("Internal error: TemplateFrame mismatch!");
}
currentTemplateFrame = currentTemplateFrame.parent;
}
// Tear down CodeFrame nesting. If no nesting happened, the code is already
// in the currentCodeFrame.
if (!isTransparentForNames) {
outerCodeFrame.addCode(currentCodeFrame.getCode());
currentCodeFrame = outerCodeFrame;
}
}
private void renderToken(Token token) {
switch (token) {
case StringToken(String s) -> {
renderStringWithDollarAndHashtagReplacements(s);
}
case NothingToken() -> {
// Nothing.
}
case HookAnchorToken(Hook hook, List<Token> tokens) -> {
case HookAnchorToken(Hook hook, ScopeTokenImpl innerScope) -> {
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);
// We need a CodeFrame to which the hook can insert code. If the nested names
// are to be local, the CodeFrame must be non-transparent for names.
CodeFrame hookCodeFrame = CodeFrame.make(outerCodeFrame, innerScope.isTransparentForNames());
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);
// We need a CodeFrame where the tokens can be rendered for code that is
// generated inside the anchor scope, but not inserted directly to the hook.
CodeFrame innerCodeFrame = CodeFrame.make(hookCodeFrame, innerScope.isTransparentForNames());
currentCodeFrame = innerCodeFrame;
renderTokenList(tokens);
renderScopeToken(innerScope);
// Close the hookCodeFrame and innerCodeFrame. hookCodeFrame code comes before the
// innerCodeFrame code from the tokens.
@ -286,20 +317,20 @@ final class Renderer {
currentCodeFrame.addCode(hookCodeFrame.getCode());
currentCodeFrame.addCode(innerCodeFrame.getCode());
}
case HookInsertToken(Hook hook, TemplateToken templateToken) -> {
case HookInsertToken(Hook hook, ScopeTokenImpl scopeToken) -> {
// 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
// by the scopeToken can be collected, and hook insertions from it can still
// be made to the hookCodeFrame before the code from the scopeToken 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);
// the hookCodeFrame, and are not limited to the CodeFrame for the scopeToken.
currentCodeFrame = CodeFrame.make(hookCodeFrame, true);
renderTemplateToken(templateToken);
renderScopeToken(scopeToken);
hookCodeFrame.addCode(currentCodeFrame.getCode());
@ -307,18 +338,68 @@ final class Renderer {
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);
}
case ScopeToken scopeToken -> {
renderScopeToken(scopeToken);
}
case NameSampleToken nameScopeToken -> {
Name name = currentCodeFrame.sampleName(nameScopeToken.predicate());
if (name == null) {
throw new RendererException("No Name found for " + nameScopeToken.predicate().toString());
}
ScopeToken scopeToken = nameScopeToken.getScopeToken(name);
renderScopeToken(scopeToken, () -> {
if (nameScopeToken.name() != null) {
addHashtagReplacement(nameScopeToken.name(), name.name());
}
if (nameScopeToken.type() != null) {
addHashtagReplacement(nameScopeToken.type(), name.type());
}
});
}
case NameForEachToken nameForEachToken -> {
List<Name> list = currentCodeFrame.listNames(nameForEachToken.predicate());
list.stream().forEach(name -> {
ScopeToken scopeToken = nameForEachToken.getScopeToken(name);
renderScopeToken(scopeToken, () -> {
if (nameForEachToken.name() != null) {
addHashtagReplacement(nameForEachToken.name(), name.name());
}
if (nameForEachToken.type() != null) {
addHashtagReplacement(nameForEachToken.type(), name.type());
}
});
});
}
case NamesToListToken nameToListToken -> {
List<Name> list = currentCodeFrame.listNames(nameToListToken.predicate());
renderScopeToken(nameToListToken.getScopeToken(list));
}
case NameCountToken nameCountToken -> {
int count = currentCodeFrame.countNames(nameCountToken.predicate());
renderScopeToken(nameCountToken.getScopeToken(count));
}
case NameHasAnyToken nameHasAnyToken -> {
boolean hasAny = currentCodeFrame.hasAnyNames(nameHasAnyToken.predicate());
renderScopeToken(nameHasAnyToken.getScopeToken(hasAny));
}
case SetFuelCostToken(float fuelCost) -> {
currentTemplateFrame.setFuelCost(fuelCost);
}
case LetToken letToken -> {
ScopeToken scopeToken = letToken.getScopeToken();
renderScopeToken(scopeToken, () -> {
addHashtagReplacement(letToken.key(), letToken.value());
});
}
case HookIsAnchoredToken hookIsAnchoredToken -> {
boolean isAnchored = currentCodeFrame.codeFrameForHook(hookIsAnchoredToken.hook()) != null;
renderScopeToken(hookIsAnchoredToken.getScopeToken(isAnchored));
}
}
}
@ -423,10 +504,6 @@ final class Renderer {
));
}
boolean isAnchored(Hook hook) {
return currentCodeFrame.codeFrameForHook(hook) != null;
}
private CodeFrame codeFrameForHook(Hook hook) {
CodeFrame codeFrame = currentCodeFrame.codeFrameForHook(hook);
if (codeFrame == null) {

View File

@ -23,12 +23,8 @@
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.
* A {@link ScopeToken} represents a scope in a {@link Template}, which can be
* created with {@link Template#scope}, {@link Template#transparentScope}, and other related methods.
*/
public record TemplateBody(List<Token> tokens) {}
public sealed interface ScopeToken extends Token permits ScopeTokenImpl {}

View File

@ -0,0 +1,42 @@
/*
* 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;
/**
* Represents a scope with its tokens. Boolean flags indicate if names,
* hashtag replacements and {@link Template#setFuelCost} are local, or escape to
* outer scopes.
*
* <p>
* Note: We want the {@link ScopeToken} to be public, but the internals of the
* record should be private. One way to solve this is with a public interface
* that exposes nothing but its name, and a private implementation via a
* record that allows easy destructuring with pattern matching.
*/
record ScopeTokenImpl(List<Token> tokens,
boolean isTransparentForNames,
boolean isTransparentForHashtags,
boolean isTransparentForSetFuelCost) implements ScopeToken, Token {}

View File

@ -23,4 +23,7 @@
package compiler.lib.template_framework;
record NothingToken() implements Token {}
/**
* Represents the setting of the fuel cost in the current scope.
*/
record SetFuelCostToken(float fuelCost) implements Token {}

View File

@ -24,6 +24,7 @@
package compiler.lib.template_framework;
import java.util.List;
import java.util.function.Function;
/**
* {@link StructuralName}s represent things like method and class names, and can be added to the local
@ -89,16 +90,34 @@ public record StructuralName(String name, StructuralName.Type type, int weight)
this(null, null);
}
// Wrap the FilteredSet as a Predicate.
private record StructuralNamePredicate(FilteredSet fs) implements NameSet.Predicate {
public boolean check(Name type) {
return fs.check(type);
}
public String toString() {
return fs.toString();
}
}
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;
};
return new StructuralNamePredicate(this);
}
boolean check(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;
}
public String toString() {
String msg1 = (subtype == null) ? "" : " subtypeOf(" + subtype + ")";
String msg2 = (supertype == null) ? "" : " supertypeOf(" + supertype + ")";
return "StructuralName.FilteredSet(" + msg1 + msg2 + ")";
}
/**
@ -146,55 +165,125 @@ public record StructuralName(String name, StructuralName.Type type, int weight)
/**
* Samples a random {@link StructuralName} from the filtered set, according to the weights
* of the contained {@link StructuralName}s.
* of the contained {@link StructuralName}s, making the sampled {@link StructuralName}
* available to an inner scope.
*
* @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.
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the sampled {@link StructuralName}.
* @return a token that represents the sampling and inner scope.
* @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());
public Token sample(Function<StructuralName, ScopeToken> function) {
return new NameSampleToken<>(predicate(), null, null, function);
}
/**
* Checks if there are any {@link StructuralName}s in the filtered set.
* Samples a random {@link StructuralName} from the filtered set, according to the weights
* of the contained {@link StructuralName}s, and makes a hashtag replacement for both
* the name and type of the {@link StructuralName}, in the current scope.
*
* @return Returns {@code true} iff there is at least one {@link StructuralName} in the filtered set.
* @param name the key of the hashtag replacement for the {@link StructuralName} name.
* @param type the key of the hashtag replacement for the {@link StructuralName} type.
* @return a token that represents the sampling and hashtag replacement definition.
* @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());
public Token sampleAndLetAs(String name, String type) {
return new NameSampleToken<StructuralName>(predicate(), name, type, n -> Template.transparentScope());
}
/**
* Collects all {@link StructuralName}s in the filtered set.
* Samples a random {@link StructuralName} from the filtered set, according to the weights
* of the contained {@link StructuralName}s, and makes a hashtag replacement for the
* name of the {@link StructuralName}, in the current scope.
*
* @param name the key of the hashtag replacement for the {@link StructuralName} name.
* @return a token that represents the sampling and hashtag replacement definition.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token sampleAndLetAs(String name) {
return new NameSampleToken<StructuralName>(predicate(), name, null, n -> Template.transparentScope());
}
/**
* Counts the number of {@link StructuralName}s in the filtered set, making the count
* available to an inner scope.
*
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the count.
* @return a token that represents the counting and inner scope.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token count(Function<Integer, ScopeToken> function) {
return new NameCountToken(predicate(), function);
}
/**
* Checks if there are any {@link StructuralName}s in the filtered set, making the resulting boolean
* available to an inner scope.
*
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the boolean indicating iff there are any {@link StructuralName}s in the filtered set.
* @return a token that represents the checking and inner scope.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token hasAny(Function<Boolean, ScopeToken> function) {
return new NameHasAnyToken(predicate(), function);
}
/**
* Collects all {@link StructuralName}s in the filtered set, making the collected list
* available to an inner scope.
*
* @param function The {@link Function} that creates the inner {@link ScopeToken} given
* the list of {@link StructuralName}.
* @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<StructuralName> toList() {
List<Name> list = Renderer.getCurrent().listNames(predicate());
return list.stream().map(n -> (StructuralName)n).toList();
public Token toList(Function<List<StructuralName>, ScopeToken> function) {
return new NamesToListToken<>(predicate(), function);
}
/**
* Calls the provided {@code function} for each {@link StructuralName}s in the filtered set,
* making each of these {@link StructuralName}s available to a separate inner scope.
*
* @param function The {@link Function} that is called to create the inner {@link ScopeToken}s
* for each of the {@link StructuralName}s in the filtereds set.
* @return The token representing the for-each execution and the respective inner scopes.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token forEach(Function<StructuralName, ScopeToken> function) {
return new NameForEachToken<>(predicate(), null, null, function);
}
/**
* Calls the provided {@code function} for each {@link StructuralName}s in the filtered set,
* making each of these {@link StructuralName}s available to a separate inner scope, and additionally
* setting hashtag replacements for the {@code name} and {@code type} of the respective
* {@link StructuralName}s.
*
* <p>
* Note, to avoid duplication of the {@code name} and {@code type}
* hashtag replacements, the scope created by the provided {@code function} should be
* non-transparent to hashtag replacements, for example {@link Template#scope} or
* {@link Template#hashtagScope}.
*
* @param name the key of the hashtag replacement for each individual {@link StructuralName} name.
* @param type the key of the hashtag replacement for each individual {@link StructuralName} type.
* @param function The {@link Function} that is called to create the inner {@link ScopeToken}s
* for each of the {@link StructuralName}s in the filtereds set.
* @return The token representing the for-each execution and the respective inner scopes.
* @throws UnsupportedOperationException If the type was not constrained with either of
* {@link #subtypeOf}, {@link #supertypeOf} or {@link #exactOf}.
*/
public Token forEach(String name, String type, Function<StructuralName, ScopeToken> function) {
return new NameForEachToken<>(predicate(), name, type, function);
}
}
}

View File

@ -65,7 +65,7 @@ import compiler.lib.ir_framework.TestFramework;
*
* <p>
* {@snippet lang=java :
* var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> body(
* var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> scope(
* let("con1", generator.next()),
* let("con2", generator.next()),
* """
@ -86,13 +86,13 @@ import compiler.lib.ir_framework.TestFramework;
* }
*
* <p>
* To get an executable test, we define a {@link Template} that produces a class body with a main method. The Template
* To get an executable test, we define a {@link Template} that produces a class scope 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.
*
* <p>
* {@snippet lang=java :
* var classTemplate = Template.make("types", (List<Type> types) -> body(
* var classTemplate = Template.make("types", (List<Type> types) -> scope(
* let("classpath", comp.getEscapedClassPathOfCompiledClasses()),
* """
* package p.xyz;
@ -148,12 +148,12 @@ import compiler.lib.ir_framework.TestFramework;
* {@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.
* It is currently only allowed to use up to three arguments.
*
* <p>
* 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}.
* (e.g. {@link Template.ZeroArgs#asToken()}), and use the {@link Token} inside another {@link Template#scope}.
*
* <p>
* Ideally, we would have used <a href="https://openjdk.org/jeps/430">string templates</a> to inject these Template
@ -161,6 +161,11 @@ import compiler.lib.ir_framework.TestFramework;
* <strong>hashtag replacements</strong> 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}.
* We have decided to keep hashtag replacements constrained to the scope of one Template. They
* do not escape to outer or inner Template uses. If one needs to pass values to inner Templates,
* this can be done with Template arguments. Keeping hashtag replacements local to Templates
* has the benefit that there is no conflict in recursive templates, where outer and inner Templates
* define the same hashtag replacement.
*
* <p>
* When using nested Templates, there can be collisions with identifiers (e.g. variable names and method names).
@ -176,25 +181,6 @@ import compiler.lib.ir_framework.TestFramework;
* {@code #{name}}.
*
* <p>
* 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.
*
* <p>
* A {@link TemplateBinding} allows the recursive use of Templates. With the indirection of such a binding,
* a Template can reference itself.
*
* <p>
* 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).
*
* <p>
* 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
@ -211,61 +197,70 @@ import compiler.lib.ir_framework.TestFramework;
* are not concerned about mutability.
*
* <p>
* 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.
* Code generation can involve keeping track of scopes in the code (e.g. liveness and availability of
* {@link DataName}s) and of the hashtag replacements in the templates. The {@link ScopeToken} serves
* this purpose, and allows the definition of transparent scopes (e.g. {@link #transparentScope}) and
* non-transparent scopes (e.g. {@link #scope}).
*
* <table border="1">
* <caption>Scopes and (non-)transparency</caption>
* <tr>
* <th> </th><th> hashtag </th><th> {@link DataName} and {@link StructuralName} </th><th> {@link #setFuelCost} </th>
* </tr>
* <tr>
* <th> {@link #scope} </th><th> non-transparent </th><th> non-transparent </th><th> non-transparent </th>
* </tr>
* <tr>
* <th> {@link #hashtagScope} </th><th> non-transparent </th><th> transparent </th><th> transparent </th>
* </tr>
* <tr>
* <th> {@link #nameScope} </th><th> transparent </th><th> non-transparent </th><th> transparent </th>
* </tr>
* <tr>
* <th> {@link #setFuelCostScope} </th><th> transparent </th><th> transparent </th><th> non-transparent </th>
* </tr>
* <tr>
* <th> {@link #transparentScope} </th><th> transparent </th><th> transparent </th><th> transparent </th>
* </tr>
* </table>
*
* <p>
* Let us look at the following example to better understand the execution order.
* In some cases, we may be deeper nested in templates and scopes, and would like to reach "back" or
* to outer scopes. This is possible with {@link Hook#anchor}ing in some outer scope, and later
* {@link Hook#insert}ing from an inner scope to the scope of the anchoring. 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.
*
* <p>
* {@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.
* ));
* }
* A {@link TemplateBinding} allows the recursive use of Templates. With the indirection of such a binding,
* a Template can reference itself.
*
* <p>
* 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 #scope(Object...)}. Recursive templates are
* supposed to terminate once the {@link #fuel} is depleted (i.e. reaches zero).
*
* <p>
* A note from the implementor to the user: We have decided to implement the Template Framework using
* a functional (lambdas) and data-oriented (tokens) model. The consequence is that there are three
* orders in template rendering: (1) the execution order in lambdas, where we usually assemble the
* tokens and pass them to some scope ({@link ScopeToken}) as arguments. (2) the token evaluation
* order, which occurs in the order of how tokens are listed in a scope. By design, the token order
* is the same order as execution in lambdas. To keep the lambda and token order in sync, most of the
* queries about the state of code generation, such as {@link DataName}s and {@link Hook}s cannot
* return the values immediately, but have to be expressed as tokens. If we had a mix of tokens and
* immediate queries, then the immediate queries would "float" by the tokens, because the immediate
* queries are executed during the lambda execution, but the tokens are only executed later. Having
* to express everything as tokens can be a little more cumbersome (e.g. sample requires a lambda
* that captures the {@link DataName}, and sample does not return the {@link DataName} directly).
* But this ensures that reasoning about execution order is relatively straight forward, namely in
* the order of the specified tokens. (3) the final code order is the same as the lambda and token
* order, except when using {@link Hook#insert}, which places the code at the innermost {@link Hook#anchor}.
*
* <p>
* 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
@ -281,10 +276,10 @@ public sealed interface Template permits Template.ZeroArgs,
/**
* A {@link Template} with no arguments.
*
* @param function The {@link Supplier} that creates the {@link TemplateBody}.
* @param function The {@link Supplier} that creates the {@link ScopeToken}.
*/
record ZeroArgs(Supplier<TemplateBody> function) implements Template {
TemplateBody instantiate() {
record ZeroArgs(Supplier<ScopeToken> function) implements Template {
ScopeToken instantiate() {
return function.get();
}
@ -324,10 +319,10 @@ public sealed interface Template permits Template.ZeroArgs,
*
* @param arg1Name The name of the (first) argument, used for hashtag replacements in the {@link Template}.
* @param <T1> The type of the (first) argument.
* @param function The {@link Function} that creates the {@link TemplateBody} given the template argument.
* @param function The {@link Function} that creates the {@link ScopeToken} given the template argument.
*/
record OneArg<T1>(String arg1Name, Function<T1, TemplateBody> function) implements Template {
TemplateBody instantiate(T1 arg1) {
record OneArg<T1>(String arg1Name, Function<T1, ScopeToken> function) implements Template {
ScopeToken instantiate(T1 arg1) {
return function.apply(arg1);
}
@ -372,10 +367,10 @@ public sealed interface Template permits Template.ZeroArgs,
* @param arg2Name The name of the second argument, used for hashtag replacements in the {@link Template}.
* @param <T1> The type of the first argument.
* @param <T2> The type of the second argument.
* @param function The {@link BiFunction} that creates the {@link TemplateBody} given the template arguments.
* @param function The {@link BiFunction} that creates the {@link ScopeToken} given the template arguments.
*/
record TwoArgs<T1, T2>(String arg1Name, String arg2Name, BiFunction<T1, T2, TemplateBody> function) implements Template {
TemplateBody instantiate(T1 arg1, T2 arg2) {
record TwoArgs<T1, T2>(String arg1Name, String arg2Name, BiFunction<T1, T2, ScopeToken> function) implements Template {
ScopeToken instantiate(T1 arg1, T2 arg2) {
return function.apply(arg1, arg2);
}
@ -447,10 +442,10 @@ public sealed interface Template permits Template.ZeroArgs,
* @param <T1> The type of the first argument.
* @param <T2> The type of the second argument.
* @param <T3> The type of the third argument.
* @param function The function with three arguments that creates the {@link TemplateBody} given the template arguments.
* @param function The function with three arguments that creates the {@link ScopeToken} given the template arguments.
*/
record ThreeArgs<T1, T2, T3>(String arg1Name, String arg2Name, String arg3Name, TriFunction<T1, T2, T3, TemplateBody> function) implements Template {
TemplateBody instantiate(T1 arg1, T2 arg2, T3 arg3) {
record ThreeArgs<T1, T2, T3>(String arg1Name, String arg2Name, String arg3Name, TriFunction<T1, T2, T3, ScopeToken> function) implements Template {
ScopeToken instantiate(T1 arg1, T2 arg2, T3 arg3) {
return function.apply(arg1, arg2, arg3);
}
@ -496,28 +491,28 @@ public sealed interface Template permits Template.ZeroArgs,
/**
* Creates a {@link Template} with no arguments.
* See {@link #body} for more details about how to construct a Template with {@link Token}s.
* See {@link #scope} for more details about how to construct a Template with {@link Token}s.
*
* <p>
* Example:
* {@snippet lang=java :
* var template = Template.make(() -> body(
* var template = Template.make(() -> scope(
* """
* Multi-line string or other tokens.
* """
* ));
* }
*
* @param body The {@link TemplateBody} created by {@link Template#body}.
* @param scope The {@link ScopeToken} created by {@link Template#scope}.
* @return A {@link Template} with zero arguments.
*/
static Template.ZeroArgs make(Supplier<TemplateBody> body) {
return new Template.ZeroArgs(body);
static Template.ZeroArgs make(Supplier<ScopeToken> scope) {
return new Template.ZeroArgs(scope);
}
/**
* Creates a {@link Template} with one argument.
* See {@link #body} for more details about how to construct a Template with {@link Token}s.
* See {@link #scope} 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.
*
* <p>
@ -525,7 +520,7 @@ public sealed interface Template permits Template.ZeroArgs,
* 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(
* var template = Template.make("a", (Integer a) -> scope(
* """
* Multi-line string or other tokens.
* We can use the hashtag replacement #a to directly insert the String value of a.
@ -534,18 +529,18 @@ public sealed interface Template permits Template.ZeroArgs,
* ));
* }
*
* @param body The {@link TemplateBody} created by {@link Template#body}.
* @param scope The {@link ScopeToken} created by {@link Template#scope}.
* @param <T1> Type of the (first) argument.
* @param arg1Name The name of the (first) argument for hashtag replacement.
* @return A {@link Template} with one argument.
*/
static <T1> Template.OneArg<T1> make(String arg1Name, Function<T1, TemplateBody> body) {
return new Template.OneArg<>(arg1Name, body);
static <T1> Template.OneArg<T1> make(String arg1Name, Function<T1, ScopeToken> scope) {
return new Template.OneArg<>(arg1Name, scope);
}
/**
* Creates a {@link Template} with two arguments.
* See {@link #body} for more details about how to construct a Template with {@link Token}s.
* See {@link #scope} 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.
*
* <p>
@ -553,7 +548,7 @@ public sealed interface Template permits Template.ZeroArgs,
* 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(
* var template = Template.make("a", "b", (Integer a, String b) -> scope(
* """
* 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.
@ -562,23 +557,23 @@ public sealed interface Template permits Template.ZeroArgs,
* ));
* }
*
* @param body The {@link TemplateBody} created by {@link Template#body}.
* @param scope The {@link ScopeToken} created by {@link Template#scope}.
* @param <T1> Type of the first argument.
* @param arg1Name The name of the first argument for hashtag replacement.
* @param <T2> Type of the second argument.
* @param arg2Name The name of the second argument for hashtag replacement.
* @return A {@link Template} with two arguments.
*/
static <T1, T2> Template.TwoArgs<T1, T2> make(String arg1Name, String arg2Name, BiFunction<T1, T2, TemplateBody> body) {
return new Template.TwoArgs<>(arg1Name, arg2Name, body);
static <T1, T2> Template.TwoArgs<T1, T2> make(String arg1Name, String arg2Name, BiFunction<T1, T2, ScopeToken> scope) {
return new Template.TwoArgs<>(arg1Name, arg2Name, scope);
}
/**
* Creates a {@link Template} with three arguments.
* See {@link #body} for more details about how to construct a Template with {@link Token}s.
* See {@link #scope} 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 scope The {@link ScopeToken} created by {@link Template#scope}.
* @param <T1> Type of the first argument.
* @param arg1Name The name of the first argument for hashtag replacement.
* @param <T2> Type of the second argument.
@ -587,18 +582,35 @@ public sealed interface Template permits Template.ZeroArgs,
* @param arg3Name The name of the third argument for hashtag replacement.
* @return A {@link Template} with three arguments.
*/
static <T1, T2, T3> Template.ThreeArgs<T1, T2, T3> make(String arg1Name, String arg2Name, String arg3Name, Template.TriFunction<T1, T2, T3, TemplateBody> body) {
return new Template.ThreeArgs<>(arg1Name, arg2Name, arg3Name, body);
static <T1, T2, T3> Template.ThreeArgs<T1, T2, T3> make(String arg1Name, String arg2Name, String arg3Name, Template.TriFunction<T1, T2, T3, ScopeToken> scope) {
return new Template.ThreeArgs<>(arg1Name, arg2Name, arg3Name, scope);
}
/**
* 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.
* Creates a {@link ScopeToken} that represents a scope that is completely
* non-transparent, <strong>not</strong> allowing anything to escape. This
* means that no {@link DataName}, {@link StructuralName}s, hashtag-replacement
* or {@link #setFuelCost} defined inside the scope is available outside. All
* these usages are only local to the defining scope here.
*
* <p>
* The scope is formed 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.
*
* <p>
* If you require a scope that is either fully transparent (i.e. everything escapes)
* or only restricts a specific kind to not escape, consider using one of the other
* provided scopes: {@link #transparentScope}, {@link #nameScope}, {@link #hashtagScope},
* or {@link #setFuelCostScope}. A "scope-transparency-matrix" can also be found in
* the interface comment for {@link Template}.
*
* <p>
* The most common use of {@link #scope} is in the construction of templates:
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> body(
* var template = Template.make(() -> scope(
* """
* Multi-line string
* """,
@ -608,14 +620,200 @@ public sealed interface Template permits Template.ZeroArgs,
* ));
* }
*
* <p>
* Note that regardless of the chosen scope for {@code Template.make},
* hashtag-replacements and {@link #setFuelCost} are always implicitly
* non-transparent (i.e. non-escaping). For example, {@link #let} will
* not escape the template scope even when using {@link #transparentScope}.
* As a default, it is recommended to use {@link #scope} for
* {@code Template.make} since in most cases template scopes align with
* code scopes that are non-transparent for fields, variables, etc. In
* rare cases, where the scope of the template needs to be transparent
* (e.g. because we need to insert a variable or field into an outer scope),
* it is recommended to use {@link #transparentScope}. This allows to make
* {@link DataName}s and {@link StructuralName}s available outside this
* template crossing the template boundary.
*
* <p>
* We can also use nested scopes inside of templates:
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* // CODE1: some code in the outer scope
* scope(
* // CODE2: some code in the inner scope. Names, hashtags and setFuelCost
* // do not escape the inner scope.
* ),
* // CODE3: more code in the outer scope, names and hashtags from CODE2 are
* // not available anymore because of the non-transparent "scope".
* transparentScope(
* // CODE4: some code in the inner "transparentScope". Names, hashtags and setFuelCost
* // escape the "transparentScope" and are still available after the "transparentScope"
* // closes.
* )
* // CODE5: we still have access to names and hashtags from CODE4.
* ));
* }
*
* @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.
* @return The {@link ScopeToken} 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(TokenParser.parse(tokens));
static ScopeToken scope(Object... tokens) {
return new ScopeTokenImpl(TokenParser.parse(tokens), false, false, false);
}
/**
* Creates a {@link ScopeToken} that represents a completely transparent scope.
* This means that {@link DataName}s, {@link StructuralName}s,
* hashtag-replacements and {@link #setFuelCost} declared inside the scope will be available
* in the outer scope.
* The scope is formed 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.
*
* <p>
* If you require a scope that is non-transparent (i.e. nothing escapes) or only restricts
* a specific kind to not escape, consider using one of the other provided scopes:
* {@link #scope}, {@link #nameScope}, {@link #hashtagScope}, or {@link #setFuelCostScope}.
* A "scope-transparency-matrix" can also be found in the interface comment for {@link Template}.
*
* @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 ScopeToken} which captures the list of validated {@link Token}s.
* @throws IllegalArgumentException if the list of tokens contains an unexpected object.
*/
static ScopeToken transparentScope(Object... tokens) {
return new ScopeTokenImpl(TokenParser.parse(tokens), true, true, true);
}
/**
* Creates a {@link ScopeToken} that represents a scope that is non-transparent for
* {@link DataName}s and {@link StructuralName}s (i.e. cannot escape), but
* transparent for hashtag-replacements and {@link #setFuelCost} (i.e. available
* in outer scope).
*
* <p>
* The scope is formed 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.
*
* <p>
* If you require a scope that is transparent or uses a different restriction, consider
* using one of the other provided scopes: {@link #scope}, {@link #transparentScope},
* {@link #hashtagScope}, or {@link #setFuelCostScope}. A "scope-transparency-matrix" can
* also be found in the interface comment for {@link Template}.
*
* @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 ScopeToken} which captures the list of validated {@link Token}s.
* @throws IllegalArgumentException if the list of tokens contains an unexpected object.
*/
static ScopeToken nameScope(Object... tokens) {
return new ScopeTokenImpl(TokenParser.parse(tokens), false, true, true);
}
/**
* Creates a {@link ScopeToken} that represents a scope that is non-transparent for
* hashtag-replacements (i.e. cannot escape), but transparent for {@link DataName}s
* and {@link StructuralName}s and {@link #setFuelCost} (i.e. available in outer scope).
*
* <p>
* The scope is formed 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.
*
* <p>
* If you require a scope that is transparent or uses a different restriction, consider
* using one of the other provided scopes: {@link #scope}, {@link #transparentScope},
* {@link #nameScope}, or {@link #setFuelCostScope}. A "scope-transparency-matrix" can
* also be found in the interface comment for {@link Template}.
*
* <p>
* Keeping hashtag-replacements local but letting {@link DataName}s escape can be
* useful in cases like the following, where we may want to reuse the hashtag
* multiple times:
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* List.of("a", "b", "c").stream().map(name -> hashtagScope(
* let("name", name), // assumes values: a, b, c
* addDataName(name, PrimitiveType.INTS, MUTABLE), // escapes
* """
* int #name = 42;
* """
* ))
* // We still have access to the three DataNames.
* ));
* }
*
* @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 ScopeToken} which captures the list of validated {@link Token}s.
* @throws IllegalArgumentException if the list of tokens contains an unexpected object.
*/
static ScopeToken hashtagScope(Object... tokens) {
return new ScopeTokenImpl(TokenParser.parse(tokens), true, false, true);
}
/**
* Creates a {@link ScopeToken} that represents a scope that is non-transparent for
* {@link #setFuelCost} (i.e. cannot escape), but transparent for hashtag-replacements,
* {@link DataName}s and {@link StructuralName}s (i.e. available in outer scope).
* The scope is formed 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.
*
* <p>
* If you require a scope that is transparent or uses a different restriction, consider
* using one of the other provided scopes: {@link #scope}, {@link #transparentScope},
* {@link #hashtagScope}, or {@link #nameScope}. A "scope-transparency-matrix" can
* also be found in the interface comment for {@link Template}.
*
* <p>
* In some cases, it can be helpful to have different {@link #setFuelCost} within
* a single template, depending on the code nesting depth. Example:
*
* <p>
* {@snippet lang=java :
* var template = Template.make(() -> scope(
* setFuelCost(1),
* // CODE1: some shallow code, allowing recursive template uses here
* // to use more fuel.
* """
* for (int i = 0; i < 1000; i++) {
* """,
* setFuelCostScope(
* setFuelCost(100)
* // CODE2: with the for-loop, we already have a deeper nesting
* // depth, and recursive template uses should not get
* // as much fuel as in CODE1.
* ),
* """
* }
* """
* // CODE3: we are back in the outer scope of CODE1, and can use
* // more fuel again in nested template uses. setFuelCost
* // is automatically restored to what was set before the
* // inner scope.
* ));
* }
*
* @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 ScopeToken} which captures the list of validated {@link Token}s.
* @throws IllegalArgumentException if the list of tokens contains an unexpected object.
*/
static ScopeToken setFuelCostScope(Object... tokens) {
return new ScopeTokenImpl(TokenParser.parse(tokens), true, true, false);
}
/**
@ -628,7 +826,7 @@ public sealed interface Template permits Template.ZeroArgs,
* 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(
* var template = Template.make(() -> scope(
* """
* int $var = 42;
* """,
@ -640,6 +838,9 @@ public sealed interface Template permits Template.ZeroArgs,
* @return The dollar replacement for the {@code 'name'}.
*/
static String $(String name) {
// Note, since the dollar replacements do not change within a template
// and the retrieval has no side effects, we can return the value immediately,
// and do not need a token.
return Renderer.getCurrent().$(name);
}
@ -648,7 +849,7 @@ public sealed interface Template permits Template.ZeroArgs,
*
* <p>
* {@snippet lang=java :
* var template = Template.make("a", (Integer a) -> body(
* var template = Template.make("a", (Integer a) -> scope(
* let("b", a * 5),
* """
* System.out.println("Use a and b with hashtag replacement: #a and #b");
@ -656,41 +857,50 @@ public sealed interface Template permits Template.ZeroArgs,
* ));
* }
*
* <p>
* Note that a {@code let} definition makes the hashtag replacement available
* for anything that follows it, until the the end of the next outer scope
* that is non-transparent for hashtag replacements. Additionally, hashtag
* replacements are limited to the template they were defined in.
* If you want to pass values from an outer to an inner template, this cannot
* be done with hashtags directly. Instead, one has to pass the values via
* template arguments.
*
* @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}.
* @return A token that represents the hashtag replacement definition.
*/
static Token let(String key, Object value) {
Renderer.getCurrent().addHashtagReplacement(key, value);
return new NothingToken();
return new LetToken(key, value, v -> transparentScope());
}
/**
* Define a hashtag replacement for {@code "#key"}, with a specific value, which is also captured
* by the provided {@code function} with type {@code <T>}.
* by the provided {@code function} with type {@code <T>}. While the argument of the lambda that
* captures the value is naturally bounded to the scope of the lambda, the hashtag replacement
* may be bound to the scope or escape it, depending on the choice of scope, see {@link #scope}
* and {@link #transparentScope}.
*
* <p>
* {@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"
* )));
* var template = Template.make("a", (Integer a) -> scope(
* let("b", a * 2, (Integer b) -> scope(
* """
* 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 <T> 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}.
* @return A {@link Token} representing the hashtag replacement definition and inner scope.
*/
static <T> TemplateBody let(String key, T value, Function<T, TemplateBody> function) {
Renderer.getCurrent().addHashtagReplacement(key, value);
return function.apply(value);
static <T> Token let(String key, T value, Function<T, ScopeToken> function) {
return new LetToken(key, value, function);
}
/**
@ -702,7 +912,7 @@ public sealed interface Template permits Template.ZeroArgs,
/**
* 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...)}.
* with {@link #setFuelCost(float)} inside {@link #scope(Object...)}.
*/
float DEFAULT_FUEL_COST = 10.0f;
@ -721,7 +931,7 @@ public sealed interface Template permits Template.ZeroArgs,
* <p>
* {@snippet lang=java :
* var binding = new TemplateBinding<Template.OneArg<Integer>>();
* var template = Template.make("depth", (Integer depth) -> body(
* var template = Template.make("depth", (Integer depth) -> scope(
* setFuelCost(5.0f),
* let("fuel", fuel()),
* """
@ -737,6 +947,9 @@ public sealed interface Template permits Template.ZeroArgs,
* @return The amount of fuel left for nested Template use.
*/
static float fuel() {
// Note, since the fuel amount does not change within a template
// and the retrieval has no side effects, we can return the value immediately,
// and do not need a token.
return Renderer.getCurrent().fuel();
}
@ -745,16 +958,17 @@ public sealed interface Template permits Template.ZeroArgs,
* {@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}.
* @return A token for convenient use in {@link Template#scope}.
*/
static Token setFuelCost(float fuelCost) {
Renderer.getCurrent().setFuelCost(fuelCost);
return new NothingToken();
return new SetFuelCostToken(fuelCost);
}
/**
* Add a {@link DataName} in the current scope, that is the innermost of either
* {@link Template#body} or {@link Hook#anchor}.
* Add a {@link DataName} in the current {@link #scope}.
* If the current scope is transparent to {@link DataName}s, it escapes to the next
* outer scope that is non-transparent, and is available for everything that follows
* the {@code addDataName} until the end of that non-transparent scope.
*
* @param name The name of the {@link DataName}, i.e. the {@link String} used in code.
* @param type The type of the {@link DataName}.
@ -779,8 +993,10 @@ public sealed interface Template permits Template.ZeroArgs,
}
/**
* 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.
* Add a {@link DataName} in the current {@link #scope}, with a {@code weight} of 1.
* If the current scope is transparent to {@link DataName}s, it escapes to the next
* outer scope that is non-transparent, and is available for everything that follows
* the {@code addDataName} until the end of that non-transparent scope.
*
* @param name The name of the {@link DataName}, i.e. the {@link String} used in code.
* @param type The type of the {@link DataName}.
@ -804,8 +1020,10 @@ public sealed interface Template permits Template.ZeroArgs,
}
/**
* Add a {@link StructuralName} in the current scope, that is the innermost of either
* {@link Template#body} or {@link Hook#anchor}.
* Add a {@link StructuralName} in the current {@link #scope}.
* If the current scope is transparent to {@link StructuralName}s, it escapes to the next
* outer scope that is non-transparent, and is available for everything that follows
* the {@code addStructuralName} until the end of that non-transparent scope.
*
* @param name The name of the {@link StructuralName}, i.e. the {@link String} used in code.
* @param type The type of the {@link StructuralName}.
@ -822,8 +1040,10 @@ public sealed interface Template permits Template.ZeroArgs,
}
/**
* 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.
* Add a {@link StructuralName} in the current {@link #scope}, with a {@code weight} of 1.
* If the current scope is transparent to {@link StructuralName}s, it escapes to the next
* outer scope that is non-transparent, and is available for everything that follows
* the {@code addStructuralName} until the end of that non-transparent scope.
*
* @param name The name of the {@link StructuralName}, i.e. the {@link String} used in code.
* @param type The type of the {@link StructuralName}.

View File

@ -27,38 +27,96 @@ 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.
* The {@link TemplateFrame} keeps track of the nested hashtag replacements available
* inside the {@link Template}, as well as the unique id of the {@link Template} use,
* and how much fuel is available for recursive {@link Template} calls. The name of
* the {@link TemplateFrame} indicates that it corresponds to the structure of the
* {@link Template}, whereas the {@link CodeFrame} corresponds to the structure of
* the generated code.
*
* <p>
* See also {@link CodeFrame} for more explanations about the frames.
* The unique id is used to deconflict names using {@link Template#$}.
*
* <p>
* A {@link Template} can have multiple {@link TemplateFrame}s, if there are nested
* scopes. The outermost {@link TemplateFrame} determines the id of the {@link Template}
* use and performs the subtraction of fuel from the outer {@link Template}. Inner
* {@link TemplateFrame}s ensure the correct availability of hashtag replacement and
* {@link Template#setFuelCost} definitions, so that they are local to their scope and
* nested scopes, and only escape if the scope is transparent.
*
* <p>
* The hashtag replacements are a set of key-value pairs from the template arguments
* and queries such as {@link Template#let} definitions. Each {@link TemplateFrame}
* has such a set of hashtag replacements, and implicitly provides access to the
* hashtag replacements of the outer {@link TemplateFrame}s, up to the outermost
* of the current {@link Template}. If a hashtag replacement is added in a scope,
* we have to traverse to outer scopes until we find one that is not transparent
* for hashtags (at most it is the frame of the Template), and insert it there.
* The hashtag replacent is local to that frame, and accessible for any frames nested
* inside it, but not inside other Templates. The hashtag replacement disappears once
* the corresponding scope is exited, i.e. the frame removed.
*
* <p>
* The {@link #parent} relationship provides a trace for the use chain of templates and
* their inner scopes. The {@link #fuel} is reduced over this chain to give a heuristic
* on how deeply nested the code is at a given point, correlating to the runtime that
* would be spent if the code was executed. The idea is that once the fuel is depleated,
* we do not want to nest more deeply, so that there is a reasonable chance that the
* execution of the generated code can terminate.
*
* <p>
* The {@link TemplateFrame} thus implements the hashtag and {@link Template#setFuelCost}
* non-transparency aspect of {@link ScopeToken}.
*
* <p>
* See also {@link CodeFrame} for more explanations about the frames. Note, that while
* {@link TemplateFrame} always nests inward, even with {@link Hook#insert}, the
* {@link CodeFrame} can also jump to the {@link Hook#anchor} {@link CodeFrame} when
* using {@link Hook#insert}.
*/
class TemplateFrame {
final TemplateFrame parent;
private final boolean isInnerScope;
private final int id;
private final Map<String, String> hashtagReplacements = new HashMap<>();
final float fuel;
private float fuelCost;
private final boolean isTransparentForHashtag;
private final boolean isTransparentForFuel;
public static TemplateFrame makeBase(int id, float fuel) {
return new TemplateFrame(null, id, fuel, 0.0f);
return new TemplateFrame(null, false, id, fuel, 0.0f, false, false);
}
public static TemplateFrame make(TemplateFrame parent, int id) {
return new TemplateFrame(parent, id, parent.fuel - parent.fuelCost, Template.DEFAULT_FUEL_COST);
float fuel = parent.fuel - parent.fuelCost;
return new TemplateFrame(parent, false, id, fuel, Template.DEFAULT_FUEL_COST, false, false);
}
private TemplateFrame(TemplateFrame parent, int id, float fuel, float fuelCost) {
public static TemplateFrame makeInnerScope(TemplateFrame parent,
boolean isTransparentForHashtag,
boolean isTransparentForFuel) {
// We keep the id of the parent, so that we have the same dollar replacements.
// And we subtract no fuel, but forward the cost.
return new TemplateFrame(parent, true, parent.id, parent.fuel, parent.fuelCost,
isTransparentForHashtag, isTransparentForFuel);
}
private TemplateFrame(TemplateFrame parent,
boolean isInnerScope,
int id,
float fuel,
float fuelCost,
boolean isTransparentForHashtag,
boolean isTransparentForFuel) {
this.parent = parent;
this.isInnerScope = isInnerScope;
this.id = id;
this.fuel = fuel;
this.fuelCost = fuelCost;
this.isTransparentForHashtag = isTransparentForHashtag;
this.isTransparentForFuel = isTransparentForFuel;
}
public String $(String name) {
@ -78,8 +136,15 @@ class TemplateFrame {
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 previous = findHashtagReplacementInScopes(key);
if (previous != null) {
throw new RendererException("Duplicate hashtag replacement for #" + key + ". " +
"previous: " + previous + ", new: " + value);
}
if (isTransparentForHashtag) {
parent.addHashtagReplacement(key, value);
} else {
hashtagReplacements.put(key, value);
}
}
@ -87,13 +152,27 @@ class TemplateFrame {
if (!Renderer.isValidHashtagOrDollarName(key)) {
throw new RendererException("Is not a valid hashtag replacement name: '" + key + "'.");
}
if (hashtagReplacements.containsKey(key)) {
return hashtagReplacements.get(key);
String value = findHashtagReplacementInScopes(key);
if (value != null) {
return value;
}
throw new RendererException("Missing hashtag replacement for #" + key);
}
private String findHashtagReplacementInScopes(String key) {
if (hashtagReplacements.containsKey(key)) {
return hashtagReplacements.get(key);
}
if (!isInnerScope) {
return null;
}
return parent.findHashtagReplacementInScopes(key);
}
void setFuelCost(float fuelCost) {
this.fuelCost = fuelCost;
if (isTransparentForFuel) {
parent.setFuelCost(fuelCost);
}
}
}

View File

@ -49,7 +49,7 @@ public sealed abstract class TemplateToken implements Token
}
@Override
public TemplateBody instantiate() {
public ScopeToken instantiate() {
return zeroArgs.instantiate();
}
@ -74,7 +74,7 @@ public sealed abstract class TemplateToken implements Token
}
@Override
public TemplateBody instantiate() {
public ScopeToken instantiate() {
return oneArgs.instantiate(arg1);
}
@ -104,7 +104,7 @@ public sealed abstract class TemplateToken implements Token
}
@Override
public TemplateBody instantiate() {
public ScopeToken instantiate() {
return twoArgs.instantiate(arg1, arg2);
}
@ -138,7 +138,7 @@ public sealed abstract class TemplateToken implements Token
}
@Override
public TemplateBody instantiate() {
public ScopeToken instantiate() {
return threeArgs.instantiate(arg1, arg2, arg3);
}
@ -150,7 +150,7 @@ public sealed abstract class TemplateToken implements Token
}
}
abstract TemplateBody instantiate();
abstract ScopeToken instantiate();
@FunctionalInterface
interface ArgumentVisitor {

View File

@ -24,16 +24,25 @@
package compiler.lib.template_framework;
/**
* The {@link Template#body} and {@link Hook#anchor} are given a list of tokens, which are either
* The {@link Template#scope} 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.
*/
public sealed interface Token permits StringToken,
TemplateToken,
TemplateToken.ZeroArgs,
TemplateToken.OneArg,
TemplateToken.TwoArgs,
TemplateToken.ThreeArgs,
HookAnchorToken,
HookInsertToken,
AddNameToken,
NothingToken {}
TemplateToken,
TemplateToken.ZeroArgs,
TemplateToken.OneArg,
TemplateToken.TwoArgs,
TemplateToken.ThreeArgs,
HookAnchorToken,
HookInsertToken,
HookIsAnchoredToken,
AddNameToken,
NameSampleToken,
NameForEachToken,
NamesToListToken,
NameCountToken,
NameHasAnyToken,
LetToken,
ScopeToken,
ScopeTokenImpl,
SetFuelCostToken {}

View File

@ -31,7 +31,7 @@ import java.util.List;
* Helper class for {@link Token}, to keep the parsing methods package private.
*
* <p>
* The {@link Template#body} and {@link Hook#anchor} are given a list of tokens, which are either
* The {@link Template#scope} 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.

View File

@ -33,7 +33,7 @@ import java.util.stream.Collectors;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
/**
* {@link Expression}s model Java expressions, that have a list of arguments with specified
@ -357,7 +357,7 @@ public class Expression {
}
tokens.add(strings.getLast());
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
tokens
));
return template.asToken();

View File

@ -33,7 +33,7 @@ import compiler.lib.generators.RestrictableGenerator;
import compiler.lib.template_framework.DataName;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
/**
* The {@link PrimitiveType} models Java's primitive types, and provides a set
@ -190,7 +190,7 @@ public final class PrimitiveType implements CodeGenerationDataNameType {
* @return a TemplateToken that holds all the {@code LibraryRNG} class.
*/
public static TemplateToken generateLibraryRNG() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
public static class LibraryRNG {
private static final Random RANDOM = Utils.getRandomInstance();

View File

@ -30,7 +30,7 @@ import compiler.lib.ir_framework.TestFramework;
import compiler.lib.compile_framework.CompileFramework;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import static compiler.lib.template_framework.Template.let;
/**
@ -51,7 +51,7 @@ public final class TestFrameworkClass {
private TestFrameworkClass() {}
/**
* This method renders a list of {@code testTemplateTokens} into the body of a class
* This method renders a list of {@code testTemplateTokens} into the scope of a class
* and generates a {@code main} method which launches the {@link TestFramework}
* to run the generated tests.
*
@ -81,7 +81,7 @@ public final class TestFrameworkClass {
final Set<String> imports,
final String classpath,
final List<TemplateToken> testTemplateTokens) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("packageName", packageName),
let("className", className),
let("classpath", classpath),
@ -96,7 +96,7 @@ public final class TestFrameworkClass {
public class #className {
// --- CLASS_HOOK insertions start ---
""",
Hooks.CLASS_HOOK.anchor(
Hooks.CLASS_HOOK.anchor(scope(
"""
// --- CLASS_HOOK insertions end ---
public static void main(String[] vmFlags) {
@ -108,7 +108,7 @@ public final class TestFrameworkClass {
// --- LIST OF TESTS start ---
""",
testTemplateTokens
),
)),
"""
// --- LIST OF TESTS end ---
}

View File

@ -61,7 +61,7 @@ import compiler.lib.compile_framework.*;
import compiler.lib.generators.Generators;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import static compiler.lib.template_framework.Template.let;
import static compiler.lib.template_framework.Template.$;
@ -333,7 +333,7 @@ public class TestAliasingFuzzer {
}
public TemplateToken index(String invar0, String[] invarRest) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("con", con),
let("ivScale", ivScale),
let("invar0Scale", invar0Scale),
@ -349,7 +349,7 @@ public class TestAliasingFuzzer {
// MemorySegment need to be long-addressed, otherwise there can be int-overflow
// in the index, and that prevents RangeCheck Elimination and Vectorization.
public TemplateToken indexLong(String invar0, String[] invarRest) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("con", con),
let("ivScale", ivScale),
let("invar0Scale", invar0Scale),
@ -365,7 +365,7 @@ public class TestAliasingFuzzer {
// Mirror the IndexForm from the generator to the test.
public static TemplateToken generateIndexForm() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
private static final Random RANDOM = Utils.getRandomInstance();
@ -610,7 +610,7 @@ public class TestAliasingFuzzer {
for (int i = 0; i < indexFormNames.length; i++) {
indexFormNames[i] = $("index" + i);
}
return body(
return scope(
"""
// --- $test start ---
""",
@ -662,7 +662,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateArrayField(String name, MyType type) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("size", containerByteSize / type.byteSize()),
let("name", name),
let("type", type),
@ -676,7 +676,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateMemorySegmentField(String name, MyType type) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("size", containerByteSize / type.byteSize()),
let("byteSize", containerByteSize),
let("name", name),
@ -698,7 +698,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateIndexField(String name, IndexForm form) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("name", name),
let("form", form.generate()),
"""
@ -709,7 +709,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateTestFields(String[] invarRest, String[] containerNames, String[] indexFormNames) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("ivType", isLongIvType ? "long" : "int"),
"""
// invarRest fields:
@ -741,7 +741,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateContainerInitArray(String name) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("size", containerByteSize / containerElementType.byteSize()),
let("name", name),
"""
@ -753,7 +753,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateContainerInitMemorySegment(String name) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("size", containerByteSize / containerElementType.byteSize()),
let("name", name),
"""
@ -765,7 +765,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateContainerInit(String[] containerNames) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
// Init containers from original data:
""",
@ -784,7 +784,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateContainerAliasingAssignment(int i, String name1, String name2, String iterations) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("i", i),
let("name1", name1),
let("name2", name2),
@ -798,7 +798,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateContainerAliasing(String[] containerNames, String iterations) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
// Container aliasing:
""",
@ -832,7 +832,7 @@ public class TestAliasingFuzzer {
if (accessIndexForm.length != 2) { throw new RuntimeException("not yet implemented"); }
var templateSplitRanges = Template.make(() -> body(
var templateSplitRanges = Template.make(() -> scope(
let("size", size),
"""
int middle = RANDOM.nextInt(#size / 3, #size * 2 / 3);
@ -865,7 +865,7 @@ public class TestAliasingFuzzer {
"""
));
var templateWholeRanges = Template.make(() -> body(
var templateWholeRanges = Template.make(() -> scope(
let("size", size),
"""
var r0 = new IndexForm.Range(0, #size);
@ -873,7 +873,7 @@ public class TestAliasingFuzzer {
"""
));
var templateRandomRanges = Template.make(() -> body(
var templateRandomRanges = Template.make(() -> scope(
let("size", size),
"""
int lo0 = RANDOM.nextInt(0, #size * 3 / 4);
@ -883,7 +883,7 @@ public class TestAliasingFuzzer {
"""
));
var templateSmallOverlapRanges = Template.make(() -> body(
var templateSmallOverlapRanges = Template.make(() -> scope(
// Idea: same size ranges, with size "range". A small overlap,
// so that bad runtime checks would create wrong results.
let("size", size),
@ -907,7 +907,7 @@ public class TestAliasingFuzzer {
// -> safe with rnd = size/10
));
var templateAnyRanges = Template.make(() -> body(
var templateAnyRanges = Template.make(() -> scope(
switch(RANDOM.nextInt(4)) {
case 0 -> templateSplitRanges.asToken();
case 1 -> templateWholeRanges.asToken();
@ -917,7 +917,7 @@ public class TestAliasingFuzzer {
}
));
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
// Generate ranges:
""",
@ -941,7 +941,7 @@ public class TestAliasingFuzzer {
// We want there to be at least 1000 iterations.
final int minIvRange = ivStrideAbs * 1000;
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("containerByteSize", containerByteSize),
"""
// Compute loop bounds and loop invariants.
@ -949,7 +949,7 @@ public class TestAliasingFuzzer {
int ivHi = ivLo + #containerByteSize;
""",
IntStream.range(0, indexFormNames.length).mapToObj(i ->
Template.make(() -> body(
Template.make(() -> scope(
let("i", i),
let("form", indexFormNames[i]),
"""
@ -990,7 +990,7 @@ public class TestAliasingFuzzer {
""",
IntStream.range(0, indexFormNames.length).mapToObj(i1 ->
IntStream.range(0, i1).mapToObj(i2 ->
Template.make(() -> body(
Template.make(() -> scope(
let("i1", i1),
let("i2", i2),
// i1 < i2 or i1 > i2
@ -1021,7 +1021,7 @@ public class TestAliasingFuzzer {
private TemplateToken generateCallMethod(String output, String methodName, String containerPrefix) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("output", output),
let("methodName", methodName),
"var #output = #methodName(",
@ -1034,7 +1034,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateIRRules() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
switch (containerKind) {
case ContainerKind.ARRAY ->
generateIRRulesArray();
@ -1094,7 +1094,7 @@ public class TestAliasingFuzzer {
// Regular array-accesses are vectorized quite predictably, and we can create nice
// IR rules - even for cases where we do not expect vectorization.
private TemplateToken generateIRRulesArray() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("T", containerElementType.letter()),
switch (accessScenario) {
case COPY_LOAD_STORE ->
@ -1145,7 +1145,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateIRRulesMemorySegmentAtIndex() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
// Unfortunately, there are some issues that prevent RangeCheck elimination.
// The cases are currently quite unpredictable, so we cannot create any IR
@ -1158,7 +1158,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateIRRulesMemorySegmentLongAdrStride() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
// Unfortunately, there are some issues that prevent RangeCheck elimination.
// The cases are currently quite unpredictable, so we cannot create any IR
@ -1169,7 +1169,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateIRRulesMemorySegmentLongAdrScale() {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
// Unfortunately, there are some issues that prevent RangeCheck elimination.
// The cases are currently quite unpredictable, so we cannot create any IR
@ -1180,7 +1180,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateTestMethod(String methodName, String[] invarRest) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("methodName", methodName),
let("containerElementType", containerElementType),
let("ivStrideAbs", ivStrideAbs),
@ -1230,7 +1230,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateTestLoopIterationArray(String[] invarRest) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("type", containerElementType),
switch (accessScenario) {
case COPY_LOAD_STORE ->
@ -1245,7 +1245,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateTestLoopIterationMemorySegmentAtIndex(String[] invarRest) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("type0", accessType[0]),
let("type1", accessType[1]),
let("type0Layout", accessType[0].layout()),
@ -1265,7 +1265,7 @@ public class TestAliasingFuzzer {
}
private TemplateToken generateTestLoopIterationMemorySegmentLongAdr(String[] invarRest) {
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
let("type0", accessType[0]),
let("type1", accessType[1]),
let("type0Layout", accessType[0].layout()),

View File

@ -43,7 +43,7 @@ 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.scope;
import static compiler.lib.template_framework.Template.let;
/**
@ -96,7 +96,7 @@ public class TestAdvanced {
// - 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(
var testTemplate = Template.make("typeName", "operator", "generator", (String typeName, String operator, MyGenerator generator) -> scope(
let("con1", generator.next()),
let("con2", generator.next()),
"""
@ -116,7 +116,7 @@ public class TestAdvanced {
));
// Template for the Class.
var classTemplate = Template.make("types", (List<Type> types) -> body(
var classTemplate = Template.make("types", (List<Type> types) -> scope(
let("classpath", comp.getEscapedClassPathOfCompiledClasses()),
"""
package p.xyz;

View File

@ -40,7 +40,7 @@ import java.util.Set;
import compiler.lib.compile_framework.*;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import static compiler.lib.template_framework.Template.let;
import compiler.lib.template_framework.library.Expression;
import compiler.lib.template_framework.library.Operations;
@ -78,7 +78,7 @@ public class TestExpressions {
// precision results from some operators. We only compare the results if we know that the
// result is deterministically the same.
TemplateToken expressionToken = expression.asToken(expression.argumentTypes.stream().map(t -> t.con()).toList());
return body(
return scope(
let("returnType", expression.returnType),
"""
@Test

View File

@ -41,7 +41,8 @@ import java.util.HashMap;
import compiler.lib.compile_framework.*;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import static compiler.lib.template_framework.Template.transparentScope;
import static compiler.lib.template_framework.Template.dataNames;
import static compiler.lib.template_framework.Template.let;
import static compiler.lib.template_framework.Template.$;
@ -77,7 +78,7 @@ public class TestPrimitiveTypes {
Map<String, TemplateToken> tests = new HashMap<>();
// The boxing tests check if we can autobox with "boxedTypeName".
var boxingTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> body(
var boxingTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> scope(
let("CON1", type.con()),
let("CON2", type.con()),
let("Boxed", type.boxedTypeName()),
@ -99,7 +100,7 @@ public class TestPrimitiveTypes {
}
// Integral and Float types have a size. Also test if "isFloating" is correct.
var integralFloatTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> body(
var integralFloatTemplate = Template.make("name", "type", (String name, PrimitiveType type) -> scope(
let("size", type.byteSize()),
let("isFloating", type.isFloating()),
"""
@ -129,27 +130,31 @@ public class TestPrimitiveTypes {
// Finally, test the type by creating some DataNames (variables), and sampling
// from them. There should be no cross-over between the types.
var variableTemplate = Template.make("type", (PrimitiveType type) -> body(
// IMPORTANT: since we are adding the DataName via an inserted Template, we
// must chose a "transparentScope", so that the DataName escapes. If we
// instead chose "scope", the test would fail, because it later
// finds no DataNames when we sample.
var variableTemplate = Template.make("type", (PrimitiveType type) -> transparentScope(
let("CON", type.con()),
addDataName($("var"), type, MUTABLE),
addDataName($("var"), type, MUTABLE), // escapes the Template
"""
#type $var = #CON;
"""
));
var sampleTemplate = Template.make("type", (PrimitiveType type) -> body(
let("var", dataNames(MUTABLE).exactOf(type).sample().name()),
var sampleTemplate = Template.make("type", (PrimitiveType type) -> scope(
let("CON", type.con()),
dataNames(MUTABLE).exactOf(type).sampleAndLetAs("var"),
"""
#var = #CON;
"""
));
var namesTemplate = Template.make(() -> body(
var namesTemplate = Template.make(() -> scope(
"""
public static void test_names() {
""",
Hooks.METHOD_HOOK.anchor(
Hooks.METHOD_HOOK.anchor(scope(
Collections.nCopies(10,
CodeGenerationDataNameType.PRIMITIVE_TYPES.stream().map(type ->
Hooks.METHOD_HOOK.insert(variableTemplate.asToken(type))
@ -161,7 +166,7 @@ public class TestPrimitiveTypes {
Collections.nCopies(10,
CodeGenerationDataNameType.PRIMITIVE_TYPES.stream().map(sampleTemplate::asToken).toList()
)
),
)),
"""
}
"""
@ -172,7 +177,7 @@ public class TestPrimitiveTypes {
// Test runtime random value generation with LibraryRNG
// Runtime random number generation of a given primitive type can be very helpful
// when writing tests that require random inputs.
var libraryRNGWithTypeTemplate = Template.make("type", (PrimitiveType type) -> body(
var libraryRNGWithTypeTemplate = Template.make("type", (PrimitiveType type) -> scope(
"""
{
// Fill an array with 1_000 random values. Every type has at least 2 values,
@ -196,7 +201,7 @@ public class TestPrimitiveTypes {
"""
));
var libraryRNGTemplate = Template.make(() -> body(
var libraryRNGTemplate = Template.make(() -> scope(
// Make sure we instantiate the LibraryRNG class.
PrimitiveType.generateLibraryRNG(),
// Now we can use it inside the test.
@ -213,7 +218,7 @@ public class TestPrimitiveTypes {
// Finally, put all the tests together in a class, and invoke all
// tests from the main method.
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"""
package p.xyz;

View File

@ -34,7 +34,7 @@ package template_framework.examples;
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.scope;
public class TestSimple {
@ -61,7 +61,7 @@ public class TestSimple {
// 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(
var template = Template.make("arg1", "arg2", (Integer arg1, String arg2) -> scope(
"""
package p.xyz;
public class InnerTest {

View File

@ -43,7 +43,7 @@ import compiler.lib.generators.Generators;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import static compiler.lib.template_framework.Template.let;
import compiler.lib.template_framework.library.Hooks;
@ -82,7 +82,7 @@ public class TestWithTestFrameworkClass {
// Generate a source Java file as String
public static String generate(CompileFramework comp) {
// A simple template that adds a comment.
var commentTemplate = Template.make(() -> body(
var commentTemplate = Template.make(() -> scope(
"""
// Comment inserted from test method to class hook.
"""
@ -103,7 +103,7 @@ public class TestWithTestFrameworkClass {
// - The test method makes use of hashtag replacements (#con2 and #op).
// - The Check method verifies the results of the test method with the
// GOLD value.
var testTemplate = Template.make("op", (String op) -> body(
var testTemplate = Template.make("op", (String op) -> scope(
let("size", Generators.G.safeRestrict(Generators.G.ints(), 10_000, 20_000).next()),
let("con1", Generators.G.ints().next()),
let("con2", Generators.G.safeRestrict(Generators.G.ints(), 1, Integer.MAX_VALUE).next()),

View File

@ -38,7 +38,7 @@ import java.util.Set;
import compiler.lib.template_framework.DataName;
import compiler.lib.template_framework.Template;
import compiler.lib.template_framework.TemplateToken;
import static compiler.lib.template_framework.Template.body;
import static compiler.lib.template_framework.Template.scope;
import compiler.lib.template_framework.library.CodeGenerationDataNameType;
import compiler.lib.template_framework.library.Expression;
@ -93,7 +93,7 @@ public class TestExpression {
Expression e3 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, ",", myTypeA1, "]");
Expression e4 = Expression.make(myTypeA, "[", myTypeA, ",", myTypeB, ",", myTypeA1, ",", myTypeA, "]");
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"xx", e1.toString(), "yy\n",
"xx", e2.toString(), "yy\n",
"xx", e3.toString(), "yy\n",
@ -141,7 +141,7 @@ public class TestExpression {
Expression e3e1 = e3.nest(0, e1);
Expression e4e5 = e4.nest(1, e5);
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"xx", e1e1.toString(), "yy\n",
"xx", e2e1.toString(), "yy\n",
"xx", e3e1.toString(), "yy\n",
@ -184,7 +184,7 @@ public class TestExpression {
// Alternating pattern
Expression deep2 = Expression.nestRandomly(myTypeA, List.of(e5, e3), 5);
var template = Template.make(() -> body(
var template = Template.make(() -> scope(
"xx", e1e2.toString(), "yy\n",
"xx", e1ex.toString(), "yy\n",
"xx", e1e4.toString(), "yy\n",

View File

@ -39,7 +39,7 @@ 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.scope;
import static compiler.lib.template_framework.Template.let;
public class TestFormat {
@ -84,7 +84,7 @@ public class TestFormat {
private static String generate(List<FormatInfo> list) {
// Generate 2 "get" methods, one that formats via "let" (hashtag), the other via direct token.
var template1 = Template.make("info", (FormatInfo info) -> body(
var template1 = Template.make("info", (FormatInfo info) -> scope(
let("id", info.id()),
let("type", info.type()),
let("value", info.value()),
@ -95,7 +95,7 @@ public class TestFormat {
));
// For each FormatInfo in list, generate the "get" methods inside InnerTest class.
var template2 = Template.make(() -> body(
var template2 = Template.make(() -> scope(
"""
package p.xyz;
public class InnerTest {