Emanuel Peter b41146cd1e 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
2025-11-20 09:32:57 +00:00

515 lines
22 KiB
Java

/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package compiler.lib.template_framework;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The {@link Renderer} class renders a tokenized {@link Template} in the form of a {@link TemplateToken}.
* It also keeps track of the states during a nested Template rendering. There can only be a single
* {@link Renderer} active at any point, since there are static methods that reference
* {@link Renderer#getCurrent}.
*
* <p>
* 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.
*
* <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#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.
*
* <p>
* 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}.
*
* <p>
* 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<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 HookAnchorToken(Hook hook, ScopeTokenImpl innerScope) -> {
CodeFrame outerCodeFrame = currentCodeFrame;
// 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 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;
renderScopeToken(innerScope);
// Close the hookCodeFrame and innerCodeFrame. hookCodeFrame code comes before the
// innerCodeFrame code from the tokens.
currentCodeFrame = outerCodeFrame;
currentCodeFrame.addCode(hookCodeFrame.getCode());
currentCodeFrame.addCode(innerCodeFrame.getCode());
}
case HookInsertToken(Hook hook, 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 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 scopeToken.
currentCodeFrame = CodeFrame.make(hookCodeFrame, true);
renderScopeToken(scopeToken);
hookCodeFrame.addCode(currentCodeFrame.getCode());
// Switch back from hook CodeFrame to caller CodeFrame.
currentCodeFrame = callerCodeFrame;
}
case TemplateToken templateToken -> {
renderTemplateToken(templateToken);
}
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));
}
}
}
private void renderTokenList(List<Token> tokens) {
CodeFrame codeFrame = currentCodeFrame;
for (Token t : tokens) {
renderToken(t);
}
if (codeFrame != currentCodeFrame) {
throw new RuntimeException("Internal error: CodeFrame mismatch.");
}
}
/**
* We split a {@link String} by "#" and "$", and then look at each part.
* Example:
*
* s: "abcdefghijklmnop #name abcdefgh${var_name} 12345#{name2}_con $field_name something"
* parts: --------0-------- ------1------ --------2------- ------3----- ----------4---------
* start: ^ ^ ^ ^ ^
* next: ^ ^ ^ ^ ^
* none hashtag dollar hashtag dollar done
*/
private void renderStringWithDollarAndHashtagReplacements(final String s) {
int count = 0; // First part needs special handling
int start = 0;
boolean startIsAfterDollar = false;
do {
// Find the next "$" or "#", after start.
int dollar = s.indexOf("$", start);
int hashtag = s.indexOf("#", start);
// If the character was not found, we want to have the rest of the
// String s, so instead of "-1" take the end/length of the String.
dollar = (dollar == -1) ? s.length() : dollar;
hashtag = (hashtag == -1) ? s.length() : hashtag;
// Take the first one.
int next = Math.min(dollar, hashtag);
String part = s.substring(start, next);
if (count == 0) {
// First part has no "#" or "$" before it.
currentCodeFrame.addString(part);
} else {
// All others must do the replacement.
renderStringWithDollarAndHashtagReplacementsPart(s, part, startIsAfterDollar);
}
if (next == s.length()) {
// No new "#" or "$" was found, we just processed the rest of the String,
// terminate now.
return;
}
start = next + 1; // skip over the "#" or "$"
startIsAfterDollar = next == dollar; // remember which character we just split with
count++;
} while (true);
}
/**
* We are parsing a part now. Before the part, there was either a "#" or "$":
* isDollar = false:
* "#part"
* "#name abcdefgh"
* ----
* "#{name2}_con "
* -------
*
* isDollar = true:
* "$part"
* "${var_name} 12345"
* ----------
* "$field_name something"
* ----------
*
* We now want to find the name pattern at the beginning of the part, and replace
* it according to the hashtag or dollar replacement strategy.
*/
private void renderStringWithDollarAndHashtagReplacementsPart(final String s, final String part, final boolean isDollar) {
Matcher matcher = NAME_PATTERN.matcher(part);
// If the string has a "#" or "$" that is not followed by a correct name
// pattern, then the matcher will not match. These can be cases like:
// "##name" -> the first hashtag leads to an empty part, and an empty name.
// "#1name" -> the name pattern does not allow a digit as the first character.
// "anything#" -> a hashtag at the end of the string leads to an empty name.
if (!matcher.find()) {
String replacement = isDollar ? "$" : "#";
throw new RendererException("Is not a valid '" + replacement + "' replacement pattern: '" +
replacement + part + "' in '" + s + "'.");
}
// We know that there is a correct pattern, and now we replace it.
currentCodeFrame.addString(matcher.replaceFirst(
(MatchResult result) -> {
// There are two groups: (1) for "name" and (2) for "{name}"
String name = result.group(1) != null ? result.group(1) : result.group(2);
if (isDollar) {
return $(name);
} else {
// replaceFirst needs some special escaping of backslashes and ollar signs.
return getHashtagReplacement(name).replace("\\", "\\\\").replace("$", "\\$");
}
}
));
}
private CodeFrame codeFrameForHook(Hook hook) {
CodeFrame codeFrame = currentCodeFrame.codeFrameForHook(hook);
if (codeFrame == null) {
throw new RendererException("Hook '" + hook.name() + "' was referenced but not found!");
}
return codeFrame;
}
}