/* * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package compiler.lib.template_framework; import java.util.List; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The {@link Renderer} class renders a tokenized {@link Template} in the form of a {@link TemplateToken}. * It also keeps track of the states during a nested Template rendering. There can only be a single * {@link Renderer} active at any point, since there are static methods that reference * {@link Renderer#getCurrent}. * *
* The {@link Renderer} instance keeps track of the current frames. * * @see TemplateFrame * @see CodeFrame */ final class Renderer { private static final String NAME_CHARACTERS = "[a-zA-Z_][a-zA-Z0-9_]*"; private static final Pattern NAME_PATTERN = Pattern.compile( // We are parsing patterns: // #name // #{name} // $name // ${name} // But the "#" or "$" have already been removed, and the String // starts at the character after that. // The pattern must be at the beginning of the String part. "^" + // We either have "name" or "{name}" "(?:" + // non-capturing group for the OR // capturing group for "name" "(" + NAME_CHARACTERS + ")" + "|" + // OR // We want to trim off the brackets, so have // another non-capturing group. "(?:\\{" + // capturing group for "name" inside "{name}" "(" + NAME_CHARACTERS + ")" + "\\})" + ")"); private static final Pattern NAME_CHARACTERS_PATTERN = Pattern.compile("^" + NAME_CHARACTERS + "$"); static boolean isValidHashtagOrDollarName(String name) { return NAME_CHARACTERS_PATTERN.matcher(name).find(); } /** * There can be at most one Renderer instance at any time. * *
* When using nested templates, the user of the Template Framework may be tempted to first render * the nested template to a {@link String}, and then use this {@link String} as a token in an outer * {@link Template#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 * that the inner {@link Template} has access to the outer {@link Template}, but they would actually * be separated. This could lead to unexpected behavior or even bugs. * *
* Instead, the user 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}. * *
* The {@link Renderer} instance exists during the whole rendering process. Should the user ever
* attempt to render a nested {@link Template} to a {@link String}, we would detect that there is
* already a {@link Renderer} instance for the outer {@link Template}, and throw a {@link RendererException}.
*/
private static Renderer renderer = null;
private int nextTemplateFrameId;
private final TemplateFrame baseTemplateFrame;
private TemplateFrame currentTemplateFrame;
private final CodeFrame baseCodeFrame;
private CodeFrame currentCodeFrame;
// We do not want any other instances, so we keep it private.
private Renderer(float fuel) {
nextTemplateFrameId = 0;
baseTemplateFrame = TemplateFrame.makeBase(nextTemplateFrameId++, fuel);
currentTemplateFrame = baseTemplateFrame;
baseCodeFrame = CodeFrame.makeBase();
currentCodeFrame = baseCodeFrame;
}
static Renderer getCurrent() {
if (renderer == null) {
throw new RendererException("A Template method such as '$', 'fuel', etc. was called outside a template rendering call.");
}
return renderer;
}
static String render(TemplateToken templateToken) {
return render(templateToken, Template.DEFAULT_FUEL);
}
static String render(TemplateToken templateToken, float fuel) {
// Check nobody else is using the Renderer.
if (renderer != null) {
throw new RendererException("Nested render not allowed. Please only use 'asToken' inside Templates, and call 'render' only once at the end.");
}
try {
renderer = new Renderer(fuel);
renderer.renderTemplateToken(templateToken);
renderer.checkFrameConsistencyAfterRendering();
return renderer.collectCode();
} finally {
// Release the Renderer.
renderer = null;
}
}
private void checkFrameConsistencyAfterRendering() {
// Ensure CodeFrame consistency.
if (baseCodeFrame != currentCodeFrame) {
throw new RuntimeException("Internal error: Renderer did not end up at base CodeFrame.");
}
// Ensure TemplateFrame consistency.
if (baseTemplateFrame != currentTemplateFrame) {
throw new RuntimeException("Internal error: Renderer did not end up at base TemplateFrame.");
}
}
private String collectCode() {
StringBuilder builder = new StringBuilder();
baseCodeFrame.getCode().renderTo(builder);
return builder.toString();
}
String $(String name) {
return currentTemplateFrame.$(name);
}
void addHashtagReplacement(String key, Object value) {
currentTemplateFrame.addHashtagReplacement(key, format(value));
}
private String getHashtagReplacement(String key) {
return currentTemplateFrame.getHashtagReplacement(key);
}
float fuel() {
return currentTemplateFrame.fuel;
}
/**
* Formats values to {@link String} with the goal of using them in Java code.
* By default, we use the overrides of {@link Object#toString}.
* But for some boxed primitives we need to create a special formatting.
*/
static String format(Object value) {
return switch (value) {
case String s -> s;
case Integer i -> i.toString();
// We need to append the "L" so that the values are not interpreted as ints,
// and then javac might complain that the values are too large for an int.
case Long l -> l.toString() + "L";
// Some Float and Double values like Infinity and NaN need a special representation.
case Float f -> formatFloat(f);
case Double d -> formatDouble(d);
default -> value.toString();
};
}
private static String formatFloat(Float f) {
if (Float.isFinite(f)) {
return f.toString() + "f";
} else if (f.isNaN()) {
return "Float.intBitsToFloat(" + Float.floatToRawIntBits(f) + " /* NaN */)";
} else if (f.isInfinite()) {
if (f > 0) {
return "Float.POSITIVE_INFINITY";
} else {
return "Float.NEGATIVE_INFINITY";
}
} else {
throw new RuntimeException("Not handled: " + f);
}
}
private static String formatDouble(Double d) {
if (Double.isFinite(d)) {
return d.toString();
} else if (d.isNaN()) {
return "Double.longBitsToDouble(" + Double.doubleToRawLongBits(d) + "L /* NaN */)";
} else if (d.isInfinite()) {
if (d > 0) {
return "Double.POSITIVE_INFINITY";
} else {
return "Double.NEGATIVE_INFINITY";
}
} else {
throw new RuntimeException("Not handled: " + d);
}
}
private void renderTemplateToken(TemplateToken templateToken) {
// 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)));
// 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!");
}
currentTemplateFrame = currentTemplateFrame.parent;
}
private void renderScopeToken(ScopeToken st) {
renderScopeToken(st, () -> {});
}
private void renderScopeToken(ScopeToken st, Runnable preamble) {
if (!(st instanceof ScopeTokenImpl(List