mirror of
https://github.com/openjdk/jdk.git
synced 2026-02-04 07:28:22 +00:00
Co-authored-by: Christian Hagedorn <chagedorn@openjdk.org> Reviewed-by: rcastanedalo, mhaessig, chagedorn
515 lines
22 KiB
Java
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;
|
|
}
|
|
}
|