/* * 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 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 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 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 tokens) { CodeFrame codeFrame = currentCodeFrame; for (Token t : tokens) { renderToken(t); } if (codeFrame != currentCodeFrame) { throw new RuntimeException("Internal error: CodeFrame mismatch."); } } /** * Finds the index where the next replacement pattern after {@code start} begins while skipping * over "$$" and "##". * * @param s string to search for replacements * @param start index from which to start searching * @return the index of the beginning of the next replacement pattern or the length of {@code s} */ private int findNextReplacement(final String s, final int start) { int next = start; for (int potentialStart = start; potentialStart < s.length() && s.charAt(next) == s.charAt(potentialStart); potentialStart = next + 1) { // If this is not the first iteration, we have found a doubled up "$" or "#" and need to skip // over the second instance. if (potentialStart != start) { potentialStart += 1; } // Find the next "$" or "#", after the potential start. int dollar = s.indexOf("$", potentialStart); int hashtag = s.indexOf("#", potentialStart); // 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. next = Math.min(dollar, hashtag); } return next; } /** * We split a {@link String} by "#" and "$", and then look at each part. * However, we escape "##" to "#" and "$$" to "$". * 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 { int next = findNextReplacement(s, start); // Detect most zero sized replacement patterns, i.e. "$#" or "#$", for better error reporting. if (next < s.length() - 2 && ((s.charAt(next) == '$' && s.charAt(next + 1) == '#') || (s.charAt(next) == '#' && s.charAt(next + 1) == '$'))) { String pattern = s.substring(next, next + 2); throw new RendererException("Found zero sized replacement pattern '" + pattern + "'."); } String part = s.substring(start, next); // Escape doubled up replacement characters. part = part.replace("##", "#"); part = part.replace("$$", "$"); 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; startIsAfterDollar = s.charAt(next) == '$'; // 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; } }