8364393: Allow templates to have # character without variable replacement

Reviewed-by: epeter, chagedorn
This commit is contained in:
Manuel Hässig 2026-02-26 08:21:48 +00:00
parent a39a1f10f7
commit 3d8ffabe5d
4 changed files with 130 additions and 37 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* 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
@ -413,8 +413,39 @@ final class Renderer {
}
}
/**
* 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"
@ -428,16 +459,19 @@ final class Renderer {
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);
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.
@ -452,8 +486,8 @@ final class Renderer {
// terminate now.
return;
}
start = next + 1; // skip over the "#" or "$"
startIsAfterDollar = next == dollar; // remember which character we just split with
start = next + 1;
startIsAfterDollar = s.charAt(next) == '$'; // remember which character we just split with
count++;
} while (true);
}

View File

@ -160,7 +160,8 @@ import compiler.lib.ir_framework.TestFramework;
* arguments into the strings. But since string templates are not (yet) available, the Templates provide
* <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}.
* of {@link #make} for examples. Additional hashtag replacements can be defined with {@link #let}. If a "#" is needed
* in the code, hashtag replacmement can be escaped by writing two hashtags, i.e. "##" will render as "#".
* 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
@ -172,7 +173,8 @@ import compiler.lib.ir_framework.TestFramework;
* For this, Templates provide <strong>dollar replacements</strong>, which automatically rename any
* {@code "$name"} in the {@link String} with a {@code "name_ID"}, where the {@code "ID"} is unique for every use of
* a Template. The dollar replacement can also be captured with {@link #$}, and passed to nested
* Templates, which allows sharing of these identifier names between Templates.
* Templates, which allows sharing of these identifier names between Templates. Similar to hashtag replacements,
* dollars can be escaped by doubling up, i.e. "$$" renders as "$".
*
* <p>
* The dollar and hashtag names must have at least one character. The first character must be a letter

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* 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
@ -226,6 +226,8 @@ public class TestTutorial {
// we automatically rename the names that have a $ prepended with
// var_1, var_2, etc.
"""
// You can escape a hashtag by doubling it up. e.g. ## will render as a
// single hashtag. The same goes for $$.
int $var = #con;
System.out.println("T1: #x, #con, " + $var);
"""

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* 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
@ -23,7 +23,7 @@
/*
* @test
* @bug 8344942
* @bug 8344942 8364393
* @summary Test some basic Template instantiations. We do not necessarily generate correct
* java code, we just test that the code generation deterministically creates the
* expected String.
@ -138,6 +138,7 @@ public class TestTemplate {
testHookWithNestedTemplates();
testHookRecursion();
testDollar();
testEscaping();
testLet1();
testLet2();
testDollarAndHashtagBrackets();
@ -182,7 +183,6 @@ public class TestTemplate {
expectRendererException(() -> testFailingDollarName5(), "Is not a valid '$' replacement pattern: '$' in '$'.");
expectRendererException(() -> testFailingDollarName6(), "Is not a valid '$' replacement pattern: '$' in 'asdf$'.");
expectRendererException(() -> testFailingDollarName7(), "Is not a valid '$' replacement pattern: '$1' in 'asdf$1'.");
expectRendererException(() -> testFailingDollarName8(), "Is not a valid '$' replacement pattern: '$' in 'abc$$abc'.");
expectRendererException(() -> testFailingLetName1(), "A hashtag replacement should not be null.");
expectRendererException(() -> testFailingHashtagName1(), "Is not a valid hashtag replacement name: ''.");
expectRendererException(() -> testFailingHashtagName2(), "Is not a valid hashtag replacement name: 'abc#abc'.");
@ -191,11 +191,12 @@ public class TestTemplate {
expectRendererException(() -> testFailingHashtagName5(), "Is not a valid '#' replacement pattern: '#' in '#'.");
expectRendererException(() -> testFailingHashtagName6(), "Is not a valid '#' replacement pattern: '#' in 'asdf#'.");
expectRendererException(() -> testFailingHashtagName7(), "Is not a valid '#' replacement pattern: '#1' in 'asdf#1'.");
expectRendererException(() -> testFailingHashtagName8(), "Is not a valid '#' replacement pattern: '#' in 'abc##abc'.");
expectRendererException(() -> testFailingDollarHashtagName1(), "Is not a valid '#' replacement pattern: '#' in '#$'.");
expectRendererException(() -> testFailingDollarHashtagName2(), "Is not a valid '$' replacement pattern: '$' in '$#'.");
expectRendererException(() -> testFailingDollarHashtagName3(), "Is not a valid '#' replacement pattern: '#' in '#$name'.");
expectRendererException(() -> testFailingDollarHashtagName4(), "Is not a valid '$' replacement pattern: '$' in '$#name'.");
expectRendererException(() -> testFailingDollarHashtagName3(), "Found zero sized replacement pattern '#$'.");
expectRendererException(() -> testFailingDollarHashtagName4(), "Found zero sized replacement pattern '$#'.");
expectRendererException(() -> testFailingDollarHashtagName5(), "Found zero sized replacement pattern '#$'.");
expectRendererException(() -> testFailingDollarHashtagName6(), "Found zero sized replacement pattern '$#'.");
expectRendererException(() -> testFailingHook(), "Hook 'Hook1' was referenced but not found!");
expectRendererException(() -> testFailingSample1a(), "No Name found for DataName.FilterdSet(MUTABLE, subtypeOf(int), supertypeOf(int))");
expectRendererException(() -> testFailingSample1b(), "No Name found for StructuralName.FilteredSet( subtypeOf(StructuralA) supertypeOf(StructuralA))");
@ -822,6 +823,60 @@ public class TestTemplate {
checkEQ(code, expected);
}
public static void testEscaping() {
var template1 = Template.make(() -> scope(
let("one", 1),
let("two", 2),
let("three", 3),
"""
abc##def
abc$$def
abc####def
abc$$$$def
##abc
$$abc
abc##
abc$$
##
$$
######
$$$$$$
abc###one
abc$$$dollar
#one ###two #####three
###{one}##
$dollar $$$dollar $$$$$dollar
$$${dollar}$$
##$dollar $$#one $$##$$##
"""
));
String code = template1.render();
String expected =
"""
abc#def
abc$def
abc##def
abc$$def
#abc
$abc
abc#
abc$
#
$
###
$$$
abc#1
abc$dollar_1
1 #2 ##3
#1#
dollar_1 $dollar_1 $$dollar_1
$dollar_1$
#dollar_1 $1 $#$#
""";
checkEQ(code, expected);
}
public static void testLet1() {
var hook1 = new Hook("Hook1");
@ -3382,13 +3437,6 @@ public class TestTemplate {
String code = template1.render();
}
public static void testFailingDollarName8() {
var template1 = Template.make(() -> scope(
"abc$$abc" // empty dollar name
));
String code = template1.render();
}
public static void testFailingLetName1() {
var template1 = Template.make(() -> scope(
let(null, $("abc")) // Null input for hashtag name
@ -3447,13 +3495,6 @@ public class TestTemplate {
String code = template1.render();
}
public static void testFailingHashtagName8() {
var template1 = Template.make(() -> scope(
"abc##abc" // empty hashtag name
));
String code = template1.render();
}
public static void testFailingDollarHashtagName1() {
var template1 = Template.make(() -> scope(
"#$" // empty hashtag name
@ -3470,14 +3511,28 @@ public class TestTemplate {
public static void testFailingDollarHashtagName3() {
var template1 = Template.make(() -> scope(
"#$name" // empty hashtag name
"#$name" // Zero sized replacement
));
String code = template1.render();
}
public static void testFailingDollarHashtagName4() {
var template1 = Template.make(() -> scope(
"$#name" // empty dollar name
"$#name" // Zero sized replacement
));
String code = template1.render();
}
public static void testFailingDollarHashtagName5() {
var template1 = Template.make(() -> scope(
"asdf#$abc" // Zero sized replacement
));
String code = template1.render();
}
public static void testFailingDollarHashtagName6() {
var template1 = Template.make(() -> scope(
"asdf$#abc" // Zero sized replacement
));
String code = template1.render();
}