diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java b/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java index 61ab9ab343c..cb83a5ea40c 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Renderer.java @@ -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); } diff --git a/test/hotspot/jtreg/compiler/lib/template_framework/Template.java b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java index f245cda0501..3cd3a9097ff 100644 --- a/test/hotspot/jtreg/compiler/lib/template_framework/Template.java +++ b/test/hotspot/jtreg/compiler/lib/template_framework/Template.java @@ -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 * hashtag replacements 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 dollar replacements, 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 "$". * *

* The dollar and hashtag names must have at least one character. The first character must be a letter diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java index ed542180bad..7de32d1bc10 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestTutorial.java @@ -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); """ diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java index 9be74d232a7..f56a9d5b231 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/tests/TestTemplate.java @@ -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(); }