diff --git a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java
index 87ac55b274a..e58ded864f0 100644
--- a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java
+++ b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java
@@ -1823,6 +1823,42 @@ abstract sealed class AbstractStringBuilder implements Appendable, CharSequence
count += end - off;
}
+ /**
+ * Used by StringConcatHelper via JLA. Adds the current builder count to the
+ * accumulation of items being concatenated. If the coder for the builder is
+ * UTF16 then upgrade the whole concatenation to UTF16.
+ *
+ * @param lengthCoder running accumulation of length and coder
+ *
+ * @return updated accumulation of length and coder
+ */
+ long mix(long lengthCoder) {
+ return (lengthCoder + count) | ((long)coder << 32);
+ }
+
+ /**
+ * Used by StringConcatHelper via JLA. Adds the characters in the builder value to the
+ * concatenation buffer and then updates the running accumulation of length.
+ *
+ * @param lengthCoder running accumulation of length and coder
+ * @param buffer concatenation buffer
+ *
+ * @return running accumulation of length and coder minus the number of characters added
+ */
+ long prepend(long lengthCoder, byte[] buffer) {
+ lengthCoder -= count;
+
+ if (lengthCoder < ((long)UTF16 << 32)) {
+ System.arraycopy(value, 0, buffer, (int)lengthCoder, count);
+ } else if (coder == LATIN1) {
+ StringUTF16.inflate(value, 0, buffer, (int)lengthCoder, count);
+ } else {
+ System.arraycopy(value, 0, buffer, (int)lengthCoder << 1, count << 1);
+ }
+
+ return lengthCoder;
+ }
+
private AbstractStringBuilder repeat(char c, int count) {
int limit = this.count + count;
ensureCapacityInternal(limit);
diff --git a/src/java.base/share/classes/java/lang/StringConcatHelper.java b/src/java.base/share/classes/java/lang/StringConcatHelper.java
index cad34b98937..139181af096 100644
--- a/src/java.base/share/classes/java/lang/StringConcatHelper.java
+++ b/src/java.base/share/classes/java/lang/StringConcatHelper.java
@@ -26,6 +26,8 @@
package java.lang;
import jdk.internal.misc.Unsafe;
+import jdk.internal.javac.PreviewFeature;
+import jdk.internal.util.FormatConcatItem;
import jdk.internal.vm.annotation.ForceInline;
import java.lang.invoke.MethodHandle;
@@ -43,6 +45,15 @@ final class StringConcatHelper {
// no instantiation
}
+ /**
+ * Return the coder for the character.
+ * @param value character
+ * @return coder
+ */
+ static long coder(char value) {
+ return StringLatin1.canEncode(value) ? LATIN1 : UTF16;
+ }
+
/**
* Check for overflow, throw exception on overflow.
*
@@ -76,7 +87,7 @@ final class StringConcatHelper {
* @return new length and coder
*/
static long mix(long lengthCoder, char value) {
- return checkOverflow(lengthCoder + 1) | (StringLatin1.canEncode(value) ? 0 : UTF16);
+ return checkOverflow(lengthCoder + 1) | coder(value);
}
/**
@@ -116,6 +127,21 @@ final class StringConcatHelper {
return checkOverflow(lengthCoder);
}
+ /**
+ * Mix value length and coder into current length and coder.
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static long mix(long lengthCoder, FormatConcatItem value) {
+ lengthCoder = value.mix(lengthCoder);
+
+ return checkOverflow(lengthCoder);
+ }
+
/**
* Prepends the stringly representation of boolean value into buffer,
* given the coder and final index. Index is measured in chars, not in bytes!
@@ -319,6 +345,49 @@ final class StringConcatHelper {
return indexCoder;
}
+ /**
+ * Prepends the stringly representation of FormatConcatItem value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param indexCoder final char index in the buffer, along with coder packed
+ * into higher bits.
+ * @param buf buffer to append to
+ * @param value String value to encode
+ * @return updated index (coder value retained)
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ private static long prepend(long indexCoder, byte[] buf,
+ FormatConcatItem value) {
+ try {
+ return value.prepend(indexCoder, buf);
+ } catch (Error ex) {
+ throw ex;
+ } catch (Throwable ex) {
+ throw new AssertionError("FormatConcatItem prepend error", ex);
+ }
+ }
+
+ /**
+ * Prepends constant and the stringly representation of value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param indexCoder final char index in the buffer, along with coder packed
+ * into higher bits.
+ * @param buf buffer to append to
+ * @param value boolean value to encode
+ * @param prefix a constant to prepend before value
+ * @return updated index (coder value retained)
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static long prepend(long indexCoder, byte[] buf,
+ FormatConcatItem value, String prefix) {
+ indexCoder = prepend(indexCoder, buf, value);
+ if (prefix != null) indexCoder = prepend(indexCoder, buf, prefix);
+ return indexCoder;
+ }
+
/**
* Instantiates the String with given buffer and coder
* @param buf buffer to use
@@ -332,7 +401,8 @@ final class StringConcatHelper {
} else if (indexCoder == UTF16) {
return new String(buf, String.UTF16);
} else {
- throw new InternalError("Storage is not completely initialized, " + (int)indexCoder + " bytes left");
+ throw new InternalError("Storage is not completely initialized, " +
+ (int)indexCoder + " bytes left");
}
}
@@ -449,6 +519,71 @@ final class StringConcatHelper {
return String.COMPACT_STRINGS ? LATIN1 : UTF16;
}
+ /*
+ * Initialize after phase1.
+ */
+ private static class LateInit {
+ static final MethodHandle GETCHAR_LATIN1_MH;
+
+ static final MethodHandle GETCHAR_UTF16_MH;
+
+ static final MethodHandle PUTCHAR_LATIN1_MH;
+
+ static final MethodHandle PUTCHAR_UTF16_MH;
+
+ static {
+ MethodType getCharMT =
+ MethodType.methodType(char.class,
+ byte[].class, int.class);
+ MethodType putCharMT =
+ MethodType.methodType(void.class,
+ byte[].class, int.class, int.class);
+ GETCHAR_LATIN1_MH = lookupStatic("getCharLatin1", getCharMT);
+ GETCHAR_UTF16_MH = lookupStatic("getCharUTF16", getCharMT);
+ PUTCHAR_LATIN1_MH = lookupStatic("putCharLatin1", putCharMT);
+ PUTCHAR_UTF16_MH = lookupStatic("putCharUTF16", putCharMT);
+ }
+
+ }
+
+ @ForceInline
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static char getCharLatin1(byte[] buffer, int index) {
+ return (char)buffer[index];
+ }
+
+ @ForceInline
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static char getCharUTF16(byte[] buffer, int index) {
+ return StringUTF16.getChar(buffer, index);
+ }
+
+ @ForceInline
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static void putCharLatin1(byte[] buffer, int index, int ch) {
+ buffer[index] = (byte)ch;
+ }
+
+ @ForceInline
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static void putCharUTF16(byte[] buffer, int index, int ch) {
+ StringUTF16.putChar(buffer, index, ch);
+ }
+
+ @ForceInline
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static MethodHandle selectGetChar(long indexCoder) {
+ return indexCoder < UTF16 ? LateInit.GETCHAR_LATIN1_MH :
+ LateInit.GETCHAR_UTF16_MH;
+ }
+
+ @ForceInline
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ static MethodHandle selectPutChar(long indexCoder) {
+ return indexCoder < UTF16 ? LateInit.PUTCHAR_LATIN1_MH :
+ LateInit.PUTCHAR_UTF16_MH;
+ }
+
static MethodHandle lookupStatic(String name, MethodType methodType) {
try {
return MethodHandles.lookup().findStatic(StringConcatHelper.class, name, methodType);
diff --git a/src/java.base/share/classes/java/lang/StringTemplate.java b/src/java.base/share/classes/java/lang/StringTemplate.java
new file mode 100644
index 00000000000..8ba94667752
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/StringTemplate.java
@@ -0,0 +1,621 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodType;
+import java.util.FormatProcessor;
+import java.util.function.Function;
+import java.util.List;
+import java.util.Objects;
+
+import jdk.internal.access.JavaTemplateAccess;
+import jdk.internal.access.SharedSecrets;
+import jdk.internal.javac.PreviewFeature;
+
+/**
+ * {@link StringTemplate} is the run-time representation of a string template or
+ * text block template in a template expression.
+ *
+ * In the source code of a Java program, a string template or text block template
+ * contains an interleaved succession of fragment literals and embedded
+ * expressions . The {@link StringTemplate#fragments()} method returns the
+ * fragment literals, and the {@link StringTemplate#values()} method returns the
+ * results of evaluating the embedded expressions. {@link StringTemplate} does not
+ * provide access to the source code of the embedded expressions themselves; it is
+ * not a compile-time representation of a string template or text block template.
+ *
+ * {@link StringTemplate} is primarily used in conjunction with a template processor
+ * to produce a string or other meaningful value. Evaluation of a template expression
+ * first produces an instance of {@link StringTemplate}, representing the right hand side
+ * of the template expression, and then passes the instance to the template processor
+ * given by the template expression.
+ *
+ * For example, the following code contains a template expression that uses the template
+ * processor {@code RAW}, which simply yields the {@link StringTemplate} passed to it:
+ * {@snippet :
+ * int x = 10;
+ * int y = 20;
+ * StringTemplate st = RAW."\{x} + \{y} = \{x + y}";
+ * List fragments = st.fragments();
+ * List values = st.values();
+ * }
+ * {@code fragments} will be equivalent to {@code List.of("", " + ", " = ", "")},
+ * which includes the empty first and last fragments. {@code values} will be the
+ * equivalent of {@code List.of(10, 20, 30)}.
+ *
+ * The following code contains a template expression with the same template but with a
+ * different template processor, {@code STR}:
+ * {@snippet :
+ * int x = 10;
+ * int y = 20;
+ * String s = STR."\{x} + \{y} = \{x + y}";
+ * }
+ * When the template expression is evaluated, an instance of {@link StringTemplate} is
+ * produced that returns the same lists from {@link StringTemplate#fragments()} and
+ * {@link StringTemplate#values()} as shown above. The {@link StringTemplate#STR} template
+ * processor uses these lists to yield an interpolated string. The value of {@code s} will
+ * be equivalent to {@code "10 + 20 = 30"}.
+ *
+ * The {@code interpolate()} method provides a direct way to perform string interpolation
+ * of a {@link StringTemplate}. Template processors can use the following code pattern:
+ * {@snippet :
+ * List fragments = st.fragments();
+ * List values = st.values();
+ * ... check or manipulate the fragments and/or values ...
+ * String result = StringTemplate.interpolate(fragments, values);
+ * }
+ * The {@link StringTemplate#process(Processor)} method, in conjunction with
+ * the {@link StringTemplate#RAW} processor, may be used to defer processing of a
+ * {@link StringTemplate}.
+ * {@snippet :
+ * StringTemplate st = RAW."\{x} + \{y} = \{x + y}";
+ * ...other steps...
+ * String result = st.process(STR);
+ * }
+ * The factory methods {@link StringTemplate#of(String)} and
+ * {@link StringTemplate#of(List, List)} can be used to construct a {@link StringTemplate}.
+ *
+ * @see Processor
+ * @see java.util.FormatProcessor
+ *
+ * @implNote Implementations of {@link StringTemplate} must minimally implement the
+ * methods {@link StringTemplate#fragments()} and {@link StringTemplate#values()}.
+ * Instances of {@link StringTemplate} are considered immutable. To preserve the
+ * semantics of string templates and text block templates, the list returned by
+ * {@link StringTemplate#fragments()} must be one element larger than the list returned
+ * by {@link StringTemplate#values()}.
+ *
+ * @since 21
+ *
+ * @jls 15.8.6 Process Template Expressions
+ */
+@PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+public interface StringTemplate {
+ /**
+ * Returns a list of fragment literals for this {@link StringTemplate}.
+ * The fragment literals are the character sequences preceding each of the embedded
+ * expressions in source code, plus the character sequence following the last
+ * embedded expression. Such character sequences may be zero-length if an embedded
+ * expression appears at the beginning or end of a template, or if two embedded
+ * expressions are directly adjacent in a template.
+ * In the example: {@snippet :
+ * String student = "Mary";
+ * String teacher = "Johnson";
+ * StringTemplate st = RAW."The student \{student} is in \{teacher}'s classroom.";
+ * List fragments = st.fragments(); // @highlight substring="fragments()"
+ * }
+ * {@code fragments} will be equivalent to
+ * {@code List.of("The student ", " is in ", "'s classroom.")}
+ *
+ * @return list of string fragments
+ *
+ * @implSpec the list returned is immutable
+ */
+ List fragments();
+
+ /**
+ * Returns a list of embedded expression results for this {@link StringTemplate}.
+ * In the example:
+ * {@snippet :
+ * String student = "Mary";
+ * String teacher = "Johnson";
+ * StringTemplate st = RAW."The student \{student} is in \{teacher}'s classroom.";
+ * List values = st.values(); // @highlight substring="values()"
+ * }
+ * {@code values} will be equivalent to {@code List.of(student, teacher)}
+ *
+ * @return list of expression values
+ *
+ * @implSpec the list returned is immutable
+ */
+ List values();
+
+ /**
+ * Returns the string interpolation of the fragments and values for this
+ * {@link StringTemplate}.
+ * @apiNote For better visibility and when practical, it is recommended to use the
+ * {@link StringTemplate#STR} processor instead of invoking the
+ * {@link StringTemplate#interpolate()} method.
+ * {@snippet :
+ * String student = "Mary";
+ * String teacher = "Johnson";
+ * StringTemplate st = RAW."The student \{student} is in \{teacher}'s classroom.";
+ * String result = st.interpolate(); // @highlight substring="interpolate()"
+ * }
+ * In the above example, the value of {@code result} will be
+ * {@code "The student Mary is in Johnson's classroom."}. This is
+ * produced by the interleaving concatenation of fragments and values from the supplied
+ * {@link StringTemplate}. To accommodate concatenation, values are converted to strings
+ * as if invoking {@link String#valueOf(Object)}.
+ *
+ * @return interpolation of this {@link StringTemplate}
+ *
+ * @implSpec The default implementation returns the result of invoking
+ * {@code StringTemplate.interpolate(this.fragments(), this.values())}.
+ */
+ default String interpolate() {
+ return StringTemplate.interpolate(fragments(), values());
+ }
+
+ /**
+ * Returns the result of applying the specified processor to this {@link StringTemplate}.
+ * This method can be used as an alternative to string template expressions. For example,
+ * {@snippet :
+ * String student = "Mary";
+ * String teacher = "Johnson";
+ * String result1 = STR."The student \{student} is in \{teacher}'s classroom.";
+ * String result2 = RAW."The student \{student} is in \{teacher}'s classroom.".process(STR); // @highlight substring="process"
+ * }
+ * Produces an equivalent result for both {@code result1} and {@code result2}.
+ *
+ * @param processor the {@link Processor} instance to process
+ *
+ * @param Processor's process result type.
+ * @param Exception thrown type.
+ *
+ * @return constructed object of type {@code R}
+ *
+ * @throws E exception thrown by the template processor when validation fails
+ * @throws NullPointerException if processor is null
+ *
+ * @implSpec The default implementation returns the result of invoking
+ * {@code processor.process(this)}. If the invocation throws an exception that
+ * exception is forwarded to the caller.
+ */
+ default R
+ process(Processor extends R, ? extends E> processor) throws E {
+ Objects.requireNonNull(processor, "processor should not be null");
+
+ return processor.process(this);
+ }
+
+ /**
+ * Produces a diagnostic string that describes the fragments and values of the supplied
+ * {@link StringTemplate}.
+ *
+ * @param stringTemplate the {@link StringTemplate} to represent
+ *
+ * @return diagnostic string representing the supplied string template
+ *
+ * @throws NullPointerException if stringTemplate is null
+ */
+ static String toString(StringTemplate stringTemplate) {
+ Objects.requireNonNull(stringTemplate, "stringTemplate should not be null");
+ return "StringTemplate{ fragments = [ \"" +
+ String.join("\", \"", stringTemplate.fragments()) +
+ "\" ], values = " +
+ stringTemplate.values() +
+ " }";
+ }
+
+ /**
+ * Returns a {@link StringTemplate} as if constructed by invoking
+ * {@code StringTemplate.of(List.of(string), List.of())}. That is, a {@link StringTemplate}
+ * with one fragment and no values.
+ *
+ * @param string single string fragment
+ *
+ * @return StringTemplate composed from string
+ *
+ * @throws NullPointerException if string is null
+ */
+ static StringTemplate of(String string) {
+ Objects.requireNonNull(string, "string must not be null");
+ JavaTemplateAccess JTA = SharedSecrets.getJavaTemplateAccess();
+ return JTA.of(List.of(string), List.of());
+ }
+
+ /**
+ * Returns a StringTemplate with the given fragments and values.
+ *
+ * @implSpec The {@code fragments} list size must be one more that the
+ * {@code values} list size.
+ *
+ * @param fragments list of string fragments
+ * @param values list of expression values
+ *
+ * @return StringTemplate composed from string
+ *
+ * @throws IllegalArgumentException if fragments list size is not one more
+ * than values list size
+ * @throws NullPointerException if fragments is null or values is null or if any fragment is null.
+ *
+ * @implNote Contents of both lists are copied to construct immutable lists.
+ */
+ static StringTemplate of(List fragments, List> values) {
+ Objects.requireNonNull(fragments, "fragments must not be null");
+ Objects.requireNonNull(values, "values must not be null");
+ if (values.size() + 1 != fragments.size()) {
+ throw new IllegalArgumentException(
+ "fragments list size is not one more than values list size");
+ }
+ JavaTemplateAccess JTA = SharedSecrets.getJavaTemplateAccess();
+ return JTA.of(fragments, values);
+ }
+
+ /**
+ * Creates a string that interleaves the elements of values between the
+ * elements of fragments. To accommodate interpolation, values are converted to strings
+ * as if invoking {@link String#valueOf(Object)}.
+ *
+ * @param fragments list of String fragments
+ * @param values list of expression values
+ *
+ * @return String interpolation of fragments and values
+ *
+ * @throws IllegalArgumentException if fragments list size is not one more
+ * than values list size
+ * @throws NullPointerException fragments or values is null or if any of the fragments is null
+ */
+ static String interpolate(List fragments, List> values) {
+ Objects.requireNonNull(fragments, "fragments must not be null");
+ Objects.requireNonNull(values, "values must not be null");
+ int fragmentsSize = fragments.size();
+ int valuesSize = values.size();
+ if (fragmentsSize != valuesSize + 1) {
+ throw new IllegalArgumentException("fragments must have one more element than values");
+ }
+ JavaTemplateAccess JTA = SharedSecrets.getJavaTemplateAccess();
+ return JTA.interpolate(fragments, values);
+ }
+
+ /**
+ * Combine zero or more {@link StringTemplate StringTemplates} into a single
+ * {@link StringTemplate}.
+ * {@snippet :
+ * StringTemplate st = StringTemplate.combine(RAW."\{a}", RAW."\{b}", RAW."\{c}");
+ * assert st.interpolate().equals(STR."\{a}\{b}\{c}");
+ * }
+ * Fragment lists from the {@link StringTemplate StringTemplates} are combined end to
+ * end with the last fragment from each {@link StringTemplate} concatenated with the
+ * first fragment of the next. To demonstrate, if we were to take two strings and we
+ * combined them as follows: {@snippet lang = "java":
+ * String s1 = "abc";
+ * String s2 = "xyz";
+ * String sc = s1 + s2;
+ * assert Objects.equals(sc, "abcxyz");
+ * }
+ * the last character {@code "c"} from the first string is juxtaposed with the first
+ * character {@code "x"} of the second string. The same would be true of combining
+ * {@link StringTemplate StringTemplates}.
+ * {@snippet lang ="java":
+ * StringTemplate st1 = RAW."a\{}b\{}c";
+ * StringTemplate st2 = RAW."x\{}y\{}z";
+ * StringTemplate st3 = RAW."a\{}b\{}cx\{}y\{}z";
+ * StringTemplate stc = StringTemplate.combine(st1, st2);
+ *
+ * assert Objects.equals(st1.fragments(), List.of("a", "b", "c"));
+ * assert Objects.equals(st2.fragments(), List.of("x", "y", "z"));
+ * assert Objects.equals(st3.fragments(), List.of("a", "b", "cx", "y", "z"));
+ * assert Objects.equals(stc.fragments(), List.of("a", "b", "cx", "y", "z"));
+ * }
+ * Values lists are simply concatenated to produce a single values list.
+ * The result is a well-formed {@link StringTemplate} with n+1 fragments and n values, where
+ * n is the total of number of values across all the supplied
+ * {@link StringTemplate StringTemplates}.
+ *
+ * @param stringTemplates zero or more {@link StringTemplate}
+ *
+ * @return combined {@link StringTemplate}
+ *
+ * @throws NullPointerException if stringTemplates is null or if any of the
+ * {@code stringTemplates} are null
+ *
+ * @implNote If zero {@link StringTemplate} arguments are provided then a
+ * {@link StringTemplate} with an empty fragment and no values is returned, as if invoking
+ * StringTemplate.of("")
. If only one {@link StringTemplate} argument is provided
+ * then it is returned unchanged.
+ */
+ static StringTemplate combine(StringTemplate... stringTemplates) {
+ JavaTemplateAccess JTA = SharedSecrets.getJavaTemplateAccess();
+ return JTA.combine(stringTemplates);
+ }
+
+ /**
+ * Combine a list of {@link StringTemplate StringTemplates} into a single
+ * {@link StringTemplate}.
+ * {@snippet :
+ * StringTemplate st = StringTemplate.combine(List.of(RAW."\{a}", RAW."\{b}", RAW."\{c}"));
+ * assert st.interpolate().equals(STR."\{a}\{b}\{c}");
+ * }
+ * Fragment lists from the {@link StringTemplate StringTemplates} are combined end to
+ * end with the last fragment from each {@link StringTemplate} concatenated with the
+ * first fragment of the next. To demonstrate, if we were to take two strings and we
+ * combined them as follows: {@snippet lang = "java":
+ * String s1 = "abc";
+ * String s2 = "xyz";
+ * String sc = s1 + s2;
+ * assert Objects.equals(sc, "abcxyz");
+ * }
+ * the last character {@code "c"} from the first string is juxtaposed with the first
+ * character {@code "x"} of the second string. The same would be true of combining
+ * {@link StringTemplate StringTemplates}.
+ * {@snippet lang ="java":
+ * StringTemplate st1 = RAW."a\{}b\{}c";
+ * StringTemplate st2 = RAW."x\{}y\{}z";
+ * StringTemplate st3 = RAW."a\{}b\{}cx\{}y\{}z";
+ * StringTemplate stc = StringTemplate.combine(List.of(st1, st2));
+ *
+ * assert Objects.equals(st1.fragments(), List.of("a", "b", "c"));
+ * assert Objects.equals(st2.fragments(), List.of("x", "y", "z"));
+ * assert Objects.equals(st3.fragments(), List.of("a", "b", "cx", "y", "z"));
+ * assert Objects.equals(stc.fragments(), List.of("a", "b", "cx", "y", "z"));
+ * }
+ * Values lists are simply concatenated to produce a single values list.
+ * The result is a well-formed {@link StringTemplate} with n+1 fragments and n values, where
+ * n is the total of number of values across all the supplied
+ * {@link StringTemplate StringTemplates}.
+ *
+ * @param stringTemplates list of {@link StringTemplate}
+ *
+ * @return combined {@link StringTemplate}
+ *
+ * @throws NullPointerException if stringTemplates is null or if any of the
+ * its elements are null
+ *
+ * @implNote If {@code stringTemplates.size() == 0} then a {@link StringTemplate} with
+ * an empty fragment and no values is returned, as if invoking
+ * StringTemplate.of("")
. If {@code stringTemplates.size() == 1}
+ * then the first element of the list is returned unchanged.
+ */
+ static StringTemplate combine(List stringTemplates) {
+ JavaTemplateAccess JTA = SharedSecrets.getJavaTemplateAccess();
+ return JTA.combine(stringTemplates.toArray(new StringTemplate[0]));
+ }
+
+ /**
+ * This {@link Processor} instance is conventionally used for the string interpolation
+ * of a supplied {@link StringTemplate}.
+ *
+ * For better visibility and when practical, it is recommended that users use the
+ * {@link StringTemplate#STR} processor instead of invoking the
+ * {@link StringTemplate#interpolate()} method.
+ * Example: {@snippet :
+ * int x = 10;
+ * int y = 20;
+ * String result = STR."\{x} + \{y} = \{x + y}"; // @highlight substring="STR"
+ * }
+ * In the above example, the value of {@code result} will be {@code "10 + 20 = 30"}. This is
+ * produced by the interleaving concatenation of fragments and values from the supplied
+ * {@link StringTemplate}. To accommodate concatenation, values are converted to strings
+ * as if invoking {@link String#valueOf(Object)}.
+ * @apiNote {@link StringTemplate#STR} is statically imported implicitly into every
+ * Java compilation unit.
+ */
+ Processor STR = StringTemplate::interpolate;
+
+ /**
+ * This {@link Processor} instance is conventionally used to indicate that the
+ * processing of the {@link StringTemplate} is to be deferred to a later time. Deferred
+ * processing can be resumed by invoking the
+ * {@link StringTemplate#process(Processor)} or
+ * {@link Processor#process(StringTemplate)} methods.
+ * {@snippet :
+ * import static java.lang.StringTemplate.RAW;
+ * ...
+ * StringTemplate st = RAW."\{x} + \{y} = \{x + y}";
+ * ...other steps...
+ * String result = STR.process(st);
+ * }
+ * @implNote Unlike {@link StringTemplate#STR}, {@link StringTemplate#RAW} must be
+ * statically imported explicitly.
+ */
+ Processor RAW = st -> st;
+
+ /**
+ * This interface describes the methods provided by a generalized string template processor. The
+ * primary method {@link Processor#process(StringTemplate)} is used to validate
+ * and compose a result using a {@link StringTemplate StringTemplate's} fragments and values lists.
+ *
+ * For example:
+ * {@snippet :
+ * class MyProcessor implements Processor {
+ * @Override
+ * public String process(StringTemplate st) throws IllegalArgumentException {
+ * StringBuilder sb = new StringBuilder();
+ * Iterator fragmentsIter = st.fragments().iterator();
+ *
+ * for (Object value : st.values()) {
+ * sb.append(fragmentsIter.next());
+ *
+ * if (value instanceof Boolean) {
+ * throw new IllegalArgumentException("I don't like Booleans");
+ * }
+ *
+ * sb.append(value);
+ * }
+ *
+ * sb.append(fragmentsIter.next());
+ *
+ * return sb.toString();
+ * }
+ * }
+ *
+ * MyProcessor myProcessor = new MyProcessor();
+ * try {
+ * int x = 10;
+ * int y = 20;
+ * String result = myProcessor."\{x} + \{y} = \{x + y}";
+ * ...
+ * } catch (IllegalArgumentException ex) {
+ * ...
+ * }
+ * }
+ * Implementations of this interface may provide, but are not limited to, validating
+ * inputs, composing inputs into a result, and transforming an intermediate string
+ * result to a non-string value before delivering the final result.
+ *
+ * The user has the option of validating inputs used in composition. For example an SQL
+ * processor could prevent injection vulnerabilities by sanitizing inputs or throwing an
+ * exception of type {@code E} if an SQL statement is a potential vulnerability.
+ *
+ * Composing allows user control over how the result is assembled. Most often, a
+ * user will construct a new string from the string template, with placeholders
+ * replaced by string representations of value list elements. These string
+ * representations are created as if invoking {@link String#valueOf}.
+ *
+ * Transforming allows the processor to return something other than a string. For
+ * instance, a JSON processor could return a JSON object, by parsing the string created
+ * by composition, instead of the composed string.
+ *
+ * {@link Processor} is a {@link FunctionalInterface}. This permits
+ * declaration of a processor using lambda expressions;
+ * {@snippet :
+ * Processor processor = st -> {
+ * List fragments = st.fragments();
+ * List values = st.values();
+ * // check or manipulate the fragments and/or values
+ * ...
+ * return StringTemplate.interpolate(fragments, values);
+ * };
+ * }
+ * The {@link StringTemplate#interpolate()} method is available for those processors
+ * that just need to work with the string interpolation;
+ * {@snippet :
+ * Processor processor = StringTemplate::interpolate;
+ * }
+ * or simply transform the string interpolation into something other than
+ * {@link String};
+ * {@snippet :
+ * Processor jsonProcessor = st -> new JSONObject(st.interpolate());
+ * }
+ * @implNote The Java compiler automatically imports {@link StringTemplate#STR}
+ *
+ * @param Processor's process result type
+ * @param Exception thrown type
+ *
+ * @see StringTemplate
+ * @see java.util.FormatProcessor
+ *
+ * @since 21
+ *
+ * @jls 15.8.6 Process Template Expressions
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ @FunctionalInterface
+ public interface Processor {
+
+ /**
+ * Constructs a result based on the template fragments and values in the
+ * supplied {@link StringTemplate stringTemplate} object.
+ * @apiNote Processing of a {@link StringTemplate} may include validation according to the particular facts relating
+ * to each situation. The {@code E} type parameter indicates the type of checked exception that is thrown by
+ * {@link #process} if validation fails, ex. {@code java.sql.SQLException}. If no checked exception is expected
+ * then {@link RuntimeException} may be used. Note that unchecked exceptions, such as {@link RuntimeException},
+ * {@link NullPointerException} or {@link IllegalArgumentException} may be thrown as part of the normal
+ * method arguments processing. Details of which exceptions are thrown will be found in the documentation
+ * of the specific implementation.
+ *
+ * @param stringTemplate a {@link StringTemplate} instance
+ *
+ * @return constructed object of type R
+ *
+ * @throws E exception thrown by the template processor when validation fails
+ */
+ R process(StringTemplate stringTemplate) throws E;
+
+ /**
+ * This factory method can be used to create a {@link Processor} containing a
+ * {@link Processor#process} method derived from a lambda expression. As an example;
+ * {@snippet :
+ * Processor mySTR = Processor.of(StringTemplate::interpolate);
+ * int x = 10;
+ * int y = 20;
+ * String str = mySTR."\{x} + \{y} = \{x + y}";
+ * }
+ * The result type of the constructed {@link Processor} may be derived from
+ * the lambda expression, thus this method may be used in a var
+ * statement. For example, {@code mySTR} from above can also be declared using;
+ * {@snippet :
+ * var mySTR = Processor.of(StringTemplate::interpolate);
+ * }
+ * {@link RuntimeException} is the assumed exception thrown type.
+ *
+ * @param process a function that takes a {@link StringTemplate} as an argument
+ * and returns the inferred result type
+ *
+ * @return a {@link Processor}
+ *
+ * @param Processor's process result type
+ */
+ static Processor of(Function super StringTemplate, ? extends T> process) {
+ return process::apply;
+ }
+
+ /**
+ * Built-in policies using this additional interface have the flexibility to
+ * specialize the composition of the templated string by returning a customized
+ * {@link MethodHandle} from {@link Linkage#linkage linkage}.
+ * These specializations are typically implemented to improve performance;
+ * specializing value types or avoiding boxing and vararg arrays.
+ *
+ * @implNote This interface is sealed to only allow standard processors.
+ *
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public sealed interface Linkage permits FormatProcessor {
+ /**
+ * This method creates a {@link MethodHandle} that when invoked with arguments of
+ * those specified in {@code type} returns a result that equals that returned by
+ * the template processor's process method. The difference being that this method
+ * can preview the template's fragments and value types in advance of usage and
+ * thereby has the opportunity to produce a specialized implementation.
+ *
+ * @param fragments string template fragments
+ * @param type method type, includes the StringTemplate receiver as
+ * well as the value types
+ *
+ * @return {@link MethodHandle} for the processor applied to template
+ *
+ * @throws NullPointerException if any of the arguments are null
+ */
+ MethodHandle linkage(List fragments, MethodType type);
+ }
+ }
+
+}
diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java
index f9cb79d2d48..dead2892e1c 100644
--- a/src/java.base/share/classes/java/lang/System.java
+++ b/src/java.base/share/classes/java/lang/System.java
@@ -77,10 +77,11 @@ import jdk.internal.reflect.CallerSensitive;
import jdk.internal.reflect.Reflection;
import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.SharedSecrets;
-import jdk.internal.misc.VM;
+import jdk.internal.javac.PreviewFeature;
import jdk.internal.logger.LoggerFinderLoader;
import jdk.internal.logger.LazyLoggers;
import jdk.internal.logger.LocalizedLoggerWrapper;
+import jdk.internal.misc.VM;
import jdk.internal.util.SystemProps;
import jdk.internal.vm.Continuation;
import jdk.internal.vm.ContinuationScope;
@@ -2522,6 +2523,23 @@ public final class System {
return StringConcatHelper.mix(lengthCoder, constant);
}
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public long stringConcatCoder(char value) {
+ return StringConcatHelper.coder(value);
+ }
+
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public long stringBuilderConcatMix(long lengthCoder,
+ StringBuilder sb) {
+ return sb.mix(lengthCoder);
+ }
+
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public long stringBuilderConcatPrepend(long lengthCoder, byte[] buf,
+ StringBuilder sb) {
+ return sb.prepend(lengthCoder, buf);
+ }
+
public String join(String prefix, String suffix, String delimiter, String[] elements, int size) {
return String.join(prefix, suffix, delimiter, elements, size);
}
diff --git a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
index 1d4278122b7..629aab424bd 100644
--- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
+++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
@@ -27,10 +27,15 @@ package java.lang.invoke;
import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.SharedSecrets;
+import jdk.internal.javac.PreviewFeature;
+import jdk.internal.util.FormatConcatItem;
import jdk.internal.vm.annotation.Stable;
import sun.invoke.util.Wrapper;
import java.lang.invoke.MethodHandles.Lookup;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
import java.util.Objects;
import static java.lang.invoke.MethodType.methodType;
@@ -110,8 +115,14 @@ public final class StringConcatFactory {
* While the maximum number of argument slots that indy call can handle is 253,
* we do not use all those slots, to let the strategies with MethodHandle
* combinators to use some arguments.
+ *
+ * @since 21
*/
- private static final int MAX_INDY_CONCAT_ARG_SLOTS = 200;
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public static final int MAX_INDY_CONCAT_ARG_SLOTS;
+ // Use static initialize block to avoid MAX_INDY_CONCAT_ARG_SLOTS being treating
+ // as a constant for constant folding.
+ static { MAX_INDY_CONCAT_ARG_SLOTS = 200; }
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
@@ -321,6 +332,7 @@ public final class StringConcatFactory {
{
Objects.requireNonNull(lookup, "Lookup is null");
Objects.requireNonNull(name, "Name is null");
+ Objects.requireNonNull(recipe, "Recipe is null");
Objects.requireNonNull(concatType, "Concat type is null");
Objects.requireNonNull(constants, "Constants are null");
@@ -488,14 +500,12 @@ public final class StringConcatFactory {
// Use int as the logical type for subword integral types
// (byte and short). char and boolean require special
// handling so don't change the logical type of those
- if (cl == byte.class || cl == short.class) {
- ptypes[i] = int.class;
- }
+ ptypes[i] = promoteToIntType(ptypes[i]);
// Object, float and double will be eagerly transformed
// into a (non-null) String as a first step after invocation.
// Set up to use String as the logical type for such arguments
// internally.
- else if (cl == Object.class) {
+ if (cl == Object.class) {
if (objFilters == null) {
objFilters = new MethodHandle[ptypes.length];
}
@@ -664,7 +674,6 @@ public final class StringConcatFactory {
return argPositions;
}
-
private static MethodHandle foldInLastMixers(MethodHandle mh, long initialLengthCoder, int pos, Class>[] ptypes, int count) {
MethodHandle mix = switch (count) {
case 1 -> mixer(ptypes[pos]);
@@ -710,6 +719,9 @@ public final class StringConcatFactory {
int idx = classIndex(cl);
MethodHandle prepend = PREPENDERS[idx];
if (prepend == null) {
+ if (idx == STRING_CONCAT_ITEM) {
+ cl = FormatConcatItem.class;
+ }
PREPENDERS[idx] = prepend = JLA.stringConcatHelper("prepend",
methodType(long.class, long.class, byte[].class,
Wrapper.asPrimitiveType(cl), String.class)).rebind();
@@ -722,13 +734,15 @@ public final class StringConcatFactory {
LONG_IDX = 2,
BOOLEAN_IDX = 3,
STRING_IDX = 4,
- TYPE_COUNT = 5;
+ STRING_CONCAT_ITEM = 5,
+ TYPE_COUNT = 6;
private static int classIndex(Class> cl) {
- if (cl == String.class) return STRING_IDX;
- if (cl == int.class) return INT_IDX;
- if (cl == boolean.class) return BOOLEAN_IDX;
- if (cl == char.class) return CHAR_IDX;
- if (cl == long.class) return LONG_IDX;
+ if (cl == String.class) return STRING_IDX;
+ if (cl == int.class) return INT_IDX;
+ if (cl == boolean.class) return BOOLEAN_IDX;
+ if (cl == char.class) return CHAR_IDX;
+ if (cl == long.class) return LONG_IDX;
+ if (FormatConcatItem.class.isAssignableFrom(cl)) return STRING_CONCAT_ITEM;
throw new IllegalArgumentException("Unexpected class: " + cl);
}
@@ -986,6 +1000,33 @@ public final class StringConcatFactory {
private static final @Stable MethodHandle[] MIXERS = new MethodHandle[TYPE_COUNT];
private static final long INITIAL_CODER = JLA.stringConcatInitialCoder();
+ /**
+ * Promote integral types to int.
+ */
+ private static Class> promoteToIntType(Class> t) {
+ // use int for subword integral types; still need special mixers
+ // and prependers for char, boolean
+ return t == byte.class || t == short.class ? int.class : t;
+ }
+
+ /**
+ * Returns a stringifier for references and floats/doubles only.
+ * Always returns null for other primitives.
+ *
+ * @param t class to stringify
+ * @return stringifier; null, if not available
+ */
+ private static MethodHandle stringifierFor(Class> t) {
+ if (t == Object.class) {
+ return objectStringifier();
+ } else if (t == float.class) {
+ return floatStringifier();
+ } else if (t == double.class) {
+ return doubleStringifier();
+ }
+ return null;
+ }
+
private static MethodHandle stringValueOf(Class> ptype) {
try {
return MethodHandles.publicLookup()
@@ -998,4 +1039,304 @@ public final class StringConcatFactory {
private StringConcatFactory() {
// no instantiation
}
+
+ /**
+ * Simplified concatenation method to facilitate {@link StringTemplate}
+ * concatenation. This method returns a single concatenation method that
+ * interleaves fragments and values. fragment|value|fragment|value|...|value|fragment.
+ * The number of fragments must be one more that the number of ptypes.
+ * The total number of slots used by the ptypes must be less than or equal
+ * to {@link #MAX_INDY_CONCAT_ARG_SLOTS}.
+ *
+ * @param fragments list of string fragments
+ * @param ptypes list of expression types
+ *
+ * @return the {@link MethodHandle} for concatenation
+ *
+ * @throws StringConcatException If any of the linkage invariants are violated.
+ * @throws NullPointerException If any of the incoming arguments is null.
+ * @throws IllegalArgumentException If the number of value slots exceed {@link #MAX_INDY_CONCAT_ARG_SLOTS}.
+ *
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public static MethodHandle makeConcatWithTemplate(
+ List fragments,
+ List> ptypes)
+ throws StringConcatException
+ {
+ Objects.requireNonNull(fragments, "fragments is null");
+ Objects.requireNonNull(ptypes, "ptypes is null");
+ ptypes = List.copyOf(ptypes);
+
+ if (fragments.size() != ptypes.size() + 1) {
+ throw new IllegalArgumentException("fragments size not equal ptypes size plus one");
+ }
+
+ if (ptypes.isEmpty()) {
+ return MethodHandles.constant(String.class, fragments.get(0));
+ }
+
+ Class>[] ttypes = new Class>[ptypes.size()];
+ MethodHandle[] filters = new MethodHandle[ptypes.size()];
+ int slots = 0;
+
+ int pos = 0;
+ for (Class> ptype : ptypes) {
+ slots += ptype == long.class || ptype == double.class ? 2 : 1;
+
+ if (MAX_INDY_CONCAT_ARG_SLOTS < slots) {
+ throw new StringConcatException("Too many concat argument slots: " +
+ slots + ", can only accept " + MAX_INDY_CONCAT_ARG_SLOTS);
+ }
+
+ boolean isSpecialized = ptype.isPrimitive();
+ boolean isFormatConcatItem = FormatConcatItem.class.isAssignableFrom(ptype);
+ Class> ttype = isSpecialized ? promoteToIntType(ptype) :
+ isFormatConcatItem ? FormatConcatItem.class : Object.class;
+ MethodHandle filter = isFormatConcatItem ? null : stringifierFor(ttype);
+
+ if (filter != null) {
+ filters[pos] = filter;
+ ttype = String.class;
+ }
+
+ ttypes[pos++] = ttype;
+ }
+
+ MethodHandle mh = MethodHandles.dropArguments(newString(), 2, ttypes);
+
+ long initialLengthCoder = INITIAL_CODER;
+ String lastFragment = "";
+ pos = 0;
+ for (String fragment : fragments) {
+ lastFragment = fragment;
+
+ if (ttypes.length <= pos) {
+ break;
+ }
+
+ Class> ttype = ttypes[pos];
+ // (long,byte[],ttype) -> long
+ MethodHandle prepender = prepender(lastFragment.isEmpty() ? null : fragment, ttype);
+ initialLengthCoder = JLA.stringConcatMix(initialLengthCoder, fragment);
+ // (byte[],long,ttypes...) -> String (unchanged)
+ mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepender,1, 0, 2 + pos);
+
+ pos++;
+ }
+
+ MethodHandle newArrayCombinator = lastFragment.isEmpty() ? newArray() :
+ newArrayWithSuffix(lastFragment);
+ // (long,ttypes...) -> String
+ mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArrayCombinator,
+ 1 // index
+ );
+
+ pos = 0;
+ for (Class> ttype : ttypes) {
+ // (long,ttype) -> long
+ MethodHandle mix = mixer(ttypes[pos]);
+ boolean lastPType = pos == ttypes.length - 1;
+
+ if (lastPType) {
+ // (ttype) -> long
+ mix = MethodHandles.insertArguments(mix, 0, initialLengthCoder);
+ // (ttypes...) -> String
+ mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, mix,
+ 1 + pos // selected argument
+ );
+ } else {
+ // (long,ttypes...) -> String
+ mh = MethodHandles.filterArgumentsWithCombiner(mh, 0, mix,
+ 0, // old-index
+ 1 + pos // selected argument
+ );
+ }
+
+ pos++;
+ }
+
+ mh = MethodHandles.filterArguments(mh, 0, filters);
+ MethodType mt = MethodType.methodType(String.class, ptypes);
+ mh = mh.viewAsType(mt, true);
+
+ return mh;
+ }
+
+ /**
+ * This method breaks up large concatenations into separate
+ * {@link MethodHandle MethodHandles} based on the number of slots required
+ * per {@link MethodHandle}. Each {@link MethodHandle} after the first will
+ * have an extra {@link String} slot for the result from the previous
+ * {@link MethodHandle}.
+ * {@link #makeConcatWithTemplate}
+ * is used to construct the {@link MethodHandle MethodHandles}. The total
+ * number of slots used by the ptypes is open ended. However, care must
+ * be given when combining the {@link MethodHandle MethodHandles} so that
+ * the combine total does not exceed the 255 slot limit.
+ *
+ * @param fragments list of string fragments
+ * @param ptypes list of expression types
+ * @param maxSlots maximum number of slots per {@link MethodHandle}.
+ *
+ * @return List of {@link MethodHandle MethodHandles}
+ *
+ * @throws IllegalArgumentException If maxSlots is not between 1 and
+ * MAX_INDY_CONCAT_ARG_SLOTS.
+ * @throws StringConcatException If any of the linkage invariants are violated.
+ * @throws NullPointerException If any of the incoming arguments is null.
+ * @throws IllegalArgumentException If the number of value slots exceed {@link #MAX_INDY_CONCAT_ARG_SLOTS}.
+ *
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public static List makeConcatWithTemplateCluster(
+ List fragments,
+ List> ptypes,
+ int maxSlots)
+ throws StringConcatException
+ {
+ Objects.requireNonNull(fragments, "fragments is null");
+ Objects.requireNonNull(ptypes, "ptypes is null");
+
+ if (fragments.size() != ptypes.size() + 1) {
+ throw new StringConcatException("fragments size not equal ptypes size plus one");
+ }
+
+ if (maxSlots < 1 || MAX_INDY_CONCAT_ARG_SLOTS < maxSlots) {
+ throw new IllegalArgumentException("maxSlots must be between 1 and " +
+ MAX_INDY_CONCAT_ARG_SLOTS);
+
+ }
+
+ if (ptypes.isEmpty()) {
+ return List.of(MethodHandles.constant(String.class, fragments.get(0)));
+ }
+
+ List mhs = new ArrayList<>();
+ List fragmentsSection = new ArrayList<>();
+ List> ptypeSection = new ArrayList<>();
+ int slots = 0;
+
+ int pos = 0;
+ for (Class> ptype : ptypes) {
+ boolean lastPType = pos == ptypes.size() - 1;
+ fragmentsSection.add(fragments.get(pos));
+ ptypeSection.add(ptype);
+
+ slots += ptype == long.class || ptype == double.class ? 2 : 1;
+
+ if (maxSlots <= slots || lastPType) {
+ fragmentsSection.add(lastPType ? fragments.get(pos + 1) : "");
+ MethodHandle mh = makeConcatWithTemplate(fragmentsSection,
+ ptypeSection);
+ mhs.add(mh);
+ fragmentsSection.clear();
+ fragmentsSection.add("");
+ ptypeSection.clear();
+ ptypeSection.add(String.class);
+ slots = 1;
+ }
+
+ pos++;
+ }
+
+ return mhs;
+ }
+
+ /**
+ * This method creates a {@link MethodHandle} expecting one input, the
+ * receiver of the supplied getters. This method uses
+ * {@link #makeConcatWithTemplateCluster}
+ * to create the intermediate {@link MethodHandle MethodHandles}.
+ *
+ * @param fragments list of string fragments
+ * @param getters list of getter {@link MethodHandle MethodHandles}
+ * @param maxSlots maximum number of slots per {@link MethodHandle} in
+ * cluster.
+ *
+ * @return the {@link MethodHandle} for concatenation
+ *
+ * @throws IllegalArgumentException If maxSlots is not between 1 and
+ * MAX_INDY_CONCAT_ARG_SLOTS or if the
+ * getters don't use the same argument type
+ * @throws StringConcatException If any of the linkage invariants are violated
+ * @throws NullPointerException If any of the incoming arguments is null
+ * @throws IllegalArgumentException If the number of value slots exceed {@link #MAX_INDY_CONCAT_ARG_SLOTS}.
+ *
+ * @since 21
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ public static MethodHandle makeConcatWithTemplateGetters(
+ List fragments,
+ List getters,
+ int maxSlots)
+ throws StringConcatException
+ {
+ Objects.requireNonNull(fragments, "fragments is null");
+ Objects.requireNonNull(getters, "getters is null");
+
+ if (fragments.size() != getters.size() + 1) {
+ throw new StringConcatException("fragments size not equal getters size plus one");
+ }
+
+ if (maxSlots < 1 || MAX_INDY_CONCAT_ARG_SLOTS < maxSlots) {
+ throw new IllegalArgumentException("maxSlots must be between 1 and " +
+ MAX_INDY_CONCAT_ARG_SLOTS);
+
+ }
+
+ if (getters.size() == 0) {
+ throw new StringConcatException("no getters supplied");
+ }
+
+ Class> receiverType = null;
+ List> ptypes = new ArrayList<>();
+
+ for (MethodHandle getter : getters) {
+ MethodType mt = getter.type();
+ Class> returnType = mt.returnType();
+
+ if (returnType == void.class || mt.parameterCount() != 1) {
+ throw new StringConcatException("not a getter " + mt);
+ }
+
+ if (receiverType == null) {
+ receiverType = mt.parameterType(0);
+ } else if (receiverType != mt.parameterType(0)) {
+ throw new StringConcatException("not the same receiever type " +
+ mt + " needs " + receiverType);
+ }
+
+ ptypes.add(returnType);
+ }
+
+ MethodType resultType = MethodType.methodType(String.class, receiverType);
+ List clusters = makeConcatWithTemplateCluster(fragments, ptypes,
+ maxSlots);
+
+ MethodHandle mh = null;
+ Iterator getterIterator = getters.iterator();
+
+ for (MethodHandle cluster : clusters) {
+ MethodType mt = cluster.type();
+ MethodHandle[] filters = new MethodHandle[mt.parameterCount()];
+ int pos = 0;
+
+ if (mh != null) {
+ filters[pos++] = mh;
+ }
+
+ while (pos < filters.length) {
+ filters[pos++] = getterIterator.next();
+ }
+
+ cluster = MethodHandles.filterArguments(cluster, 0, filters);
+ mh = MethodHandles.permuteArguments(cluster, resultType,
+ new int[filters.length]);
+ }
+
+ return mh;
+ }
}
diff --git a/src/java.base/share/classes/java/lang/runtime/Carriers.java b/src/java.base/share/classes/java/lang/runtime/Carriers.java
new file mode 100644
index 00000000000..e0ebc998ee5
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/Carriers.java
@@ -0,0 +1,1004 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.invoke.MethodType;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+import jdk.internal.misc.Unsafe;
+
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * A carrier is an opaque object that can be used to store component values
+ * while avoiding primitive boxing associated with collection objects. Component values
+ * can be primitive or Object.
+ *
+ * Clients can create new carrier instances by describing a carrier shape , that
+ * is, a {@linkplain MethodType method type} whose parameter types describe the types of
+ * the carrier component values, or by providing the parameter types directly.
+ *
+ * {@snippet :
+ * // Create a carrier for a string and an integer
+ * CarrierElements elements = CarrierFactory.of(String.class, int.class);
+ * // Fetch the carrier constructor MethodHandle
+ * MethodHandle initializingConstructor = elements.initializingConstructor();
+ * // Fetch the list of carrier component MethodHandles
+ * List components = elements.components();
+ *
+ * // Create an instance of the carrier with a string and an integer
+ * Object carrier = initializingConstructor.invokeExact("abc", 10);
+ * // Extract the first component, type string
+ * String string = (String)components.get(0).invokeExact(carrier);
+ * // Extract the second component, type int
+ * int i = (int)components.get(1).invokeExact(carrier);
+ * }
+ *
+ * Alternatively, the client can use static methods when the carrier use is scattered.
+ * This is possible since {@link Carriers} ensures that the same underlying carrier
+ * class is used when the same component types are provided.
+ *
+ * {@snippet :
+ * // Describe carrier using a MethodType
+ * MethodType mt = MethodType.methodType(Object.class, String.class, int.class);
+ * // Fetch the carrier constructor MethodHandle
+ * MethodHandle constructor = Carriers.constructor(mt);
+ * // Fetch the list of carrier component MethodHandles
+ * List components = Carriers.components(mt);
+ * }
+ *
+ * @implNote The strategy for storing components is deliberately left unspecified
+ * so that future improvements will not be hampered by issues of backward compatibility.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+final class Carriers {
+ /**
+ * Maximum number of components in a carrier (based on the maximum
+ * number of args to a constructor.)
+ */
+ public static final int MAX_COMPONENTS = 255 - /* this */ 1;
+
+ /**
+ * Number of integer slots used by a long.
+ */
+ static final int LONG_SLOTS = Long.SIZE / Integer.SIZE;
+
+ /*
+ * Initialize {@link MethodHandle} constants.
+ */
+ static {
+ try {
+ Lookup lookup = MethodHandles.lookup();
+ FLOAT_TO_INT = lookup.findStatic(Float.class, "floatToRawIntBits",
+ methodType(int.class, float.class));
+ INT_TO_FLOAT = lookup.findStatic(Float.class, "intBitsToFloat",
+ methodType(float.class, int.class));
+ DOUBLE_TO_LONG = lookup.findStatic(Double.class, "doubleToRawLongBits",
+ methodType(long.class, double.class));
+ LONG_TO_DOUBLE = lookup.findStatic(Double.class, "longBitsToDouble",
+ methodType(double.class, long.class));
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError("carrier static init fail", ex);
+ }
+ }
+
+ /*
+ * float/double conversions.
+ */
+ private static final MethodHandle FLOAT_TO_INT;
+ private static final MethodHandle INT_TO_FLOAT;
+ private static final MethodHandle DOUBLE_TO_LONG;
+ private static final MethodHandle LONG_TO_DOUBLE;
+
+ /**
+ * Given an initializer {@link MethodHandle} recast and reorder arguments to
+ * match shape.
+ *
+ * @param carrierShape carrier shape
+ * @param initializer carrier constructor to reshape
+ *
+ * @return constructor with arguments recasted and reordered
+ */
+ static MethodHandle reshapeInitializer(CarrierShape carrierShape,
+ MethodHandle initializer) {
+ int count = carrierShape.count();
+ Class>[] ptypes = carrierShape.ptypes();
+ int objectIndex = carrierShape.objectOffset() + 1;
+ int intIndex = carrierShape.intOffset() + 1;
+ int longIndex = carrierShape.longOffset() + 1;
+ int[] reorder = new int[count + 1];
+ Class>[] permutePTypes = new Class>[count + 1];
+ MethodHandle[] filters = new MethodHandle[count + 1];
+ boolean hasFilters = false;
+ permutePTypes[0] = CarrierObject.class;
+ reorder[0] = 0;
+ int index = 1;
+
+ for (Class> ptype : ptypes) {
+ MethodHandle filter = null;
+ int from;
+
+ if (!ptype.isPrimitive()) {
+ from = objectIndex++;
+ ptype = Object.class;
+ } else if (ptype == double.class) {
+ from = longIndex++;
+ filter = DOUBLE_TO_LONG;
+ } else if (ptype == float.class) {
+ from = intIndex++;
+ filter = FLOAT_TO_INT;
+ } else if (ptype == long.class) {
+ from = longIndex++;
+ } else {
+ from = intIndex++;
+ ptype = int.class;
+ }
+
+ permutePTypes[index] = ptype;
+ reorder[from] = index++;
+
+ if (filter != null) {
+ filters[from] = filter;
+ hasFilters = true;
+ }
+ }
+
+ if (hasFilters) {
+ initializer = MethodHandles.filterArguments(initializer, 0, filters);
+ }
+
+ MethodType permutedMethodType =
+ methodType(initializer.type().returnType(), permutePTypes);
+ initializer = MethodHandles.permuteArguments(initializer,
+ permutedMethodType, reorder);
+ initializer = MethodHandles.explicitCastArguments(initializer,
+ methodType(CarrierObject.class, ptypes).insertParameterTypes(0, CarrierObject.class));
+
+ return initializer;
+ }
+
+ /**
+ * Given components array, recast and reorder components to match shape.
+ *
+ * @param carrierShape carrier reshape
+ * @param components carrier components to reshape
+ *
+ * @return list of components reshaped
+ */
+ static List reshapeComponents(CarrierShape carrierShape,
+ MethodHandle[] components) {
+ int count = carrierShape.count();
+ Class>[] ptypes = carrierShape.ptypes();
+ MethodHandle[] reorder = new MethodHandle[count];
+ int objectIndex = carrierShape.objectOffset();
+ int intIndex = carrierShape.intOffset();
+ int longIndex = carrierShape.longOffset();
+ int index = 0;
+
+ for (Class> ptype : ptypes) {
+ MethodHandle component;
+
+ if (!ptype.isPrimitive()) {
+ component = components[objectIndex++];
+ } else if (ptype == double.class) {
+ component = MethodHandles.filterReturnValue(
+ components[longIndex++], LONG_TO_DOUBLE);
+ } else if (ptype == float.class) {
+ component = MethodHandles.filterReturnValue(
+ components[intIndex++], INT_TO_FLOAT);
+ } else if (ptype == long.class) {
+ component = components[longIndex++];
+ } else {
+ component = components[intIndex++];
+ }
+
+ MethodType methodType = methodType(ptype, CarrierObject.class);
+ reorder[index++] =
+ MethodHandles.explicitCastArguments(component, methodType);
+ }
+
+ return List.of(reorder);
+ }
+
+ /**
+ * Factory for carriers that are backed by long[] and Object[].
+ */
+ static final class CarrierObjectFactory {
+ /**
+ * Unsafe access.
+ */
+ private static final Unsafe UNSAFE;
+
+ /*
+ * Constructor accessor MethodHandles.
+ */
+ private static final MethodHandle CONSTRUCTOR;
+ private static final MethodHandle GET_LONG;
+ private static final MethodHandle PUT_LONG;
+ private static final MethodHandle GET_INTEGER;
+ private static final MethodHandle PUT_INTEGER;
+ private static final MethodHandle GET_OBJECT;
+ private static final MethodHandle PUT_OBJECT;
+
+ static {
+ try {
+ UNSAFE = Unsafe.getUnsafe();
+ Lookup lookup = MethodHandles.lookup();
+ CONSTRUCTOR = lookup.findConstructor(CarrierObject.class,
+ methodType(void.class, int.class, int.class));
+ GET_LONG = lookup.findVirtual(CarrierObject.class, "getLong",
+ methodType(long.class, int.class));
+ PUT_LONG = lookup.findVirtual(CarrierObject.class, "putLong",
+ methodType(CarrierObject.class, int.class, long.class));
+ GET_INTEGER = lookup.findVirtual(CarrierObject.class, "getInteger",
+ methodType(int.class, int.class));
+ PUT_INTEGER = lookup.findVirtual(CarrierObject.class, "putInteger",
+ methodType(CarrierObject.class, int.class, int.class));
+ GET_OBJECT = lookup.findVirtual(CarrierObject.class, "getObject",
+ methodType(Object.class, int.class));
+ PUT_OBJECT = lookup.findVirtual(CarrierObject.class, "putObject",
+ methodType(CarrierObject.class, int.class, Object.class));
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError("carrier static init fail", ex);
+ }
+ }
+
+ /**
+ * Constructor builder.
+ *
+ * @param carrierShape carrier object shape
+ *
+ * @return {@link MethodHandle} to generic carrier constructor.
+ */
+ MethodHandle constructor(CarrierShape carrierShape) {
+ int objectCount = carrierShape.objectCount();
+ int primitiveCount = carrierShape.primitiveCount();
+
+ MethodHandle constructor = MethodHandles.insertArguments(CONSTRUCTOR,
+ 0, primitiveCount, objectCount);
+
+ return constructor;
+ }
+
+ /**
+ * Adds constructor arguments for each of the allocated slots.
+ *
+ * @param carrierShape carrier object shape
+ *
+ * @return {@link MethodHandle} to specific carrier constructor.
+ */
+ MethodHandle initializer(CarrierShape carrierShape) {
+ int longCount = carrierShape.longCount();
+ int intCount = carrierShape.intCount();
+ int objectCount = carrierShape.objectCount();
+ MethodHandle initializer = MethodHandles.identity(CarrierObject.class);
+
+ // long array index
+ int index = 0;
+ for (int i = 0; i < longCount; i++) {
+ MethodHandle put = MethodHandles.insertArguments(PUT_LONG, 1, index++);
+ initializer = MethodHandles.collectArguments(put, 0, initializer);
+ }
+
+ // transition to int array index (double number of longs)
+ index *= LONG_SLOTS;
+ for (int i = 0; i < intCount; i++) {
+ MethodHandle put = MethodHandles.insertArguments(PUT_INTEGER, 1, index++);
+ initializer = MethodHandles.collectArguments(put, 0, initializer);
+ }
+
+ for (int i = 0; i < objectCount; i++) {
+ MethodHandle put = MethodHandles.insertArguments(PUT_OBJECT, 1, i);
+ initializer = MethodHandles.collectArguments(put, 0, initializer);
+ }
+
+ return initializer;
+ }
+
+ /**
+ * Utility to construct the basic accessors from the components.
+ *
+ * @param carrierShape carrier object shape
+ *
+ * @return array of carrier accessors
+ */
+ MethodHandle[] createComponents(CarrierShape carrierShape) {
+ int longCount = carrierShape.longCount();
+ int intCount = carrierShape.intCount();
+ int objectCount = carrierShape.objectCount();
+ MethodHandle[] components =
+ new MethodHandle[carrierShape.ptypes().length];
+
+ // long array index
+ int index = 0;
+ // component index
+ int comIndex = 0;
+ for (int i = 0; i < longCount; i++) {
+ components[comIndex++] = MethodHandles.insertArguments(GET_LONG, 1, index++);
+ }
+
+ // transition to int array index (double number of longs)
+ index *= LONG_SLOTS;
+ for (int i = 0; i < intCount; i++) {
+ components[comIndex++] = MethodHandles.insertArguments(GET_INTEGER, 1, index++);
+ }
+
+ for (int i = 0; i < objectCount; i++) {
+ components[comIndex++] = MethodHandles.insertArguments(GET_OBJECT, 1, i);
+ }
+ return components;
+ }
+
+ /**
+ * Cache mapping {@link MethodType} to previously defined {@link CarrierElements}.
+ */
+ private static final Map
+ methodTypeCache = ReferencedKeyMap.create(ConcurrentHashMap::new);
+
+ /**
+ * Permute a raw constructor and component accessor {@link MethodHandle MethodHandles} to
+ * match the order and types of the parameter types.
+ *
+ * @param carrierShape carrier object shape
+ *
+ * @return {@link CarrierElements} instance
+ */
+ CarrierElements carrier(CarrierShape carrierShape) {
+ return methodTypeCache.computeIfAbsent(carrierShape.methodType, (mt) -> {
+ MethodHandle constructor = constructor(carrierShape);
+ MethodHandle initializer = initializer(carrierShape);
+ MethodHandle[] components = createComponents(carrierShape);
+ return new CarrierElements(
+ carrierShape,
+ CarrierObject.class,
+ constructor,
+ reshapeInitializer(carrierShape, initializer),
+ reshapeComponents(carrierShape, components));
+ });
+ }
+ }
+
+ /**
+ * Wrapper object for carrier data. Instance types are stored in the {@code objects}
+ * array, while primitive types are recast to {@code int/long} and stored in the
+ * {@code primitives} array. Primitive byte, short, char, boolean and int are stored as
+ * integers. Longs and doubles are stored as longs. Longs take up the first part of the
+ * primitives array using normal indices. Integers follow using int[] indices offset beyond
+ * the longs using unsafe getInt/putInt.
+ */
+ static class CarrierObject {
+ /**
+ * Carrier for primitive values.
+ */
+ private final long[] primitives;
+
+ /**
+ * Carrier for objects;
+ */
+ private final Object[] objects;
+
+ /**
+ * Constructor.
+ *
+ * @param primitiveCount slot count required for primitives
+ * @param objectCount slot count required for objects
+ */
+ protected CarrierObject(int primitiveCount, int objectCount) {
+ this.primitives = createPrimitivesArray(primitiveCount);
+ this.objects = createObjectsArray(objectCount);
+ }
+
+ /**
+ * Create a primitives array of an appropriate length.
+ *
+ * @param primitiveCount slot count required for primitives
+ *
+ * @return primitives array of an appropriate length.
+ */
+ private long[] createPrimitivesArray(int primitiveCount) {
+ return primitiveCount != 0 ? new long[(primitiveCount + 1) / LONG_SLOTS] : null;
+ }
+
+ /**
+ * Create a objects array of an appropriate length.
+ *
+ * @param objectCount slot count required for objects
+ *
+ * @return objects array of an appropriate length.
+ */
+ private Object[] createObjectsArray(int objectCount) {
+ return objectCount != 0 ? new Object[objectCount] : null;
+ }
+
+ /**
+ * Compute offset for unsafe access to long.
+ *
+ * @param i index in primitive[]
+ *
+ * @return offset for unsafe access
+ */
+ private static long offsetToLong(int i) {
+ return Unsafe.ARRAY_LONG_BASE_OFFSET +
+ (long)i * Unsafe.ARRAY_LONG_INDEX_SCALE;
+ }
+
+ /**
+ * Compute offset for unsafe access to int.
+ *
+ * @param i index in primitive[]
+ *
+ * @return offset for unsafe access
+ */
+ private static long offsetToInt(int i) {
+ return Unsafe.ARRAY_LONG_BASE_OFFSET +
+ (long)i * Unsafe.ARRAY_INT_INDEX_SCALE;
+ }
+
+ /**
+ * Compute offset for unsafe access to object.
+ *
+ * @param i index in objects[]
+ *
+ * @return offset for unsafe access
+ */
+ private static long offsetToObject(int i) {
+ return Unsafe.ARRAY_OBJECT_BASE_OFFSET +
+ (long)i * Unsafe.ARRAY_OBJECT_INDEX_SCALE;
+ }
+
+ /**
+ * {@return long value at index}
+ *
+ * @param i array index
+ */
+ private long getLong(int i) {
+ return CarrierObjectFactory.UNSAFE.getLong(primitives, offsetToLong(i));
+ }
+
+ /**
+ * Put a long value into the primitive[].
+ *
+ * @param i array index
+ * @param value long value to store
+ *
+ * @return this object
+ */
+ private CarrierObject putLong(int i, long value) {
+ CarrierObjectFactory.UNSAFE.putLong(primitives, offsetToLong(i), value);
+
+ return this;
+ }
+
+ /**
+ * {@return int value at index}
+ *
+ * @param i array index
+ */
+ private int getInteger(int i) {
+ return CarrierObjectFactory.UNSAFE.getInt(primitives, offsetToInt(i));
+ }
+
+ /**
+ * Put a int value into the int[].
+ *
+ * @param i array index
+ * @param value int value to store
+ *
+ * @return this object
+ */
+ private CarrierObject putInteger(int i, int value) {
+ CarrierObjectFactory.UNSAFE.putInt(primitives, offsetToInt(i), value);
+
+ return this;
+ }
+
+ /**
+ * {@return Object value at index}
+ *
+ * @param i array index
+ */
+ private Object getObject(int i) {
+ return CarrierObjectFactory.UNSAFE.getReference(objects, offsetToObject(i));
+ }
+
+ /**
+ * Put a object value into the objects[].
+ *
+ * @param i array index
+ * @param value object value to store
+ *
+ * @return this object
+ */
+ private CarrierObject putObject(int i, Object value) {
+ CarrierObjectFactory.UNSAFE.putReference(objects, offsetToObject(i), value);
+
+ return this;
+ }
+ }
+
+ /**
+ * Class used to tally and track the number of ints, longs and objects.
+ *
+ * @param longCount number of longs and doubles
+ * @param intCount number of byte, short, int, chars and booleans
+ * @param objectCount number of objects
+ */
+ private record CarrierCounts(int longCount, int intCount, int objectCount) {
+ /**
+ * Count the number of fields required in each of Object, int and long.
+ *
+ * @param ptypes parameter types
+ *
+ * @return a {@link CarrierCounts} instance containing counts
+ */
+ static CarrierCounts tally(Class>[] ptypes) {
+ return tally(ptypes, ptypes.length);
+ }
+
+ /**
+ * Count the number of fields required in each of Object, int and long
+ * limited to the first {@code n} parameters.
+ *
+ * @param ptypes parameter types
+ * @param n number of parameters to check
+ *
+ * @return a {@link CarrierCounts} instance containing counts
+ */
+ private static CarrierCounts tally(Class>[] ptypes, int n) {
+ int longCount = 0;
+ int intCount = 0;
+ int objectCount = 0;
+
+ for (int i = 0; i < n; i++) {
+ Class> ptype = ptypes[i];
+
+ if (!ptype.isPrimitive()) {
+ objectCount++;
+ } else if (ptype == long.class || ptype == double.class) {
+ longCount++;
+ } else {
+ intCount++;
+ }
+ }
+
+ return new CarrierCounts(longCount, intCount, objectCount);
+ }
+
+ /**
+ * {@return total number of components}
+ */
+ private int count() {
+ return longCount + intCount + objectCount;
+ }
+
+ /**
+ * {@return total number of slots}
+ */
+ private int slotCount() {
+ return longCount * LONG_SLOTS + intCount + objectCount;
+ }
+
+ }
+
+ /**
+ * Constructor
+ */
+ private Carriers() {
+ throw new AssertionError("private constructor");
+ }
+
+ /**
+ * Shape of carrier based on counts of each of the three fundamental data
+ * types.
+ */
+ private static class CarrierShape {
+ /**
+ * {@link MethodType} providing types for the carrier's components.
+ */
+ final MethodType methodType;
+
+ /**
+ * Counts of different parameter types.
+ */
+ final CarrierCounts counts;
+
+ /**
+ * Constructor.
+ *
+ * @param methodType {@link MethodType} providing types for the
+ * carrier's components
+ */
+ public CarrierShape(MethodType methodType) {
+ this.methodType = methodType;
+ this.counts = CarrierCounts.tally(methodType.parameterArray());
+ }
+
+ /**
+ * {@return number of long fields needed}
+ */
+ int longCount() {
+ return counts.longCount();
+ }
+
+ /**
+ * {@return number of int fields needed}
+ */
+ int intCount() {
+ return counts.intCount();
+ }
+
+ /**
+ * {@return number of object fields needed}
+ */
+ int objectCount() {
+ return counts.objectCount();
+ }
+
+ /**
+ * {@return slot count required for primitives}
+ */
+ int primitiveCount() {
+ return counts.longCount() * LONG_SLOTS + counts.intCount();
+ }
+
+ /**
+ * {@return array of parameter types}
+ */
+ Class>[] ptypes() {
+ return methodType.parameterArray();
+ }
+
+ /**
+ * {@return number of components}
+ */
+ int count() {
+ return counts.count();
+ }
+
+ /**
+ * {@return number of slots used}
+ */
+ int slotCount() {
+ return counts.slotCount();
+ }
+
+ /**
+ * {@return index of first long component}
+ */
+ int longOffset() {
+ return 0;
+ }
+
+ /**
+ * {@return index of first int component}
+ */
+ int intOffset() {
+ return longCount();
+ }
+
+ /**
+ * {@return index of first object component}
+ */
+ int objectOffset() {
+ return longCount() + intCount();
+ }
+ }
+
+ /**
+ * This factory class generates {@link CarrierElements} instances containing the
+ * {@link MethodHandle MethodHandles} to the constructor and accessors of a carrier
+ * object.
+ *
+ * Clients can create instances by describing a carrier shape , that
+ * is, a {@linkplain MethodType method type} whose parameter types describe the types of
+ * the carrier component values, or by providing the parameter types directly.
+ */
+ static final class CarrierFactory {
+ /**
+ * Constructor
+ */
+ private CarrierFactory() {
+ throw new AssertionError("private constructor");
+ }
+
+ private static final CarrierObjectFactory FACTORY = new CarrierObjectFactory();
+
+ /**
+ * Factory method to return a {@link CarrierElements} instance that matches the shape of
+ * the supplied {@link MethodType}. The return type of the {@link MethodType} is ignored.
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the
+ * the shape of the carrier's components
+ *
+ * @return {@link CarrierElements} instance
+ *
+ * @throws NullPointerException is methodType is null
+ * @throws IllegalArgumentException if number of component slots exceeds maximum
+ */
+ static CarrierElements of(MethodType methodType) {
+ Objects.requireNonNull(methodType, "methodType must not be null");
+ MethodType constructorMT = methodType.changeReturnType(Object.class);
+ CarrierShape carrierShape = new CarrierShape(constructorMT);
+ int slotCount = carrierShape.slotCount();
+
+ if (MAX_COMPONENTS < slotCount) {
+ throw new IllegalArgumentException("Exceeds maximum number of component slots");
+ }
+
+ return FACTORY.carrier(carrierShape);
+ }
+
+ /**
+ * Factory method to return a {@link CarrierElements} instance that matches the shape of
+ * the supplied parameter types.
+ *
+ * @param ptypes parameter types that supply the shape of the carrier's components
+ *
+ * @return {@link CarrierElements} instance
+ *
+ * @throws NullPointerException is ptypes is null
+ * @throws IllegalArgumentException if number of component slots exceeds maximum
+ */
+ static CarrierElements of(Class>...ptypes) {
+ Objects.requireNonNull(ptypes, "ptypes must not be null");
+ return of(methodType(Object.class, ptypes));
+ }
+ }
+
+ /**
+ * Instances of this class provide the {@link MethodHandle MethodHandles} to the
+ * constructor and accessors of a carrier object. The original component types can be
+ * gleaned from the parameter types of the constructor {@link MethodHandle} or by the
+ * return types of the components' {@link MethodHandle MethodHandles}.
+ */
+ static final class CarrierElements {
+ /**
+ * Slot count required for objects.
+ */
+ private final int objectCount;
+
+ /**
+ * Slot count required for primitives.
+ */
+ private final int primitiveCount;
+
+ /**
+ * Underlying carrier class.
+ */
+ private final Class> carrierClass;
+
+ /**
+ * Constructor {@link MethodHandle}.
+ */
+ private final MethodHandle constructor;
+
+ /**
+ * Initializer {@link MethodHandle}.
+ */
+ private final MethodHandle initializer;
+
+ /**
+ * List of component {@link MethodHandle MethodHandles}
+ */
+ private final List components;
+
+ /**
+ * Constructor
+ */
+ private CarrierElements() {
+ throw new AssertionError("private constructor");
+ }
+
+ /**
+ * Constructor
+ */
+ CarrierElements(CarrierShape carrierShape,
+ Class> carrierClass,
+ MethodHandle constructor,
+ MethodHandle initializer,
+ List components) {
+ this.objectCount = carrierShape.objectCount();
+ this.primitiveCount = carrierShape.primitiveCount();
+ this.carrierClass = carrierClass;
+ this.constructor = constructor;
+ this.initializer = initializer;
+ this.components = components;
+ }
+
+ /**
+ * {@return slot count required for objects}
+ */
+ int objectCount() {
+ return objectCount;
+ }
+
+ /**
+ * {@return slot count required for primitives}
+ */
+ int primitiveCount() {
+ return primitiveCount;
+ }
+
+ /**
+ * {@return the underlying carrier class}
+ */
+ Class> carrierClass() {
+ return carrierClass;
+ }
+
+ /**
+ * {@return the constructor {@link MethodHandle} for the carrier. The
+ * carrier constructor will always have a return type of {@link Object} }
+ */
+ MethodHandle constructor() {
+ return constructor;
+ }
+
+ /**
+ * {@return the initializer {@link MethodHandle} for the carrier}
+ */
+ MethodHandle initializer() {
+ return initializer;
+ }
+
+ /**
+ * Return the constructor plus initializer {@link MethodHandle} for the carrier.
+ * The {@link MethodHandle} will always have a return type of {@link Object}.
+ * @return the constructor plus initializer {@link MethodHandle}
+ */
+ MethodHandle initializingConstructor() {
+ return MethodHandles.foldArguments(initializer, 0, constructor);
+ }
+
+ /**
+ * {@return immutable list of component accessor {@link MethodHandle MethodHandles}
+ * for all the carrier's components. The receiver type of the accessors
+ * will always be {@link Object} }
+ */
+ List components() {
+ return components;
+ }
+
+ /**
+ * {@return a component accessor {@link MethodHandle} for component {@code i}.
+ * The receiver type of the accessor will be {@link Object} }
+ *
+ * @param i component index
+ *
+ * @throws IllegalArgumentException if {@code i} is out of bounds
+ */
+ MethodHandle component(int i) {
+ if (i < 0 || components.size() <= i) {
+ throw new IllegalArgumentException("i is out of bounds " + i +
+ " of " + components.size());
+ }
+
+ return components.get(i);
+ }
+
+ @Override
+ public String toString() {
+ return "Carrier" + constructor.type().parameterList();
+ }
+ }
+
+ /**
+ * {@return the underlying carrier class of the carrier representing {@code methodType} }
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the shape of the
+ * carrier's components
+ */
+ static Class> carrierClass(MethodType methodType) {
+ return CarrierFactory.of(methodType).carrierClass();
+ }
+
+ /**
+ * {@return the constructor {@link MethodHandle} for the carrier representing {@code
+ * methodType}. The carrier constructor will always have a return type of {@link Object} }
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the shape of the
+ * carrier's components
+ */
+ static MethodHandle constructor(MethodType methodType) {
+ MethodHandle constructor = CarrierFactory.of(methodType).constructor();
+ constructor = constructor.asType(constructor.type().changeReturnType(Object.class));
+ return constructor;
+ }
+
+ /**
+ * {@return the initializer {@link MethodHandle} for the carrier representing {@code
+ * methodType}. The carrier initializer will always take an {@link Object} along with
+ * component values and a return type of {@link Object} }
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the shape of the
+ * carrier's components
+ */
+ static MethodHandle initializer(MethodType methodType) {
+ MethodHandle initializer = CarrierFactory.of(methodType).initializer();
+ initializer = initializer.asType(initializer.type()
+ .changeReturnType(Object.class).changeParameterType(0, Object.class));
+ return initializer;
+ }
+
+ /**
+ * {@return the combination {@link MethodHandle} of the constructor and initializer
+ * for the carrier representing {@code methodType}. The carrier constructor/initializer
+ * will always take the component values and a return type of {@link Object} }
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the shape of the
+ * carrier's components
+ */
+ static MethodHandle initializingConstructor(MethodType methodType) {
+ MethodHandle constructor = CarrierFactory.of(methodType).initializingConstructor();
+ constructor = constructor.asType(constructor.type().changeReturnType(Object.class));
+ return constructor;
+ }
+
+ /**
+ * {@return immutable list of component accessor {@link MethodHandle MethodHandles} for
+ * all the components of the carrier representing {@code methodType}. The receiver type of
+ * the accessors will always be {@link Object} }
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the shape of the
+ * carrier's components
+ */
+ static List components(MethodType methodType) {
+ return CarrierFactory
+ .of(methodType)
+ .components()
+ .stream()
+ .map(c -> c.asType(c.type().changeParameterType(0, Object.class)))
+ .toList();
+ }
+
+ /**
+ * {@return a component accessor {@link MethodHandle} for component {@code i} of the
+ * carrier representing {@code methodType}. The receiver type of the accessor will always
+ * be {@link Object} }
+ *
+ * @param methodType {@link MethodType} whose parameter types supply the shape of the
+ * carrier's components
+ * @param i component index
+ *
+ * @throws IllegalArgumentException if {@code i} is out of bounds
+ */
+ static MethodHandle component(MethodType methodType, int i) {
+ MethodHandle component = CarrierFactory.of(methodType).component(i);
+ component = component.asType(component.type().changeParameterType(0, Object.class));
+ return component;
+ }
+
+}
diff --git a/src/java.base/share/classes/java/lang/runtime/ReferenceKey.java b/src/java.base/share/classes/java/lang/runtime/ReferenceKey.java
new file mode 100644
index 00000000000..eac24753243
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/ReferenceKey.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+
+/**
+ * View/wrapper of keys used by the backing {@link ReferencedKeyMap}.
+ * There are two style of keys; one for entries in the backing map and
+ * one for queries to the backing map. This second style avoids the
+ * overhead of a {@link Reference} object.
+ *
+ * @param key type
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+interface ReferenceKey {
+ /**
+ * {@return the value of the unwrapped key}
+ */
+ T get();
+
+ /**
+ * Cleanup unused key.
+ */
+ void unused();
+
+ /**
+ * {@link WeakReference} wrapper key for entries in the backing map.
+ *
+ * @param key type
+ *
+ * @since 21
+ */
+ class WeakKey extends WeakReference implements ReferenceKey {
+ /**
+ * Saved hashcode of the key. Used when {@link WeakReference} is
+ * null.
+ */
+ private final int hashcode;
+
+ /**
+ * Private constructor.
+ *
+ * @param key unwrapped key value
+ * @param queue reference queue
+ */
+ WeakKey(T key, ReferenceQueue queue) {
+ super(key, queue);
+ this.hashcode = Objects.hashCode(key);
+ }
+
+ /**
+ * Cleanup unused key. No need to enqueue since the key did not make it
+ * into the map.
+ */
+ @Override
+ public void unused() {
+ clear();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Necessary when removing a null reference
+ if (obj == this) {
+ return true;
+ }
+ // Necessary when comparing an unwrapped key
+ if (obj instanceof ReferenceKey> key) {
+ obj = key.get();
+ }
+ return Objects.equals(get(), obj);
+ }
+
+ @Override
+ public int hashCode() {
+ // Use saved hashcode
+ return hashcode;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getCanonicalName() + "#" + System.identityHashCode(this);
+ }
+ }
+
+ /**
+ * {@link SoftReference} wrapper key for entries in the backing map.
+ *
+ * @param key type
+ *
+ * @since 21
+ */
+ class SoftKey extends SoftReference implements ReferenceKey {
+ /**
+ * Saved hashcode of the key. Used when {@link SoftReference} is
+ * null.
+ */
+ private final int hashcode;
+
+ /**
+ * Private constructor.
+ *
+ * @param key unwrapped key value
+ * @param queue reference queue
+ */
+ SoftKey(T key, ReferenceQueue queue) {
+ super(key, queue);
+ this.hashcode = Objects.hashCode(key);
+ }
+
+ /**
+ * Cleanup unused key. No need to enqueue since the key did not make it
+ * into the map.
+ */
+ @Override
+ public void unused() {
+ clear();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Necessary when removing a null reference
+ if (obj == this) {
+ return true;
+ }
+ // Necessary when comparing an unwrapped key
+ if (obj instanceof ReferenceKey> key) {
+ obj = key.get();
+ }
+ return Objects.equals(get(), obj);
+ }
+
+ @Override
+ public int hashCode() {
+ // Use saved hashcode
+ return hashcode;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getCanonicalName() + "#" + System.identityHashCode(this);
+ }
+ }
+
+ /**
+ * Wrapper for querying the backing map. Avoids the overhead of an
+ * {@link Reference} object.
+ *
+ * @param key type
+ *
+ * @since 21
+ */
+ class StrongKey implements ReferenceKey {
+ T key;
+
+ /**
+ * Private constructor.
+ *
+ * @param key unwrapped key value
+ */
+ StrongKey(T key) {
+ this.key = key;
+ }
+
+ /**
+ * {@return the unwrapped key}
+ */
+ @Override
+ public T get() {
+ return key;
+ }
+
+ @Override
+ public void unused() {
+ key = null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Necessary when comparing an unwrapped key
+ if (obj instanceof ReferenceKey> key) {
+ obj = key.get();
+ }
+ return Objects.equals(get(), obj);
+ }
+
+ @Override
+ public int hashCode() {
+ // Use unwrapped key hash code
+ return get().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getCanonicalName() + "#" + System.identityHashCode(this);
+ }
+ }
+
+}
diff --git a/src/java.base/share/classes/java/lang/runtime/ReferencedKeyMap.java b/src/java.base/share/classes/java/lang/runtime/ReferencedKeyMap.java
new file mode 100644
index 00000000000..fd79c4c4161
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/ReferencedKeyMap.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Objects;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This class provides management of {@link Map maps} where it is desirable to
+ * remove entries automatically when the key is garbage collected. This is
+ * accomplished by using a backing map where the keys are either a
+ * {@link WeakReference} or a {@link SoftReference}.
+ *
+ * To create a {@link ReferencedKeyMap} the user must provide a {@link Supplier}
+ * of the backing map and whether {@link WeakReference} or
+ * {@link SoftReference} is to be used.
+ *
+ * {@snippet :
+ * // Use HashMap and WeakReference
+ * Map map = ReferencedKeyMap.create(false, HashMap::new);
+ * map.put(10_000_000L, "a");
+ * map.put(10_000_001L, "b");
+ * map.put(10_000_002L, "c");
+ * map.put(10_000_003L, "d");
+ * map.put(10_000_004L, "e");
+ *
+ * // Use ConcurrentHashMap and SoftReference
+ * map = ReferencedKeyMap.create(true, ConcurrentHashMap::new);
+ * map.put(20_000_000L, "v");
+ * map.put(20_000_001L, "w");
+ * map.put(20_000_002L, "x");
+ * map.put(20_000_003L, "y");
+ * map.put(20_000_004L, "z");
+ * }
+ *
+ * @implNote Care must be given that the backing map does replacement by
+ * replacing the value in the map entry instead of deleting the old entry and
+ * adding a new entry, otherwise replaced entries may end up with a strongly
+ * referenced key. {@link HashMap} and {@link ConcurrentHashMap} are known
+ * to be safe.
+ *
+ * @param the type of keys maintained by this map
+ * @param the type of mapped values
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+final class ReferencedKeyMap implements Map {
+ /**
+ * true if {@link SoftReference} keys are to be used,
+ * {@link WeakReference} otherwise.
+ */
+ private final boolean isSoft;
+
+ /**
+ * Backing {@link Map}.
+ */
+ private final Map, V> map;
+
+ /**
+ * {@link ReferenceQueue} for cleaning up {@link ReferenceKey.WeakKey EntryKeys}.
+ */
+ private final ReferenceQueue stale;
+
+ /**
+ * Private constructor.
+ *
+ * @param isSoft true if {@link SoftReference} keys are to
+ * be used, {@link WeakReference} otherwise.
+ * @param map backing map
+ */
+ private ReferencedKeyMap(boolean isSoft, Map, V> map) {
+ this.isSoft = isSoft;
+ this.map = map;
+ this.stale = new ReferenceQueue<>();
+ }
+
+ /**
+ * Create a new {@link ReferencedKeyMap} map.
+ *
+ * @param isSoft true if {@link SoftReference} keys are to
+ * be used, {@link WeakReference} otherwise.
+ * @param supplier {@link Supplier} of the backing map
+ *
+ * @return a new map with {@link Reference} keys
+ *
+ * @param the type of keys maintained by the new map
+ * @param the type of mapped values
+ */
+ static ReferencedKeyMap
+ create(boolean isSoft, Supplier, V>> supplier) {
+ return new ReferencedKeyMap(isSoft, supplier.get());
+ }
+
+ /**
+ * Create a new {@link ReferencedKeyMap} map using
+ * {@link WeakReference} keys.
+ *
+ * @param supplier {@link Supplier} of the backing map
+ *
+ * @return a new map with {@link Reference} keys
+ *
+ * @param the type of keys maintained by the new map
+ * @param the type of mapped values
+ */
+ static ReferencedKeyMap
+ create(Supplier, V>> supplier) {
+ return new ReferencedKeyMap(false, supplier.get());
+ }
+
+ /**
+ * {@return a key suitable for a map entry}
+ *
+ * @param key unwrapped key
+ */
+ @SuppressWarnings("unchecked")
+ private ReferenceKey entryKey(Object key) {
+ if (isSoft) {
+ return new ReferenceKey.SoftKey<>((K)key, stale);
+ } else {
+ return new ReferenceKey.WeakKey<>((K)key, stale);
+ }
+ }
+
+ /**
+ * {@return a key suitable for lookup}
+ *
+ * @param key unwrapped key
+ */
+ @SuppressWarnings("unchecked")
+ private ReferenceKey lookupKey(Object key) {
+ return new ReferenceKey.StrongKey<>((K)key);
+ }
+
+ @Override
+ public int size() {
+ removeStaleReferences();
+ return map.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ removeStaleReferences();
+ return map.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ Objects.requireNonNull(key, "key must not be null");
+ removeStaleReferences();
+ return map.containsKey(lookupKey(key));
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ Objects.requireNonNull(value, "value must not be null");
+ removeStaleReferences();
+ return map.containsValue(value);
+ }
+
+ @Override
+ public V get(Object key) {
+ Objects.requireNonNull(key, "key must not be null");
+ removeStaleReferences();
+ return map.get(lookupKey(key));
+ }
+
+ @Override
+ public V put(K key, V newValue) {
+ Objects.requireNonNull(key, "key must not be null");
+ Objects.requireNonNull(newValue, "value must not be null");
+ removeStaleReferences();
+ ReferenceKey entryKey = entryKey(key);
+ // If {@code put} returns non-null then was actually a {@code replace}
+ // and older key was used. In that case the new key was not used and the
+ // reference marked stale.
+ V oldValue = map.put(entryKey, newValue);
+ if (oldValue != null) {
+ entryKey.unused();
+ }
+ return oldValue;
+ }
+
+ @Override
+ public V remove(Object key) {
+ // Rely on gc to clean up old key.
+ return map.remove(lookupKey(key));
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ removeStaleReferences();
+ for (Entry extends K, ? extends V> entry : m.entrySet()) {
+ K key = entry.getKey();
+ V value = entry.getValue();
+ put(key, value);
+ }
+ }
+
+ @Override
+ public void clear() {
+ removeStaleReferences();
+ // Rely on gc to clean up old keys.
+ map.clear();
+ }
+
+ /**
+ * Common routine for collecting the current set of keys.
+ *
+ * @return {@link Stream} of valid keys (unwrapped)
+ */
+ private Stream filterKeySet() {
+ return map.keySet()
+ .stream()
+ .map(ReferenceKey::get)
+ .filter(Objects::nonNull);
+ }
+
+ @Override
+ public Set keySet() {
+ removeStaleReferences();
+ return filterKeySet().collect(Collectors.toSet());
+ }
+
+ @Override
+ public Collection values() {
+ removeStaleReferences();
+ return map.values();
+ }
+
+ @Override
+ public Set> entrySet() {
+ removeStaleReferences();
+ return filterKeySet()
+ .map(k -> new AbstractMap.SimpleEntry<>(k, get(k)))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public V putIfAbsent(K key, V newValue) {
+ removeStaleReferences();
+ ReferenceKey entryKey = entryKey(key);
+ // If {@code putIfAbsent} returns non-null then was actually a
+ // {@code replace} and older key was used. In that case the new key was
+ // not used and the reference marked stale.
+ V oldValue = map.putIfAbsent(entryKey, newValue);
+ if (oldValue != null) {
+ entryKey.unused();
+ }
+ return oldValue;
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ // Rely on gc to clean up old key.
+ return map.remove(lookupKey(key), value);
+ }
+
+ @Override
+ public boolean replace(K key, V oldValue, V newValue) {
+ removeStaleReferences();
+ // If replace is successful then the older key will be used and the
+ // lookup key will suffice.
+ return map.replace(lookupKey(key), oldValue, newValue);
+ }
+
+ @Override
+ public V replace(K key, V value) {
+ removeStaleReferences();
+ // If replace is successful then the older key will be used and the
+ // lookup key will suffice.
+ return map.replace(lookupKey(key), value);
+ }
+
+ @Override
+ public String toString() {
+ removeStaleReferences();
+ return filterKeySet()
+ .map(k -> k + "=" + get(k))
+ .collect(Collectors.joining(", ", "{", "}"));
+ }
+
+ /**
+ * Removes enqueued weak references from map.
+ */
+ @SuppressWarnings("unchecked")
+ public void removeStaleReferences() {
+ while (true) {
+ ReferenceKey.WeakKey key = (ReferenceKey.WeakKey)stale.poll();
+ if (key == null) {
+ break;
+ }
+ map.remove(key);
+ }
+ }
+
+}
diff --git a/src/java.base/share/classes/java/lang/runtime/StringTemplateImpl.java b/src/java.base/share/classes/java/lang/runtime/StringTemplateImpl.java
new file mode 100644
index 00000000000..be510fe62b0
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/StringTemplateImpl.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.lang.invoke.MethodHandle;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class implements specialized {@link StringTemplate StringTemplates} produced by
+ * string template bootstrap method callsites generated by the compiler. Instances of this
+ * class are produced by {@link StringTemplateImplFactory}.
+ *
+ * Values are stored by subclassing {@link Carriers.CarrierObject}. This allows specializations
+ * and sharing of value shapes without creating a new class for each shape.
+ *
+ * {@link StringTemplate} fragments are shared via binding to the
+ * {@link java.lang.invoke.CallSite CallSite's} {@link MethodHandle}.
+ *
+ * The {@link StringTemplateImpl} instance also carries
+ * specialized {@link MethodHandle MethodHandles} for producing the values list and interpolation.
+ * These {@link MethodHandle MethodHandles} are also shared by binding to the
+ * {@link java.lang.invoke.CallSite CallSite}.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+final class StringTemplateImpl extends Carriers.CarrierObject implements StringTemplate {
+ /**
+ * List of string fragments for the string template. This value of this list is shared by
+ * all instances created at the {@link java.lang.invoke.CallSite CallSite}.
+ */
+ private final List fragments;
+
+ /**
+ * Specialized {@link MethodHandle} used to implement the {@link StringTemplate StringTemplate's}
+ * {@code values} method. This {@link MethodHandle} is shared by all instances created at the
+ * {@link java.lang.invoke.CallSite CallSite}.
+ */
+ private final MethodHandle valuesMH;
+
+ /**
+ * Specialized {@link MethodHandle} used to implement the {@link StringTemplate StringTemplate's}
+ * {@code interpolate} method. This {@link MethodHandle} is shared by all instances created at the
+ * {@link java.lang.invoke.CallSite CallSite}.
+ */
+ private final MethodHandle interpolateMH;
+
+ /**
+ * Constructor.
+ *
+ * @param primitiveCount number of primitive slots required (bound at callsite)
+ * @param objectCount number of object slots required (bound at callsite)
+ * @param fragments list of string fragments (bound in (bound at callsite)
+ * @param valuesMH {@link MethodHandle} to produce list of values (bound at callsite)
+ * @param interpolateMH {@link MethodHandle} to produce interpolation (bound at callsite)
+ */
+ StringTemplateImpl(int primitiveCount, int objectCount,
+ List fragments, MethodHandle valuesMH, MethodHandle interpolateMH) {
+ super(primitiveCount, objectCount);
+ this.fragments = fragments;
+ this.valuesMH = valuesMH;
+ this.interpolateMH = interpolateMH;
+ }
+
+ @Override
+ public List fragments() {
+ return fragments;
+ }
+
+ @Override
+ public List values() {
+ try {
+ return (List)valuesMH.invokeExact(this);
+ } catch (RuntimeException | Error ex) {
+ throw ex;
+ } catch (Throwable ex) {
+ throw new RuntimeException("string template values failure", ex);
+ }
+ }
+
+ @Override
+ public String interpolate() {
+ try {
+ return (String)interpolateMH.invokeExact(this);
+ } catch (RuntimeException | Error ex) {
+ throw ex;
+ } catch (Throwable ex) {
+ throw new RuntimeException("string template interpolate failure", ex);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof StringTemplate st &&
+ Objects.equals(fragments(), st.fragments()) &&
+ Objects.equals(values(), st.values());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fragments(), values());
+ }
+
+ @Override
+ public String toString() {
+ return StringTemplate.toString(this);
+ }
+}
diff --git a/src/java.base/share/classes/java/lang/runtime/StringTemplateImplFactory.java b/src/java.base/share/classes/java/lang/runtime/StringTemplateImplFactory.java
new file mode 100644
index 00000000000..a8e1f1f347b
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/StringTemplateImplFactory.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.StringConcatException;
+import java.lang.invoke.StringConcatFactory;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class synthesizes {@link StringTemplate StringTemplates} based on
+ * fragments and bootstrap method type. Usage is primarily from
+ * {@link java.lang.runtime.TemplateRuntime}.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+final class StringTemplateImplFactory {
+
+ /**
+ * Private constructor.
+ */
+ StringTemplateImplFactory() {
+ throw new AssertionError("private constructor");
+ }
+
+ /*
+ * {@link StringTemplateImpl} constructor MethodHandle.
+ */
+ private static final MethodHandle CONSTRUCTOR;
+
+
+ /*
+ * Frequently used method types.
+ */
+ private static final MethodType MT_STRING_STIMPL =
+ MethodType.methodType(String.class, StringTemplateImpl.class);
+ private static final MethodType MT_LIST_STIMPL =
+ MethodType.methodType(List.class, StringTemplateImpl.class);
+
+ /**
+ * List (for nullable) of MethodHandle;
+ */
+ private static final MethodHandle TO_LIST;
+
+ static {
+ try {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+
+ MethodType mt = MethodType.methodType(void.class, int.class, int.class, List.class,
+ MethodHandle.class, MethodHandle.class);
+ CONSTRUCTOR = lookup.findConstructor(StringTemplateImpl.class, mt)
+ .asType(mt.changeReturnType(Carriers.CarrierObject.class));
+
+ mt = MethodType.methodType(List.class, Object[].class);
+ TO_LIST = lookup.findStatic(StringTemplateImplFactory.class, "toList", mt);
+ } catch(ReflectiveOperationException ex) {
+ throw new AssertionError("carrier static init fail", ex);
+ }
+ }
+
+ /**
+ * Create a new {@link StringTemplateImpl} constructor.
+ *
+ * @param fragments string template fragments
+ * @param type values types with StringTemplate return
+ *
+ * @return {@link MethodHandle} that can construct a {@link StringTemplateImpl} with arguments
+ * used as values.
+ */
+ static MethodHandle createStringTemplateImplMH(List fragments, MethodType type) {
+ Carriers.CarrierElements elements = Carriers.CarrierFactory.of(type);
+ MethodHandle[] components = elements
+ .components()
+ .stream()
+ .map(c -> c.asType(c.type().changeParameterType(0, StringTemplateImpl.class)))
+ .toArray(MethodHandle[]::new);
+ Class>[] ptypes = elements
+ .components()
+ .stream()
+ .map(c -> c.type().returnType())
+ .toArray(Class>[]::new);
+ int[] permute = new int[ptypes.length];
+
+ MethodHandle interpolateMH;
+ MethodType mt;
+ try {
+ interpolateMH = StringConcatFactory.makeConcatWithTemplate(fragments, List.of(ptypes));
+ } catch (StringConcatException ex) {
+ throw new RuntimeException("constructing internal string template", ex);
+ }
+ interpolateMH = MethodHandles.filterArguments(interpolateMH, 0, components);
+ interpolateMH = MethodHandles.permuteArguments(interpolateMH, MT_STRING_STIMPL, permute);
+
+ mt = MethodType.methodType(List.class, ptypes);
+ MethodHandle valuesMH = TO_LIST.asCollector(Object[].class, components.length).asType(mt);
+ valuesMH = MethodHandles.filterArguments(valuesMH, 0, components);
+ valuesMH = MethodHandles.permuteArguments(valuesMH, MT_LIST_STIMPL, permute);
+
+ MethodHandle constructor = MethodHandles.insertArguments(CONSTRUCTOR, 0,
+ elements.primitiveCount(), elements.objectCount(),
+ fragments, valuesMH, interpolateMH);
+ constructor = MethodHandles.foldArguments(elements.initializer(), 0, constructor);
+
+ mt = MethodType.methodType(StringTemplate.class, ptypes);
+ constructor = constructor.asType(mt);
+
+ return constructor;
+ }
+
+ /**
+ * Generic {@link StringTemplate}.
+ *
+ * @param fragments immutable list of string fragments from string template
+ * @param values immutable list of expression values
+ */
+ private record SimpleStringTemplate(List fragments, List values)
+ implements StringTemplate {
+ @Override
+ public String toString() {
+ return StringTemplate.toString(this);
+ }
+ }
+
+ /**
+ * Returns a new StringTemplate composed from fragments and values.
+ *
+ * @param fragments array of string fragments
+ * @param values array of expression values
+ *
+ * @return StringTemplate composed from fragments and values
+ */
+ static StringTemplate newTrustedStringTemplate(String[] fragments, Object[] values) {
+ return new SimpleStringTemplate(List.of(fragments), toList(values));
+ }
+
+ /**
+ * Returns a new StringTemplate composed from fragments and values.
+ *
+ * @param fragments list of string fragments
+ * @param values array of expression values
+ *
+ * @return StringTemplate composed from fragments and values
+ */
+ static StringTemplate newTrustedStringTemplate(List fragments, Object[] values) {
+ return new SimpleStringTemplate(List.copyOf(fragments), toList(values));
+ }
+
+ /**
+ * Returns a new StringTemplate composed from fragments and values.
+ *
+ * @param fragments list of string fragments
+ * @param values list of expression values
+ *
+ * @return StringTemplate composed from fragments and values
+ */
+
+ static StringTemplate newStringTemplate(List fragments, List> values) {
+ @SuppressWarnings("unchecked")
+ List copy = (List)values.stream().toList();
+ return new SimpleStringTemplate(List.copyOf(fragments), copy);
+ }
+
+ /**
+ * Collect nullable elements from an array into a unmodifiable list.
+ * Elements are guaranteed to be safe.
+ *
+ * @param elements elements to place in list
+ *
+ * @return unmodifiable list.
+ */
+ private static List toList(Object[] elements) {
+ return Arrays.stream(elements).toList();
+ }
+
+}
diff --git a/src/java.base/share/classes/java/lang/runtime/TemplateRuntime.java b/src/java.base/share/classes/java/lang/runtime/TemplateRuntime.java
new file mode 100644
index 00000000000..5b41fdb506f
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/TemplateRuntime.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.lang.invoke.CallSite;
+import java.lang.invoke.ConstantCallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.StringTemplate.Processor;
+import java.lang.StringTemplate.Processor.Linkage;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import jdk.internal.access.JavaTemplateAccess;
+import jdk.internal.access.SharedSecrets;
+import jdk.internal.javac.PreviewFeature;
+
+/**
+ * Manages string template bootstrap methods. These methods may be used, for example,
+ * by Java compiler implementations to create {@link StringTemplate} instances. For example,
+ * the java compiler will translate the following code;
+ * {@snippet :
+ * int x = 10;
+ * int y = 20;
+ * StringTemplate st = RAW."\{x} + \{y} = \{x + y}";
+ * }
+ * to byte code that invokes the {@link java.lang.runtime.TemplateRuntime#newStringTemplate}
+ * bootstrap method to construct a {@link CallSite} that accepts two integers and produces a new
+ * {@link StringTemplate} instance.
+ * {@snippet :
+ * MethodHandles.Lookup lookup = MethodHandles.lookup();
+ * MethodType mt = MethodType.methodType(StringTemplate.class, int.class, int.class);
+ * CallSite cs = TemplateRuntime.newStringTemplate(lookup, "", mt, "", " + ", " = ", "");
+ * ...
+ * int x = 10;
+ * int y = 20;
+ * StringTemplate st = (StringTemplate)cs.getTarget().invokeExact(x, y);
+ * }
+ * If the string template requires more than
+ * {@link java.lang.invoke.StringConcatFactory#MAX_INDY_CONCAT_ARG_SLOTS} value slots,
+ * then the java compiler will use the
+ * {@link java.lang.runtime.TemplateRuntime#newLargeStringTemplate} bootstrap method
+ * instead. For example, the java compiler will translate the following code;
+ * {@snippet :
+ * int[] a = new int[1000], b = new int[1000];
+ * ...
+ * StringTemplate st = """
+ * \{a[0]} - \{b[0]}
+ * \{a[1]} - \{b[1]}
+ * ...
+ * \{a[999]} - \{b[999]}
+ * """;
+ * }
+ * to byte code that invokes the {@link java.lang.runtime.TemplateRuntime#newLargeStringTemplate}
+ * bootstrap method to construct a {@link CallSite} that accepts an array of integers and produces a new
+ * {@link StringTemplate} instance.
+ * {@snippet :
+ * MethodType mt = MethodType.methodType(StringTemplate.class, String[].class, Object[].class);
+ * CallSite cs = TemplateRuntime.newStringTemplate(lookup, "", mt);
+ * ...
+ * int[] a = new int[1000], b = new int[1000];
+ * ...
+ * StringTemplate st = (StringTemplate)cs.getTarget().invokeExact(
+ * new String[] { "", " - ", "\n", " - ", "\n", ... " - ", "\n" },
+ * new Object[] { a[0], b[0], a[1], b[1], ..., a[999], b[999]}
+ * );
+ * }
+ *
+ * @since 21
+ */
+@PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+public final class TemplateRuntime {
+ private static final JavaTemplateAccess JTA = SharedSecrets.getJavaTemplateAccess();
+
+ /**
+ * {@link MethodHandle} to {@link TemplateRuntime#defaultProcess}.
+ */
+ private static final MethodHandle DEFAULT_PROCESS_MH;
+
+ /**
+ * {@link MethodHandle} to {@link TemplateRuntime#newTrustedStringTemplate}.
+ */
+ private static final MethodHandle NEW_TRUSTED_STRING_TEMPLATE;
+
+ /**
+ * Initialize {@link MethodHandle MethodHandles}.
+ */
+ static {
+ try {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+
+ MethodType mt = MethodType.methodType(Object.class,
+ List.class, Processor.class, Object[].class);
+ DEFAULT_PROCESS_MH =
+ lookup.findStatic(TemplateRuntime.class, "defaultProcess", mt);
+
+ mt = MethodType.methodType(StringTemplate.class, String[].class, Object[].class);
+ NEW_TRUSTED_STRING_TEMPLATE =
+ lookup.findStatic(StringTemplateImplFactory.class, "newTrustedStringTemplate", mt);
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError("string bootstrap fail", ex);
+ }
+ }
+
+ /**
+ * Private constructor.
+ */
+ private TemplateRuntime() {
+ throw new AssertionError("private constructor");
+ }
+
+ /**
+ * String template bootstrap method for creating string templates.
+ * The static arguments include the fragments list.
+ * The non-static arguments are the values.
+ *
+ * @param lookup method lookup from call site
+ * @param name method name - not used
+ * @param type method type
+ * (ptypes...) -> StringTemplate
+ * @param fragments fragment array for string template
+ *
+ * @return {@link CallSite} to handle create string template
+ *
+ * @throws NullPointerException if any of the arguments is null
+ * @throws Throwable if linkage fails
+ */
+ public static CallSite newStringTemplate(MethodHandles.Lookup lookup,
+ String name,
+ MethodType type,
+ String... fragments) throws Throwable {
+ Objects.requireNonNull(lookup, "lookup is null");
+ Objects.requireNonNull(name, "name is null");
+ Objects.requireNonNull(type, "type is null");
+ Objects.requireNonNull(fragments, "fragments is null");
+
+ MethodHandle mh = StringTemplateImplFactory
+ .createStringTemplateImplMH(List.of(fragments), type).asType(type);
+
+ return new ConstantCallSite(mh);
+ }
+
+ /**
+ * String template bootstrap method for creating large string templates,
+ * i.e., when the number of value slots exceeds
+ * {@link java.lang.invoke.StringConcatFactory#MAX_INDY_CONCAT_ARG_SLOTS}.
+ * The non-static arguments are the fragments array and values array.
+ *
+ * @param lookup method lookup from call site
+ * @param name method name - not used
+ * @param type method type
+ * (String[], Object[]) -> StringTemplate
+ *
+ * @return {@link CallSite} to handle create large string template
+ *
+ * @throws NullPointerException if any of the arguments is null
+ * @throws Throwable if linkage fails
+ */
+ public static CallSite newLargeStringTemplate(MethodHandles.Lookup lookup,
+ String name,
+ MethodType type) throws Throwable {
+ Objects.requireNonNull(lookup, "lookup is null");
+ Objects.requireNonNull(name, "name is null");
+ Objects.requireNonNull(type, "type is null");
+
+ return new ConstantCallSite(NEW_TRUSTED_STRING_TEMPLATE.asType(type));
+ }
+
+ /**
+ * String template bootstrap method for static final processors.
+ * The static arguments include the fragments array and a {@link MethodHandle}
+ * to retrieve the value of the static final processor.
+ * The non-static arguments are the values.
+ *
+ * @param lookup method lookup from call site
+ * @param name method name - not used
+ * @param type method type
+ * (ptypes...) -> Object
+ * @param processorGetter {@link MethodHandle} to get static final processor
+ * @param fragments fragments from string template
+ *
+ * @return {@link CallSite} to handle string template processing
+ *
+ * @throws NullPointerException if any of the arguments is null
+ * @throws Throwable if linkage fails
+ *
+ * @implNote this method is likely to be revamped before exiting preview.
+ */
+ public static CallSite processStringTemplate(MethodHandles.Lookup lookup,
+ String name,
+ MethodType type,
+ MethodHandle processorGetter,
+ String... fragments) throws Throwable {
+ Objects.requireNonNull(lookup, "lookup is null");
+ Objects.requireNonNull(name, "name is null");
+ Objects.requireNonNull(type, "type is null");
+ Objects.requireNonNull(processorGetter, "processorGetter is null");
+ Objects.requireNonNull(fragments, "fragments is null");
+
+ Processor, ?> processor = (Processor, ?>)processorGetter.invoke();
+ MethodHandle mh = processor instanceof Linkage linkage
+ ? linkage.linkage(List.of(fragments), type)
+ : defaultProcessMethodHandle(type, processor, List.of(fragments));
+
+ return new ConstantCallSite(mh);
+ }
+
+ /**
+ * Creates a simple {@link StringTemplate} and then invokes the processor's process method.
+ *
+ * @param fragments fragments from string template
+ * @param processor {@link Processor} to process
+ * @param values array of expression values
+ *
+ * @return result of processing the string template
+ *
+ * @throws Throwable when {@link Processor#process(StringTemplate)} throws
+ */
+ private static Object defaultProcess(
+ List fragments,
+ Processor, ?> processor,
+ Object[] values
+ ) throws Throwable {
+ return processor.process(StringTemplate.of(fragments, Arrays.stream(values).toList()));
+ }
+
+ /**
+ * Generate a {@link MethodHandle} which is effectively invokes
+ * {@code processor.process(new StringTemplate(fragments, values...)}.
+ *
+ * @return default process {@link MethodHandle}
+ */
+ private static MethodHandle defaultProcessMethodHandle(
+ MethodType type,
+ Processor, ?> processor,
+ List fragments
+ ) {
+ MethodHandle mh = MethodHandles.insertArguments(DEFAULT_PROCESS_MH, 0, fragments, processor);
+ return mh.asCollector(Object[].class, type.parameterCount()).asType(type);
+ }
+}
+
diff --git a/src/java.base/share/classes/java/lang/runtime/TemplateSupport.java b/src/java.base/share/classes/java/lang/runtime/TemplateSupport.java
new file mode 100644
index 00000000000..a754e69d794
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/runtime/TemplateSupport.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.lang.runtime;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import jdk.internal.access.JavaLangAccess;
+import jdk.internal.access.JavaTemplateAccess;
+import jdk.internal.access.SharedSecrets;
+
+/**
+ * This class provides runtime support for string templates. The methods within
+ * are intended for internal use only.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+final class TemplateSupport implements JavaTemplateAccess {
+
+ /**
+ * Private constructor.
+ */
+ private TemplateSupport() {
+ }
+
+ static {
+ SharedSecrets.setJavaTemplateAccess(new TemplateSupport());
+ }
+
+ private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
+
+ /**
+ * Returns a StringTemplate composed from fragments and values.
+ *
+ * @implSpec The {@code fragments} list size must be one more that the
+ * {@code values} list size.
+ *
+ * @param fragments list of string fragments
+ * @param values list of expression values
+ *
+ * @return StringTemplate composed from fragments and values
+ *
+ * @throws IllegalArgumentException if fragments list size is not one more
+ * than values list size
+ * @throws NullPointerException if fragments is null or values is null or if any fragment is null.
+ *
+ * @implNote Contents of both lists are copied to construct immutable lists.
+ */
+ @Override
+ public StringTemplate of(List fragments, List> values) {
+ return StringTemplateImplFactory.newStringTemplate(fragments, values);
+ }
+
+ /**
+ * Creates a string that interleaves the elements of values between the
+ * elements of fragments.
+ *
+ * @param fragments list of String fragments
+ * @param values list of expression values
+ *
+ * @return String interpolation of fragments and values
+ */
+ @Override
+ public String interpolate(List fragments, List> values) {
+ int fragmentsSize = fragments.size();
+ int valuesSize = values.size();
+ if (fragmentsSize == 1) {
+ return fragments.get(0);
+ }
+ int size = fragmentsSize + valuesSize;
+ String[] strings = new String[size];
+ int i = 0, j = 0;
+ for (; j < valuesSize; j++) {
+ strings[i++] = fragments.get(j);
+ strings[i++] = String.valueOf(values.get(j));
+ }
+ strings[i] = fragments.get(j);
+ return JLA.join("", "", "", strings, size);
+ }
+
+ /**
+ * Combine one or more {@link StringTemplate StringTemplates} to produce a combined {@link StringTemplate}.
+ * {@snippet :
+ * StringTemplate st = StringTemplate.combine("\{a}", "\{b}", "\{c}");
+ * assert st.interpolate().equals("\{a}\{b}\{c}");
+ * }
+ *
+ * @param sts zero or more {@link StringTemplate}
+ *
+ * @return combined {@link StringTemplate}
+ *
+ * @throws NullPointerException if sts is null or if any element of sts is null
+ */
+ @Override
+ public StringTemplate combine(StringTemplate... sts) {
+ Objects.requireNonNull(sts, "sts must not be null");
+ if (sts.length == 0) {
+ return StringTemplate.of("");
+ } else if (sts.length == 1) {
+ return Objects.requireNonNull(sts[0], "string templates should not be null");
+ }
+ int size = 0;
+ for (StringTemplate st : sts) {
+ Objects.requireNonNull(st, "string templates should not be null");
+ size += st.values().size();
+ }
+ String[] combinedFragments = new String[size + 1];
+ Object[] combinedValues = new Object[size];
+ combinedFragments[0] = "";
+ int fragmentIndex = 1;
+ int valueIndex = 0;
+ for (StringTemplate st : sts) {
+ Iterator iterator = st.fragments().iterator();
+ combinedFragments[fragmentIndex - 1] += iterator.next();
+ while (iterator.hasNext()) {
+ combinedFragments[fragmentIndex++] = iterator.next();
+ }
+ for (Object value : st.values()) {
+ combinedValues[valueIndex++] = value;
+ }
+ }
+ return StringTemplateImplFactory.newTrustedStringTemplate(combinedFragments, combinedValues);
+ }
+
+}
diff --git a/src/java.base/share/classes/java/util/Digits.java b/src/java.base/share/classes/java/util/Digits.java
new file mode 100644
index 00000000000..266d9116775
--- /dev/null
+++ b/src/java.base/share/classes/java/util/Digits.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.util;
+
+import java.lang.invoke.MethodHandle;
+
+import jdk.internal.vm.annotation.Stable;
+
+/**
+ * Digits provides a fast methodology for converting integers and longs to
+ * ASCII strings.
+ *
+ * @since 21
+ */
+sealed interface Digits permits Digits.DecimalDigits, Digits.HexDigits, Digits.OctalDigits {
+ /**
+ * Insert digits for long value in buffer from high index to low index.
+ *
+ * @param value value to convert
+ * @param buffer byte buffer to copy into
+ * @param index insert point + 1
+ * @param putCharMH method to put character
+ *
+ * @return the last index used
+ *
+ * @throws Throwable if putCharMH fails (unusual).
+ */
+ int digits(long value, byte[] buffer, int index,
+ MethodHandle putCharMH) throws Throwable;
+
+ /**
+ * Calculate the number of digits required to represent the long.
+ *
+ * @param value value to convert
+ *
+ * @return number of digits
+ */
+ int size(long value);
+
+ /**
+ * Digits class for decimal digits.
+ */
+ final class DecimalDigits implements Digits {
+ @Stable
+ private static final short[] DIGITS;
+
+ /**
+ * Singleton instance of DecimalDigits.
+ */
+ static final Digits INSTANCE = new DecimalDigits();
+
+ static {
+ short[] digits = new short[10 * 10];
+
+ for (int i = 0; i < 10; i++) {
+ short hi = (short) ((i + '0') << 8);
+
+ for (int j = 0; j < 10; j++) {
+ short lo = (short) (j + '0');
+ digits[i * 10 + j] = (short) (hi | lo);
+ }
+ }
+
+ DIGITS = digits;
+ }
+
+ /**
+ * Constructor.
+ */
+ private DecimalDigits() {
+ }
+
+ @Override
+ public int digits(long value, byte[] buffer, int index,
+ MethodHandle putCharMH) throws Throwable {
+ boolean negative = value < 0;
+ if (!negative) {
+ value = -value;
+ }
+
+ long q;
+ int r;
+ while (value <= Integer.MIN_VALUE) {
+ q = value / 100;
+ r = (int)((q * 100) - value);
+ value = q;
+ int digits = DIGITS[r];
+
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ int iq, ivalue = (int)value;
+ while (ivalue <= -100) {
+ iq = ivalue / 100;
+ r = (iq * 100) - ivalue;
+ ivalue = iq;
+ int digits = DIGITS[r];
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ if (ivalue < 0) {
+ ivalue = -ivalue;
+ }
+
+ int digits = DIGITS[ivalue];
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+
+ if (9 < ivalue) {
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ if (negative) {
+ putCharMH.invokeExact(buffer, --index, (int)'-');
+ }
+
+ return index;
+ }
+
+ @Override
+ public int size(long value) {
+ boolean negative = value < 0;
+ int sign = negative ? 1 : 0;
+
+ if (!negative) {
+ value = -value;
+ }
+
+ long precision = -10;
+ for (int i = 1; i < 19; i++) {
+ if (value > precision)
+ return i + sign;
+
+ precision = 10 * precision;
+ }
+
+ return 19 + sign;
+ }
+ }
+
+ /**
+ * Digits class for hexadecimal digits.
+ */
+ final class HexDigits implements Digits {
+ @Stable
+ private static final short[] DIGITS;
+
+ /**
+ * Singleton instance of HexDigits.
+ */
+ static final Digits INSTANCE = new HexDigits();
+
+ static {
+ short[] digits = new short[16 * 16];
+
+ for (int i = 0; i < 16; i++) {
+ short hi = (short) ((i < 10 ? i + '0' : i - 10 + 'a') << 8);
+
+ for (int j = 0; j < 16; j++) {
+ short lo = (short) (j < 10 ? j + '0' : j - 10 + 'a');
+ digits[(i << 4) + j] = (short) (hi | lo);
+ }
+ }
+
+ DIGITS = digits;
+ }
+
+ /**
+ * Constructor.
+ */
+ private HexDigits() {
+ }
+
+ @Override
+ public int digits(long value, byte[] buffer, int index,
+ MethodHandle putCharMH) throws Throwable {
+ while ((value & ~0xFF) != 0) {
+ int digits = DIGITS[(int) (value & 0xFF)];
+ value >>>= 8;
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ int digits = DIGITS[(int) (value & 0xFF)];
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+
+ if (0xF < value) {
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ return index;
+ }
+
+ @Override
+ public int size(long value) {
+ return value == 0 ? 1 :
+ 67 - Long.numberOfLeadingZeros(value) >> 2;
+ }
+ }
+
+ /**
+ * Digits class for octal digits.
+ */
+ final class OctalDigits implements Digits {
+ @Stable
+ private static final short[] DIGITS;
+
+ /**
+ * Singleton instance of OctalDigits.
+ */
+ static final Digits INSTANCE = new OctalDigits();
+
+ static {
+ short[] digits = new short[8 * 8];
+
+ for (int i = 0; i < 8; i++) {
+ short hi = (short) ((i + '0') << 8);
+
+ for (int j = 0; j < 8; j++) {
+ short lo = (short) (j + '0');
+ digits[(i << 3) + j] = (short) (hi | lo);
+ }
+ }
+
+ DIGITS = digits;
+ }
+
+ /**
+ * Constructor.
+ */
+ private OctalDigits() {
+ }
+
+ @Override
+ public int digits(long value, byte[] buffer, int index,
+ MethodHandle putCharMH) throws Throwable {
+ while ((value & ~0x3F) != 0) {
+ int digits = DIGITS[(int) (value & 0x3F)];
+ value >>>= 6;
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ int digits = DIGITS[(int) (value & 0x3F)];
+ putCharMH.invokeExact(buffer, --index, digits & 0xFF);
+
+ if (7 < value) {
+ putCharMH.invokeExact(buffer, --index, digits >> 8);
+ }
+
+ return index;
+ }
+
+ @Override
+ public int size(long value) {
+ return (66 - Long.numberOfLeadingZeros(value)) / 3;
+ }
+ }
+}
diff --git a/src/java.base/share/classes/java/util/FormatItem.java b/src/java.base/share/classes/java/util/FormatItem.java
new file mode 100644
index 00000000000..7524c4bdea9
--- /dev/null
+++ b/src/java.base/share/classes/java/util/FormatItem.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.util;
+
+import java.io.IOException;
+import java.lang.invoke.*;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormatSymbols;
+import java.util.Digits.*;
+import java.util.Formatter.FormatSpecifier;
+
+import jdk.internal.access.JavaLangAccess;
+import jdk.internal.access.SharedSecrets;
+import jdk.internal.util.FormatConcatItem;
+
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * A specialized objects used by FormatterBuilder that knows how to insert
+ * themselves into a concatenation performed by StringConcatFactory.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+class FormatItem {
+ private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
+
+ private static final MethodHandle CHAR_MIX =
+ JLA.stringConcatHelper("mix",
+ MethodType.methodType(long.class, long.class,char.class));
+
+ private static final MethodHandle STRING_PREPEND =
+ JLA.stringConcatHelper("prepend",
+ MethodType.methodType(long.class, long.class, byte[].class,
+ String.class, String.class));
+
+ private static final MethodHandle SELECT_GETCHAR_MH =
+ JLA.stringConcatHelper("selectGetChar",
+ MethodType.methodType(MethodHandle.class, long.class));
+
+ private static final MethodHandle SELECT_PUTCHAR_MH =
+ JLA.stringConcatHelper("selectPutChar",
+ MethodType.methodType(MethodHandle.class, long.class));
+
+ private static long charMix(long lengthCoder, char value) {
+ try {
+ return (long)CHAR_MIX.invokeExact(lengthCoder, value);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
+ } catch (Throwable ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private static long stringMix(long lengthCoder, String value) {
+ return JLA.stringConcatMix(lengthCoder, value);
+ }
+
+ private static long stringPrepend(long lengthCoder, byte[] buffer,
+ String value) throws Throwable {
+ return (long)STRING_PREPEND.invokeExact(lengthCoder, buffer, value,
+ (String)null);
+ }
+
+ private static MethodHandle selectGetChar(long indexCoder) throws Throwable {
+ return (MethodHandle)SELECT_GETCHAR_MH.invokeExact(indexCoder);
+ }
+
+ private static MethodHandle selectPutChar(long indexCoder) throws Throwable {
+ return (MethodHandle)SELECT_PUTCHAR_MH.invokeExact(indexCoder);
+ }
+
+ private static final MethodHandle PUT_CHAR_DIGIT;
+
+ static {
+ try {
+ Lookup lookup = MethodHandles.lookup();
+ PUT_CHAR_DIGIT = lookup.findStatic(FormatItem.class, "putByte",
+ MethodType.methodType(void.class,
+ byte[].class, int.class, int.class));
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError("putByte lookup failed", ex);
+ }
+ }
+
+ private static void putByte(byte[] buffer, int index, int ch) {
+ buffer[index] = (byte)ch;
+ }
+
+ private FormatItem() {
+ throw new AssertionError("private constructor");
+ }
+
+ /**
+ * Decimal value format item.
+ */
+ static final class FormatItemDecimal implements FormatConcatItem {
+ private final char groupingSeparator;
+ private final char zeroDigit;
+ private final char minusSign;
+ private final int digitOffset;
+ private final byte[] digits;
+ private final int length;
+ private final boolean isNegative;
+ private final int width;
+ private final byte prefixSign;
+ private final int groupSize;
+ private final long value;
+ private final boolean parentheses;
+
+ FormatItemDecimal(DecimalFormatSymbols dfs, int width, char sign,
+ boolean parentheses, int groupSize, long value) throws Throwable {
+ this.groupingSeparator = dfs.getGroupingSeparator();
+ this.zeroDigit = dfs.getZeroDigit();
+ this.minusSign = dfs.getMinusSign();
+ this.digitOffset = this.zeroDigit - '0';
+ int length = DecimalDigits.INSTANCE.size(value);
+ this.digits = new byte[length];
+ DecimalDigits.INSTANCE.digits(value, this.digits, length, PUT_CHAR_DIGIT);
+ this.isNegative = value < 0L;
+ this.length = this.isNegative ? length - 1 : length;
+ this.width = width;
+ this.groupSize = groupSize;
+ this.value = value;
+ this.parentheses = parentheses && isNegative;
+ this.prefixSign = (byte)(isNegative ? (parentheses ? '\0' : minusSign) : sign);
+ }
+
+ private int signLength() {
+ return (prefixSign != '\0' ? 1 : 0) + (parentheses ? 2 : 0);
+ }
+
+ private int groupLength() {
+ return 0 < groupSize ? (length - 1) / groupSize : 0;
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return JLA.stringConcatCoder(zeroDigit) |
+ (lengthCoder +
+ Integer.max(length + signLength() + groupLength(), width));
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+
+ if (parentheses) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)')');
+ }
+
+ if (0 < groupSize) {
+ int groupIndex = groupSize;
+
+ for (int i = 1; i <= length; i++) {
+ if (groupIndex-- == 0) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder,
+ (int)groupingSeparator);
+ groupIndex = groupSize - 1;
+ }
+
+ putCharMH.invokeExact(buffer, (int)--lengthCoder,
+ digits[digits.length - i] + digitOffset);
+ }
+ } else {
+ for (int i = 1; i <= length; i++) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder,
+ digits[digits.length - i] + digitOffset);
+ }
+ }
+
+ for (int i = length + signLength() + groupLength(); i < width; i++) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
+ }
+
+ if (parentheses) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'(');
+ }
+ if (prefixSign != '\0') {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)prefixSign);
+ }
+
+ return lengthCoder;
+ }
+ }
+
+ /**
+ * Hexadecimal format item.
+ */
+ static final class FormatItemHexadecimal implements FormatConcatItem {
+ private final int width;
+ private final boolean hasPrefix;
+ private final long value;
+ private final int length;
+
+ FormatItemHexadecimal(int width, boolean hasPrefix, long value) {
+ this.width = width;
+ this.hasPrefix = hasPrefix;
+ this.value = value;
+ this.length = HexDigits.INSTANCE.size(value);
+ }
+
+ private int prefixLength() {
+ return hasPrefix ? 2 : 0;
+ }
+
+ private int zeroesLength() {
+ return Integer.max(0, width - length - prefixLength());
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return lengthCoder + length + prefixLength() + zeroesLength();
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+ HexDigits.INSTANCE.digits(value, buffer, (int)lengthCoder, putCharMH);
+ lengthCoder -= length;
+
+ for (int i = 0; i < zeroesLength(); i++) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
+ }
+
+ if (hasPrefix) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'x');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
+ }
+
+ return lengthCoder;
+ }
+ }
+
+ /**
+ * Hexadecimal format item.
+ */
+ static final class FormatItemOctal implements FormatConcatItem {
+ private final int width;
+ private final boolean hasPrefix;
+ private final long value;
+ private final int length;
+
+ FormatItemOctal(int width, boolean hasPrefix, long value) {
+ this.width = width;
+ this.hasPrefix = hasPrefix;
+ this.value = value;
+ this.length = OctalDigits.INSTANCE.size(value);
+ }
+
+ private int prefixLength() {
+ return hasPrefix && value != 0 ? 1 : 0;
+ }
+
+ private int zeroesLength() {
+ return Integer.max(0, width - length - prefixLength());
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return lengthCoder + length + prefixLength() + zeroesLength();
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+ OctalDigits.INSTANCE.digits(value, buffer, (int)lengthCoder, putCharMH);
+ lengthCoder -= length;
+
+ for (int i = 0; i < zeroesLength(); i++) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
+ }
+
+ if (hasPrefix && value != 0) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
+ }
+
+ return lengthCoder;
+ }
+ }
+
+ /**
+ * Boolean format item.
+ */
+ static final class FormatItemBoolean implements FormatConcatItem {
+ private final boolean value;
+
+ FormatItemBoolean(boolean value) {
+ this.value = value;
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return lengthCoder + (value ? "true".length() : "false".length());
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+
+ if (value) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'e');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'u');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'r');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'t');
+ } else {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'e');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'s');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'l');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'a');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'f');
+ }
+
+ return lengthCoder;
+ }
+ }
+
+ /**
+ * Character format item.
+ */
+ static final class FormatItemCharacter implements FormatConcatItem {
+ private final char value;
+
+ FormatItemCharacter(char value) {
+ this.value = value;
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return charMix(lengthCoder, value);
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)value);
+
+ return lengthCoder;
+ }
+ }
+
+ /**
+ * String format item.
+ */
+ static final class FormatItemString implements FormatConcatItem {
+ private String value;
+
+ FormatItemString(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return stringMix(lengthCoder, value);
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ return stringPrepend(lengthCoder, buffer, value);
+ }
+ }
+
+ /**
+ * FormatSpecifier format item.
+ */
+ static final class FormatItemFormatSpecifier implements FormatConcatItem {
+ private StringBuilder sb;
+
+ FormatItemFormatSpecifier(FormatSpecifier fs, Locale locale, Object value) {
+ this.sb = new StringBuilder(64);
+ Formatter formatter = new Formatter(this.sb, locale);
+
+ try {
+ fs.print(formatter, value, locale);
+ } catch (IOException ex) {
+ throw new AssertionError("FormatItemFormatSpecifier IOException", ex);
+ }
+ }
+
+ FormatItemFormatSpecifier(Locale locale,
+ int flags, int width, int precision,
+ Formattable formattable) {
+ this.sb = new StringBuilder(64);
+ Formatter formatter = new Formatter(this.sb, locale);
+ formattable.formatTo(formatter, flags, width, precision);
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return JLA.stringBuilderConcatMix(lengthCoder, sb);
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ return JLA.stringBuilderConcatPrepend(lengthCoder, buffer, sb);
+ }
+ }
+
+ protected static abstract sealed class FormatItemModifier implements FormatConcatItem
+ permits FormatItemFillLeft,
+ FormatItemFillRight
+ {
+ private final long itemLengthCoder;
+ protected final FormatConcatItem item;
+
+ FormatItemModifier(FormatConcatItem item) {
+ this.itemLengthCoder = item.mix(0L);
+ this.item = item;
+ }
+
+ int length() {
+ return (int)itemLengthCoder;
+ }
+
+ long coder() {
+ return itemLengthCoder & ~Integer.MAX_VALUE;
+ }
+
+ @Override
+ public abstract long mix(long lengthCoder);
+
+ @Override
+ public abstract long prepend(long lengthCoder, byte[] buffer) throws Throwable;
+ }
+
+ /**
+ * Fill left format item.
+ */
+ static final class FormatItemFillLeft extends FormatItemModifier
+ implements FormatConcatItem {
+ private final int width;
+
+ FormatItemFillLeft(int width, FormatConcatItem item) {
+ super(item);
+ this.width = Integer.max(length(), width);
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return (lengthCoder | coder()) + width;
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+ lengthCoder = item.prepend(lengthCoder, buffer);
+
+ for (int i = length(); i < width; i++) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)' ');
+ }
+
+ return lengthCoder;
+ }
+ }
+
+ /**
+ * Fill right format item.
+ */
+ static final class FormatItemFillRight extends FormatItemModifier
+ implements FormatConcatItem {
+ private final int width;
+
+ FormatItemFillRight(int width, FormatConcatItem item) {
+ super(item);
+ this.width = Integer.max(length(), width);
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return (lengthCoder | coder()) + width;
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+
+ for (int i = length(); i < width; i++) {
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)' ');
+ }
+
+ lengthCoder = item.prepend(lengthCoder, buffer);
+
+ return lengthCoder;
+ }
+ }
+
+
+ /**
+ * Null format item.
+ */
+ static final class FormatItemNull implements FormatConcatItem {
+ FormatItemNull() {
+ }
+
+ @Override
+ public long mix(long lengthCoder) {
+ return lengthCoder + "null".length();
+ }
+
+ @Override
+ public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
+ MethodHandle putCharMH = selectPutChar(lengthCoder);
+
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'l');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'l');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'u');
+ putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'n');
+
+ return lengthCoder;
+ }
+ }
+}
diff --git a/src/java.base/share/classes/java/util/FormatProcessor.java b/src/java.base/share/classes/java/util/FormatProcessor.java
new file mode 100644
index 00000000000..d5121ae5c4f
--- /dev/null
+++ b/src/java.base/share/classes/java/util/FormatProcessor.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.util;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.StringTemplate.Processor;
+import java.lang.StringTemplate.Processor.Linkage;
+import java.util.regex.Matcher;
+
+import jdk.internal.javac.PreviewFeature;
+
+/**
+ * This {@link Processor} constructs a {@link String} result using
+ * {@link Formatter} specifications and values found in the {@link StringTemplate}.
+ * Unlike {@link Formatter}, {@link FormatProcessor} uses the value from the
+ * embedded expression that immediately follows, without whitespace, the
+ * format specifier .
+ * For example:
+ * {@snippet :
+ * FormatProcessor fmt = FormatProcessor.create(Locale.ROOT);
+ * int x = 10;
+ * int y = 20;
+ * String result = fmt."%05d\{x} + %05d\{y} = %05d\{x + y}";
+ * }
+ * In the above example, the value of {@code result} will be {@code "00010 + 00020 = 00030"}.
+ *
+ * Embedded expressions without a preceeding format specifier, use {@code %s}
+ * by default.
+ * {@snippet :
+ * FormatProcessor fmt = FormatProcessor.create(Locale.ROOT);
+ * int x = 10;
+ * int y = 20;
+ * String result1 = fmt."\{x} + \{y} = \{x + y}";
+ * String result2 = fmt."%s\{x} + %s\{y} = %s\{x + y}";
+ * }
+ * In the above example, the value of {@code result1} and {@code result2} will
+ * both be {@code "10 + 20 = 30"}.
+ *
+ * The {@link FormatProcessor} format specification used and exceptions thrown are the
+ * same as those of {@link Formatter}.
+ *
+ * However, there are two significant differences related to the position of arguments.
+ * An explict {@code n$} and relative {@code <} index will cause an exception due to
+ * a missing argument list.
+ * Whitespace appearing between the specification and the embedded expression will
+ * also cause an exception.
+ *
+ * {@link FormatProcessor} allows the use of different locales. For example:
+ * {@snippet :
+ * Locale locale = Locale.forLanguageTag("th-TH-u-nu-thai");
+ * FormatProcessor thaiFMT = FormatProcessor.create(locale);
+ * int x = 10;
+ * int y = 20;
+ * String result = thaiFMT."%4d\{x} + %4d\{y} = %5d\{x + y}";
+ * }
+ * In the above example, the value of {@code result} will be
+ * {@code " \u0E51\u0E50 + \u0E52\u0E50 = \u0E53\u0E50"}.
+ *
+ * For day to day use, the predefined {@link FormatProcessor#FMT} {@link FormatProcessor}
+ * is available. {@link FormatProcessor#FMT} is defined using the {@link Locale#ROOT}.
+ * Example: {@snippet :
+ * int x = 10;
+ * int y = 20;
+ * String result = FMT."0x%04x\{x} + 0x%04x\{y} = 0x%04x\{x + y}"; // @highlight substring="FMT"
+ * }
+ * In the above example, the value of {@code result} will be {@code "0x000a + 0x0014 = 0x001E"}.
+ *
+ * @since 21
+ *
+ * @see Processor
+ */
+@PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+public final class FormatProcessor implements Processor, Linkage {
+ /**
+ * {@link Locale} used to format
+ */
+ private final Locale locale;
+
+ /**
+ * Constructor.
+ *
+ * @param locale {@link Locale} used to format
+ */
+ private FormatProcessor(Locale locale) {
+ this.locale = locale;
+ }
+
+ /**
+ * Create a new {@link FormatProcessor} using the specified locale.
+ *
+ * @param locale {@link Locale} used to format
+ *
+ * @return a new instance of {@link FormatProcessor}
+ *
+ * @throws java.lang.NullPointerException if locale is null
+ */
+ public static FormatProcessor create(Locale locale) {
+ Objects.requireNonNull(locale);
+ return new FormatProcessor(locale);
+ }
+
+ /**
+ * Constructs a {@link String} based on the fragments, format
+ * specifications found in the fragments and values in the
+ * supplied {@link StringTemplate} object. This method constructs a
+ * format string from the fragments, gathers up the values and
+ * evaluates the expression asif evaulating
+ * {@code new Formatter(locale).format(format, values).toString()}.
+ *
+ * If an embedded expression is not immediately preceded by a
+ * specifier then a {@code %s} is inserted in the format.
+ *
+ * @param stringTemplate a {@link StringTemplate} instance
+ *
+ * @return constructed {@link String}
+
+ * @throws IllegalFormatException
+ * If a format specifier contains an illegal syntax, a format
+ * specifier that is incompatible with the given arguments,
+ * a specifier not followed immediately by an embedded expression or
+ * other illegal conditions. For specification of all possible
+ * formatting errors, see the
+ * details
+ * section of the formatter class specification.
+ * @throws NullPointerException if stringTemplate is null
+ *
+ * @see java.util.Formatter
+ */
+ @Override
+ public final String process(StringTemplate stringTemplate) {
+ Objects.requireNonNull(stringTemplate);
+ String format = stringTemplateFormat(stringTemplate.fragments());
+ Object[] values = stringTemplate.values().toArray();
+
+ return new Formatter(locale).format(format, values).toString();
+ }
+
+ /**
+ * Constructs a {@link MethodHandle} that when supplied with the values from
+ * a {@link StringTemplate} will produce a result equivalent to that provided by
+ * {@link FormatProcessor#process(StringTemplate)}. This {@link MethodHandle}
+ * is used by {@link FormatProcessor#FMT} and the ilk to perform a more
+ * specialized composition of a result. This specialization is done by
+ * prescanning the fragments and value types of a {@link StringTemplate}.
+ *
+ * Process template expressions can be specialized when the processor is
+ * of type {@link Linkage} and fetched from a static constant as is
+ * {@link FormatProcessor#FMT} ({@code static final FormatProcessor}).
+ *
+ * Other {@link FormatProcessor FormatProcessors} can be specialized when stored in a static
+ * final.
+ * For example:
+ * {@snippet :
+ * FormatProcessor THAI_FMT = FormatProcessor.create(Locale.forLanguageTag("th-TH-u-nu-thai"));
+ * }
+ * {@code THAI_FMT} will now produce specialized {@link MethodHandle MethodHandles} by way
+ * of {@link FormatProcessor#linkage(List, MethodType)}.
+ *
+ * See {@link FormatProcessor#process(StringTemplate)} for more information.
+ *
+ * @throws IllegalFormatException
+ * If a format specifier contains an illegal syntax, a format
+ * specifier that is incompatible with the given arguments,
+ * a specifier not followed immediately by an embedded expression or
+ * other illegal conditions. For specification of all possible
+ * formatting errors, see the
+ * details
+ * section of the formatter class specification.
+ * @throws NullPointerException if fragments or type is null
+ *
+ * @see java.util.Formatter
+ */
+ @Override
+ public MethodHandle linkage(List fragments, MethodType type) {
+ Objects.requireNonNull(fragments);
+ Objects.requireNonNull(type);
+ String format = stringTemplateFormat(fragments);
+ Class>[] ptypes = type.dropParameterTypes(0, 1).parameterArray();
+ MethodHandle mh = new FormatterBuilder(format, locale, ptypes).build();
+ mh = MethodHandles.dropArguments(mh, 0, type.parameterType(0));
+
+ return mh;
+ }
+
+ /**
+ * Find a format specification at the end of a fragment.
+ *
+ * @param fragment fragment to check
+ * @param needed if the specification is needed
+ *
+ * @return true if the specification is found and needed
+ *
+ * @throws MissingFormatArgumentException if not at end or found and not needed
+ */
+ private static boolean findFormat(String fragment, boolean needed) {
+ Matcher matcher = Formatter.FORMAT_SPECIFIER_PATTERN.matcher(fragment);
+ String group;
+
+ while (matcher.find()) {
+ group = matcher.group();
+
+ if (!group.equals("%%") && !group.equals("%n")) {
+ if (matcher.end() == fragment.length() && needed) {
+ return true;
+ }
+
+ throw new MissingFormatArgumentException(group +
+ " is not immediately followed by an embedded expression");
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Convert {@link StringTemplate} fragments, containing format specifications,
+ * to a form that can be passed on to {@link Formatter}. The method scans each fragment,
+ * matching up formatter specifications with the following expression. If no
+ * specification is found, the method inserts "%s".
+ *
+ * @param fragments string template fragments
+ *
+ * @return format string
+ */
+ private static String stringTemplateFormat(List fragments) {
+ StringBuilder sb = new StringBuilder();
+ int lastIndex = fragments.size() - 1;
+ List formats = fragments.subList(0, lastIndex);
+ String last = fragments.get(lastIndex);
+
+ for (String format : formats) {
+ if (findFormat(format, true)) {
+ sb.append(format);
+ } else {
+ sb.append(format);
+ sb.append("%s");
+ }
+ }
+
+ if (!findFormat(last, false)) {
+ sb.append(last);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * This predefined {@link FormatProcessor} instance constructs a {@link String} result using
+ * the Locale.ROOT {@link Locale}. See {@link FormatProcessor} for more details.
+ * Example: {@snippet :
+ * int x = 10;
+ * int y = 20;
+ * String result = FMT."0x%04x\{x} + 0x%04x\{y} = 0x%04x\{x + y}"; // @highlight substring="FMT"
+ * }
+ * In the above example, the value of {@code result} will be {@code "0x000a + 0x0014 = 0x001E"}.
+ *
+ * @see java.util.FormatProcessor
+ */
+ public static final FormatProcessor FMT = FormatProcessor.create(Locale.ROOT);
+
+}
diff --git a/src/java.base/share/classes/java/util/Formatter.java b/src/java.base/share/classes/java/util/Formatter.java
index ca9b9deb92e..5febf0d9153 100644
--- a/src/java.base/share/classes/java/util/Formatter.java
+++ b/src/java.base/share/classes/java/util/Formatter.java
@@ -36,6 +36,7 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandle;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
@@ -60,6 +61,7 @@ import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQueries;
import java.time.temporal.UnsupportedTemporalTypeException;
+import jdk.internal.javac.PreviewFeature;
import jdk.internal.math.DoubleConsts;
import jdk.internal.math.FormattedFPDecimal;
import sun.util.locale.provider.LocaleProviderAdapter;
@@ -2770,8 +2772,7 @@ public final class Formatter implements Closeable, Flushable {
int lasto = -1;
List fsa = parse(format);
- for (int i = 0; i < fsa.size(); i++) {
- var fs = fsa.get(i);
+ for (FormatString fs : fsa) {
int index = fs.index();
try {
switch (index) {
@@ -2789,7 +2790,7 @@ public final class Formatter implements Closeable, Flushable {
throw new MissingFormatArgumentException(fs.toString());
fs.print(this, (args == null ? null : args[lasto]), l);
}
- default -> { // explicit index
+ default -> { // explicit index
last = index - 1;
if (args != null && last > args.length - 1)
throw new MissingFormatArgumentException(fs.toString());
@@ -2804,15 +2805,15 @@ public final class Formatter implements Closeable, Flushable {
}
// %[argument_index$][flags][width][.precision][t]conversion
- private static final String formatSpecifier
+ static final String FORMAT_SPECIFIER
= "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
- private static final Pattern fsPattern = Pattern.compile(formatSpecifier);
+ static final Pattern FORMAT_SPECIFIER_PATTERN = Pattern.compile(FORMAT_SPECIFIER);
/**
* Finds format specifiers in the format string.
*/
- private List parse(String s) {
+ static List parse(String s) {
ArrayList al = new ArrayList<>();
int i = 0;
int max = s.length();
@@ -2840,7 +2841,7 @@ public final class Formatter implements Closeable, Flushable {
i++;
} else {
if (m == null) {
- m = fsPattern.matcher(s);
+ m = FORMAT_SPECIFIER_PATTERN.matcher(s);
}
// We have already parsed a '%' at n, so we either have a
// match or the specifier at n is invalid
@@ -2855,7 +2856,7 @@ public final class Formatter implements Closeable, Flushable {
return al;
}
- private interface FormatString {
+ interface FormatString {
int index();
void print(Formatter fmt, Object arg, Locale l) throws IOException;
String toString();
@@ -2891,14 +2892,15 @@ public final class Formatter implements Closeable, Flushable {
DECIMAL_FLOAT
};
- private static class FormatSpecifier implements FormatString {
+ static class FormatSpecifier implements FormatString {
+ private static final double SCALEUP = Math.scalb(1.0, 54);
- private int index = 0;
- private int flags = Flags.NONE;
- private int width = -1;
- private int precision = -1;
- private boolean dt = false;
- private char c;
+ int index = 0;
+ int flags = Flags.NONE;
+ int width = -1;
+ int precision = -1;
+ boolean dt = false;
+ char c;
private void index(String s, int start, int end) {
if (start >= 0) {
@@ -3548,8 +3550,8 @@ public final class Formatter implements Closeable, Flushable {
if (width != -1) {
newW = adjustWidth(width - exp.length - 1, flags, neg);
}
- localizedMagnitude(fmt, sb, mant, 0, flags, newW, l);
+ localizedMagnitude(fmt, sb, mant, 0, flags, newW, l);
sb.append(Flags.contains(flags, Flags.UPPERCASE) ? 'E' : 'e');
char sign = exp[0];
@@ -3719,8 +3721,7 @@ public final class Formatter implements Closeable, Flushable {
// If this is subnormal input so normalize (could be faster to
// do as integer operation).
if (subnormal) {
- double scaleUp = Math.scalb(1.0, 54);
- d *= scaleUp;
+ d *= SCALEUP;
// Calculate the exponent. This is not just exponent + 54
// since the former is not the normalized exponent.
exponent = Math.getExponent(d);
@@ -4623,7 +4624,7 @@ public final class Formatter implements Closeable, Flushable {
}
}
- private static class Flags {
+ static class Flags {
static final int NONE = 0; // ''
@@ -4701,7 +4702,7 @@ public final class Formatter implements Closeable, Flushable {
}
}
- private static class Conversion {
+ static class Conversion {
// Byte, Short, Integer, Long, BigInteger
// (and associated primitives due to autoboxing)
static final char DECIMAL_INTEGER = 'd';
@@ -4826,7 +4827,7 @@ public final class Formatter implements Closeable, Flushable {
}
}
- private static class DateTime {
+ static class DateTime {
static final char HOUR_OF_DAY_0 = 'H'; // (00 - 23)
static final char HOUR_0 = 'I'; // (01 - 12)
static final char HOUR_OF_DAY = 'k'; // (0 - 23) -- like H
@@ -4877,4 +4878,5 @@ public final class Formatter implements Closeable, Flushable {
};
}
}
+
}
diff --git a/src/java.base/share/classes/java/util/FormatterBuilder.java b/src/java.base/share/classes/java/util/FormatterBuilder.java
new file mode 100644
index 00000000000..854fdfef68d
--- /dev/null
+++ b/src/java.base/share/classes/java/util/FormatterBuilder.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 java.util;
+
+import java.io.IOException;
+import java.lang.invoke.*;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.text.spi.NumberFormatProvider;
+import java.util.FormatItem.*;
+import java.util.Formatter.*;
+
+import jdk.internal.util.FormatConcatItem;
+
+import sun.invoke.util.Wrapper;
+import sun.util.locale.provider.LocaleProviderAdapter;
+import sun.util.locale.provider.ResourceBundleBasedAdapter;
+
+import static java.util.Formatter.Conversion.*;
+import static java.util.Formatter.Flags.*;
+import static java.lang.invoke.MethodHandles.*;
+import static java.lang.invoke.MethodType.*;
+
+/**
+ * This package private class supports the construction of the {@link MethodHandle}
+ * used by {@link FormatProcessor}.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+final class FormatterBuilder {
+ private static final Lookup LOOKUP = lookup();
+
+ private final String format;
+ private final Locale locale;
+ private final Class>[] ptypes;
+ private final DecimalFormatSymbols dfs;
+ private final boolean isGenericDFS;
+
+ FormatterBuilder(String format, Locale locale, Class>[] ptypes) {
+ this.format = format;
+ this.locale = locale;
+ this.ptypes = ptypes;
+ this.dfs = DecimalFormatSymbols.getInstance(locale);
+ this.isGenericDFS = isGenericDFS(this.dfs);
+ }
+
+ private static boolean isGenericDFS(DecimalFormatSymbols dfs) {
+ return dfs.getZeroDigit() == '0' &&
+ dfs.getDecimalSeparator() == '.' &&
+ dfs.getGroupingSeparator() == ',' &&
+ dfs.getMinusSign() == '-';
+ }
+
+ private static Class> mapType(Class> type) {
+ return type.isPrimitive() || type == String.class ? type : Object.class;
+ }
+
+ private static MethodHandle findStringConcatItemConstructor(Class> cls,
+ Class>... ptypes) {
+ MethodType methodType = methodType(void.class, ptypes);
+
+ try {
+ MethodHandle mh = LOOKUP.findConstructor(cls, methodType);
+
+ return mh.asType(mh.type().changeReturnType(FormatConcatItem.class));
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError("Missing constructor in " +
+ cls + ": " + methodType);
+ }
+ }
+
+ private static MethodHandle findMethod(Class> cls, String name,
+ Class> rType, Class>... ptypes) {
+ MethodType methodType = methodType(rType, ptypes);
+
+ try {
+ return LOOKUP.findVirtual(cls, name, methodType);
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError("Missing method in " +
+ cls + ": " + name + " " + methodType);
+ }
+ }
+
+ private static MethodHandle findStaticMethod(Class> cls, String name,
+ Class> rType, Class>... ptypes) {
+ MethodType methodType = methodType(rType, ptypes);
+
+ try {
+ return LOOKUP.findStatic(cls, name, methodType);
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError("Missing static method in " +
+ cls + ": " + name + " " + methodType);
+ }
+ }
+
+ private static final MethodHandle FIDecimal_MH =
+ findStringConcatItemConstructor(FormatItemDecimal.class,
+ DecimalFormatSymbols.class, int.class, char.class, boolean.class,
+ int.class, long.class);
+
+ private static final MethodHandle FIHexadecimal_MH =
+ findStringConcatItemConstructor(FormatItemHexadecimal.class,
+ int.class, boolean.class, long.class);
+
+ private static final MethodHandle FIOctal_MH =
+ findStringConcatItemConstructor(FormatItemOctal.class,
+ int.class, boolean.class, long.class);
+
+ private static final MethodHandle FIBoolean_MH =
+ findStringConcatItemConstructor(FormatItemBoolean.class,
+ boolean.class);
+
+ private static final MethodHandle FICharacter_MH =
+ findStringConcatItemConstructor(FormatItemCharacter.class,
+ char.class);
+
+ private static final MethodHandle FIString_MH =
+ findStringConcatItemConstructor(FormatItemString.class,
+ String.class);
+
+ private static final MethodHandle FIFormatSpecifier_MH =
+ findStringConcatItemConstructor(FormatItemFormatSpecifier.class,
+ FormatSpecifier.class, Locale.class, Object.class);
+
+ private static final MethodHandle FIFormattable_MH =
+ findStringConcatItemConstructor(FormatItemFormatSpecifier.class,
+ Locale.class, int.class, int.class, int.class,
+ Formattable.class);
+
+ private static final MethodHandle FIFillLeft_MH =
+ findStringConcatItemConstructor(FormatItemFillLeft.class,
+ int.class, FormatConcatItem.class);
+
+ private static final MethodHandle FIFillRight_MH =
+ findStringConcatItemConstructor(FormatItemFillRight.class,
+ int.class, FormatConcatItem.class);
+
+ private static final MethodHandle FINull_MH =
+ findStringConcatItemConstructor(FormatItemNull.class);
+
+ private static final MethodHandle NullCheck_MH =
+ findStaticMethod(FormatterBuilder.class, "nullCheck", boolean.class,
+ Object.class);
+
+ private static final MethodHandle FormattableCheck_MH =
+ findStaticMethod(FormatterBuilder.class, "formattableCheck", boolean.class,
+ Object.class);
+
+ private static final MethodHandle ToLong_MH =
+ findStaticMethod(java.util.FormatterBuilder.class, "toLong", long.class,
+ int.class);
+
+ private static final MethodHandle ToString_MH =
+ findStaticMethod(String.class, "valueOf", String.class,
+ Object.class);
+
+ private static final MethodHandle HashCode_MH =
+ findStaticMethod(Objects.class, "hashCode", int.class,
+ Object.class);
+
+ private static boolean nullCheck(Object object) {
+ return object == null;
+ }
+
+ private static boolean formattableCheck(Object object) {
+ return Formattable.class.isAssignableFrom(object.getClass());
+ }
+
+ private static long toLong(int value) {
+ return (long)value & 0xFFFFFFFFL;
+ }
+
+ private static boolean isFlag(int value, int flags) {
+ return (value & flags) != 0;
+ }
+
+ private static boolean validFlags(int value, int flags) {
+ return (value & ~flags) == 0;
+ }
+
+ private static int groupSize(Locale locale, DecimalFormatSymbols dfs) {
+ if (isGenericDFS(dfs)) {
+ return 3;
+ }
+
+ DecimalFormat df;
+ NumberFormat nf = NumberFormat.getNumberInstance(locale);
+
+ if (nf instanceof DecimalFormat) {
+ df = (DecimalFormat)nf;
+ } else {
+ LocaleProviderAdapter adapter = LocaleProviderAdapter
+ .getAdapter(NumberFormatProvider.class, locale);
+
+ if (!(adapter instanceof ResourceBundleBasedAdapter)) {
+ adapter = LocaleProviderAdapter.getResourceBundleBased();
+ }
+
+ String[] all = adapter.getLocaleResources(locale)
+ .getNumberPatterns();
+
+ df = new DecimalFormat(all[0], dfs);
+ }
+
+ return df.isGroupingUsed() ? df.getGroupingSize() : 0;
+ }
+
+ private MethodHandle formatSpecifier(FormatSpecifier fs, Class> ptype) {
+ boolean isPrimitive = ptype.isPrimitive();
+ MethodHandle mh = identity(ptype);
+ MethodType mt = mh.type();
+
+//cannot cast to primitive types as it breaks null values formatting
+// if (ptype == byte.class || ptype == short.class ||
+// ptype == Byte.class || ptype == Short.class ||
+// ptype == Integer.class) {
+// mt = mt.changeReturnType(int.class);
+// } else if (ptype == Long.class) {
+// mt = mt.changeReturnType(long.class);
+// } else if (ptype == float.class || ptype == Float.class ||
+// ptype == Double.class) {
+// mt = mt.changeReturnType(double.class);
+// } else if (ptype == Boolean.class) {
+// mt = mt.changeReturnType(boolean.class);
+// } else if (ptype == Character.class) {
+// mt = mt.changeReturnType(char.class);
+// }
+
+ Class> itype = mt.returnType();
+
+ if (itype != ptype) {
+ mh = explicitCastArguments(mh, mt);
+ }
+
+ boolean handled = false;
+ int flags = fs.flags;
+ int width = fs.width;
+ int precision = fs.precision;
+ Character conv = fs.dt ? 't' : fs.c;
+
+ switch (Character.toLowerCase(conv)) {
+ case BOOLEAN -> {
+ if (itype == boolean.class && precision == -1) {
+ if (flags == 0 && width == -1 && isPrimitive) {
+ return null;
+ }
+
+ if (validFlags(flags, LEFT_JUSTIFY)) {
+ handled = true;
+ mh = filterReturnValue(mh, FIBoolean_MH);
+ }
+ }
+ }
+ case STRING -> {
+ if (flags == 0 && width == -1 && precision == -1) {
+ if (isPrimitive || ptype == String.class) {
+ return null;
+ } else if (itype.isPrimitive()) {
+ return mh;
+ }
+ }
+
+ if (validFlags(flags, LEFT_JUSTIFY) && precision == -1) {
+ if (itype == String.class) {
+ handled = true;
+ mh = filterReturnValue(mh, FIString_MH);
+ } else if (!itype.isPrimitive()) {
+ handled = true;
+ MethodHandle test = FormattableCheck_MH;
+ test = test.asType(test.type().changeParameterType(0, ptype));
+ MethodHandle pass = insertArguments(FIFormattable_MH,
+ 0, locale, flags, width, precision);
+ pass = pass.asType(pass.type().changeParameterType(0, ptype));
+ MethodHandle fail = ToString_MH;
+ fail = filterReturnValue(fail, FIString_MH);
+ fail = fail.asType(fail.type().changeParameterType(0, ptype));
+ mh = guardWithTest(test, pass, fail);
+ }
+ }
+ }
+ case CHARACTER -> {
+ if (itype == char.class && precision == -1) {
+ if (flags == 0 && width == -1) {
+ return isPrimitive ? null : mh;
+ }
+
+ if (validFlags(flags, LEFT_JUSTIFY)) {
+ handled = true;
+ mh = filterReturnValue(mh, FICharacter_MH);
+ }
+ }
+ }
+ case DECIMAL_INTEGER -> {
+ if ((itype == int.class || itype == long.class) && precision == -1) {
+ if (itype == int.class) {
+ mh = explicitCastArguments(mh,
+ mh.type().changeReturnType(long.class));
+ }
+
+ if (flags == 0 && isGenericDFS && width == -1) {
+ return mh;
+ } else if (validFlags(flags, PLUS | LEADING_SPACE |
+ ZERO_PAD | GROUP |
+ PARENTHESES)) {
+ handled = true;
+ int zeroPad = isFlag(flags, ZERO_PAD) ? width : -1;
+ char sign = isFlag(flags, PLUS) ? '+' :
+ isFlag(flags, LEADING_SPACE) ? ' ' : '\0';
+ boolean parentheses = isFlag(flags, PARENTHESES);
+ int groupSize = isFlag(flags, GROUP) ?
+ groupSize(locale, dfs) : 0;
+ mh = filterReturnValue(mh,
+ insertArguments(FIDecimal_MH, 0, dfs, zeroPad,
+ sign, parentheses, groupSize));
+ }
+ }
+ }
+ case OCTAL_INTEGER -> {
+ if ((itype == int.class || itype == long.class) &&
+ precision == -1 &&
+ validFlags(flags, ZERO_PAD | ALTERNATE)) {
+ handled = true;
+
+ if (itype == int.class) {
+ mh = filterReturnValue(mh, ToLong_MH);
+ }
+
+ int zeroPad = isFlag(flags, ZERO_PAD) ? width : -1;
+ boolean hasPrefix = isFlag(flags, ALTERNATE);
+ mh = filterReturnValue(mh,
+ insertArguments(FIOctal_MH, 0, zeroPad, hasPrefix));
+ }
+ }
+ case HEXADECIMAL_INTEGER -> {
+ if ((itype == int.class || itype == long.class) &&
+ precision == -1 &&
+ validFlags(flags, ZERO_PAD | ALTERNATE)) {
+ handled = true;
+
+ if (itype == int.class) {
+ mh = filterReturnValue(mh, ToLong_MH);
+ }
+
+ int zeroPad = isFlag(flags, ZERO_PAD) ? width : -1;
+ boolean hasPrefix = isFlag(flags, ALTERNATE);
+ mh = filterReturnValue(mh,
+ insertArguments(FIHexadecimal_MH, 0, zeroPad, hasPrefix));
+ }
+ }
+ default -> {
+ // pass thru
+ }
+ }
+
+ if (handled) {
+ if (!isPrimitive) {
+ MethodHandle test = NullCheck_MH.asType(
+ NullCheck_MH.type().changeParameterType(0, ptype));
+ MethodHandle pass = dropArguments(FINull_MH, 0, ptype);
+ mh = guardWithTest(test, pass, mh);
+ }
+
+ if (0 < width) {
+ if (isFlag(flags, LEFT_JUSTIFY)) {
+ mh = filterReturnValue(mh,
+ insertArguments(FIFillRight_MH, 0, width));
+ } else {
+ mh = filterReturnValue(mh,
+ insertArguments(FIFillLeft_MH, 0, width));
+ }
+ }
+
+ if (!isFlag(flags, UPPERCASE)) {
+ return mh;
+ }
+ }
+
+ mh = insertArguments(FIFormatSpecifier_MH, 0, fs, locale);
+ mh = mh.asType(mh.type().changeParameterType(0, ptype));
+
+ return mh;
+ }
+
+ /**
+ * Construct concat {@link MethodHandle} for based on format.
+ *
+ * @param fsa list of specifiers
+ *
+ * @return concat {@link MethodHandle} for based on format
+ */
+ private MethodHandle buildFilters(List fsa,
+ List segments,
+ MethodHandle[] filters) {
+ MethodHandle mh = null;
+ int iParam = 0;
+ StringBuilder segment = new StringBuilder();
+
+ for (FormatString fs : fsa) {
+ int index = fs.index();
+
+ switch (index) {
+ case -2: // fixed string, "%n", or "%%"
+ String string = fs.toString();
+
+ if ("%%".equals(string)) {
+ segment.append('%');
+ } else if ("%n".equals(string)) {
+ segment.append(System.lineSeparator());
+ } else {
+ segment.append(string);
+ }
+ break;
+ case 0: // ordinary index
+ segments.add(segment.toString());
+ segment.setLength(0);
+
+ if (iParam < ptypes.length) {
+ Class> ptype = ptypes[iParam];
+ filters[iParam++] = formatSpecifier((FormatSpecifier)fs, ptype);
+ } else {
+ throw new MissingFormatArgumentException(fs.toString());
+ }
+ break;
+ case -1: // relative index
+ default: // explicit index
+ throw new IllegalFormatFlagsException("Indexing not allowed: " + fs.toString());
+ }
+ }
+
+ segments.add(segment.toString());
+
+ return mh;
+ }
+
+ /**
+ * Build a {@link MethodHandle} to format arguments.
+ *
+ * @return new {@link MethodHandle} to format arguments
+ */
+ MethodHandle build() {
+ List segments = new ArrayList<>();
+ MethodHandle[] filters = new MethodHandle[ptypes.length];
+ buildFilters(Formatter.parse(format), segments, filters);
+ Class>[] ftypes = new Class>[filters.length];
+
+ for (int i = 0; i < filters.length; i++) {
+ MethodHandle filter = filters[i];
+ ftypes[i] = filter == null ? ptypes[i] : filter.type().returnType();
+ }
+
+ try {
+ MethodHandle mh = StringConcatFactory.makeConcatWithTemplate(segments,
+ List.of(ftypes));
+ mh = filterArguments(mh, 0, filters);
+
+ return mh;
+ } catch (StringConcatException ex) {
+ throw new AssertionError("concat fail", ex);
+ }
+ }
+}
diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
index e1f7a4c2350..b03ebb7bcd2 100644
--- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
+++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
@@ -45,6 +45,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
import java.util.stream.Stream;
+import jdk.internal.javac.PreviewFeature;
import jdk.internal.misc.CarrierThreadLocal;
import jdk.internal.module.ServicesCatalog;
import jdk.internal.reflect.ConstantPool;
@@ -420,6 +421,24 @@ public interface JavaLangAccess {
*/
long stringConcatMix(long lengthCoder, String constant);
+ /**
+ * Get the coder for the supplied character.
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ long stringConcatCoder(char value);
+
+ /**
+ * Update lengthCoder for StringBuilder.
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ long stringBuilderConcatMix(long lengthCoder, StringBuilder sb);
+
+ /**
+ * Prepend StringBuilder content.
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ long stringBuilderConcatPrepend(long lengthCoder, byte[] buf, StringBuilder sb);
+
/**
* Join strings
*/
diff --git a/src/java.base/share/classes/jdk/internal/access/JavaTemplateAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaTemplateAccess.java
new file mode 100644
index 00000000000..da99f7806d1
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/access/JavaTemplateAccess.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 jdk.internal.access;
+
+import java.util.List;
+
+public interface JavaTemplateAccess {
+
+ /**
+ * Returns a StringTemplate composed from fragments and values.
+ *
+ * @implSpec The {@code fragments} list size must be one more that the
+ * {@code values} list size.
+ *
+ * @param fragments list of string fragments
+ * @param values list of expression values
+ *
+ * @return StringTemplate composed from fragments and values
+ *
+ * @throws IllegalArgumentException if fragments list size is not one more
+ * than values list size
+ * @throws NullPointerException if fragments is null or values is null or if any fragment is null.
+ *
+ * @implNote Contents of both lists are copied to construct immutable lists.
+ */
+ StringTemplate of(List fragments, List> values);
+
+ /**
+ * Creates a string that interleaves the elements of values between the
+ * elements of fragments.
+ *
+ * @param fragments list of String fragments
+ * @param values list of expression values
+ *
+ * @return String interpolation of fragments and values
+ */
+ String interpolate(List fragments, List> values);
+
+ /**
+ * Combine one or more {@link StringTemplate StringTemplates} to produce a combined {@link StringTemplate}.
+ * {@snippet :
+ * StringTemplate st = StringTemplate.combine("\{a}", "\{b}", "\{c}");
+ * assert st.interpolate().equals("\{a}\{b}\{c}");
+ * }
+ *
+ * @param sts zero or more {@link StringTemplate}
+ *
+ * @return combined {@link StringTemplate}
+ *
+ * @throws NullPointerException if sts is null or if any element of sts is null
+ */
+ StringTemplate combine(StringTemplate... sts);
+
+}
+
diff --git a/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java b/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java
index cf76aa9ff94..919d758a6e3 100644
--- a/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java
+++ b/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java
@@ -89,6 +89,7 @@ public class SharedSecrets {
private static JavaSecuritySpecAccess javaSecuritySpecAccess;
private static JavaxCryptoSealedObjectAccess javaxCryptoSealedObjectAccess;
private static JavaxCryptoSpecAccess javaxCryptoSpecAccess;
+ private static JavaTemplateAccess javaTemplateAccess;
public static void setJavaUtilCollectionAccess(JavaUtilCollectionAccess juca) {
javaUtilCollectionAccess = juca;
@@ -516,6 +517,21 @@ public class SharedSecrets {
return access;
}
+ public static void setJavaTemplateAccess(JavaTemplateAccess jta) {
+ javaTemplateAccess = jta;
+ }
+
+ public static JavaTemplateAccess getJavaTemplateAccess() {
+ var access = javaTemplateAccess;
+ if (access == null) {
+ try {
+ Class.forName("java.lang.runtime.TemplateSupport", true, null);
+ access = javaTemplateAccess;
+ } catch (ClassNotFoundException e) {}
+ }
+ return access;
+ }
+
private static void ensureClassInitialized(Class> c) {
try {
MethodHandles.lookup().ensureInitialized(c);
diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
index ccd62c1d744..59d07cdbf1e 100644
--- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
+++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
@@ -72,6 +72,8 @@ public @interface PreviewFeature {
VIRTUAL_THREADS,
@JEP(number=442, title="Foreign Function & Memory API", status="Third Preview")
FOREIGN,
+ @JEP(number=430, title="String Templates", status="First Preview")
+ STRING_TEMPLATES,
/**
* A key for testing.
*/
diff --git a/src/java.base/share/classes/jdk/internal/util/FormatConcatItem.java b/src/java.base/share/classes/jdk/internal/util/FormatConcatItem.java
new file mode 100644
index 00000000000..b4588968a3e
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/util/FormatConcatItem.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 jdk.internal.util;
+
+/**
+ * Implementations of this class provide information necessary to
+ * assist {@link java.lang.invoke.StringConcatFactory} perform optimal
+ * insertion.
+ *
+ * @since 21
+ *
+ * Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
+ * Do not rely on its availability.
+ */
+public interface FormatConcatItem {
+ /**
+ * Calculate the length of the insertion.
+ *
+ * @param lengthCoder current value of the length + coder
+ * @return adjusted value of the length + coder
+ */
+ long mix(long lengthCoder);
+
+ /**
+ * Insert content into buffer prior to the current length.
+ *
+ * @param lengthCoder current value of the length + coder
+ * @param buffer buffer to append to
+ *
+ * @return adjusted value of the length + coder
+ *
+ * @throws Throwable if fails to prepend value (unusual).
+ */
+ long prepend(long lengthCoder, byte[] buffer) throws Throwable;
+}
diff --git a/src/jdk.compiler/share/classes/com/sun/source/tree/ClassTree.java b/src/jdk.compiler/share/classes/com/sun/source/tree/ClassTree.java
index 7e6b50cc08f..ee1f40dc196 100644
--- a/src/jdk.compiler/share/classes/com/sun/source/tree/ClassTree.java
+++ b/src/jdk.compiler/share/classes/com/sun/source/tree/ClassTree.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 2023, 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
@@ -25,7 +25,6 @@
package com.sun.source.tree;
-import java.util.Collections;
import java.util.List;
import javax.lang.model.element.Name;
diff --git a/src/jdk.compiler/share/classes/com/sun/source/tree/StringTemplateTree.java b/src/jdk.compiler/share/classes/com/sun/source/tree/StringTemplateTree.java
new file mode 100644
index 00000000000..6e3ca7d13b5
--- /dev/null
+++ b/src/jdk.compiler/share/classes/com/sun/source/tree/StringTemplateTree.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 com.sun.source.tree;
+
+import java.util.List;
+
+import jdk.internal.javac.PreviewFeature;
+
+/**
+ * A tree node for a string template expression.
+ *
+ */
+@PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES, reflective=true)
+public interface StringTemplateTree extends ExpressionTree {
+ /**
+ * Returns templated string processor (may be qualified) or null.
+ *
+ * @return templated string processor
+ */
+ ExpressionTree getProcessor();
+
+ /**
+ * Returns string fragments.
+ *
+ * @return string fragments
+ */
+ List getFragments();
+
+ /**
+ * Returns list of expressions.
+ *
+ * @return list of expressions
+ */
+ List extends ExpressionTree> getExpressions();
+}
diff --git a/src/jdk.compiler/share/classes/com/sun/source/tree/Tree.java b/src/jdk.compiler/share/classes/com/sun/source/tree/Tree.java
index a74e4874074..ceb7ace553a 100644
--- a/src/jdk.compiler/share/classes/com/sun/source/tree/Tree.java
+++ b/src/jdk.compiler/share/classes/com/sun/source/tree/Tree.java
@@ -175,6 +175,12 @@ public interface Tree {
*/
INSTANCE_OF(InstanceOfTree.class),
+ /**
+ * Used for instances of {@link StringTemplateTree}.
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES, reflective=true)
+ TEMPLATE(StringTemplateTree.class),
+
/**
* Used for instances of {@link LabeledStatementTree}.
*/
diff --git a/src/jdk.compiler/share/classes/com/sun/source/tree/TreeVisitor.java b/src/jdk.compiler/share/classes/com/sun/source/tree/TreeVisitor.java
index 04fbb3bc997..98ff3cdc749 100644
--- a/src/jdk.compiler/share/classes/com/sun/source/tree/TreeVisitor.java
+++ b/src/jdk.compiler/share/classes/com/sun/source/tree/TreeVisitor.java
@@ -259,6 +259,15 @@ public interface TreeVisitor {
*/
R visitLiteral(LiteralTree node, P p);
+ /**
+ * Visits a StringTemplateTree node.
+ * @param node the node being visited
+ * @param p a parameter value
+ * @return a result value
+ */
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES, reflective=true)
+ R visitStringTemplate(StringTemplateTree node, P p);
+
/**
* Visits a {@code BindingPatternTree} node.
* @param node the node being visited
diff --git a/src/jdk.compiler/share/classes/com/sun/source/util/SimpleTreeVisitor.java b/src/jdk.compiler/share/classes/com/sun/source/util/SimpleTreeVisitor.java
index 74872f25ec7..048c67af68d 100644
--- a/src/jdk.compiler/share/classes/com/sun/source/util/SimpleTreeVisitor.java
+++ b/src/jdk.compiler/share/classes/com/sun/source/util/SimpleTreeVisitor.java
@@ -628,6 +628,19 @@ public class SimpleTreeVisitor implements TreeVisitor {
return defaultAction(node, p);
}
+ /**
+ * {@inheritDoc} This implementation calls {@code defaultAction}.
+ *
+ * @param node {@inheritDoc}
+ * @param p {@inheritDoc}
+ * @return the result of {@code defaultAction}
+ */
+ @Override
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES, reflective=true)
+ public R visitStringTemplate(StringTemplateTree node, P p) {
+ return defaultAction(node, p);
+ }
+
/**
* {@inheritDoc}
*
diff --git a/src/jdk.compiler/share/classes/com/sun/source/util/TreeScanner.java b/src/jdk.compiler/share/classes/com/sun/source/util/TreeScanner.java
index 1a591de94cf..b339ad3dd9b 100644
--- a/src/jdk.compiler/share/classes/com/sun/source/util/TreeScanner.java
+++ b/src/jdk.compiler/share/classes/com/sun/source/util/TreeScanner.java
@@ -759,6 +759,23 @@ public class TreeScanner implements TreeVisitor {
return r;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @implSpec This implementation scans the children in left to right order.
+ *
+ * @param node {@inheritDoc}
+ * @param p {@inheritDoc}
+ * @return the result of scanning
+ */
+ @Override
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES, reflective=true)
+ public R visitStringTemplate(StringTemplateTree node, P p) {
+ R r = scan(node.getProcessor(), p);
+ r = scanAndReduce(node.getExpressions(), p, r);
+ return r;
+ }
+
/**
* {@inheritDoc}
*
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java
index fc023219b33..9e0ababaaf7 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java
@@ -211,6 +211,7 @@ public class Preview {
return switch (feature) {
case CASE_NULL -> true;
case PATTERN_SWITCH -> true;
+ case STRING_TEMPLATES -> true;
case UNCONDITIONAL_PATTERN_IN_INSTANCEOF -> true;
case RECORD_PATTERNS -> true;
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java
index 8931aa26a89..59bd648dce9 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java
@@ -235,6 +235,7 @@ public enum Source {
CASE_NULL(JDK17, Fragments.FeatureCaseNull, DiagKind.NORMAL),
PATTERN_SWITCH(JDK17, Fragments.FeaturePatternSwitch, DiagKind.PLURAL),
REDUNDANT_STRICTFP(JDK17),
+ STRING_TEMPLATES(JDK21, Fragments.FeatureStringTemplates, DiagKind.PLURAL),
UNCONDITIONAL_PATTERN_IN_INSTANCEOF(JDK19, Fragments.FeatureUnconditionalPatternsInInstanceof, DiagKind.PLURAL),
RECORD_PATTERNS(JDK19, Fragments.FeatureDeconstructionPatterns, DiagKind.PLURAL),
WARN_ON_ILLEGAL_UTF8(MIN, JDK21),
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java
index a01f95190c2..e63ea1234cb 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symtab.java
@@ -174,6 +174,7 @@ public class Symtab {
public final Type serializedLambdaType;
public final Type varHandleType;
public final Type methodHandleType;
+ public final Type methodHandlesType;
public final Type methodHandleLookupType;
public final Type methodTypeType;
public final Type nativeHeaderType;
@@ -234,6 +235,12 @@ public class Symtab {
public final Type objectStreamExceptionType;
public final Type externalizableType;
+ // For string templates
+ public final Type stringTemplateType;
+ public final Type templateRuntimeType;
+ public final Type processorType;
+ public final Type linkageType;
+
/** The symbol representing the length field of an array.
*/
public final VarSymbol lengthVar;
@@ -543,6 +550,7 @@ public class Symtab {
serializedLambdaType = enterClass("java.lang.invoke.SerializedLambda");
varHandleType = enterClass("java.lang.invoke.VarHandle");
methodHandleType = enterClass("java.lang.invoke.MethodHandle");
+ methodHandlesType = enterClass("java.lang.invoke.MethodHandles");
methodHandleLookupType = enterClass("java.lang.invoke.MethodHandles$Lookup");
methodTypeType = enterClass("java.lang.invoke.MethodType");
errorType = enterClass("java.lang.Error");
@@ -610,7 +618,6 @@ public class Symtab {
ioExceptionType = enterClass("java.io.IOException");
objectStreamExceptionType = enterClass("java.io.ObjectStreamException");
externalizableType = enterClass("java.io.Externalizable");
-
synthesizeEmptyInterfaceIfMissing(autoCloseableType);
synthesizeEmptyInterfaceIfMissing(cloneableType);
synthesizeEmptyInterfaceIfMissing(serializableType);
@@ -621,6 +628,12 @@ public class Symtab {
synthesizeBoxTypeIfMissing(floatType);
synthesizeBoxTypeIfMissing(voidType);
+ // For string templates
+ stringTemplateType = enterClass("java.lang.StringTemplate");
+ templateRuntimeType = enterClass("java.lang.runtime.TemplateRuntime");
+ processorType = enterClass("java.lang.StringTemplate$Processor");
+ linkageType = enterClass("java.lang.StringTemplate$Processor$Linkage");
+
// Enter a synthetic class that is used to mark internal
// proprietary classes in ct.sym. This class does not have a
// class file.
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Attr.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Attr.java
index 051aadd09e1..2c6fbfe4043 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Attr.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Attr.java
@@ -4984,6 +4984,27 @@ public class Attr extends JCTree.Visitor {
return (tag == CLASS) ? syms.stringType : syms.typeOfTag[tag.ordinal()];
}
+ public void visitStringTemplate(JCStringTemplate tree) {
+ JCExpression processor = tree.processor;
+ Type resultType = syms.stringTemplateType;
+
+ if (processor != null) {
+ resultType = attribTree(processor, env, new ResultInfo(KindSelector.VAL, Type.noType));
+ resultType = chk.checkProcessorType(processor, resultType, env);
+ }
+
+ Env localEnv = env.dup(tree, env.info.dup());
+
+ for (JCExpression arg : tree.expressions) {
+ chk.checkNonVoid(arg.pos(), attribExpr(arg, localEnv));
+ }
+
+ tree.type = resultType;
+ result = resultType;
+
+ check(tree, resultType, KindSelector.VAL, resultInfo);
+ }
+
public void visitTypeIdent(JCPrimitiveTypeTree tree) {
result = check(tree, syms.typeOfTag[tree.typetag.ordinal()], KindSelector.TYP, resultInfo);
}
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Check.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Check.java
index c9eb8369332..bf7ae0c5d59 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Check.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Check.java
@@ -117,6 +117,8 @@ public class Check {
private final Preview preview;
private final boolean warnOnAnyAccessToMembers;
+ public boolean disablePreviewCheck;
+
// The set of lint options currently in effect. It is initialized
// from the context, and then is set/reset as needed by Attr as it
// visits all the various parts of the trees during attribution.
@@ -155,6 +157,8 @@ public class Check {
target = Target.instance(context);
warnOnAnyAccessToMembers = options.isSet("warnOnAccessToMembers");
+ disablePreviewCheck = false;
+
Target target = Target.instance(context);
syntheticNameChar = target.syntheticNameChar();
@@ -3793,7 +3797,7 @@ public class Check {
}
void checkPreview(DiagnosticPosition pos, Symbol other, Symbol s) {
- if ((s.flags() & PREVIEW_API) != 0 && !preview.participatesInPreview(syms, other, s)) {
+ if ((s.flags() & PREVIEW_API) != 0 && !preview.participatesInPreview(syms, other, s) && !disablePreviewCheck) {
if ((s.flags() & PREVIEW_REFLECTIVE) == 0) {
if (!preview.isEnabled()) {
log.error(pos, Errors.IsPreview(s));
@@ -4311,6 +4315,27 @@ public class Check {
}
}
+ public Type checkProcessorType(JCExpression processor, Type resultType, Env env) {
+ Type processorType = processor.type;
+ Type interfaceType = types.asSuper(processorType, syms.processorType.tsym);
+
+ if (interfaceType != null) {
+ List typeArguments = interfaceType.getTypeArguments();
+
+ if (typeArguments.size() == 2) {
+ resultType = typeArguments.head;
+ } else {
+ log.error(DiagnosticFlag.RESOLVE_ERROR, processor.pos,
+ Errors.ProcessorTypeCannotBeARawType(processorType.tsym));
+ }
+ } else {
+ log.error(DiagnosticFlag.RESOLVE_ERROR, processor.pos,
+ Errors.NotAProcessorType(processorType.tsym));
+ }
+
+ return resultType;
+ }
+
public void checkLeaksNotAccessible(Env env, JCClassDecl check) {
JCCompilationUnit toplevel = env.toplevel;
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/CompileStates.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/CompileStates.java
index b044f956fdf..0f808af0308 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/CompileStates.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/CompileStates.java
@@ -59,10 +59,11 @@ public class CompileStates extends HashMap, CompileStates.Compi
ATTR(4),
FLOW(5),
TRANSTYPES(6),
- TRANSPATTERNS(7),
- UNLAMBDA(8),
- LOWER(9),
- GENERATE(10);
+ TRANSLITERALS(7),
+ TRANSPATTERNS(8),
+ UNLAMBDA(9),
+ LOWER(10),
+ GENERATE(11);
CompileState(int value) {
this.value = value;
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java
index e0609a8542e..47086190b78 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java
@@ -1528,6 +1528,30 @@ public class Flow {
}
}
+ @Override
+ public void visitStringTemplate(JCStringTemplate tree) {
+ JCExpression processor = tree.processor;
+
+ if (processor != null) {
+ scan(processor);
+ Type interfaceType = types.asSuper(processor.type, syms.processorType.tsym);
+
+ if (interfaceType != null) {
+ List typeArguments = interfaceType.getTypeArguments();
+
+ if (typeArguments.size() == 2) {
+ Type throwType = typeArguments.tail.head;
+
+ if (throwType != null) {
+ markThrown(tree, throwType);
+ }
+ }
+ }
+ }
+
+ scan(tree.expressions);
+ }
+
void checkCaughtType(DiagnosticPosition pos, Type exc, List thrownInTry, List caughtInTry) {
if (chk.subset(exc, caughtInTry)) {
log.error(pos, Errors.ExceptAlreadyCaught(exc));
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransLiterals.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransLiterals.java
new file mode 100644
index 00000000000..2ec6ef206a8
--- /dev/null
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransLiterals.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (c) 2023, 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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 com.sun.tools.javac.comp;
+
+import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.ClassSymbol;
+import com.sun.tools.javac.code.Symbol.DynamicMethodSymbol;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import com.sun.tools.javac.code.Symbol.VarSymbol;
+import com.sun.tools.javac.code.Symtab;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.Type.*;
+import com.sun.tools.javac.code.Types;
+import com.sun.tools.javac.jvm.PoolConstant.LoadableConstant;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.*;
+import com.sun.tools.javac.tree.TreeInfo;
+import com.sun.tools.javac.tree.TreeMaker;
+import com.sun.tools.javac.tree.TreeTranslator;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
+import com.sun.tools.javac.util.List;
+import com.sun.tools.javac.util.Name;
+import com.sun.tools.javac.util.Names;
+
+import java.util.Iterator;
+
+/** This pass translates constructed literals (string templates, ...) to conventional Java.
+ *
+ * This is NOT part of any supported API.
+ * If you write code that depends on this, you do so at your own risk.
+ * This code and its internal interfaces are subject to change or
+ * deletion without notice.
+ */
+public final class TransLiterals extends TreeTranslator {
+ /**
+ * The context key for the TransTypes phase.
+ */
+ protected static final Context.Key transLiteralsKey = new Context.Key<>();
+
+ /**
+ * Get the instance for this context.
+ */
+ public static TransLiterals instance(Context context) {
+ TransLiterals instance = context.get(transLiteralsKey);
+ if (instance == null)
+ instance = new TransLiterals(context);
+ return instance;
+ }
+
+ private final Symtab syms;
+ private final Resolve rs;
+ private final Types types;
+ private final Operators operators;
+ private final Names names;
+ private TreeMaker make = null;
+ private Env env = null;
+ private ClassSymbol currentClass = null;
+ private MethodSymbol currentMethodSym = null;
+
+ protected TransLiterals(Context context) {
+ context.put(transLiteralsKey, this);
+ syms = Symtab.instance(context);
+ rs = Resolve.instance(context);
+ make = TreeMaker.instance(context);
+ types = Types.instance(context);
+ operators = Operators.instance(context);
+ names = Names.instance(context);
+ }
+
+ JCExpression makeLit(Type type, Object value) {
+ return make.Literal(type.getTag(), value).setType(type.constType(value));
+ }
+
+ JCExpression makeString(String string) {
+ return makeLit(syms.stringType, string);
+ }
+
+ List makeStringList(List strings) {
+ List exprs = List.nil();
+ for (String string : strings) {
+ exprs = exprs.append(makeString(string));
+ }
+ return exprs;
+ }
+
+ JCBinary makeBinary(JCTree.Tag optag, JCExpression lhs, JCExpression rhs) {
+ JCBinary tree = make.Binary(optag, lhs, rhs);
+ tree.operator = operators.resolveBinary(tree, optag, lhs.type, rhs.type);
+ tree.type = tree.operator.type.getReturnType();
+ return tree;
+ }
+
+ MethodSymbol lookupMethod(DiagnosticPosition pos, Name name, Type qual, List args) {
+ return rs.resolveInternalMethod(pos, env, qual, name, args, List.nil());
+ }
+
+ @Override
+ public void visitClassDef(JCClassDecl tree) {
+ ClassSymbol prevCurrentClass = currentClass;
+ try {
+ currentClass = tree.sym;
+ super.visitClassDef(tree);
+ } finally {
+ currentClass = prevCurrentClass;
+ }
+ }
+
+ @Override
+ public void visitMethodDef(JCMethodDecl tree) {
+ MethodSymbol prevMethodSym = currentMethodSym;
+ try {
+ currentMethodSym = tree.sym;
+ super.visitMethodDef(tree);
+ } finally {
+ currentMethodSym = prevMethodSym;
+ }
+ }
+
+ final class TransStringTemplate {
+ final JCStringTemplate tree;
+ final JCExpression processor;
+ final List fragments;
+ final List expressions;
+ final List expressionTypes;
+ final boolean useValuesList;
+
+ TransStringTemplate(JCStringTemplate tree) {
+ this.tree = tree;
+ this.processor = tree.processor;
+ this.fragments = tree.fragments;
+ this.expressions = translate(tree.expressions);
+ this.expressionTypes = expressions.stream()
+ .map(arg -> arg.type == syms.botType ? syms.objectType : arg.type)
+ .collect(List.collector());
+ int slots = expressionTypes.stream()
+ .mapToInt(t -> types.isSameType(t, syms.longType) ||
+ types.isSameType(t, syms.doubleType) ? 2 : 1).sum();
+ this.useValuesList = 200 < slots; // StringConcatFactory.MAX_INDY_CONCAT_ARG_SLOTS
+ }
+
+ JCExpression concatExpression(List fragments, List expressions) {
+ JCExpression expr = null;
+ Iterator iterator = expressions.iterator();
+ for (String fragment : fragments) {
+ expr = expr == null ? makeString(fragment)
+ : makeBinary(Tag.PLUS, expr, makeString(fragment));
+ if (iterator.hasNext()) {
+ JCExpression expression = iterator.next();
+ Type expressionType = expression.type;
+ expr = makeBinary(Tag.PLUS, expr, expression.setType(expressionType));
+ }
+ }
+ return expr;
+ }
+
+ JCExpression bsmCall(Name name, Name bootstrapName, Type type,
+ List args,
+ List argTypes,
+ List staticArgValues,
+ List staticArgsTypes) {
+ Symbol bsm = rs.resolveQualifiedMethod(tree.pos(), env,
+ syms.templateRuntimeType, bootstrapName, staticArgsTypes, List.nil());
+ MethodType indyType = new MethodType(argTypes, type, List.nil(), syms.methodClass);
+ DynamicMethodSymbol dynSym = new DynamicMethodSymbol(
+ name,
+ syms.noSymbol,
+ ((MethodSymbol)bsm).asHandle(),
+ indyType,
+ staticArgValues.toArray(new LoadableConstant[0])
+ );
+ JCFieldAccess qualifier = make.Select(make.Type(syms.processorType), dynSym.name);
+ qualifier.sym = dynSym;
+ qualifier.type = type;
+ JCMethodInvocation apply = make.Apply(List.nil(), qualifier, args);
+ apply.type = type;
+ return apply;
+ }
+
+ JCExpression processCall(JCExpression stringTemplate) {
+ MethodSymbol appyMeth = lookupMethod(tree.pos(), names.process,
+ syms.processorType, List.of(syms.stringTemplateType));
+ JCExpression applySelect = make.Select(processor, appyMeth);
+ JCExpression process = make.Apply(null, applySelect, List.of(stringTemplate))
+ .setType(syms.objectType);
+ JCTypeCast cast = make.TypeCast(tree.type, process);
+ return cast;
+ }
+
+ JCExpression newStringTemplate() {
+ List staticArgValues = List.nil();
+ List staticArgsTypes =
+ List.of(syms.methodHandleLookupType, syms.stringType,
+ syms.methodTypeType);
+ if (useValuesList) {
+ JCNewArray fragmentArray = make.NewArray(make.Type(syms.stringType),
+ List.nil(), makeStringList(fragments));
+ fragmentArray.type = new ArrayType(syms.stringType, syms.arrayClass);
+ JCNewArray valuesArray = make.NewArray(make.Type(syms.objectType),
+ List.nil(), expressions);
+ valuesArray.type = new ArrayType(syms.objectType, syms.arrayClass);
+ return bsmCall(names.process, names.newLargeStringTemplate, syms.stringTemplateType,
+ List.of(fragmentArray, valuesArray),
+ List.of(fragmentArray.type, valuesArray.type),
+ staticArgValues, staticArgsTypes);
+ } else {
+ for (String fragment : fragments) {
+ staticArgValues = staticArgValues.append(LoadableConstant.String(fragment));
+ staticArgsTypes = staticArgsTypes.append(syms.stringType);
+ }
+ return bsmCall(names.process, names.newStringTemplate, syms.stringTemplateType,
+ expressions, expressionTypes, staticArgValues, staticArgsTypes);
+ }
+ }
+
+ JCExpression bsmProcessCall() {
+ List args = expressions.prepend(processor);
+ List argTypes = expressionTypes.prepend(processor.type);
+ VarSymbol processorSym = (VarSymbol)TreeInfo.symbol(processor);
+ List staticArgValues = List.of(processorSym.asMethodHandle(true));
+ List staticArgsTypes =
+ List.of(syms.methodHandleLookupType, syms.stringType,
+ syms.methodTypeType, syms.methodHandleType);
+ for (String fragment : fragments) {
+ staticArgValues = staticArgValues.append(LoadableConstant.String(fragment));
+ staticArgsTypes = staticArgsTypes.append(syms.stringType);
+ }
+ return bsmCall(names.process, names.processStringTemplate, tree.type,
+ args, argTypes, staticArgValues, staticArgsTypes);
+ }
+
+ boolean isNamedProcessor(Name name) {
+ if (processor instanceof JCIdent ident && ident.sym instanceof VarSymbol varSym) {
+ if (varSym.flags() == (Flags.PUBLIC | Flags.FINAL | Flags.STATIC) &&
+ varSym.name == name &&
+ types.isSameType(varSym.owner.type, syms.stringTemplateType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isLinkageProcessor() {
+ return processor != null &&
+ !useValuesList &&
+ types.isSubtype(processor.type, syms.linkageType) &&
+ processor.type.isFinal() &&
+ TreeInfo.symbol(processor) instanceof VarSymbol varSymbol &&
+ varSymbol.isStatic() &&
+ varSymbol.isFinal();
+ }
+
+ JCExpression visit() {
+ JCExpression result;
+ make.at(tree.pos);
+
+ if (processor == null || isNamedProcessor(names.RAW)) {
+ result = newStringTemplate();
+ } else if (isNamedProcessor(names.STR)) {
+ result = concatExpression(fragments, expressions);
+ } else if (isLinkageProcessor()) {
+ result = bsmProcessCall();
+ } else {
+ result = processCall(newStringTemplate());
+ }
+
+ return result;
+ }
+ }
+
+ public void visitStringTemplate(JCStringTemplate tree) {
+ int prevPos = make.pos;
+ try {
+ tree.processor = translate(tree.processor);
+ tree.expressions = translate(tree.expressions);
+
+ TransStringTemplate transStringTemplate = new TransStringTemplate(tree);
+
+ result = transStringTemplate.visit();
+ } catch (Throwable ex) {
+ ex.printStackTrace();
+ throw ex;
+ } finally {
+ make.at(prevPos);
+ }
+ }
+
+ public void visitVarDef(JCVariableDecl tree) {
+ MethodSymbol prevMethodSym = currentMethodSym;
+ try {
+ tree.mods = translate(tree.mods);
+ tree.vartype = translate(tree.vartype);
+ if (currentMethodSym == null) {
+ // A class or instance field initializer.
+ currentMethodSym =
+ new MethodSymbol((tree.mods.flags& Flags.STATIC) | Flags.BLOCK,
+ names.empty, null,
+ currentClass);
+ }
+ if (tree.init != null) tree.init = translate(tree.init);
+ result = tree;
+ } finally {
+ currentMethodSym = prevMethodSym;
+ }
+ }
+
+ public JCTree translateTopLevelClass(Env env, JCTree cdef, TreeMaker make) {
+ try {
+ this.make = make;
+ this.env = env;
+ translate(cdef);
+ } finally {
+ this.make = null;
+ this.env = null;
+ }
+
+ return cdef;
+ }
+
+}
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransTypes.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransTypes.java
index 4e5d7cc4b8c..246a3f69b10 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransTypes.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransTypes.java
@@ -838,6 +838,13 @@ public class TransTypes extends TreeTranslator {
}
}
+ public void visitStringTemplate(JCStringTemplate tree) {
+ tree.expressions = tree.expressions.stream()
+ .map(e -> translate(e, erasure(e.type))).collect(List.collector());
+ tree.type = erasure(tree.type);
+ result = tree;
+ }
+
public void visitSelect(JCFieldAccess tree) {
Type t = types.skipTypeVars(tree.selected.type, false);
if (t.isCompound()) {
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TypeEnter.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TypeEnter.java
index 19d9dea772b..313ee5c16d1 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TypeEnter.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TypeEnter.java
@@ -25,22 +25,23 @@
package com.sun.tools.javac.comp;
-import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;
-import java.util.stream.Collectors;
import javax.tools.JavaFileObject;
import com.sun.tools.javac.code.*;
import com.sun.tools.javac.code.Lint.LintCategory;
import com.sun.tools.javac.code.Scope.ImportFilter;
+import com.sun.tools.javac.code.Scope.ImportScope;
import com.sun.tools.javac.code.Scope.NamedImportScope;
import com.sun.tools.javac.code.Scope.StarImportScope;
import com.sun.tools.javac.code.Scope.WriteableScope;
import com.sun.tools.javac.code.Source.Feature;
import com.sun.tools.javac.comp.Annotate.AnnotationTypeMetadata;
+import com.sun.tools.javac.parser.Parser;
+import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.*;
import com.sun.tools.javac.util.*;
import com.sun.tools.javac.util.DefinedBy.Api;
@@ -113,6 +114,7 @@ public class TypeEnter implements Completer {
private final Lint lint;
private final TypeEnvs typeEnvs;
private final Dependencies dependencies;
+ private final ParserFactory parserFactory;
private final Preview preview;
public static TypeEnter instance(Context context) {
@@ -141,6 +143,7 @@ public class TypeEnter implements Completer {
lint = Lint.instance(context);
typeEnvs = TypeEnvs.instance(context);
dependencies = Dependencies.instance(context);
+ parserFactory = ParserFactory.instance(context);
preview = Preview.instance(context);
Source source = Source.instance(context);
allowDeprecationOnImport = Feature.DEPRECATION_ON_IMPORT.allowedInSource(source);
@@ -326,6 +329,40 @@ public class TypeEnter implements Completer {
sym.owner.complete();
}
+ private void importJavaLang(JCCompilationUnit tree, Env env, ImportFilter typeImportFilter) {
+ // Import-on-demand java.lang.
+ PackageSymbol javaLang = syms.enterPackage(syms.java_base, names.java_lang);
+ if (javaLang.members().isEmpty() && !javaLang.exists()) {
+ log.error(Errors.NoJavaLang);
+ throw new Abort();
+ }
+ importAll(make.at(tree.pos()).Import(make.Select(make.QualIdent(javaLang.owner), javaLang), false),
+ javaLang, env);
+ }
+
+ private void staticImports(JCCompilationUnit tree, Env env, ImportFilter staticImportFilter) {
+ if (preview.isEnabled() && preview.isPreview(Feature.STRING_TEMPLATES)) {
+ Lint prevLint = chk.setLint(lint.suppress(LintCategory.DEPRECATION, LintCategory.REMOVAL, LintCategory.PREVIEW));
+ boolean prevPreviewCheck = chk.disablePreviewCheck;
+
+ try {
+ chk.disablePreviewCheck = true;
+ String autoImports = """
+ import static java.lang.StringTemplate.STR;
+ """;
+ Parser parser = parserFactory.newParser(autoImports, false, false, false, false);
+ JCCompilationUnit importTree = parser.parseCompilationUnit();
+
+ for (JCImport imp : importTree.getImports()) {
+ doImport(imp);
+ }
+ } finally {
+ chk.setLint(prevLint);
+ chk.disablePreviewCheck = prevPreviewCheck;
+ }
+ }
+ }
+
private void resolveImports(JCCompilationUnit tree, Env env) {
if (tree.starImportScope.isFilled()) {
// we must have already processed this toplevel
@@ -348,14 +385,8 @@ public class TypeEnter implements Completer {
(origin, sym) -> sym.kind == TYP &&
chk.importAccessible(sym, packge);
- // Import-on-demand java.lang.
- PackageSymbol javaLang = syms.enterPackage(syms.java_base, names.java_lang);
- if (javaLang.members().isEmpty() && !javaLang.exists()) {
- log.error(Errors.NoJavaLang);
- throw new Abort();
- }
- importAll(make.at(tree.pos()).Import(make.Select(make.QualIdent(javaLang.owner), javaLang), false),
- javaLang, env);
+ importJavaLang(tree, env, typeImportFilter);
+ staticImports(tree, env, staticImportFilter);
JCModuleDecl decl = tree.getModuleDecl();
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java
index c9ba28d09f2..6988e7b7189 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java
@@ -1594,6 +1594,12 @@ public class JavaCompiler {
env.tree = transTypes.translateTopLevelClass(env.tree, localMake);
compileStates.put(env, CompileState.TRANSTYPES);
+ if (shouldStop(CompileState.TRANSLITERALS))
+ return;
+
+ env.tree = TransLiterals.instance(context).translateTopLevelClass(env, env.tree, localMake);
+ compileStates.put(env, CompileState.TRANSLITERALS);
+
if (shouldStop(CompileState.TRANSPATTERNS))
return;
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java
index 23fc4076caf..b0b176b36df 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1999, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2023, 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
@@ -38,8 +38,8 @@ import com.sun.tools.javac.util.*;
import com.sun.tools.javac.util.JCDiagnostic.*;
import java.nio.CharBuffer;
+import java.util.Iterator;
import java.util.Set;
-import java.util.regex.Pattern;
import static com.sun.tools.javac.parser.Tokens.*;
import static com.sun.tools.javac.util.LayoutCharacters.EOI;
@@ -62,17 +62,17 @@ public class JavaTokenizer extends UnicodeReader {
/**
* Sentinel for non-value.
*/
- private int NOT_FOUND = -1;
+ private final static int NOT_FOUND = -1;
/**
* The source language setting. Copied from scanner factory.
*/
- private Source source;
+ private final Source source;
/**
* The preview language setting. Copied from scanner factory.
*/
- private Preview preview;
+ private final Preview preview;
/**
* The log to be used for error reporting. Copied from scanner factory.
@@ -89,6 +89,26 @@ public class JavaTokenizer extends UnicodeReader {
*/
private final Names names;
+ /**
+ * Origin scanner factory.
+ */
+ protected final ScannerFactory fac;
+
+ /**
+ * Buffer for building literals, used by nextToken().
+ */
+ protected final StringBuilder sb;
+
+ /**
+ * Tokens pending to be read from string template embedded expressions.
+ */
+ protected List pendingTokens;
+
+ /**
+ * String template fragment ranges; end-endPos pairs.
+ */
+ protected List fragmentRanges;
+
/**
* The token kind, set by nextToken().
*/
@@ -120,21 +140,21 @@ public class JavaTokenizer extends UnicodeReader {
protected boolean hasEscapeSequences;
/**
- * Buffer for building literals, used by nextToken().
+ * true if contains templated string escape sequences, set by nextToken().
*/
- protected StringBuilder sb;
+ protected boolean isStringTemplate;
/**
- * Origin scanner factory.
+ * true if errors are pending from embedded expressions.
*/
- protected ScannerFactory fac;
+ protected boolean hasStringTemplateErrors;
/**
* The set of lint options currently in effect. It is initialized
* from the context, and then is set/reset as needed by Attr as it
* visits all the various parts of the trees during attribution.
*/
- protected Lint lint;
+ protected final Lint lint;
/**
* Construct a Java token scanner from the input character buffer.
@@ -149,9 +169,9 @@ public class JavaTokenizer extends UnicodeReader {
/**
* Construct a Java token scanner from the input character array.
*
- * @param fac the factory which created this Scanner
- * @param array the input character array.
- * @param length The length of the meaningful content in the array.
+ * @param fac factory which created this Scanner
+ * @param array input character array
+ * @param length length of the meaningful content in the array
*/
protected JavaTokenizer(ScannerFactory fac, char[] array, int length) {
super(fac, array, length);
@@ -163,6 +183,8 @@ public class JavaTokenizer extends UnicodeReader {
this.preview = fac.preview;
this.lint = fac.lint;
this.sb = new StringBuilder(256);
+ this.pendingTokens = List.nil();
+ this.fragmentRanges = List.nil();
}
/**
@@ -318,19 +340,106 @@ public class JavaTokenizer extends UnicodeReader {
}
/**
- * Processes the current character and places in the literal buffer. If the current
- * character is a backslash then the next character is validated as a proper
- * escape character. Conversion of escape sequences takes place at end of nextToken().
+ * Scan the content of a string template expression.
*
- * @param pos position of the first character in literal.
+ * @param pos start of literal
+ * @param endPos start of embedded expression
+ */
+ private void scanEmbeddedExpression(int pos, int endPos) {
+ // If first embedded expression.
+ if (!isStringTemplate) {
+ checkSourceLevel(pos, Feature.STRING_TEMPLATES);
+ fragmentRanges = fragmentRanges.append(pos);
+ isStringTemplate = true;
+ }
+ // Track end of previous fragment.
+ fragmentRanges = fragmentRanges.append(endPos);
+ // Keep backslash and add rest of placeholder.
+ sb.append("{}");
+
+ // Separate tokenizer for the embedded expression.
+ JavaTokenizer tokenizer = new JavaTokenizer(fac, buffer(), length());
+ tokenizer.reset(position());
+
+ // Track brace depth.
+ int braceCount = 0;
+
+ // Accumulate tokens.
+ List tokens = List.nil();
+
+ // Stash first left brace.
+ Token token = tokenizer.readToken();
+ tokens = tokens.append(token);
+
+ while (isAvailable()) {
+ // Read and stash next token.
+ token = tokenizer.readToken();
+ tokens = tokens.append(token);
+
+ // Intercept errors
+ if (token.kind == TokenKind.ERROR) {
+ // Track start of next fragment.
+ if (isTextBlock) {
+ reset(length());
+ } else {
+ skipToEOLN();
+ }
+ hasStringTemplateErrors = true;
+ return;
+ }
+
+ if (token.kind == TokenKind.RBRACE) {
+ // Potential closing brace.
+ if (braceCount == 0) {
+ break;
+ }
+
+ braceCount--;
+ } else if (token.kind == TokenKind.LBRACE) {
+ // Nesting deeper.
+ braceCount++;
+ } else if (token.kind == TokenKind.STRINGFRAGMENT) {
+ tokens = tokens.appendList(tokenizer.pendingTokens);
+ tokenizer.pendingTokens = List.nil();
+ } else if (token.kind == TokenKind.EOF) {
+ break;
+ }
+ }
+
+ // If no closing brace will be picked up as an unterminated string.
+
+ // Set main tokenizer to continue at next position.
+ int position = tokenizer.position();
+ reset(position);
+
+ // Track start of next fragment.
+ fragmentRanges = fragmentRanges.append(position);
+
+ // Pend the expression tokens after the STRINGFRAGMENT.
+ pendingTokens = pendingTokens.appendList(tokens);
+ }
+
+ /**
+ * Processes the current character and places in the literal buffer. If the current
+ * character is a backslash then the next character is assumed to be a proper
+ * escape character. Actual conversion of escape sequences takes place
+ * during at the end of readToken.
+ *
+ * @param pos position of the first character in literal.
*/
private void scanLitChar(int pos) {
+ int backslash = position();
if (acceptThenPut('\\')) {
hasEscapeSequences = true;
-
switch (get()) {
- case '0': case '1': case '2': case '3':
- case '4': case '5': case '6': case '7':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
char leadch = get();
putThenNext();
@@ -370,6 +479,13 @@ public class JavaTokenizer extends UnicodeReader {
}
break;
+ case '{':
+ scanEmbeddedExpression(pos, backslash);
+ if (hasStringTemplateErrors) {
+ return;
+ }
+ break;
+
default:
lexError(position(), Errors.IllegalEscChar);
break;
@@ -385,10 +501,10 @@ public class JavaTokenizer extends UnicodeReader {
* @param pos position of the first character in literal.
*/
private void scanString(int pos) {
- // Assume the best.
- tk = Tokens.TokenKind.STRINGLITERAL;
// Track the end of first line for error recovery.
int firstEOLN = NOT_FOUND;
+ tk = TokenKind.STRINGLITERAL;
+
// Check for text block delimiter.
isTextBlock = accept("\"\"\"");
@@ -409,7 +525,13 @@ public class JavaTokenizer extends UnicodeReader {
// While characters are available.
while (isAvailable()) {
- if (accept("\"\"\"")) {
+ if (hasStringTemplateErrors) {
+ break;
+ } else if (accept("\"\"\"")) {
+ if (isStringTemplate && tk == TokenKind.STRINGLITERAL) {
+ tk = TokenKind.STRINGFRAGMENT;
+ }
+
return;
}
@@ -433,7 +555,12 @@ public class JavaTokenizer extends UnicodeReader {
// While characters are available.
while (isAvailable()) {
- if (accept('\"')) {
+ if (hasStringTemplateErrors) {
+ break;
+ } else if (accept('\"')) {
+ if (isStringTemplate && tk == TokenKind.STRINGLITERAL) {
+ tk = TokenKind.STRINGFRAGMENT;
+ }
return;
}
@@ -448,10 +575,18 @@ public class JavaTokenizer extends UnicodeReader {
}
}
- // String ended without close delimiter sequence.
- lexError(pos, isTextBlock ? Errors.UnclosedTextBlock : Errors.UnclosedStrLit);
+ // String ended without close delimiter sequence or has embedded expression errors.
+ if (isStringTemplate) {
+ lexError(pos, isTextBlock ? Errors.TextBlockTemplateIsNotWellFormed
+ : Errors.StringTemplateIsNotWellFormed);
+ fragmentRanges = List.nil();
+ pendingTokens = List.nil();
+ } else {
+ lexError(pos, isTextBlock ? Errors.UnclosedTextBlock
+ : Errors.UnclosedStrLit);
+ }
- if (firstEOLN != NOT_FOUND) {
+ if (!hasStringTemplateErrors && firstEOLN != NOT_FOUND) {
// Reset recovery position to point after text block open delimiter sequence.
reset(firstEOLN);
}
@@ -772,11 +907,20 @@ public class JavaTokenizer extends UnicodeReader {
* Read token (main entrypoint.)
*/
public Token readToken() {
+ if (pendingTokens.nonEmpty()) {
+ Token token = pendingTokens.head;
+ pendingTokens = pendingTokens.tail;
+ return token;
+ }
+
sb.setLength(0);
name = null;
radix = 0;
isTextBlock = false;
hasEscapeSequences = false;
+ isStringTemplate = false;
+ hasStringTemplateErrors = false;
+ fragmentRanges = List.nil();
int pos;
List comments = null;
@@ -971,6 +1115,7 @@ public class JavaTokenizer extends UnicodeReader {
lexError(pos, Errors.IllegalLineEndInCharLit);
}
+ int errorPos = position();
scanLitChar(pos);
if (accept('\'')) {
@@ -980,7 +1125,6 @@ public class JavaTokenizer extends UnicodeReader {
}
}
break loop;
-
case '\"': // (Spec. 3.10)
scanString(pos);
break loop;
@@ -1017,8 +1161,8 @@ public class JavaTokenizer extends UnicodeReader {
arg = String.format("\\u%04x\\u%04x", (int) hi, (int) lo);
} else {
char ch = get();
- arg = (32 < ch && ch < 127) ? String.format("%s", ch) :
- String.format("\\u%04x", (int) ch);
+ arg = (32 < ch && ch < 127) ? String.valueOf(ch) :
+ "\\u%04x".formatted((int) ch);
}
lexError(pos, Errors.IllegalChar(arg));
@@ -1031,6 +1175,11 @@ public class JavaTokenizer extends UnicodeReader {
int endPos = position();
+ // Track end of final fragment.
+ if (isStringTemplate) {
+ fragmentRanges = fragmentRanges.append(endPos);
+ }
+
if (tk.tag == Token.Tag.DEFAULT) {
return new Token(tk, pos, endPos, comments);
} else if (tk.tag == Token.Tag.NAMED) {
@@ -1062,6 +1211,11 @@ public class JavaTokenizer extends UnicodeReader {
}
}
+ if (isStringTemplate) {
+ // Break string into fragments and then return the first of the framents.
+ return getFragments(string, comments);
+ }
+
// Translate escape sequences if present.
if (hasEscapeSequences) {
try {
@@ -1091,6 +1245,66 @@ public class JavaTokenizer extends UnicodeReader {
}
}
+ /**
+ * Convert the string into a list of pending tokens to precede embedded
+ * expressions.
+ *
+ * @param string string to fragment
+ * @param comments comments for first token
+ *
+ * @return first pending token.
+ */
+ private Token getFragments(String string, List comments) {
+ List tokens = List.nil();
+ Iterator rangeIter = fragmentRanges.iterator();
+ for (String fragment : fragment(string)) {
+ fragment = fragment.translateEscapes();
+ int fragmentPos = rangeIter.next();
+ int fragmentEndPos = rangeIter.next();
+ Token token = new StringToken(TokenKind.STRINGFRAGMENT,
+ fragmentPos, fragmentEndPos, fragment, comments);
+ comments = null;
+ tokens = tokens.append(token);
+ }
+ pendingTokens = tokens.appendList(pendingTokens);
+ Token first = pendingTokens.head;
+ pendingTokens = pendingTokens.tail;
+ return first;
+ }
+
+ /**
+ * Break string template up into fragments. "\{}" indicates where
+ * embedded expressions occur.
+ *
+ * @param string string template
+ *
+ * @return list of fragment strings
+ */
+ List fragment(String string) {
+ List fragments = List.nil();
+ StringBuilder sb = new StringBuilder();
+ int length = string.length();
+ for (int i = 0; i < length; i++) {
+ char ch = string.charAt(i);
+ if (ch != '\\') {
+ sb.append(ch);
+ } else if (i + 2 < length && string.charAt(i + 1) == '{'
+ && string.charAt(i + 2) == '}') {
+ fragments = fragments.append(sb.toString());
+ sb.setLength(0);
+ i += 2;
+ } else if (i + 1 < length){
+ sb.append('\\');
+ sb.append(string.charAt(i + 1));
+ i++;
+ } else {
+ // Error already reported.
+ }
+ }
+ fragments = fragments.append(sb.toString());
+ return fragments;
+ }
+
/**
* Appends a comment to the list of comments preceding the current token.
*
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavacParser.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavacParser.java
index 30aa73095e2..b9e52cf6293 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavacParser.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavacParser.java
@@ -651,6 +651,59 @@ public class JavacParser implements Parser {
return t;
}
+ /**
+ * StringTemplate =
+ * [STRINGFRAGMENT] [EmbeddedExpression]
+ * | STRINGLITERAL
+ *
+ * EmbeddedExpression =
+ * LBRACE term RBRACE
+ */
+ JCExpression stringTemplate(JCExpression processor) {
+ checkSourceLevel(Feature.STRING_TEMPLATES);
+ // Disable standalone string templates
+ if (processor == null) {
+ log.error(DiagnosticFlag.SYNTAX, token.pos,
+ Errors.ProcessorMissingFromStringTemplateExpression);
+ }
+ int oldmode = mode;
+ selectExprMode();
+ Token stringToken = token;
+ int pos = stringToken.pos;
+ int endPos = stringToken.endPos;
+ TokenKind kind = stringToken.kind;
+ String string = token.stringVal();
+ List fragments = List.of(string);
+ List expressions = List.nil();
+ nextToken();
+ if (kind != STRINGLITERAL) {
+ while (token.kind == STRINGFRAGMENT) {
+ stringToken = token;
+ endPos = stringToken.endPos;
+ string = stringToken.stringVal();
+ fragments = fragments.append(string);
+ nextToken();
+ }
+ while (token.pos < endPos && token.kind != DEFAULT && token.kind != ERROR) {
+ accept(LBRACE);
+ JCExpression expression = token.kind == RBRACE ? F.at(pos).Literal(TypeTag.BOT, null)
+ : term(EXPR);
+ expressions = expressions.append(expression);
+ if (token.kind != ERROR) {
+ accept(RBRACE);
+ }
+ }
+ // clean up remaining expression tokens if error
+ while (token.pos < endPos && token.kind != DEFAULT) {
+ nextToken();
+ }
+ S.setPrevToken(stringToken);
+ }
+ JCExpression t = toP(F.at(pos).StringTemplate(processor, fragments, expressions));
+ setMode(oldmode);
+ return t;
+ }
+
JCExpression literal(Name prefix) {
return literal(prefix, token.pos);
}
@@ -1279,6 +1332,14 @@ public class JavacParser implements Parser {
t = literal(names.empty);
} else return illegal();
break;
+ case STRINGFRAGMENT:
+ if (typeArgs == null && isMode(EXPR)) {
+ selectExprMode();
+ t = stringTemplate(null);
+ } else {
+ return illegal();
+ }
+ break;
case NEW:
if (typeArgs != null) return illegal();
if (isMode(EXPR)) {
@@ -1409,6 +1470,12 @@ public class JavacParser implements Parser {
t = innerCreator(pos1, typeArgs, t);
typeArgs = null;
break loop;
+ case STRINGFRAGMENT:
+ case STRINGLITERAL:
+ if (typeArgs != null) return illegal();
+ t = stringTemplate(t);
+ typeArgs = null;
+ break loop;
}
}
@@ -1631,6 +1698,12 @@ public class JavacParser implements Parser {
if (token.kind == LT) typeArgs = typeArguments(false);
t = innerCreator(pos2, typeArgs, t);
typeArgs = null;
+ } else if (token.kind == TokenKind.STRINGFRAGMENT ||
+ token.kind == TokenKind.STRINGLITERAL) {
+ if (typeArgs != null) {
+ return illegal();
+ }
+ t = stringTemplate(t);
} else {
List tyannos = null;
if (isMode(TYPE) && token.kind == MONKEYS_AT) {
@@ -1790,6 +1863,7 @@ public class JavacParser implements Parser {
case LPAREN: case THIS: case SUPER:
case INTLITERAL: case LONGLITERAL: case FLOATLITERAL:
case DOUBLELITERAL: case CHARLITERAL: case STRINGLITERAL:
+ case STRINGFRAGMENT:
case TRUE: case FALSE: case NULL:
case NEW: case IDENTIFIER: case ASSERT: case ENUM: case UNDERSCORE:
case SWITCH:
@@ -2707,6 +2781,7 @@ public class JavacParser implements Parser {
boolean isYieldStatement;
switch (next.kind) {
case PLUS: case SUB: case STRINGLITERAL: case CHARLITERAL:
+ case STRINGFRAGMENT:
case INTLITERAL: case LONGLITERAL: case FLOATLITERAL: case DOUBLELITERAL:
case NULL: case IDENTIFIER: case TRUE: case FALSE:
case NEW: case SWITCH: case THIS: case SUPER:
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Lexer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Lexer.java
index f31df5e26b5..2223999bebe 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Lexer.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Lexer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 2023, 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
@@ -59,6 +59,11 @@ public interface Lexer {
*/
Token prevToken();
+ /**
+ * Sets the previous token.
+ */
+ void setPrevToken(Token prevToken);
+
/**
* Splits the current token in two and return the first (split) token.
* For instance {@literal '<<<'} is split into two tokens
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Scanner.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Scanner.java
index de449205579..81fa103a7e7 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Scanner.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Scanner.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2023, 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
@@ -30,8 +30,6 @@ import java.util.List;
import java.util.ArrayList;
import com.sun.tools.javac.util.Position.LineMap;
-import com.sun.tools.javac.parser.JavaTokenizer.*;
-
import static com.sun.tools.javac.parser.Tokens.*;
/** The lexical analyzer maps an input stream consisting of
@@ -44,7 +42,7 @@ import static com.sun.tools.javac.parser.Tokens.*;
*/
public class Scanner implements Lexer {
- private Tokens tokens;
+ private final Tokens tokens;
/** The token, set by nextToken().
*/
@@ -56,9 +54,9 @@ public class Scanner implements Lexer {
/** Buffer of saved tokens (used during lookahead)
*/
- private List savedTokens = new ArrayList<>();
+ private final List savedTokens = new ArrayList<>();
- private JavaTokenizer tokenizer;
+ private final JavaTokenizer tokenizer;
/**
* Create a scanner from the input array. This method might
@@ -98,7 +96,7 @@ public class Scanner implements Lexer {
}
//where
private void ensureLookahead(int lookahead) {
- for (int i = savedTokens.size() ; i < lookahead ; i ++) {
+ for (int i = savedTokens.size() ; i < lookahead ; i++) {
savedTokens.add(tokenizer.readToken());
}
}
@@ -107,6 +105,10 @@ public class Scanner implements Lexer {
return prevToken;
}
+ public void setPrevToken(Token prevToken) {
+ this.prevToken = prevToken;
+ }
+
public void nextToken() {
prevToken = token;
if (!savedTokens.isEmpty()) {
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Tokens.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Tokens.java
index e513cab734c..864696e95c9 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Tokens.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/Tokens.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1999, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2023, 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
@@ -152,6 +152,7 @@ public class Tokens {
DOUBLELITERAL(Tag.NUMERIC),
CHARLITERAL(Tag.NUMERIC),
STRINGLITERAL(Tag.STRING),
+ STRINGFRAGMENT(Tag.STRING),
TRUE("true", Tag.NAMED),
FALSE("false", Tag.NAMED),
NULL("null", Tag.NAMED),
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java
index 3af23f50250..d4121080506 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2011, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2023, 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
@@ -147,6 +147,15 @@ public class UnicodeReader {
nextCodePoint();
}
+ /**
+ * Returns the character buffer.
+ *
+ * @return character buffer.
+ */
+ protected char[] buffer() {
+ return buffer;
+ }
+
/**
* Returns the length of the buffer. This is length of meaningful content in buffer and
* not the length of the buffer array.
@@ -410,6 +419,9 @@ public class UnicodeReader {
protected boolean isOneOf(char ch1, char ch2, char ch3) {
return is(ch1) || is(ch2) || is(ch3);
}
+ protected boolean isOneOf(char ch1, char ch2, char ch3, char ch4) {
+ return is(ch1) || is(ch2) || is(ch3) || is(ch4);
+ }
protected boolean isOneOf(char ch1, char ch2, char ch3, char ch4, char ch5, char ch6) {
return is(ch1) || is(ch2) || is(ch3) || is(ch4) || is(ch5) || is(ch6);
}
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties
index c53c241334a..4473b7603f0 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties
@@ -1322,6 +1322,23 @@ compiler.err.unclosed.str.lit=\
compiler.err.unclosed.text.block=\
unclosed text block
+compiler.err.string.template.is.not.well.formed=\
+ string template is not well formed
+
+compiler.err.text.block.template.is.not.well.formed=\
+ text block template is not well formed
+
+compiler.err.processor.missing.from.string.template.expression=\
+ processor missing from string template expression
+
+# 0: symbol
+compiler.err.processor.type.cannot.be.a.raw.type=\
+ processor type cannot be a raw type: {0}
+
+# 0: symbol
+compiler.err.not.a.processor.type=\
+ not a processor type: {0}
+
# 0: string
compiler.err.unsupported.encoding=\
unsupported encoding: {0}
@@ -3127,6 +3144,9 @@ compiler.misc.feature.case.null=\
compiler.misc.feature.pattern.switch=\
patterns in switch statements
+compiler.misc.feature.string.templates=\
+ string templates
+
compiler.misc.feature.unconditional.patterns.in.instanceof=\
unconditional patterns in instanceof
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/JCTree.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/JCTree.java
index 8626dc2dfab..9d884631b4f 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/JCTree.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/JCTree.java
@@ -269,6 +269,10 @@ public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
*/
LITERAL,
+ /** String template expression.
+ */
+ STRING_TEMPLATE,
+
/** Basic type identifiers, of type TypeIdent.
*/
TYPEIDENT,
@@ -2499,6 +2503,58 @@ public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
}
+ /**
+ * String template expression.
+ */
+ public static class JCStringTemplate extends JCExpression implements StringTemplateTree {
+ public JCExpression processor;
+ public List fragments;
+ public List expressions;
+
+ protected JCStringTemplate(JCExpression processor,
+ List fragments,
+ List expressions) {
+ this.processor = processor;
+ this.fragments = fragments;
+ this.expressions = expressions;
+ }
+
+ @Override
+ public ExpressionTree getProcessor() {
+ return processor;
+ }
+
+ @Override
+ public List getFragments() {
+ return fragments;
+ }
+
+ @Override
+ public List extends ExpressionTree> getExpressions() {
+ return expressions;
+ }
+
+ @Override @DefinedBy(Api.COMPILER_TREE)
+ public Kind getKind() {
+ return Kind.TEMPLATE;
+ }
+
+ @Override @DefinedBy(Api.COMPILER_TREE)
+ public Tag getTag() {
+ return STRING_TEMPLATE;
+ }
+
+ @Override @DefinedBy(Api.COMPILER_TREE)
+ public void accept(Visitor v) {
+ v.visitStringTemplate(this);
+ }
+
+ @Override @DefinedBy(Api.COMPILER_TREE)
+ public R accept(TreeVisitor v, D d) {
+ return v.visitStringTemplate(this, d);
+ }
+ }
+
/**
* An array selection
*/
@@ -3478,6 +3534,9 @@ public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
JCFieldAccess Select(JCExpression selected, Name selector);
JCIdent Ident(Name idname);
JCLiteral Literal(TypeTag tag, Object value);
+ JCStringTemplate StringTemplate(JCExpression processor,
+ List fragments,
+ List expressions);
JCPrimitiveTypeTree TypeIdent(TypeTag typetag);
JCArrayTypeTree TypeArray(JCExpression elemtype);
JCTypeApply TypeApply(JCExpression clazz, List arguments);
@@ -3549,6 +3608,7 @@ public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
public void visitReference(JCMemberReference that) { visitTree(that); }
public void visitIdent(JCIdent that) { visitTree(that); }
public void visitLiteral(JCLiteral that) { visitTree(that); }
+ public void visitStringTemplate(JCStringTemplate that) { visitTree(that); }
public void visitTypeIdent(JCPrimitiveTypeTree that) { visitTree(that); }
public void visitTypeArray(JCArrayTypeTree that) { visitTree(that); }
public void visitTypeApply(JCTypeApply that) { visitTree(that); }
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/Pretty.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/Pretty.java
index f87d5c05a74..a18e789661a 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/Pretty.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/Pretty.java
@@ -26,6 +26,7 @@
package com.sun.tools.javac.tree;
import java.io.*;
+import java.util.stream.Collectors;
import com.sun.source.tree.MemberReferenceTree.ReferenceMode;
import com.sun.source.tree.ModuleTree.ModuleKind;
@@ -1474,6 +1475,23 @@ public class Pretty extends JCTree.Visitor {
}
}
+ public void visitStringTemplate(JCStringTemplate tree) {
+ try {
+ JCExpression processor = tree.processor;
+ print("[");
+ if (processor != null) {
+ printExpr(processor);
+ }
+ print("]");
+ print("\"" + tree.fragments.stream().collect(Collectors.joining("\\{}")) + "\"");
+ print("(");
+ printExprs(tree.expressions);
+ print(")");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
public void visitTypeIdent(JCPrimitiveTypeTree tree) {
try {
switch(tree.typetag) {
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeCopier.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeCopier.java
index afa9302e39d..5775a797208 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeCopier.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeCopier.java
@@ -282,6 +282,14 @@ public class TreeCopier implements TreeVisitor {
return M.at(t.pos).Literal(t.typetag, t.value);
}
+ @DefinedBy(Api.COMPILER_TREE)
+ public JCTree visitStringTemplate(StringTemplateTree node, P p) {
+ JCStringTemplate t = (JCStringTemplate) node;
+ JCExpression processor = copy(t.processor, p);
+ List expressions = copy(t.expressions, p);
+ return M.at(t.pos).StringTemplate(processor, t.fragments, expressions);
+ }
+
@DefinedBy(Api.COMPILER_TREE)
public JCTree visitMethod(MethodTree node, P p) {
JCMethodDecl t = (JCMethodDecl) node;
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeInfo.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeInfo.java
index a9d406c9584..ae0b6dfa627 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeInfo.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeInfo.java
@@ -330,6 +330,7 @@ public class TreeInfo {
case PLUS_ASG: case MINUS_ASG:
case MUL_ASG: case DIV_ASG: case MOD_ASG:
case APPLY: case NEWCLASS:
+ case STRING_TEMPLATE:
case ERRONEOUS:
return true;
default:
@@ -545,6 +546,14 @@ public class TreeInfo {
JCBindingPattern node = (JCBindingPattern)tree;
return getStartPos(node.var);
}
+ case STRING_TEMPLATE: {
+ JCStringTemplate node = (JCStringTemplate) tree;
+ if (node.processor == null) {
+ return node.pos;
+ } else {
+ return getStartPos(node.processor);
+ }
+ }
case ERRONEOUS: {
JCErroneous node = (JCErroneous)tree;
if (node.errs != null && node.errs.nonEmpty()) {
@@ -973,6 +982,8 @@ public class TreeInfo {
return symbol(((JCAnnotatedType) tree).underlyingType);
case REFERENCE:
return ((JCMemberReference) tree).sym;
+ case CLASSDEF:
+ return ((JCClassDecl) tree).sym;
default:
return null;
}
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeMaker.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeMaker.java
index e8522c0cfcd..eb28a54121a 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeMaker.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeMaker.java
@@ -550,6 +550,14 @@ public class TreeMaker implements JCTree.Factory {
return tree;
}
+ public JCStringTemplate StringTemplate(JCExpression processor,
+ List fragments,
+ List expressions) {
+ JCStringTemplate tree = new JCStringTemplate(processor, fragments, expressions);
+ tree.pos = pos;
+ return tree;
+ }
+
public JCPrimitiveTypeTree TypeIdent(TypeTag typetag) {
JCPrimitiveTypeTree tree = new JCPrimitiveTypeTree(typetag);
tree.pos = pos;
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeScanner.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeScanner.java
index c7e437dc529..83ccd2e4ec8 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeScanner.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeScanner.java
@@ -353,6 +353,11 @@ public class TreeScanner extends Visitor {
public void visitLiteral(JCLiteral tree) {
}
+ public void visitStringTemplate(JCStringTemplate tree) {
+ scan(tree.processor);
+ scan(tree.expressions);
+ }
+
public void visitTypeIdent(JCPrimitiveTypeTree tree) {
}
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeTranslator.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeTranslator.java
index a3cf0040175..387b4d20908 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeTranslator.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/TreeTranslator.java
@@ -411,6 +411,13 @@ public class TreeTranslator extends JCTree.Visitor {
result = tree;
}
+ public void visitStringTemplate(JCStringTemplate tree) {
+ tree.processor = translate(tree.processor);
+ tree.expressions = translate(tree.expressions);
+
+ result = tree;
+ }
+
public void visitTypeIdent(JCPrimitiveTypeTree tree) {
result = tree;
}
diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Names.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Names.java
index 303cd3c2eb9..217a7e6ae1d 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Names.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Names.java
@@ -222,6 +222,14 @@ public class Names {
public final Name typeSwitch;
public final Name enumSwitch;
+ // templated string
+ public final Name process;
+ public final Name STR;
+ public final Name RAW;
+ public final Name newStringTemplate;
+ public final Name newLargeStringTemplate;
+ public final Name processStringTemplate;
+
public final Name.Table table;
@SuppressWarnings("this-escape")
@@ -396,6 +404,14 @@ public class Names {
permits = fromString("permits");
sealed = fromString("sealed");
+ // templated string
+ process = fromString("process");
+ STR = fromString("STR");
+ RAW = fromString("RAW");
+ newStringTemplate = fromString("newStringTemplate");
+ newLargeStringTemplate = fromString("newLargeStringTemplate");
+ processStringTemplate = fromString("processStringTemplate");
+
// pattern switches
typeSwitch = fromString("typeSwitch");
enumSwitch = fromString("enumSwitch");
diff --git a/src/jdk.jshell/share/classes/jdk/jshell/CompletenessAnalyzer.java b/src/jdk.jshell/share/classes/jdk/jshell/CompletenessAnalyzer.java
index ffa83bdd2d4..59e4384bef7 100644
--- a/src/jdk.jshell/share/classes/jdk/jshell/CompletenessAnalyzer.java
+++ b/src/jdk.jshell/share/classes/jdk/jshell/CompletenessAnalyzer.java
@@ -272,6 +272,7 @@ class CompletenessAnalyzer {
DOUBLELITERAL(TokenKind.DOUBLELITERAL, XEXPR1|XTERM), //
CHARLITERAL(TokenKind.CHARLITERAL, XEXPR1|XTERM), //
STRINGLITERAL(TokenKind.STRINGLITERAL, XEXPR1|XTERM), //
+ STRINGFRAGMENT(TokenKind.STRINGFRAGMENT, XEXPR1|XTERM),
TRUE(TokenKind.TRUE, XEXPR1|XTERM), // true
FALSE(TokenKind.FALSE, XEXPR1|XTERM), // false
NULL(TokenKind.NULL, XEXPR1|XTERM), // null
@@ -479,8 +480,16 @@ class CompletenessAnalyzer {
private Token advance() {
Token prev = current;
- scanner.nextToken();
- current = scanner.token();
+ if (current != null && current.kind == TokenKind.STRINGFRAGMENT) {
+ int endPos = current.endPos;
+ do {
+ scanner.nextToken();
+ current = scanner.token();
+ } while (current != null && current.endPos <= endPos && current.kind != TokenKind.EOF);
+ } else {
+ scanner.nextToken();
+ current = scanner.token();
+ }
return prev;
}
diff --git a/test/jdk/java/lang/String/concat/MakeConcatWithTemplate.java b/test/jdk/java/lang/String/concat/MakeConcatWithTemplate.java
new file mode 100644
index 00000000000..2927afe4f9d
--- /dev/null
+++ b/test/jdk/java/lang/String/concat/MakeConcatWithTemplate.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.StringConcatFactory;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @test
+ * @summary Test StringConcatFactory.makeConcatWithTemplate... methods.
+ * @enablePreview true
+ */
+
+public class MakeConcatWithTemplate {
+ public static void main(String... args) {
+ makeConcatWithTemplate();
+ makeConcatWithTemplateCluster();
+ makeConcatWithTemplateGetters();
+ }
+
+ static List fragments(int n) {
+ String[] array = new String[n];
+ Arrays.fill(array, "abc");
+ return Arrays.asList(array);
+ }
+
+ static List> types(int n) {
+ Class>[] array = new Class>[n];
+ Arrays.fill(array, int.class);
+ return Arrays.asList(array);
+ }
+
+ static List values(int n) {
+ Integer[] array = new Integer[n];
+ Arrays.fill(array, 123);
+ return Arrays.asList(array);
+ }
+
+ static List getters(int n) {
+ MethodHandle[] array = new MethodHandle[n];
+ MethodHandle m = MethodHandles.dropArguments(MethodHandles.constant(int.class, 123), 0, Object.class);
+ Arrays.fill(array, m);
+ return Arrays.asList(array);
+ }
+
+ static void makeConcatWithTemplate() {
+ try {
+ int n = StringConcatFactory.MAX_INDY_CONCAT_ARG_SLOTS - 1;
+ MethodHandle m = StringConcatFactory.makeConcatWithTemplate(fragments(n + 1), types(n));
+ m.invokeWithArguments(values(n));
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ int n = StringConcatFactory.MAX_INDY_CONCAT_ARG_SLOTS;
+ MethodHandle m = StringConcatFactory.makeConcatWithTemplate(fragments(n + 1), types(n));
+ m.invokeWithArguments(values(n));
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+
+ boolean threw = false;
+ try {
+ int n = StringConcatFactory.MAX_INDY_CONCAT_ARG_SLOTS + 1;
+ MethodHandle m = StringConcatFactory.makeConcatWithTemplate(fragments(n + 1), types(n));
+ m.invokeWithArguments(values(n));
+ } catch (Throwable e) {
+ threw = true;
+ }
+
+ if (!threw) {
+ throw new RuntimeException("Exception expected - makeConcatWithTemplate");
+ }
+ }
+
+ static void makeConcatWithTemplateCluster() {
+ int n = StringConcatFactory.MAX_INDY_CONCAT_ARG_SLOTS;
+ int c = 3;
+ try {
+ List ms = StringConcatFactory.makeConcatWithTemplateCluster(fragments(c * n + 1), types(c * n), n);
+ MethodHandle m0 = ms.get(0);
+ MethodHandle m1 = ms.get(1);
+ MethodHandle m2 = ms.get(2);
+ MethodHandle m3 = ms.get(3);
+
+ String s = (String)m0.invokeWithArguments(values(n));
+ List args = new ArrayList<>();
+ args.add(s);
+ args.addAll(values(n - 1)); // one less for carry over string
+ s = (String)m1.invokeWithArguments(args);
+ args.clear();
+ args.add(s);
+ args.addAll(values(n - 1)); // one less for carry over string
+ s = (String)m2.invokeWithArguments(args);
+ args.clear();
+ args.add(s);
+ args.addAll(values(2)); // two remaining carry overs
+ s = (String)m3.invokeWithArguments(args);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static void makeConcatWithTemplateGetters() {
+ int n = StringConcatFactory.MAX_INDY_CONCAT_ARG_SLOTS;
+ int c = 3;
+ try {
+ MethodHandle m = StringConcatFactory.makeConcatWithTemplateGetters(fragments(c * n + 1), getters(c * n), n);
+ String s = (String)m.invoke(null);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/test/jdk/java/lang/runtime/CarriersTest.java b/test/jdk/java/lang/runtime/CarriersTest.java
new file mode 100644
index 00000000000..9b805f2c832
--- /dev/null
+++ b/test/jdk/java/lang/runtime/CarriersTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/*
+ * @test
+ * @summary Test features provided by the Carriers class.
+ * @modules java.base/java.lang.runtime
+ * @enablePreview true
+ * @compile --patch-module java.base=${test.src} CarriersTest.java
+ * @run main/othervm --patch-module java.base=${test.class.path} java.lang.runtime.CarriersTest
+ */
+
+package java.lang.runtime;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodType;
+import java.util.Arrays;
+import java.util.List;
+
+public class CarriersTest {
+ public static void main(String[] args) throws Throwable {
+ primitivesTest();
+ primitivesTestLarge();
+ limitsTest();
+ }
+
+ static void assertTrue(boolean test, String message) {
+ if (!test) {
+ throw new RuntimeException(message);
+ }
+ }
+
+ static final int MAX_COMPONENTS = 254;
+
+ static void primitivesTest() throws Throwable {
+ MethodType methodType =
+ MethodType.methodType(Object.class, byte.class, short.class,
+ char.class, int.class, long.class,
+ float.class, double.class,
+ boolean.class, String.class);
+ MethodHandle constructor = Carriers.initializingConstructor(methodType);
+ Object object = (Object)constructor.invokeExact((byte)0xFF, (short)0xFFFF,
+ 'C', 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFFL,
+ 1.0f / 3.0f, 1.0 / 3.0,
+ true, "abcde");
+ List components = Carriers.components(methodType);
+ assertTrue((byte)components.get(0).invokeExact(object) == (byte)0xFF,
+ "primitive byte test failure");
+ assertTrue((short)components.get(1).invokeExact(object) == (short)0xFFFF,
+ "primitive short test failure");
+ assertTrue((char)components.get(2).invokeExact(object) == 'C',
+ "primitive char test failure");
+ assertTrue((int)components.get(3).invokeExact(object) == 0xFFFFFFFF,
+ "primitive int test failure");
+ assertTrue((long)components.get(4).invokeExact(object) == 0xFFFFFFFFFFFFFFFFL,
+ "primitive long test failure");
+ assertTrue((float)components.get(5).invokeExact(object) == 1.0f / 3.0f,
+ "primitive float test failure");
+ assertTrue((double)components.get(6).invokeExact(object) == 1.0 / 3.0,
+ "primitive double test failure");
+ assertTrue((boolean)components.get(7).invokeExact(object),
+ "primitive boolean test failure");
+ assertTrue("abcde".equals((String)components.get(8).invokeExact(object)),
+ "primitive String test failure");
+ }
+
+ static void primitivesTestLarge() throws Throwable {
+ MethodType methodType =
+ MethodType.methodType(Object.class, byte.class, short.class,
+ char.class, int.class, long.class,
+ float.class, double.class,
+ boolean.class, String.class,
+ Object.class, Object.class,Object.class,Object.class,
+ Object.class, Object.class,Object.class,Object.class,
+ Object.class, Object.class,Object.class,Object.class,
+ Object.class, Object.class,Object.class,Object.class,
+ Object.class, Object.class,Object.class,Object.class,
+ Object.class, Object.class,Object.class,Object.class,
+ Object.class, Object.class,Object.class,Object.class
+ );
+ MethodHandle constructor = Carriers.initializingConstructor(methodType);
+ Object object = (Object)constructor.invokeExact((byte)0xFF, (short)0xFFFF,
+ 'C', 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFFL,
+ 1.0f / 3.0f, 1.0 / 3.0,
+ true, "abcde",
+ (Object)null, (Object)null, (Object)null, (Object)null,
+ (Object)null, (Object)null, (Object)null, (Object)null,
+ (Object)null, (Object)null, (Object)null, (Object)null,
+ (Object)null, (Object)null, (Object)null, (Object)null,
+ (Object)null, (Object)null, (Object)null, (Object)null,
+ (Object)null, (Object)null, (Object)null, (Object)null,
+ (Object)null, (Object)null, (Object)null, (Object)null
+ );
+ List components = Carriers.components(methodType);
+ assertTrue((byte)components.get(0).invokeExact(object) == (byte)0xFF,
+ "large primitive byte test failure");
+ assertTrue((short)components.get(1).invokeExact(object) == (short)0xFFFF,
+ "large primitive short test failure");
+ assertTrue((char)components.get(2).invokeExact(object) == 'C',
+ "large primitive char test failure");
+ assertTrue((int)components.get(3).invokeExact(object) == 0xFFFFFFFF,
+ "large primitive int test failure");
+ assertTrue((long)components.get(4).invokeExact(object) == 0xFFFFFFFFFFFFFFFFL,
+ "large primitive long test failure");
+ assertTrue((float)components.get(5).invokeExact(object) == 1.0f / 3.0f,
+ "large primitive float test failure");
+ assertTrue((double)components.get(6).invokeExact(object) == 1.0 / 3.0,
+ "large primitive double test failure");
+ assertTrue((boolean)components.get(7).invokeExact(object),
+ "large primitive boolean test failure");
+ assertTrue("abcde".equals((String)components.get(8).invokeExact(object)),
+ "large primitive String test failure");
+ }
+
+ static void limitsTest() {
+ boolean passed;
+
+ passed = false;
+ try {
+ Class>[] ptypes = new Class>[MAX_COMPONENTS + 1];
+ Arrays.fill(ptypes, Object.class);
+ MethodType methodType = MethodType.methodType(Object.class, ptypes);
+ MethodHandle constructor = Carriers.constructor(methodType);
+ } catch (IllegalArgumentException ex) {
+ passed = true;
+ }
+
+ if (!passed) {
+ throw new RuntimeException("failed to report too many components ");
+ }
+
+ passed = false;
+ try {
+ Class>[] ptypes = new Class>[MAX_COMPONENTS / 2 + 1];
+ Arrays.fill(ptypes, long.class);
+ MethodType methodType = MethodType.methodType(Object.class, ptypes);
+ MethodHandle constructor = Carriers.constructor(methodType);
+ } catch (IllegalArgumentException ex) {
+ passed = true;
+ }
+
+ if (!passed) {
+ throw new RuntimeException("failed to report too many components ");
+ }
+ }
+}
diff --git a/test/jdk/java/lang/runtime/ReferencedKeyTest.java b/test/jdk/java/lang/runtime/ReferencedKeyTest.java
new file mode 100644
index 00000000000..9234cffb98a
--- /dev/null
+++ b/test/jdk/java/lang/runtime/ReferencedKeyTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/*
+ * @test
+ * @summary Test features provided by the ReferencedKeyMap class.
+ * @modules java.base/java.lang.runtime
+ * @enablePreview
+ * @compile --patch-module java.base=${test.src} ReferencedKeyTest.java
+ * @run main/othervm --patch-module java.base=${test.class.path} java.lang.runtime.ReferencedKeyTest
+ */
+
+package java.lang.runtime;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+public class ReferencedKeyTest {
+ static long BASE_KEY = 10_000_000L;
+
+ public static void main(String[] args) throws Throwable {
+ mapTest(false, HashMap::new);
+ mapTest(true, HashMap::new);
+ mapTest(false, ConcurrentHashMap::new);
+ mapTest(true, ConcurrentHashMap::new);
+ }
+
+ static void assertTrue(boolean test, String message) {
+ if (!test) {
+ throw new RuntimeException(message);
+ }
+ }
+
+ static void mapTest(boolean isSoft, Supplier, String>> supplier) {
+ Map map = ReferencedKeyMap.create(isSoft, supplier);
+ populate(map);
+ collect();
+ // assertTrue(map.isEmpty() || isSoft, "Weak not collecting");
+ populate(map);
+ methods(map);
+ }
+
+ static void methods(Map map) {
+ assertTrue(map.size() == 26, "missing key");
+ assertTrue(map.containsKey(BASE_KEY + 'a' -'a'), "missing key");
+ assertTrue(map.get(BASE_KEY + 'b' -'a').equals("b"), "wrong key");
+ assertTrue(map.containsValue("c"), "missing value");
+ map.remove(BASE_KEY + 'd' -'a');
+ assertTrue(map.get(BASE_KEY + 'd' -'a') == null, "not removed");
+ map.putAll(Map.of(1L, "A", 2L, "B"));
+ assertTrue(map.get(2L).equals("B"), "collection not added");
+ assertTrue(map.keySet().contains(1L), "key missing");
+ assertTrue(map.values().contains("A"), "key missing");
+ assertTrue(map.entrySet().contains(Map.entry(1L, "A")), "key missing");
+ map.putIfAbsent(3L, "C");
+ assertTrue(map.get(3L).equals("C"), "key missing");
+ map.putIfAbsent(2L, "D");
+ assertTrue(map.get(2L).equals("B"), "key replaced");
+ map.remove(3L);
+ assertTrue(map.get(3L) == null, "key not removed");
+ map.replace(2L, "D");
+ assertTrue(map.get(2L).equals("D"), "key not replaced");
+ map.replace(2L, "B", "E");
+ assertTrue(map.get(2L).equals("D"), "key replaced");
+ }
+
+ static void collect() {
+ System.gc();
+ sleep();
+ }
+
+ static void sleep() {
+ try {
+ Thread.sleep(100L);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static void populate(Map map) {
+ for (int i = 0; i < 26; i++) {
+ Long key = BASE_KEY + i;
+ String value = String.valueOf((char) ('a' + i));
+ map.put(key, value);
+ }
+ }
+}
diff --git a/test/jdk/java/lang/template/Basic.java b/test/jdk/java/lang/template/Basic.java
new file mode 100644
index 00000000000..e0096ce0b57
--- /dev/null
+++ b/test/jdk/java/lang/template/Basic.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/*
+ * @test
+ * @bug 0000000
+ * @summary Exercise runtime handing of templated strings.
+ * @enablePreview true
+ */
+
+import java.lang.StringTemplate.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.*;
+
+import static java.lang.StringTemplate.RAW;
+
+public class Basic {
+ public static void main(String... arg) {
+ equalsHashCode();
+ concatenationTests();
+ componentTests();
+ limitsTests();
+ processorTests();
+ stringTemplateCoverage();
+ simpleProcessorCoverage();
+ }
+
+ static void ASSERT(String a, String b) {
+ if (!Objects.equals(a, b)) {
+ System.out.println(a);
+ System.out.println(b);
+ throw new RuntimeException("Test failed");
+ }
+ }
+
+ static void ASSERT(Object a, Object b) {
+ if (!Objects.deepEquals(a, b)) {
+ System.out.println(a);
+ System.out.println(b);
+ throw new RuntimeException("Test failed");
+ }
+ }
+
+ /*
+ * equals and hashCode tests.
+ */
+ static void equalsHashCode() {
+ int x = 10;
+ int y = 20;
+ int a = 10;
+ int b = 20;
+
+ StringTemplate st0 = RAW."\{x} + \{y} = \{x + y}";
+ StringTemplate st1 = RAW."\{a} + \{b} = \{a + b}";
+ StringTemplate st2 = RAW."\{x} + \{y} = \{x + y}!";
+ x++;
+ StringTemplate st3 = RAW."\{x} + \{y} = \{x + y}";
+
+ if (!st0.equals(st1)) throw new RuntimeException("st0 != st1");
+ if (st0.equals(st2)) throw new RuntimeException("st0 == st2");
+ if (st0.equals(st3)) throw new RuntimeException("st0 == st3");
+
+ if (st0.hashCode() != st1.hashCode()) throw new RuntimeException("st0.hashCode() != st1.hashCode()");
+ }
+
+ /*
+ * Concatenation tests.
+ */
+ static void concatenationTests() {
+ int x = 10;
+ int y = 20;
+
+ ASSERT(STR."\{x} \{y}", x + " " + y);
+ ASSERT(STR."\{x + y}", "" + (x + y));
+ ASSERT(STR.process(RAW."\{x} \{y}"), x + " " + y);
+ ASSERT(STR.process(RAW."\{x + y}"), "" + (x + y));
+ ASSERT((RAW."\{x} \{y}").process(STR), x + " " + y);
+ ASSERT((RAW."\{x + y}").process(STR), "" + (x + y));
+ }
+
+ /*
+ * Component tests.
+ */
+ static void componentTests() {
+ int x = 10;
+ int y = 20;
+
+ StringTemplate st = RAW."\{x} + \{y} = \{x + y}";
+ ASSERT(st.values(), List.of(x, y, x + y));
+ ASSERT(st.fragments(), List.of("", " + ", " = ", ""));
+ ASSERT(st.interpolate(), x + " + " + y + " = " + (x + y));
+ }
+
+ /*
+ * Limits tests.
+ */
+ static void limitsTests() {
+ int x = 9;
+
+ StringTemplate ts250 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ """;
+ ASSERT(ts250.values().size(), 250);
+ ASSERT(ts250.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999
+ """);
+
+ StringTemplate ts251 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}
+ """;
+ ASSERT(ts251.values().size(), 251);
+ ASSERT(ts251.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9
+ """);
+
+ StringTemplate ts252 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}
+ """;
+ ASSERT(ts252.values().size(), 252);
+ ASSERT(ts252.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 99
+ """);
+
+ StringTemplate ts253 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}
+ """;
+ ASSERT(ts253.values().size(), 253);
+ ASSERT(ts253.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 999
+ """);
+
+ StringTemplate ts254 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}
+ """;
+ ASSERT(ts254.values().size(), 254);
+ ASSERT(ts254.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999
+ """);
+
+ StringTemplate ts255 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}
+ """;
+ ASSERT(ts255.values().size(), 255);
+ ASSERT(ts255.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 99999
+ """);
+
+ StringTemplate ts256 = RAW."""
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}
+ \{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x}\{x} \{x}\{x}\{x}\{x}\{x}\{x}
+ """;
+ ASSERT(ts256.values().size(), 256);
+ ASSERT(ts256.interpolate(), """
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 9999999999
+
+ 9999999999 9999999999
+ 9999999999 9999999999
+ 9999999999 999999
+ """);
+
+ }
+
+ /*
+ * Processor tests.
+ */
+ public static final Processor STRINGIFY = st -> {
+ List values = st.values()
+ .stream()
+ .map(v -> (Object)String.valueOf(v))
+ .toList();
+
+ return StringTemplate.of(st.fragments(), values);
+ };
+
+ public static final Processor UPPER = st -> {
+ List fragments = st.fragments()
+ .stream()
+ .map(String::toUpperCase)
+ .toList();
+
+ return StringTemplate.of(fragments, st.values());
+ };
+
+ public static final Processor CHAIN = st -> {
+ st = STRINGIFY.process(st);
+ st = UPPER.process(st);
+ return STR.process(st);
+ };
+
+ static void processorTests() {
+ String name = "Joan";
+ int age = 25;
+ ASSERT(CHAIN."\{name} is \{age} years old", "Joan IS 25 YEARS OLD");
+ }
+
+ /*
+ * StringTemplate coverage
+ */
+ static void stringTemplateCoverage() {
+ StringTemplate tsNoValues = StringTemplate.of("No Values");
+
+ ASSERT(tsNoValues.values(), List.of());
+ ASSERT(tsNoValues.fragments(), List.of("No Values"));
+ ASSERT(tsNoValues.interpolate(), STR."No Values");
+
+ int x = 10, y = 20;
+ StringTemplate src = RAW."\{x} + \{y} = \{x + y}";
+ StringTemplate tsValues = StringTemplate.of(src.fragments(), src.values());
+ ASSERT(tsValues.fragments(), List.of("", " + ", " = ", ""));
+ ASSERT(tsValues.values(), List.of(x, y, x + y));
+ ASSERT(tsValues.interpolate(), x + " + " + y + " = " + (x + y));
+ ASSERT(StringTemplate.combine(src, src).interpolate(),
+ RAW."\{x} + \{y} = \{x + y}\{x} + \{y} = \{x + y}".interpolate());
+ ASSERT(StringTemplate.combine(src), src);
+ ASSERT(StringTemplate.combine().interpolate(), "");
+ ASSERT(StringTemplate.combine(List.of(src, src)).interpolate(),
+ RAW."\{x} + \{y} = \{x + y}\{x} + \{y} = \{x + y}".interpolate());
+ }
+
+ /*
+ * SimpleProcessor coverage.
+ */
+
+ static class Processor0 implements Processor {
+ @Override
+ public String process(StringTemplate stringTemplate) throws IllegalArgumentException {
+ StringBuilder sb = new StringBuilder();
+ Iterator fragmentsIter = stringTemplate.fragments().iterator();
+
+ for (Object value : stringTemplate.values()) {
+ sb.append(fragmentsIter.next());
+
+ if (value instanceof Boolean) {
+ throw new IllegalArgumentException("I don't like Booleans");
+ }
+
+ sb.append(value);
+ }
+
+ sb.append(fragmentsIter.next());
+
+ return sb.toString();
+ }
+ }
+
+ static Processor0 processor0 = new Processor0();
+
+ static Processor processor1 =
+ st -> st.interpolate();
+
+ static Processor processor2 = st -> st.interpolate();
+
+ static Processor processor3 = st -> st.interpolate();
+
+ static Processor processor4 = st ->
+ StringTemplate.interpolate(st.fragments(), st.values());
+
+
+ static void simpleProcessorCoverage() {
+ try {
+ int x = 10;
+ int y = 20;
+ ASSERT(processor0."\{x} + \{y} = \{x + y}", "10 + 20 = 30");
+ ASSERT(processor1."\{x} + \{y} = \{x + y}", "10 + 20 = 30");
+ ASSERT(processor2."\{x} + \{y} = \{x + y}", "10 + 20 = 30");
+ ASSERT(processor3."\{x} + \{y} = \{x + y}", "10 + 20 = 30");
+ ASSERT(processor4."\{x} + \{y} = \{x + y}", "10 + 20 = 30");
+ } catch (IllegalArgumentException ex) {
+ throw new RuntimeException("processor fail");
+ }
+ }
+
+ static String justify(String string, int width) {
+ boolean leftJustify = width < 0;
+ int length = string.length();
+ width = Math.abs(width);
+ int diff = width - length;
+
+ if (diff < 0) {
+ string = "*".repeat(width);
+ } else if (0 < diff) {
+ if (leftJustify) {
+ string += " ".repeat(diff);
+ } else {
+ string = " ".repeat(diff) + string;
+ }
+ }
+
+ return string;
+ }
+
+}
diff --git a/test/jdk/java/lang/template/FormatterBuilder.java b/test/jdk/java/lang/template/FormatterBuilder.java
new file mode 100644
index 00000000000..6406721f8e2
--- /dev/null
+++ b/test/jdk/java/lang/template/FormatterBuilder.java
@@ -0,0 +1,915 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/*
+ * @test
+ * @bug 0000000
+ * @summary Exercise format builder.
+ * @enablePreview true
+ */
+
+import java.util.FormatProcessor;
+import java.util.Objects;
+import java.util.Locale;
+
+import static java.util.FormatProcessor.FMT;
+
+public class FormatterBuilder {
+ public static void main(String... args) {
+ Locale.setDefault(Locale.US);
+ suite(FMT);
+ Locale thai = Locale.forLanguageTag("th-TH-u-nu-thai");
+ FormatProcessor thaiFormat = FormatProcessor.create(thai);
+ Locale.setDefault(thai);
+ suite(thaiFormat);
+ }
+
+ static void test(String a, String b) {
+ if (!Objects.equals(a, b)) {
+ throw new RuntimeException("format and FMT do not match: " + a + " : " + b);
+ }
+ }
+
+ static void suite(FormatProcessor fmt) {
+ Object nullObject = null;
+ test(String.format("%b", false), fmt."%b\{false}");
+ test(String.format("%b", true), fmt."%b\{true}");
+ test(String.format("%10b", false), fmt."%10b\{false}");
+ test(String.format("%10b", true), fmt."%10b\{true}");
+ test(String.format("%-10b", false), fmt."%-10b\{false}");
+ test(String.format("%-10b", true), fmt."%-10b\{true}");
+ test(String.format("%B", false), fmt."%B\{false}");
+ test(String.format("%B", true), fmt."%B\{true}");
+ test(String.format("%10B", false), fmt."%10B\{false}");
+ test(String.format("%10B", true), fmt."%10B\{true}");
+ test(String.format("%-10B", false), fmt."%-10B\{false}");
+ test(String.format("%-10B", true), fmt."%-10B\{true}");
+
+ test(String.format("%h", 12345), fmt."%h\{12345}");
+ test(String.format("%h", 0xABCDE), fmt."%h\{0xABCDE}");
+ test(String.format("%10h", 12345), fmt."%10h\{12345}");
+ test(String.format("%10h", 0xABCDE), fmt."%10h\{0xABCDE}");
+ test(String.format("%-10h", 12345), fmt."%-10h\{12345}");
+ test(String.format("%-10h", 0xABCDE), fmt."%-10h\{0xABCDE}");
+ test(String.format("%H", 12345), fmt."%H\{12345}");
+ test(String.format("%H", 0xABCDE), fmt."%H\{0xABCDE}");
+ test(String.format("%10H", 12345), fmt."%10H\{12345}");
+ test(String.format("%10H", 0xABCDE), fmt."%10H\{0xABCDE}");
+ test(String.format("%-10H", 12345), fmt."%-10H\{12345}");
+ test(String.format("%-10H", 0xABCDE), fmt."%-10H\{0xABCDE}");
+
+ test(String.format("%s", (byte)0xFF), fmt."%s\{(byte)0xFF}");
+ test(String.format("%s", (short)0xFFFF), fmt."%s\{(short)0xFFFF}");
+ test(String.format("%s", 12345), fmt."%s\{12345}");
+ test(String.format("%s", 12345L), fmt."%s\{12345L}");
+ test(String.format("%s", 1.33f), fmt."%s\{1.33f}");
+ test(String.format("%s", 1.33), fmt."%s\{1.33}");
+ test(String.format("%s", "abcde"), fmt."%s\{"abcde"}");
+ test(String.format("%s", nullObject), fmt."%s\{nullObject}");
+ test(String.format("%10s", (byte)0xFF), fmt."%10s\{(byte)0xFF}");
+ test(String.format("%10s", (short)0xFFFF), fmt."%10s\{(short)0xFFFF}");
+ test(String.format("%10s", 12345), fmt."%10s\{12345}");
+ test(String.format("%10s", 12345L), fmt."%10s\{12345L}");
+ test(String.format("%10s", 1.33f), fmt."%10s\{1.33f}");
+ test(String.format("%10s", 1.33), fmt."%10s\{1.33}");
+ test(String.format("%10s", "abcde"), fmt."%10s\{"abcde"}");
+ test(String.format("%10s", nullObject), fmt."%10s\{nullObject}");
+ test(String.format("%-10s", (byte)0xFF), fmt."%-10s\{(byte)0xFF}");
+ test(String.format("%-10s", (short)0xFFFF), fmt."%-10s\{(short)0xFFFF}");
+ test(String.format("%-10s", 12345), fmt."%-10s\{12345}");
+ test(String.format("%-10s", 12345L), fmt."%-10s\{12345L}");
+ test(String.format("%-10s", 1.33f), fmt."%-10s\{1.33f}");
+ test(String.format("%-10s", 1.33), fmt."%-10s\{1.33}");
+ test(String.format("%-10s", "abcde"), fmt."%-10s\{"abcde"}");
+ test(String.format("%-10s", nullObject), fmt."%-10s\{nullObject}");
+ test(String.format("%S", (byte)0xFF), fmt."%S\{(byte)0xFF}");
+ test(String.format("%S", (short)0xFFFF), fmt."%S\{(short)0xFFFF}");
+ test(String.format("%S", 12345), fmt."%S\{12345}");
+ test(String.format("%S", 12345L), fmt."%S\{12345L}");
+ test(String.format("%S", 1.33f), fmt."%S\{1.33f}");
+ test(String.format("%S", 1.33), fmt."%S\{1.33}");
+ test(String.format("%S", "abcde"), fmt."%S\{"abcde"}");
+ test(String.format("%S", nullObject), fmt."%S\{nullObject}");
+ test(String.format("%10S", (byte)0xFF), fmt."%10S\{(byte)0xFF}");
+ test(String.format("%10S", (short)0xFFFF), fmt."%10S\{(short)0xFFFF}");
+ test(String.format("%10S", 12345), fmt."%10S\{12345}");
+ test(String.format("%10S", 12345L), fmt."%10S\{12345L}");
+ test(String.format("%10S", 1.33f), fmt."%10S\{1.33f}");
+ test(String.format("%10S", 1.33), fmt."%10S\{1.33}");
+ test(String.format("%10S", "abcde"), fmt."%10S\{"abcde"}");
+ test(String.format("%10S", nullObject), fmt."%10S\{nullObject}");
+ test(String.format("%-10S", (byte)0xFF), fmt."%-10S\{(byte)0xFF}");
+ test(String.format("%-10S", (short)0xFFFF), fmt."%-10S\{(short)0xFFFF}");
+ test(String.format("%-10S", 12345), fmt."%-10S\{12345}");
+ test(String.format("%-10S", 12345L), fmt."%-10S\{12345L}");
+ test(String.format("%-10S", 1.33f), fmt."%-10S\{1.33f}");
+ test(String.format("%-10S", 1.33), fmt."%-10S\{1.33}");
+ test(String.format("%-10S", "abcde"), fmt."%-10S\{"abcde"}");
+ test(String.format("%-10S", nullObject), fmt."%-10S\{nullObject}");
+
+ test(String.format("%c", 'a'), fmt."%c\{'a'}");
+ test(String.format("%10c", 'a'), fmt."%10c\{'a'}");
+ test(String.format("%-10c", 'a'), fmt."%-10c\{'a'}");
+ test(String.format("%C", 'a'), fmt."%C\{'a'}");
+ test(String.format("%10C", 'a'), fmt."%10C\{'a'}");
+ test(String.format("%-10C", 'a'), fmt."%-10C\{'a'}");
+
+ test(String.format("%d", -12345), fmt."%d\{-12345}");
+ test(String.format("%d", 0), fmt."%d\{0}");
+ test(String.format("%d", 12345), fmt."%d\{12345}");
+ test(String.format("%10d", -12345), fmt."%10d\{-12345}");
+ test(String.format("%10d", 0), fmt."%10d\{0}");
+ test(String.format("%10d", 12345), fmt."%10d\{12345}");
+ test(String.format("%-10d", -12345), fmt."%-10d\{-12345}");
+ test(String.format("%-10d", 0), fmt."%-10d\{0}");
+ test(String.format("%-10d", 12345), fmt."%-10d\{12345}");
+ test(String.format("%,d", -12345), fmt."%,d\{-12345}");
+ test(String.format("%,d", 0), fmt."%,d\{0}");
+ test(String.format("%,d", 12345), fmt."%,d\{12345}");
+ test(String.format("%,10d", -12345), fmt."%,10d\{-12345}");
+ test(String.format("%,10d", 0), fmt."%,10d\{0}");
+ test(String.format("%,10d", 12345), fmt."%,10d\{12345}");
+ test(String.format("%,-10d", -12345), fmt."%,-10d\{-12345}");
+ test(String.format("%,-10d", 0), fmt."%,-10d\{0}");
+ test(String.format("%,-10d", 12345), fmt."%,-10d\{12345}");
+ test(String.format("%010d", -12345), fmt."%010d\{-12345}");
+ test(String.format("%010d", 0), fmt."%010d\{0}");
+ test(String.format("%010d", 12345), fmt."%010d\{12345}");
+ test(String.format("%,010d", -12345), fmt."%,010d\{-12345}");
+ test(String.format("%,010d", 0), fmt."%,010d\{0}");
+ test(String.format("%,010d", 12345), fmt."%,010d\{12345}");
+
+ test(String.format("%d", -12345), fmt."%d\{-12345}");
+ test(String.format("%d", 0), fmt."%d\{0}");
+ test(String.format("%d", 12345), fmt."%d\{12345}");
+ test(String.format("%10d", -12345), fmt."%10d\{-12345}");
+ test(String.format("%10d", 0), fmt."%10d\{0}");
+ test(String.format("%10d", 12345), fmt."%10d\{12345}");
+ test(String.format("%-10d", -12345), fmt."%-10d\{-12345}");
+ test(String.format("%-10d", 0), fmt."%-10d\{0}");
+ test(String.format("%-10d", 12345), fmt."%-10d\{12345}");
+ test(String.format("%,d", -12345), fmt."%,d\{-12345}");
+ test(String.format("%,d", 0), fmt."%,d\{0}");
+ test(String.format("%,d", 12345), fmt."%,d\{12345}");
+ test(String.format("%,10d", -12345), fmt."%,10d\{-12345}");
+ test(String.format("%,10d", 0), fmt."%,10d\{0}");
+ test(String.format("%,10d", 12345), fmt."%,10d\{12345}");
+ test(String.format("%,-10d", -12345), fmt."%,-10d\{-12345}");
+ test(String.format("%,-10d", 0), fmt."%,-10d\{0}");
+ test(String.format("%,-10d", 12345), fmt."%,-10d\{12345}");
+ test(String.format("% d", -12345), fmt."% d\{-12345}");
+ test(String.format("% d", 0), fmt."% d\{0}");
+ test(String.format("% d", 12345), fmt."% d\{12345}");
+ test(String.format("% 10d", -12345), fmt."% 10d\{-12345}");
+ test(String.format("% 10d", 0), fmt."% 10d\{0}");
+ test(String.format("% 10d", 12345), fmt."% 10d\{12345}");
+ test(String.format("% -10d", -12345), fmt."% -10d\{-12345}");
+ test(String.format("% -10d", 0), fmt."% -10d\{0}");
+ test(String.format("% -10d", 12345), fmt."% -10d\{12345}");
+ test(String.format("%, d", -12345), fmt."%, d\{-12345}");
+ test(String.format("%, d", 0), fmt."%, d\{0}");
+ test(String.format("%, d", 12345), fmt."%, d\{12345}");
+ test(String.format("%, 10d", -12345), fmt."%, 10d\{-12345}");
+ test(String.format("%, 10d", 0), fmt."%, 10d\{0}");
+ test(String.format("%, 10d", 12345), fmt."%, 10d\{12345}");
+ test(String.format("%, -10d", -12345), fmt."%, -10d\{-12345}");
+ test(String.format("%, -10d", 0), fmt."%, -10d\{0}");
+ test(String.format("%, -10d", 12345), fmt."%, -10d\{12345}");
+ test(String.format("%010d", -12345), fmt."%010d\{-12345}");
+ test(String.format("%010d", 0), fmt."%010d\{0}");
+ test(String.format("%010d", 12345), fmt."%010d\{12345}");
+ test(String.format("%,010d", -12345), fmt."%,010d\{-12345}");
+ test(String.format("%,010d", 0), fmt."%,010d\{0}");
+ test(String.format("%,010d", 12345), fmt."%,010d\{12345}");
+ test(String.format("% 010d", -12345), fmt."% 010d\{-12345}");
+ test(String.format("% 010d", 0), fmt."% 010d\{0}");
+ test(String.format("% 010d", 12345), fmt."% 010d\{12345}");
+ test(String.format("%, 010d", -12345), fmt."%, 010d\{-12345}");
+ test(String.format("%, 010d", 0), fmt."%, 010d\{0}");
+ test(String.format("%, 010d", 12345), fmt."%, 010d\{12345}");
+
+ test(String.format("%d", -12345), fmt."%d\{-12345}");
+ test(String.format("%d", 0), fmt."%d\{0}");
+ test(String.format("%d", 12345), fmt."%d\{12345}");
+ test(String.format("%10d", -12345), fmt."%10d\{-12345}");
+ test(String.format("%10d", 0), fmt."%10d\{0}");
+ test(String.format("%10d", 12345), fmt."%10d\{12345}");
+ test(String.format("%-10d", -12345), fmt."%-10d\{-12345}");
+ test(String.format("%-10d", 0), fmt."%-10d\{0}");
+ test(String.format("%-10d", 12345), fmt."%-10d\{12345}");
+ test(String.format("%,d", -12345), fmt."%,d\{-12345}");
+ test(String.format("%,d", 0), fmt."%,d\{0}");
+ test(String.format("%,d", 12345), fmt."%,d\{12345}");
+ test(String.format("%,10d", -12345), fmt."%,10d\{-12345}");
+ test(String.format("%,10d", 0), fmt."%,10d\{0}");
+ test(String.format("%,10d", 12345), fmt."%,10d\{12345}");
+ test(String.format("%,-10d", -12345), fmt."%,-10d\{-12345}");
+ test(String.format("%,-10d", 0), fmt."%,-10d\{0}");
+ test(String.format("%,-10d", 12345), fmt."%,-10d\{12345}");
+ test(String.format("%+d", -12345), fmt."%+d\{-12345}");
+ test(String.format("%+d", 0), fmt."%+d\{0}");
+ test(String.format("%+d", 12345), fmt."%+d\{12345}");
+ test(String.format("%+10d", -12345), fmt."%+10d\{-12345}");
+ test(String.format("%+10d", 0), fmt."%+10d\{0}");
+ test(String.format("%+10d", 12345), fmt."%+10d\{12345}");
+ test(String.format("%+-10d", -12345), fmt."%+-10d\{-12345}");
+ test(String.format("%+-10d", 0), fmt."%+-10d\{0}");
+ test(String.format("%+-10d", 12345), fmt."%+-10d\{12345}");
+ test(String.format("%,+d", -12345), fmt."%,+d\{-12345}");
+ test(String.format("%,+d", 0), fmt."%,+d\{0}");
+ test(String.format("%,+d", 12345), fmt."%,+d\{12345}");
+ test(String.format("%,+10d", -12345), fmt."%,+10d\{-12345}");
+ test(String.format("%,+10d", 0), fmt."%,+10d\{0}");
+ test(String.format("%,+10d", 12345), fmt."%,+10d\{12345}");
+ test(String.format("%,+-10d", -12345), fmt."%,+-10d\{-12345}");
+ test(String.format("%,+-10d", 0), fmt."%,+-10d\{0}");
+ test(String.format("%,+-10d", 12345), fmt."%,+-10d\{12345}");
+ test(String.format("%010d", -12345), fmt."%010d\{-12345}");
+ test(String.format("%010d", 0), fmt."%010d\{0}");
+ test(String.format("%010d", 12345), fmt."%010d\{12345}");
+ test(String.format("%,010d", -12345), fmt."%,010d\{-12345}");
+ test(String.format("%,010d", 0), fmt."%,010d\{0}");
+ test(String.format("%,010d", 12345), fmt."%,010d\{12345}");
+ test(String.format("%+010d", -12345), fmt."%+010d\{-12345}");
+ test(String.format("%+010d", 0), fmt."%+010d\{0}");
+ test(String.format("%+010d", 12345), fmt."%+010d\{12345}");
+ test(String.format("%,+010d", -12345), fmt."%,+010d\{-12345}");
+ test(String.format("%,+010d", 0), fmt."%,+010d\{0}");
+ test(String.format("%,+010d", 12345), fmt."%,+010d\{12345}");
+
+ test(String.format("%d", -12345), fmt."%d\{-12345}");
+ test(String.format("%d", 0), fmt."%d\{0}");
+ test(String.format("%d", 12345), fmt."%d\{12345}");
+ test(String.format("%10d", -12345), fmt."%10d\{-12345}");
+ test(String.format("%10d", 0), fmt."%10d\{0}");
+ test(String.format("%10d", 12345), fmt."%10d\{12345}");
+ test(String.format("%-10d", -12345), fmt."%-10d\{-12345}");
+ test(String.format("%-10d", 0), fmt."%-10d\{0}");
+ test(String.format("%-10d", 12345), fmt."%-10d\{12345}");
+ test(String.format("%,d", -12345), fmt."%,d\{-12345}");
+ test(String.format("%,d", 0), fmt."%,d\{0}");
+ test(String.format("%,d", 12345), fmt."%,d\{12345}");
+ test(String.format("%,10d", -12345), fmt."%,10d\{-12345}");
+ test(String.format("%,10d", 0), fmt."%,10d\{0}");
+ test(String.format("%,10d", 12345), fmt."%,10d\{12345}");
+ test(String.format("%,-10d", -12345), fmt."%,-10d\{-12345}");
+ test(String.format("%,-10d", 0), fmt."%,-10d\{0}");
+ test(String.format("%,-10d", 12345), fmt."%,-10d\{12345}");
+ test(String.format("%(d", -12345), fmt."%(d\{-12345}");
+ test(String.format("%(d", 0), fmt."%(d\{0}");
+ test(String.format("%(d", 12345), fmt."%(d\{12345}");
+ test(String.format("%(10d", -12345), fmt."%(10d\{-12345}");
+ test(String.format("%(10d", 0), fmt."%(10d\{0}");
+ test(String.format("%(10d", 12345), fmt."%(10d\{12345}");
+ test(String.format("%(-10d", -12345), fmt."%(-10d\{-12345}");
+ test(String.format("%(-10d", 0), fmt."%(-10d\{0}");
+ test(String.format("%(-10d", 12345), fmt."%(-10d\{12345}");
+ test(String.format("%,(d", -12345), fmt."%,(d\{-12345}");
+ test(String.format("%,(d", 0), fmt."%,(d\{0}");
+ test(String.format("%,(d", 12345), fmt."%,(d\{12345}");
+ test(String.format("%,(10d", -12345), fmt."%,(10d\{-12345}");
+ test(String.format("%,(10d", 0), fmt."%,(10d\{0}");
+ test(String.format("%,(10d", 12345), fmt."%,(10d\{12345}");
+ test(String.format("%,(-10d", -12345), fmt."%,(-10d\{-12345}");
+ test(String.format("%,(-10d", 0), fmt."%,(-10d\{0}");
+ test(String.format("%,(-10d", 12345), fmt."%,(-10d\{12345}");
+ test(String.format("%010d", -12345), fmt."%010d\{-12345}");
+ test(String.format("%010d", 0), fmt."%010d\{0}");
+ test(String.format("%010d", 12345), fmt."%010d\{12345}");
+ test(String.format("%,010d", -12345), fmt."%,010d\{-12345}");
+ test(String.format("%,010d", 0), fmt."%,010d\{0}");
+ test(String.format("%,010d", 12345), fmt."%,010d\{12345}");
+ test(String.format("%(010d", -12345), fmt."%(010d\{-12345}");
+ test(String.format("%(010d", 0), fmt."%(010d\{0}");
+ test(String.format("%(010d", 12345), fmt."%(010d\{12345}");
+ test(String.format("%,(010d", -12345), fmt."%,(010d\{-12345}");
+ test(String.format("%,(010d", 0), fmt."%,(010d\{0}");
+ test(String.format("%,(010d", 12345), fmt."%,(010d\{12345}");
+
+ test(String.format("%o", -12345), fmt."%o\{-12345}");
+ test(String.format("%o", 0), fmt."%o\{0}");
+ test(String.format("%o", 12345), fmt."%o\{12345}");
+ test(String.format("%10o", -12345), fmt."%10o\{-12345}");
+ test(String.format("%10o", 0), fmt."%10o\{0}");
+ test(String.format("%10o", 12345), fmt."%10o\{12345}");
+ test(String.format("%-10o", -12345), fmt."%-10o\{-12345}");
+ test(String.format("%-10o", 0), fmt."%-10o\{0}");
+ test(String.format("%-10o", 12345), fmt."%-10o\{12345}");
+ test(String.format("%#o", -12345), fmt."%#o\{-12345}");
+ test(String.format("%#o", 0), fmt."%#o\{0}");
+ test(String.format("%#o", 12345), fmt."%#o\{12345}");
+ test(String.format("%#10o", -12345), fmt."%#10o\{-12345}");
+ test(String.format("%#10o", 0), fmt."%#10o\{0}");
+ test(String.format("%#10o", 12345), fmt."%#10o\{12345}");
+ test(String.format("%#-10o", -12345), fmt."%#-10o\{-12345}");
+ test(String.format("%#-10o", 0), fmt."%#-10o\{0}");
+ test(String.format("%#-10o", 12345), fmt."%#-10o\{12345}");
+ test(String.format("%010o", -12345), fmt."%010o\{-12345}");
+ test(String.format("%010o", 0), fmt."%010o\{0}");
+ test(String.format("%010o", 12345), fmt."%010o\{12345}");
+ test(String.format("%#010o", -12345), fmt."%#010o\{-12345}");
+ test(String.format("%#010o", 0), fmt."%#010o\{0}");
+ test(String.format("%#010o", 12345), fmt."%#010o\{12345}");
+
+ test(String.format("%x", -12345), fmt."%x\{-12345}");
+ test(String.format("%x", 0), fmt."%x\{0}");
+ test(String.format("%x", 12345), fmt."%x\{12345}");
+ test(String.format("%10x", -12345), fmt."%10x\{-12345}");
+ test(String.format("%10x", 0), fmt."%10x\{0}");
+ test(String.format("%10x", 12345), fmt."%10x\{12345}");
+ test(String.format("%-10x", -12345), fmt."%-10x\{-12345}");
+ test(String.format("%-10x", 0), fmt."%-10x\{0}");
+ test(String.format("%-10x", 12345), fmt."%-10x\{12345}");
+ test(String.format("%X", -12345), fmt."%X\{-12345}");
+ test(String.format("%X", 0), fmt."%X\{0}");
+ test(String.format("%X", 12345), fmt."%X\{12345}");
+ test(String.format("%10X", -12345), fmt."%10X\{-12345}");
+ test(String.format("%10X", 0), fmt."%10X\{0}");
+ test(String.format("%10X", 12345), fmt."%10X\{12345}");
+ test(String.format("%-10X", -12345), fmt."%-10X\{-12345}");
+ test(String.format("%-10X", 0), fmt."%-10X\{0}");
+ test(String.format("%-10X", 12345), fmt."%-10X\{12345}");
+ test(String.format("%#x", -12345), fmt."%#x\{-12345}");
+ test(String.format("%#x", 0), fmt."%#x\{0}");
+ test(String.format("%#x", 12345), fmt."%#x\{12345}");
+ test(String.format("%#10x", -12345), fmt."%#10x\{-12345}");
+ test(String.format("%#10x", 0), fmt."%#10x\{0}");
+ test(String.format("%#10x", 12345), fmt."%#10x\{12345}");
+ test(String.format("%#-10x", -12345), fmt."%#-10x\{-12345}");
+ test(String.format("%#-10x", 0), fmt."%#-10x\{0}");
+ test(String.format("%#-10x", 12345), fmt."%#-10x\{12345}");
+ test(String.format("%#X", -12345), fmt."%#X\{-12345}");
+ test(String.format("%#X", 0), fmt."%#X\{0}");
+ test(String.format("%#X", 12345), fmt."%#X\{12345}");
+ test(String.format("%#10X", -12345), fmt."%#10X\{-12345}");
+ test(String.format("%#10X", 0), fmt."%#10X\{0}");
+ test(String.format("%#10X", 12345), fmt."%#10X\{12345}");
+ test(String.format("%#-10X", -12345), fmt."%#-10X\{-12345}");
+ test(String.format("%#-10X", 0), fmt."%#-10X\{0}");
+ test(String.format("%#-10X", 12345), fmt."%#-10X\{12345}");
+ test(String.format("%010x", -12345), fmt."%010x\{-12345}");
+ test(String.format("%010x", 0), fmt."%010x\{0}");
+ test(String.format("%010x", 12345), fmt."%010x\{12345}");
+ test(String.format("%010X", -12345), fmt."%010X\{-12345}");
+ test(String.format("%010X", 0), fmt."%010X\{0}");
+ test(String.format("%010X", 12345), fmt."%010X\{12345}");
+ test(String.format("%#010x", -12345), fmt."%#010x\{-12345}");
+ test(String.format("%#010x", 0), fmt."%#010x\{0}");
+ test(String.format("%#010x", 12345), fmt."%#010x\{12345}");
+ test(String.format("%#010X", -12345), fmt."%#010X\{-12345}");
+ test(String.format("%#010X", 0), fmt."%#010X\{0}");
+ test(String.format("%#010X", 12345), fmt."%#010X\{12345}");
+
+ test(String.format("%f", -12345.6), fmt."%f\{-12345.6}");
+ test(String.format("%f", 0.0), fmt."%f\{0.0}");
+ test(String.format("%f", 12345.6), fmt."%f\{12345.6}");
+ test(String.format("%10f", -12345.6), fmt."%10f\{-12345.6}");
+ test(String.format("%10f", 0.0), fmt."%10f\{0.0}");
+ test(String.format("%10f", 12345.6), fmt."%10f\{12345.6}");
+ test(String.format("%-10f", -12345.6), fmt."%-10f\{-12345.6}");
+ test(String.format("%-10f", 0.0), fmt."%-10f\{0.0}");
+ test(String.format("%-10f", 12345.6), fmt."%-10f\{12345.6}");
+ test(String.format("%,f", -12345.6), fmt."%,f\{-12345.6}");
+ test(String.format("%,f", 0.0), fmt."%,f\{0.0}");
+ test(String.format("%,f", 12345.6), fmt."%,f\{12345.6}");
+ test(String.format("%,10f", -12345.6), fmt."%,10f\{-12345.6}");
+ test(String.format("%,10f", 0.0), fmt."%,10f\{0.0}");
+ test(String.format("%,10f", 12345.6), fmt."%,10f\{12345.6}");
+ test(String.format("%,-10f", -12345.6), fmt."%,-10f\{-12345.6}");
+ test(String.format("%,-10f", 0.0), fmt."%,-10f\{0.0}");
+ test(String.format("%,-10f", 12345.6), fmt."%,-10f\{12345.6}");
+
+ test(String.format("%f", -12345.6), fmt."%f\{-12345.6}");
+ test(String.format("%f", 0.0), fmt."%f\{0.0}");
+ test(String.format("%f", 12345.6), fmt."%f\{12345.6}");
+ test(String.format("%10f", -12345.6), fmt."%10f\{-12345.6}");
+ test(String.format("%10f", 0.0), fmt."%10f\{0.0}");
+ test(String.format("%10f", 12345.6), fmt."%10f\{12345.6}");
+ test(String.format("%-10f", -12345.6), fmt."%-10f\{-12345.6}");
+ test(String.format("%-10f", 0.0), fmt."%-10f\{0.0}");
+ test(String.format("%-10f", 12345.6), fmt."%-10f\{12345.6}");
+ test(String.format("%,f", -12345.6), fmt."%,f\{-12345.6}");
+ test(String.format("%,f", 0.0), fmt."%,f\{0.0}");
+ test(String.format("%,f", 12345.6), fmt."%,f\{12345.6}");
+ test(String.format("%,10f", -12345.6), fmt."%,10f\{-12345.6}");
+ test(String.format("%,10f", 0.0), fmt."%,10f\{0.0}");
+ test(String.format("%,10f", 12345.6), fmt."%,10f\{12345.6}");
+ test(String.format("%,-10f", -12345.6), fmt."%,-10f\{-12345.6}");
+ test(String.format("%,-10f", 0.0), fmt."%,-10f\{0.0}");
+ test(String.format("%,-10f", 12345.6), fmt."%,-10f\{12345.6}");
+ test(String.format("% f", -12345.6), fmt."% f\{-12345.6}");
+ test(String.format("% f", 0.0), fmt."% f\{0.0}");
+ test(String.format("% f", 12345.6), fmt."% f\{12345.6}");
+ test(String.format("% 10f", -12345.6), fmt."% 10f\{-12345.6}");
+ test(String.format("% 10f", 0.0), fmt."% 10f\{0.0}");
+ test(String.format("% 10f", 12345.6), fmt."% 10f\{12345.6}");
+ test(String.format("% -10f", -12345.6), fmt."% -10f\{-12345.6}");
+ test(String.format("% -10f", 0.0), fmt."% -10f\{0.0}");
+ test(String.format("% -10f", 12345.6), fmt."% -10f\{12345.6}");
+ test(String.format("%, f", -12345.6), fmt."%, f\{-12345.6}");
+ test(String.format("%, f", 0.0), fmt."%, f\{0.0}");
+ test(String.format("%, f", 12345.6), fmt."%, f\{12345.6}");
+ test(String.format("%, 10f", -12345.6), fmt."%, 10f\{-12345.6}");
+ test(String.format("%, 10f", 0.0), fmt."%, 10f\{0.0}");
+ test(String.format("%, 10f", 12345.6), fmt."%, 10f\{12345.6}");
+ test(String.format("%, -10f", -12345.6), fmt."%, -10f\{-12345.6}");
+ test(String.format("%, -10f", 0.0), fmt."%, -10f\{0.0}");
+ test(String.format("%, -10f", 12345.6), fmt."%, -10f\{12345.6}");
+
+ test(String.format("%f", -12345.6), fmt."%f\{-12345.6}");
+ test(String.format("%f", 0.0), fmt."%f\{0.0}");
+ test(String.format("%f", 12345.6), fmt."%f\{12345.6}");
+ test(String.format("%10f", -12345.6), fmt."%10f\{-12345.6}");
+ test(String.format("%10f", 0.0), fmt."%10f\{0.0}");
+ test(String.format("%10f", 12345.6), fmt."%10f\{12345.6}");
+ test(String.format("%-10f", -12345.6), fmt."%-10f\{-12345.6}");
+ test(String.format("%-10f", 0.0), fmt."%-10f\{0.0}");
+ test(String.format("%-10f", 12345.6), fmt."%-10f\{12345.6}");
+ test(String.format("%,f", -12345.6), fmt."%,f\{-12345.6}");
+ test(String.format("%,f", 0.0), fmt."%,f\{0.0}");
+ test(String.format("%,f", 12345.6), fmt."%,f\{12345.6}");
+ test(String.format("%,10f", -12345.6), fmt."%,10f\{-12345.6}");
+ test(String.format("%,10f", 0.0), fmt."%,10f\{0.0}");
+ test(String.format("%,10f", 12345.6), fmt."%,10f\{12345.6}");
+ test(String.format("%,-10f", -12345.6), fmt."%,-10f\{-12345.6}");
+ test(String.format("%,-10f", 0.0), fmt."%,-10f\{0.0}");
+ test(String.format("%,-10f", 12345.6), fmt."%,-10f\{12345.6}");
+ test(String.format("%+f", -12345.6), fmt."%+f\{-12345.6}");
+ test(String.format("%+f", 0.0), fmt."%+f\{0.0}");
+ test(String.format("%+f", 12345.6), fmt."%+f\{12345.6}");
+ test(String.format("%+10f", -12345.6), fmt."%+10f\{-12345.6}");
+ test(String.format("%+10f", 0.0), fmt."%+10f\{0.0}");
+ test(String.format("%+10f", 12345.6), fmt."%+10f\{12345.6}");
+ test(String.format("%+-10f", -12345.6), fmt."%+-10f\{-12345.6}");
+ test(String.format("%+-10f", 0.0), fmt."%+-10f\{0.0}");
+ test(String.format("%+-10f", 12345.6), fmt."%+-10f\{12345.6}");
+ test(String.format("%,+f", -12345.6), fmt."%,+f\{-12345.6}");
+ test(String.format("%,+f", 0.0), fmt."%,+f\{0.0}");
+ test(String.format("%,+f", 12345.6), fmt."%,+f\{12345.6}");
+ test(String.format("%,+10f", -12345.6), fmt."%,+10f\{-12345.6}");
+ test(String.format("%,+10f", 0.0), fmt."%,+10f\{0.0}");
+ test(String.format("%,+10f", 12345.6), fmt."%,+10f\{12345.6}");
+ test(String.format("%,+-10f", -12345.6), fmt."%,+-10f\{-12345.6}");
+ test(String.format("%,+-10f", 0.0), fmt."%,+-10f\{0.0}");
+ test(String.format("%,+-10f", 12345.6), fmt."%,+-10f\{12345.6}");
+
+ test(String.format("%f", -12345.6), fmt."%f\{-12345.6}");
+ test(String.format("%f", 0.0), fmt."%f\{0.0}");
+ test(String.format("%f", 12345.6), fmt."%f\{12345.6}");
+ test(String.format("%10f", -12345.6), fmt."%10f\{-12345.6}");
+ test(String.format("%10f", 0.0), fmt."%10f\{0.0}");
+ test(String.format("%10f", 12345.6), fmt."%10f\{12345.6}");
+ test(String.format("%-10f", -12345.6), fmt."%-10f\{-12345.6}");
+ test(String.format("%-10f", 0.0), fmt."%-10f\{0.0}");
+ test(String.format("%-10f", 12345.6), fmt."%-10f\{12345.6}");
+ test(String.format("%,f", -12345.6), fmt."%,f\{-12345.6}");
+ test(String.format("%,f", 0.0), fmt."%,f\{0.0}");
+ test(String.format("%,f", 12345.6), fmt."%,f\{12345.6}");
+ test(String.format("%,10f", -12345.6), fmt."%,10f\{-12345.6}");
+ test(String.format("%,10f", 0.0), fmt."%,10f\{0.0}");
+ test(String.format("%,10f", 12345.6), fmt."%,10f\{12345.6}");
+ test(String.format("%,-10f", -12345.6), fmt."%,-10f\{-12345.6}");
+ test(String.format("%,-10f", 0.0), fmt."%,-10f\{0.0}");
+ test(String.format("%,-10f", 12345.6), fmt."%,-10f\{12345.6}");
+ test(String.format("%(f", -12345.6), fmt."%(f\{-12345.6}");
+ test(String.format("%(f", 0.0), fmt."%(f\{0.0}");
+ test(String.format("%(f", 12345.6), fmt."%(f\{12345.6}");
+ test(String.format("%(10f", -12345.6), fmt."%(10f\{-12345.6}");
+ test(String.format("%(10f", 0.0), fmt."%(10f\{0.0}");
+ test(String.format("%(10f", 12345.6), fmt."%(10f\{12345.6}");
+ test(String.format("%(-10f", -12345.6), fmt."%(-10f\{-12345.6}");
+ test(String.format("%(-10f", 0.0), fmt."%(-10f\{0.0}");
+ test(String.format("%(-10f", 12345.6), fmt."%(-10f\{12345.6}");
+ test(String.format("%,(f", -12345.6), fmt."%,(f\{-12345.6}");
+ test(String.format("%,(f", 0.0), fmt."%,(f\{0.0}");
+ test(String.format("%,(f", 12345.6), fmt."%,(f\{12345.6}");
+ test(String.format("%,(10f", -12345.6), fmt."%,(10f\{-12345.6}");
+ test(String.format("%,(10f", 0.0), fmt."%,(10f\{0.0}");
+ test(String.format("%,(10f", 12345.6), fmt."%,(10f\{12345.6}");
+ test(String.format("%,(-10f", -12345.6), fmt."%,(-10f\{-12345.6}");
+ test(String.format("%,(-10f", 0.0), fmt."%,(-10f\{0.0}");
+ test(String.format("%,(-10f", 12345.6), fmt."%,(-10f\{12345.6}");
+ test(String.format("%+f", -12345.6), fmt."%+f\{-12345.6}");
+ test(String.format("%+f", 0.0), fmt."%+f\{0.0}");
+ test(String.format("%+f", 12345.6), fmt."%+f\{12345.6}");
+ test(String.format("%+10f", -12345.6), fmt."%+10f\{-12345.6}");
+ test(String.format("%+10f", 0.0), fmt."%+10f\{0.0}");
+ test(String.format("%+10f", 12345.6), fmt."%+10f\{12345.6}");
+ test(String.format("%+-10f", -12345.6), fmt."%+-10f\{-12345.6}");
+ test(String.format("%+-10f", 0.0), fmt."%+-10f\{0.0}");
+ test(String.format("%+-10f", 12345.6), fmt."%+-10f\{12345.6}");
+ test(String.format("%,+f", -12345.6), fmt."%,+f\{-12345.6}");
+ test(String.format("%,+f", 0.0), fmt."%,+f\{0.0}");
+ test(String.format("%,+f", 12345.6), fmt."%,+f\{12345.6}");
+ test(String.format("%,+10f", -12345.6), fmt."%,+10f\{-12345.6}");
+ test(String.format("%,+10f", 0.0), fmt."%,+10f\{0.0}");
+ test(String.format("%,+10f", 12345.6), fmt."%,+10f\{12345.6}");
+ test(String.format("%,+-10f", -12345.6), fmt."%,+-10f\{-12345.6}");
+ test(String.format("%,+-10f", 0.0), fmt."%,+-10f\{0.0}");
+ test(String.format("%,+-10f", 12345.6), fmt."%,+-10f\{12345.6}");
+ test(String.format("%(+f", -12345.6), fmt."%(+f\{-12345.6}");
+ test(String.format("%(+f", 0.0), fmt."%(+f\{0.0}");
+ test(String.format("%(+f", 12345.6), fmt."%(+f\{12345.6}");
+ test(String.format("%(+10f", -12345.6), fmt."%(+10f\{-12345.6}");
+ test(String.format("%(+10f", 0.0), fmt."%(+10f\{0.0}");
+ test(String.format("%(+10f", 12345.6), fmt."%(+10f\{12345.6}");
+ test(String.format("%(+-10f", -12345.6), fmt."%(+-10f\{-12345.6}");
+ test(String.format("%(+-10f", 0.0), fmt."%(+-10f\{0.0}");
+ test(String.format("%(+-10f", 12345.6), fmt."%(+-10f\{12345.6}");
+ test(String.format("%,(+f", -12345.6), fmt."%,(+f\{-12345.6}");
+ test(String.format("%,(+f", 0.0), fmt."%,(+f\{0.0}");
+ test(String.format("%,(+f", 12345.6), fmt."%,(+f\{12345.6}");
+ test(String.format("%,(+10f", -12345.6), fmt."%,(+10f\{-12345.6}");
+ test(String.format("%,(+10f", 0.0), fmt."%,(+10f\{0.0}");
+ test(String.format("%,(+10f", 12345.6), fmt."%,(+10f\{12345.6}");
+ test(String.format("%,(+-10f", -12345.6), fmt."%,(+-10f\{-12345.6}");
+ test(String.format("%,(+-10f", 0.0), fmt."%,(+-10f\{0.0}");
+ test(String.format("%,(+-10f", 12345.6), fmt."%,(+-10f\{12345.6}");
+
+ test(String.format("%e", -12345.6), fmt."%e\{-12345.6}");
+ test(String.format("%e", 0.0), fmt."%e\{0.0}");
+ test(String.format("%e", 12345.6), fmt."%e\{12345.6}");
+ test(String.format("%10e", -12345.6), fmt."%10e\{-12345.6}");
+ test(String.format("%10e", 0.0), fmt."%10e\{0.0}");
+ test(String.format("%10e", 12345.6), fmt."%10e\{12345.6}");
+ test(String.format("%-10e", -12345.6), fmt."%-10e\{-12345.6}");
+ test(String.format("%-10e", 0.0), fmt."%-10e\{0.0}");
+ test(String.format("%-10e", 12345.6), fmt."%-10e\{12345.6}");
+ test(String.format("%E", -12345.6), fmt."%E\{-12345.6}");
+ test(String.format("%E", 0.0), fmt."%E\{0.0}");
+ test(String.format("%E", 12345.6), fmt."%E\{12345.6}");
+ test(String.format("%10E", -12345.6), fmt."%10E\{-12345.6}");
+ test(String.format("%10E", 0.0), fmt."%10E\{0.0}");
+ test(String.format("%10E", 12345.6), fmt."%10E\{12345.6}");
+ test(String.format("%-10E", -12345.6), fmt."%-10E\{-12345.6}");
+ test(String.format("%-10E", 0.0), fmt."%-10E\{0.0}");
+ test(String.format("%-10E", 12345.6), fmt."%-10E\{12345.6}");
+
+ test(String.format("%g", -12345.6), fmt."%g\{-12345.6}");
+ test(String.format("%g", 0.0), fmt."%g\{0.0}");
+ test(String.format("%g", 12345.6), fmt."%g\{12345.6}");
+ test(String.format("%10g", -12345.6), fmt."%10g\{-12345.6}");
+ test(String.format("%10g", 0.0), fmt."%10g\{0.0}");
+ test(String.format("%10g", 12345.6), fmt."%10g\{12345.6}");
+ test(String.format("%-10g", -12345.6), fmt."%-10g\{-12345.6}");
+ test(String.format("%-10g", 0.0), fmt."%-10g\{0.0}");
+ test(String.format("%-10g", 12345.6), fmt."%-10g\{12345.6}");
+ test(String.format("%G", -12345.6), fmt."%G\{-12345.6}");
+ test(String.format("%G", 0.0), fmt."%G\{0.0}");
+ test(String.format("%G", 12345.6), fmt."%G\{12345.6}");
+ test(String.format("%10G", -12345.6), fmt."%10G\{-12345.6}");
+ test(String.format("%10G", 0.0), fmt."%10G\{0.0}");
+ test(String.format("%10G", 12345.6), fmt."%10G\{12345.6}");
+ test(String.format("%-10G", -12345.6), fmt."%-10G\{-12345.6}");
+ test(String.format("%-10G", 0.0), fmt."%-10G\{0.0}");
+ test(String.format("%-10G", 12345.6), fmt."%-10G\{12345.6}");
+ test(String.format("%,g", -12345.6), fmt."%,g\{-12345.6}");
+ test(String.format("%,g", 0.0), fmt."%,g\{0.0}");
+ test(String.format("%,g", 12345.6), fmt."%,g\{12345.6}");
+ test(String.format("%,10g", -12345.6), fmt."%,10g\{-12345.6}");
+ test(String.format("%,10g", 0.0), fmt."%,10g\{0.0}");
+ test(String.format("%,10g", 12345.6), fmt."%,10g\{12345.6}");
+ test(String.format("%,-10g", -12345.6), fmt."%,-10g\{-12345.6}");
+ test(String.format("%,-10g", 0.0), fmt."%,-10g\{0.0}");
+ test(String.format("%,-10g", 12345.6), fmt."%,-10g\{12345.6}");
+ test(String.format("%,G", -12345.6), fmt."%,G\{-12345.6}");
+ test(String.format("%,G", 0.0), fmt."%,G\{0.0}");
+ test(String.format("%,G", 12345.6), fmt."%,G\{12345.6}");
+ test(String.format("%,10G", -12345.6), fmt."%,10G\{-12345.6}");
+ test(String.format("%,10G", 0.0), fmt."%,10G\{0.0}");
+ test(String.format("%,10G", 12345.6), fmt."%,10G\{12345.6}");
+ test(String.format("%,-10G", -12345.6), fmt."%,-10G\{-12345.6}");
+ test(String.format("%,-10G", 0.0), fmt."%,-10G\{0.0}");
+ test(String.format("%,-10G", 12345.6), fmt."%,-10G\{12345.6}");
+
+ test(String.format("%g", -12345.6), fmt."%g\{-12345.6}");
+ test(String.format("%g", 0.0), fmt."%g\{0.0}");
+ test(String.format("%g", 12345.6), fmt."%g\{12345.6}");
+ test(String.format("%10g", -12345.6), fmt."%10g\{-12345.6}");
+ test(String.format("%10g", 0.0), fmt."%10g\{0.0}");
+ test(String.format("%10g", 12345.6), fmt."%10g\{12345.6}");
+ test(String.format("%-10g", -12345.6), fmt."%-10g\{-12345.6}");
+ test(String.format("%-10g", 0.0), fmt."%-10g\{0.0}");
+ test(String.format("%-10g", 12345.6), fmt."%-10g\{12345.6}");
+ test(String.format("%G", -12345.6), fmt."%G\{-12345.6}");
+ test(String.format("%G", 0.0), fmt."%G\{0.0}");
+ test(String.format("%G", 12345.6), fmt."%G\{12345.6}");
+ test(String.format("%10G", -12345.6), fmt."%10G\{-12345.6}");
+ test(String.format("%10G", 0.0), fmt."%10G\{0.0}");
+ test(String.format("%10G", 12345.6), fmt."%10G\{12345.6}");
+ test(String.format("%-10G", -12345.6), fmt."%-10G\{-12345.6}");
+ test(String.format("%-10G", 0.0), fmt."%-10G\{0.0}");
+ test(String.format("%-10G", 12345.6), fmt."%-10G\{12345.6}");
+ test(String.format("%,g", -12345.6), fmt."%,g\{-12345.6}");
+ test(String.format("%,g", 0.0), fmt."%,g\{0.0}");
+ test(String.format("%,g", 12345.6), fmt."%,g\{12345.6}");
+ test(String.format("%,10g", -12345.6), fmt."%,10g\{-12345.6}");
+ test(String.format("%,10g", 0.0), fmt."%,10g\{0.0}");
+ test(String.format("%,10g", 12345.6), fmt."%,10g\{12345.6}");
+ test(String.format("%,-10g", -12345.6), fmt."%,-10g\{-12345.6}");
+ test(String.format("%,-10g", 0.0), fmt."%,-10g\{0.0}");
+ test(String.format("%,-10g", 12345.6), fmt."%,-10g\{12345.6}");
+ test(String.format("%,G", -12345.6), fmt."%,G\{-12345.6}");
+ test(String.format("%,G", 0.0), fmt."%,G\{0.0}");
+ test(String.format("%,G", 12345.6), fmt."%,G\{12345.6}");
+ test(String.format("%,10G", -12345.6), fmt."%,10G\{-12345.6}");
+ test(String.format("%,10G", 0.0), fmt."%,10G\{0.0}");
+ test(String.format("%,10G", 12345.6), fmt."%,10G\{12345.6}");
+ test(String.format("%,-10G", -12345.6), fmt."%,-10G\{-12345.6}");
+ test(String.format("%,-10G", 0.0), fmt."%,-10G\{0.0}");
+ test(String.format("%,-10G", 12345.6), fmt."%,-10G\{12345.6}");
+ test(String.format("% g", -12345.6), fmt."% g\{-12345.6}");
+ test(String.format("% g", 0.0), fmt."% g\{0.0}");
+ test(String.format("% g", 12345.6), fmt."% g\{12345.6}");
+ test(String.format("% 10g", -12345.6), fmt."% 10g\{-12345.6}");
+ test(String.format("% 10g", 0.0), fmt."% 10g\{0.0}");
+ test(String.format("% 10g", 12345.6), fmt."% 10g\{12345.6}");
+ test(String.format("% -10g", -12345.6), fmt."% -10g\{-12345.6}");
+ test(String.format("% -10g", 0.0), fmt."% -10g\{0.0}");
+ test(String.format("% -10g", 12345.6), fmt."% -10g\{12345.6}");
+ test(String.format("% G", -12345.6), fmt."% G\{-12345.6}");
+ test(String.format("% G", 0.0), fmt."% G\{0.0}");
+ test(String.format("% G", 12345.6), fmt."% G\{12345.6}");
+ test(String.format("% 10G", -12345.6), fmt."% 10G\{-12345.6}");
+ test(String.format("% 10G", 0.0), fmt."% 10G\{0.0}");
+ test(String.format("% 10G", 12345.6), fmt."% 10G\{12345.6}");
+ test(String.format("% -10G", -12345.6), fmt."% -10G\{-12345.6}");
+ test(String.format("% -10G", 0.0), fmt."% -10G\{0.0}");
+ test(String.format("% -10G", 12345.6), fmt."% -10G\{12345.6}");
+ test(String.format("%, g", -12345.6), fmt."%, g\{-12345.6}");
+ test(String.format("%, g", 0.0), fmt."%, g\{0.0}");
+ test(String.format("%, g", 12345.6), fmt."%, g\{12345.6}");
+ test(String.format("%, 10g", -12345.6), fmt."%, 10g\{-12345.6}");
+ test(String.format("%, 10g", 0.0), fmt."%, 10g\{0.0}");
+ test(String.format("%, 10g", 12345.6), fmt."%, 10g\{12345.6}");
+ test(String.format("%, -10g", -12345.6), fmt."%, -10g\{-12345.6}");
+ test(String.format("%, -10g", 0.0), fmt."%, -10g\{0.0}");
+ test(String.format("%, -10g", 12345.6), fmt."%, -10g\{12345.6}");
+ test(String.format("%, G", -12345.6), fmt."%, G\{-12345.6}");
+ test(String.format("%, G", 0.0), fmt."%, G\{0.0}");
+ test(String.format("%, G", 12345.6), fmt."%, G\{12345.6}");
+ test(String.format("%, 10G", -12345.6), fmt."%, 10G\{-12345.6}");
+ test(String.format("%, 10G", 0.0), fmt."%, 10G\{0.0}");
+ test(String.format("%, 10G", 12345.6), fmt."%, 10G\{12345.6}");
+ test(String.format("%, -10G", -12345.6), fmt."%, -10G\{-12345.6}");
+ test(String.format("%, -10G", 0.0), fmt."%, -10G\{0.0}");
+ test(String.format("%, -10G", 12345.6), fmt."%, -10G\{12345.6}");
+
+ test(String.format("%g", -12345.6), fmt."%g\{-12345.6}");
+ test(String.format("%g", 0.0), fmt."%g\{0.0}");
+ test(String.format("%g", 12345.6), fmt."%g\{12345.6}");
+ test(String.format("%10g", -12345.6), fmt."%10g\{-12345.6}");
+ test(String.format("%10g", 0.0), fmt."%10g\{0.0}");
+ test(String.format("%10g", 12345.6), fmt."%10g\{12345.6}");
+ test(String.format("%-10g", -12345.6), fmt."%-10g\{-12345.6}");
+ test(String.format("%-10g", 0.0), fmt."%-10g\{0.0}");
+ test(String.format("%-10g", 12345.6), fmt."%-10g\{12345.6}");
+ test(String.format("%G", -12345.6), fmt."%G\{-12345.6}");
+ test(String.format("%G", 0.0), fmt."%G\{0.0}");
+ test(String.format("%G", 12345.6), fmt."%G\{12345.6}");
+ test(String.format("%10G", -12345.6), fmt."%10G\{-12345.6}");
+ test(String.format("%10G", 0.0), fmt."%10G\{0.0}");
+ test(String.format("%10G", 12345.6), fmt."%10G\{12345.6}");
+ test(String.format("%-10G", -12345.6), fmt."%-10G\{-12345.6}");
+ test(String.format("%-10G", 0.0), fmt."%-10G\{0.0}");
+ test(String.format("%-10G", 12345.6), fmt."%-10G\{12345.6}");
+ test(String.format("%,g", -12345.6), fmt."%,g\{-12345.6}");
+ test(String.format("%,g", 0.0), fmt."%,g\{0.0}");
+ test(String.format("%,g", 12345.6), fmt."%,g\{12345.6}");
+ test(String.format("%,10g", -12345.6), fmt."%,10g\{-12345.6}");
+ test(String.format("%,10g", 0.0), fmt."%,10g\{0.0}");
+ test(String.format("%,10g", 12345.6), fmt."%,10g\{12345.6}");
+ test(String.format("%,-10g", -12345.6), fmt."%,-10g\{-12345.6}");
+ test(String.format("%,-10g", 0.0), fmt."%,-10g\{0.0}");
+ test(String.format("%,-10g", 12345.6), fmt."%,-10g\{12345.6}");
+ test(String.format("%,G", -12345.6), fmt."%,G\{-12345.6}");
+ test(String.format("%,G", 0.0), fmt."%,G\{0.0}");
+ test(String.format("%,G", 12345.6), fmt."%,G\{12345.6}");
+ test(String.format("%,10G", -12345.6), fmt."%,10G\{-12345.6}");
+ test(String.format("%,10G", 0.0), fmt."%,10G\{0.0}");
+ test(String.format("%,10G", 12345.6), fmt."%,10G\{12345.6}");
+ test(String.format("%,-10G", -12345.6), fmt."%,-10G\{-12345.6}");
+ test(String.format("%,-10G", 0.0), fmt."%,-10G\{0.0}");
+ test(String.format("%,-10G", 12345.6), fmt."%,-10G\{12345.6}");
+ test(String.format("%+g", -12345.6), fmt."%+g\{-12345.6}");
+ test(String.format("%+g", 0.0), fmt."%+g\{0.0}");
+ test(String.format("%+g", 12345.6), fmt."%+g\{12345.6}");
+ test(String.format("%+10g", -12345.6), fmt."%+10g\{-12345.6}");
+ test(String.format("%+10g", 0.0), fmt."%+10g\{0.0}");
+ test(String.format("%+10g", 12345.6), fmt."%+10g\{12345.6}");
+ test(String.format("%+-10g", -12345.6), fmt."%+-10g\{-12345.6}");
+ test(String.format("%+-10g", 0.0), fmt."%+-10g\{0.0}");
+ test(String.format("%+-10g", 12345.6), fmt."%+-10g\{12345.6}");
+ test(String.format("%+G", -12345.6), fmt."%+G\{-12345.6}");
+ test(String.format("%+G", 0.0), fmt."%+G\{0.0}");
+ test(String.format("%+G", 12345.6), fmt."%+G\{12345.6}");
+ test(String.format("%+10G", -12345.6), fmt."%+10G\{-12345.6}");
+ test(String.format("%+10G", 0.0), fmt."%+10G\{0.0}");
+ test(String.format("%+10G", 12345.6), fmt."%+10G\{12345.6}");
+ test(String.format("%+-10G", -12345.6), fmt."%+-10G\{-12345.6}");
+ test(String.format("%+-10G", 0.0), fmt."%+-10G\{0.0}");
+ test(String.format("%+-10G", 12345.6), fmt."%+-10G\{12345.6}");
+ test(String.format("%,+g", -12345.6), fmt."%,+g\{-12345.6}");
+ test(String.format("%,+g", 0.0), fmt."%,+g\{0.0}");
+ test(String.format("%,+g", 12345.6), fmt."%,+g\{12345.6}");
+ test(String.format("%,+10g", -12345.6), fmt."%,+10g\{-12345.6}");
+ test(String.format("%,+10g", 0.0), fmt."%,+10g\{0.0}");
+ test(String.format("%,+10g", 12345.6), fmt."%,+10g\{12345.6}");
+ test(String.format("%,+-10g", -12345.6), fmt."%,+-10g\{-12345.6}");
+ test(String.format("%,+-10g", 0.0), fmt."%,+-10g\{0.0}");
+ test(String.format("%,+-10g", 12345.6), fmt."%,+-10g\{12345.6}");
+ test(String.format("%,+G", -12345.6), fmt."%,+G\{-12345.6}");
+ test(String.format("%,+G", 0.0), fmt."%,+G\{0.0}");
+ test(String.format("%,+G", 12345.6), fmt."%,+G\{12345.6}");
+ test(String.format("%,+10G", -12345.6), fmt."%,+10G\{-12345.6}");
+ test(String.format("%,+10G", 0.0), fmt."%,+10G\{0.0}");
+ test(String.format("%,+10G", 12345.6), fmt."%,+10G\{12345.6}");
+ test(String.format("%,+-10G", -12345.6), fmt."%,+-10G\{-12345.6}");
+ test(String.format("%,+-10G", 0.0), fmt."%,+-10G\{0.0}");
+ test(String.format("%,+-10G", 12345.6), fmt."%,+-10G\{12345.6}");
+
+ test(String.format("%g", -12345.6), fmt."%g\{-12345.6}");
+ test(String.format("%g", 0.0), fmt."%g\{0.0}");
+ test(String.format("%g", 12345.6), fmt."%g\{12345.6}");
+ test(String.format("%10g", -12345.6), fmt."%10g\{-12345.6}");
+ test(String.format("%10g", 0.0), fmt."%10g\{0.0}");
+ test(String.format("%10g", 12345.6), fmt."%10g\{12345.6}");
+ test(String.format("%-10g", -12345.6), fmt."%-10g\{-12345.6}");
+ test(String.format("%-10g", 0.0), fmt."%-10g\{0.0}");
+ test(String.format("%-10g", 12345.6), fmt."%-10g\{12345.6}");
+ test(String.format("%G", -12345.6), fmt."%G\{-12345.6}");
+ test(String.format("%G", 0.0), fmt."%G\{0.0}");
+ test(String.format("%G", 12345.6), fmt."%G\{12345.6}");
+ test(String.format("%10G", -12345.6), fmt."%10G\{-12345.6}");
+ test(String.format("%10G", 0.0), fmt."%10G\{0.0}");
+ test(String.format("%10G", 12345.6), fmt."%10G\{12345.6}");
+ test(String.format("%-10G", -12345.6), fmt."%-10G\{-12345.6}");
+ test(String.format("%-10G", 0.0), fmt."%-10G\{0.0}");
+ test(String.format("%-10G", 12345.6), fmt."%-10G\{12345.6}");
+ test(String.format("%,g", -12345.6), fmt."%,g\{-12345.6}");
+ test(String.format("%,g", 0.0), fmt."%,g\{0.0}");
+ test(String.format("%,g", 12345.6), fmt."%,g\{12345.6}");
+ test(String.format("%,10g", -12345.6), fmt."%,10g\{-12345.6}");
+ test(String.format("%,10g", 0.0), fmt."%,10g\{0.0}");
+ test(String.format("%,10g", 12345.6), fmt."%,10g\{12345.6}");
+ test(String.format("%,-10g", -12345.6), fmt."%,-10g\{-12345.6}");
+ test(String.format("%,-10g", 0.0), fmt."%,-10g\{0.0}");
+ test(String.format("%,-10g", 12345.6), fmt."%,-10g\{12345.6}");
+ test(String.format("%,G", -12345.6), fmt."%,G\{-12345.6}");
+ test(String.format("%,G", 0.0), fmt."%,G\{0.0}");
+ test(String.format("%,G", 12345.6), fmt."%,G\{12345.6}");
+ test(String.format("%,10G", -12345.6), fmt."%,10G\{-12345.6}");
+ test(String.format("%,10G", 0.0), fmt."%,10G\{0.0}");
+ test(String.format("%,10G", 12345.6), fmt."%,10G\{12345.6}");
+ test(String.format("%,-10G", -12345.6), fmt."%,-10G\{-12345.6}");
+ test(String.format("%,-10G", 0.0), fmt."%,-10G\{0.0}");
+ test(String.format("%,-10G", 12345.6), fmt."%,-10G\{12345.6}");
+ test(String.format("%(g", -12345.6), fmt."%(g\{-12345.6}");
+ test(String.format("%(g", 0.0), fmt."%(g\{0.0}");
+ test(String.format("%(g", 12345.6), fmt."%(g\{12345.6}");
+ test(String.format("%(10g", -12345.6), fmt."%(10g\{-12345.6}");
+ test(String.format("%(10g", 0.0), fmt."%(10g\{0.0}");
+ test(String.format("%(10g", 12345.6), fmt."%(10g\{12345.6}");
+ test(String.format("%(-10g", -12345.6), fmt."%(-10g\{-12345.6}");
+ test(String.format("%(-10g", 0.0), fmt."%(-10g\{0.0}");
+ test(String.format("%(-10g", 12345.6), fmt."%(-10g\{12345.6}");
+ test(String.format("%(G", -12345.6), fmt."%(G\{-12345.6}");
+ test(String.format("%(G", 0.0), fmt."%(G\{0.0}");
+ test(String.format("%(G", 12345.6), fmt."%(G\{12345.6}");
+ test(String.format("%(10G", -12345.6), fmt."%(10G\{-12345.6}");
+ test(String.format("%(10G", 0.0), fmt."%(10G\{0.0}");
+ test(String.format("%(10G", 12345.6), fmt."%(10G\{12345.6}");
+ test(String.format("%(-10G", -12345.6), fmt."%(-10G\{-12345.6}");
+ test(String.format("%(-10G", 0.0), fmt."%(-10G\{0.0}");
+ test(String.format("%(-10G", 12345.6), fmt."%(-10G\{12345.6}");
+ test(String.format("%,(g", -12345.6), fmt."%,(g\{-12345.6}");
+ test(String.format("%,(g", 0.0), fmt."%,(g\{0.0}");
+ test(String.format("%,(g", 12345.6), fmt."%,(g\{12345.6}");
+ test(String.format("%,(10g", -12345.6), fmt."%,(10g\{-12345.6}");
+ test(String.format("%,(10g", 0.0), fmt."%,(10g\{0.0}");
+ test(String.format("%,(10g", 12345.6), fmt."%,(10g\{12345.6}");
+ test(String.format("%,(-10g", -12345.6), fmt."%,(-10g\{-12345.6}");
+ test(String.format("%,(-10g", 0.0), fmt."%,(-10g\{0.0}");
+ test(String.format("%,(-10g", 12345.6), fmt."%,(-10g\{12345.6}");
+ test(String.format("%,(G", -12345.6), fmt."%,(G\{-12345.6}");
+ test(String.format("%,(G", 0.0), fmt."%,(G\{0.0}");
+ test(String.format("%,(G", 12345.6), fmt."%,(G\{12345.6}");
+ test(String.format("%,(10G", -12345.6), fmt."%,(10G\{-12345.6}");
+ test(String.format("%,(10G", 0.0), fmt."%,(10G\{0.0}");
+ test(String.format("%,(10G", 12345.6), fmt."%,(10G\{12345.6}");
+ test(String.format("%,(-10G", -12345.6), fmt."%,(-10G\{-12345.6}");
+ test(String.format("%,(-10G", 0.0), fmt."%,(-10G\{0.0}");
+ test(String.format("%,(-10G", 12345.6), fmt."%,(-10G\{12345.6}");
+ test(String.format("%+g", -12345.6), fmt."%+g\{-12345.6}");
+ test(String.format("%+g", 0.0), fmt."%+g\{0.0}");
+ test(String.format("%+g", 12345.6), fmt."%+g\{12345.6}");
+ test(String.format("%+10g", -12345.6), fmt."%+10g\{-12345.6}");
+ test(String.format("%+10g", 0.0), fmt."%+10g\{0.0}");
+ test(String.format("%+10g", 12345.6), fmt."%+10g\{12345.6}");
+ test(String.format("%+-10g", -12345.6), fmt."%+-10g\{-12345.6}");
+ test(String.format("%+-10g", 0.0), fmt."%+-10g\{0.0}");
+ test(String.format("%+-10g", 12345.6), fmt."%+-10g\{12345.6}");
+ test(String.format("%+G", -12345.6), fmt."%+G\{-12345.6}");
+ test(String.format("%+G", 0.0), fmt."%+G\{0.0}");
+ test(String.format("%+G", 12345.6), fmt."%+G\{12345.6}");
+ test(String.format("%+10G", -12345.6), fmt."%+10G\{-12345.6}");
+ test(String.format("%+10G", 0.0), fmt."%+10G\{0.0}");
+ test(String.format("%+10G", 12345.6), fmt."%+10G\{12345.6}");
+ test(String.format("%+-10G", -12345.6), fmt."%+-10G\{-12345.6}");
+ test(String.format("%+-10G", 0.0), fmt."%+-10G\{0.0}");
+ test(String.format("%+-10G", 12345.6), fmt."%+-10G\{12345.6}");
+ test(String.format("%,+g", -12345.6), fmt."%,+g\{-12345.6}");
+ test(String.format("%,+g", 0.0), fmt."%,+g\{0.0}");
+ test(String.format("%,+g", 12345.6), fmt."%,+g\{12345.6}");
+ test(String.format("%,+10g", -12345.6), fmt."%,+10g\{-12345.6}");
+ test(String.format("%,+10g", 0.0), fmt."%,+10g\{0.0}");
+ test(String.format("%,+10g", 12345.6), fmt."%,+10g\{12345.6}");
+ test(String.format("%,+-10g", -12345.6), fmt."%,+-10g\{-12345.6}");
+ test(String.format("%,+-10g", 0.0), fmt."%,+-10g\{0.0}");
+ test(String.format("%,+-10g", 12345.6), fmt."%,+-10g\{12345.6}");
+ test(String.format("%,+G", -12345.6), fmt."%,+G\{-12345.6}");
+ test(String.format("%,+G", 0.0), fmt."%,+G\{0.0}");
+ test(String.format("%,+G", 12345.6), fmt."%,+G\{12345.6}");
+ test(String.format("%,+10G", -12345.6), fmt."%,+10G\{-12345.6}");
+ test(String.format("%,+10G", 0.0), fmt."%,+10G\{0.0}");
+ test(String.format("%,+10G", 12345.6), fmt."%,+10G\{12345.6}");
+ test(String.format("%,+-10G", -12345.6), fmt."%,+-10G\{-12345.6}");
+ test(String.format("%,+-10G", 0.0), fmt."%,+-10G\{0.0}");
+ test(String.format("%,+-10G", 12345.6), fmt."%,+-10G\{12345.6}");
+ test(String.format("%(+g", -12345.6), fmt."%(+g\{-12345.6}");
+ test(String.format("%(+g", 0.0), fmt."%(+g\{0.0}");
+ test(String.format("%(+g", 12345.6), fmt."%(+g\{12345.6}");
+ test(String.format("%(+10g", -12345.6), fmt."%(+10g\{-12345.6}");
+ test(String.format("%(+10g", 0.0), fmt."%(+10g\{0.0}");
+ test(String.format("%(+10g", 12345.6), fmt."%(+10g\{12345.6}");
+ test(String.format("%(+-10g", -12345.6), fmt."%(+-10g\{-12345.6}");
+ test(String.format("%(+-10g", 0.0), fmt."%(+-10g\{0.0}");
+ test(String.format("%(+-10g", 12345.6), fmt."%(+-10g\{12345.6}");
+ test(String.format("%(+G", -12345.6), fmt."%(+G\{-12345.6}");
+ test(String.format("%(+G", 0.0), fmt."%(+G\{0.0}");
+ test(String.format("%(+G", 12345.6), fmt."%(+G\{12345.6}");
+ test(String.format("%(+10G", -12345.6), fmt."%(+10G\{-12345.6}");
+ test(String.format("%(+10G", 0.0), fmt."%(+10G\{0.0}");
+ test(String.format("%(+10G", 12345.6), fmt."%(+10G\{12345.6}");
+ test(String.format("%(+-10G", -12345.6), fmt."%(+-10G\{-12345.6}");
+ test(String.format("%(+-10G", 0.0), fmt."%(+-10G\{0.0}");
+ test(String.format("%(+-10G", 12345.6), fmt."%(+-10G\{12345.6}");
+ test(String.format("%,(+g", -12345.6), fmt."%,(+g\{-12345.6}");
+ test(String.format("%,(+g", 0.0), fmt."%,(+g\{0.0}");
+ test(String.format("%,(+g", 12345.6), fmt."%,(+g\{12345.6}");
+ test(String.format("%,(+10g", -12345.6), fmt."%,(+10g\{-12345.6}");
+ test(String.format("%,(+10g", 0.0), fmt."%,(+10g\{0.0}");
+ test(String.format("%,(+10g", 12345.6), fmt."%,(+10g\{12345.6}");
+ test(String.format("%,(+-10g", -12345.6), fmt."%,(+-10g\{-12345.6}");
+ test(String.format("%,(+-10g", 0.0), fmt."%,(+-10g\{0.0}");
+ test(String.format("%,(+-10g", 12345.6), fmt."%,(+-10g\{12345.6}");
+ test(String.format("%,(+G", -12345.6), fmt."%,(+G\{-12345.6}");
+ test(String.format("%,(+G", 0.0), fmt."%,(+G\{0.0}");
+ test(String.format("%,(+G", 12345.6), fmt."%,(+G\{12345.6}");
+ test(String.format("%,(+10G", -12345.6), fmt."%,(+10G\{-12345.6}");
+ test(String.format("%,(+10G", 0.0), fmt."%,(+10G\{0.0}");
+ test(String.format("%,(+10G", 12345.6), fmt."%,(+10G\{12345.6}");
+ test(String.format("%,(+-10G", -12345.6), fmt."%,(+-10G\{-12345.6}");
+ test(String.format("%,(+-10G", 0.0), fmt."%,(+-10G\{0.0}");
+ test(String.format("%,(+-10G", 12345.6), fmt."%,(+-10G\{12345.6}");
+
+ test(String.format("%a", -12345.6), fmt."%a\{-12345.6}");
+ test(String.format("%a", 0.0), fmt."%a\{0.0}");
+ test(String.format("%a", 12345.6), fmt."%a\{12345.6}");
+ test(String.format("%10a", -12345.6), fmt."%10a\{-12345.6}");
+ test(String.format("%10a", 0.0), fmt."%10a\{0.0}");
+ test(String.format("%10a", 12345.6), fmt."%10a\{12345.6}");
+ test(String.format("%-10a", -12345.6), fmt."%-10a\{-12345.6}");
+ test(String.format("%-10a", 0.0), fmt."%-10a\{0.0}");
+ test(String.format("%-10a", 12345.6), fmt."%-10a\{12345.6}");
+ test(String.format("%A", -12345.6), fmt."%A\{-12345.6}");
+ test(String.format("%A", 0.0), fmt."%A\{0.0}");
+ test(String.format("%A", 12345.6), fmt."%A\{12345.6}");
+ test(String.format("%10A", -12345.6), fmt."%10A\{-12345.6}");
+ test(String.format("%10A", 0.0), fmt."%10A\{0.0}");
+ test(String.format("%10A", 12345.6), fmt."%10A\{12345.6}");
+ test(String.format("%-10A", -12345.6), fmt."%-10A\{-12345.6}");
+ test(String.format("%-10A", 0.0), fmt."%-10A\{0.0}");
+ test(String.format("%-10A", 12345.6), fmt."%-10A\{12345.6}");
+ }
+}
diff --git a/test/jdk/java/lang/template/StringTemplateTest.java b/test/jdk/java/lang/template/StringTemplateTest.java
new file mode 100644
index 00000000000..63556f75360
--- /dev/null
+++ b/test/jdk/java/lang/template/StringTemplateTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/*
+ * @test
+ * @bug 0000000
+ * @summary Exercise runtime handing of templated strings.
+ * @enablePreview true
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.*;
+import java.util.function.Supplier;
+import javax.tools.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import static javax.tools.StandardLocation.CLASS_OUTPUT;
+import javax.tools.ToolProvider;
+
+public class StringTemplateTest {
+ enum Category{GENERAL, CHARACTER, INTEGRAL, BIG_INT, FLOATING, BIG_FLOAT, DATE};
+
+ static final String[] GENERAL = {"true", "false", "(Object)null", "STR", "BO", "BOOL", "(Boolean)null"};
+ static final String[] CHARS = {"C", "CHAR", "(Character)null"};
+ static final String[] INTS = {"L", "LONG", "I", "INT", "S", "SHORT", "BY", "BYTE", "Long.MAX_VALUE", "Long.MIN_VALUE", "(Long)null", "(Integer)null", "(Short)null", "(Byte)null"};
+ static final String[] BIGINTS = {};
+ static final String[] FLOATS = {"F", "FLOAT", "D", "DOUBLE", "Double.NEGATIVE_INFINITY", "Double.NaN", "Double.MAX_VALUE", "(Double)null", "(Float)null"};
+ static final String[] BIGFLOATS = {};
+ static final String[] DATES = {};
+
+ final Random r = new Random(1);
+
+ String randomValue(Category category) {
+ return switch (category) {
+ case GENERAL -> randomChoice(
+ GENERAL,
+ () -> randomValue(Category.CHARACTER),
+ () -> randomValue(Category.INTEGRAL),
+ () -> randomValue(Category.BIG_INT),
+ () -> randomValue(Category.FLOATING),
+ () -> randomValue(Category.BIG_FLOAT),
+ () -> randomValue(Category.DATE),
+ () -> "\"" + randomString(r.nextInt(10)) + "\"");
+ case CHARACTER -> randomChoice(
+ CHARS,
+ () -> "\'" + randomString(1) + "\'");
+ case INTEGRAL -> randomChoice(
+ INTS,
+ () -> "(byte)" + String.valueOf(r.nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE)),
+ () -> "(short)" + String.valueOf(r.nextInt(Short.MIN_VALUE, Short.MAX_VALUE)),
+ () -> String.valueOf(r.nextInt()),
+ () -> r.nextLong() + "l");
+ case BIG_INT -> randomChoice(
+ BIGINTS,
+ () -> "new java.math.BigInteger(\"" + r.nextLong() + "\")");
+ case FLOATING -> randomChoice(
+ FLOATS,
+ () -> String.valueOf(r.nextDouble()),
+ () -> r.nextFloat() + "f");
+ case BIG_FLOAT -> randomChoice(
+ BIGFLOATS,
+ () -> "new java.math.BigDecimal(" + r.nextDouble() + ")");
+ case DATE -> randomChoice(
+ DATES,
+ () -> "new java.util.Date(" + r.nextLong() + "l)",
+ () -> r.nextLong() + "l");
+ };
+ }
+
+ String randomChoice(Supplier... suppl) {
+ return suppl[r.nextInt(suppl.length)].get();
+ }
+
+ String randomChoice(String... values) {
+ return values[r.nextInt(values.length)];
+ }
+
+ String randomChoice(String[] values, Supplier... suppl) {
+ int i = r.nextInt(values.length + suppl.length);
+ return i < values.length ? values[i] : suppl[i - values.length].get();
+ }
+
+ String randomString(int length) {
+ var sb = new StringBuilder(length << 2);
+ while (length-- > 0) {
+ char ch = (char)r.nextInt(9, 128);
+ var s = switch (ch) {
+ case '\t' -> "\\t";
+ case '\'' -> "\\\'";
+ case '"' -> "\\\"";
+ case '\r' -> "\\r";
+ case '\\' -> "\\\\";
+ case '\n' -> "\\n";
+ case '\f' -> "\\f";
+ case '\b' -> "\\b";
+ default -> ch + "";
+ };
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ String randomFormat(Category category) {
+ char c;
+ return "%" + switch (category) {
+ case GENERAL -> randomWidth("-") + randomPrecision() + randomChar("bBhHsS");
+ case CHARACTER -> randomWidth("-") + randomChar("cC");
+ case INTEGRAL -> switch (c = randomChar("doxX")) {
+ case 'd' -> randomFlags("+ ,(");
+ default -> randomFlags("");
+ } + randomWidth("-0") + c;
+ case BIG_INT -> switch (c = randomChar("doxX")) {
+ case 'd' -> randomFlags("+ ,(");
+ default -> randomFlags("+ (");
+ } + randomWidth("-0") + c;
+ case FLOATING -> switch (c = randomChar("eEfaAgG")) {
+ case 'a', 'A' -> randomFlags("+ ") + randomWidth("-0");
+ case 'e', 'E' -> randomFlags("+ (") + randomWidth("-0") + randomPrecision();
+ default -> randomFlags("+ ,(") + randomWidth("-0") + randomPrecision();
+ } + c;
+ case BIG_FLOAT -> switch (c = randomChar("eEfgG")) {
+ case 'e', 'E' -> randomFlags("+ (") + randomWidth("-0") + randomPrecision();
+ default -> randomFlags("+ ,(") + randomWidth("-0") + randomPrecision();
+ } + c;
+ case DATE -> randomWidth("-") + randomChar("tT") + randomChar("BbhAaCYyjmdeRTrDFc");
+ };
+ }
+
+ String randomFlags(String flags) {
+ var sb = new StringBuilder(flags.length());
+ for (var f : flags.toCharArray()) {
+ if (r.nextBoolean() && (f != ' ' || sb.length() == 0 || sb.charAt(sb.length() - 1) != '+')) sb.append(f);
+ }
+ return sb.toString();
+ }
+
+ char randomChar(String chars) {
+ return chars.charAt(r.nextInt(chars.length()));
+ }
+
+ String randomWidth(String flags) {
+ var f = r.nextInt(flags.length() + 1);
+ return r.nextBoolean() ? (r.nextBoolean() ? flags.charAt(r.nextInt(flags.length())) : "") + String.valueOf(r.nextInt(10) + 1) : "";
+ }
+
+ String randomPrecision() {
+ return r.nextBoolean() ? '.' + String.valueOf(r.nextInt(10) + 1) : "";
+ }
+
+ public Class> compile() throws Exception {
+ var classes = new HashMap();
+ var fileManager = new ForwardingJavaFileManager(ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null)) {
+ @Override
+ public ClassLoader getClassLoader(JavaFileManager.Location location) {
+ return new ClassLoader() {
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ try {
+ return super.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ byte[] classData = classes.get(name);
+ return defineClass(name, classData, 0, classData.length);
+ }
+ }
+ };
+ }
+ @Override
+ public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location, String name, JavaFileObject.Kind kind, FileObject originatingSource) throws UnsupportedOperationException {
+ return new SimpleJavaFileObject(URI.create(name + ".class"), JavaFileObject.Kind.CLASS) {
+ @Override
+ public OutputStream openOutputStream() {
+ return new FilterOutputStream(new ByteArrayOutputStream()) {
+ @Override
+ public void close() throws IOException {
+ classes.put(name, ((ByteArrayOutputStream)out).toByteArray());
+ }
+ };
+ }
+ };
+ }
+ };
+ var source = genSource();
+// System.out.println(source);
+ if (ToolProvider.getSystemJavaCompiler().getTask(null, fileManager, null,
+ List.of("--enable-preview", "-source", String.valueOf(Runtime.version().feature())), null,
+ List.of(new SimpleJavaFileObject(URI.create("StringTemplateTest$.java"), JavaFileObject.Kind.SOURCE) {
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ return source;
+ }
+ })).call()) {
+ return fileManager.getClassLoader(CLASS_OUTPUT).loadClass("StringTemplateTest$");
+ } else {
+ throw new AssertionError("compilation failed");
+ }
+ }
+
+ String genFragments(Category c) {
+ var fragments = new LinkedList();
+ for (int i = 0; i < 1500; i++) {
+ var format = randomFormat(c);
+ var value = randomValue(c);
+ var qValue = value.replace("\\", "\\\\").replace("\"", "\\\"");
+ fragments.add(STR."test(FMT.\"\{format}\\{\{value}}\", \"\{format}\", \"\{qValue}\", \{value}, log);");
+ }
+ return String.join("\n ", fragments);
+ }
+
+ String genSource() {
+ return STR."""
+ import java.util.FormatProcessor;
+ import java.util.Locale;
+
+ public class StringTemplateTest$ {
+ static final FormatProcessor FMT = FormatProcessor.create(Locale.US);
+ static String STR = "this is static String";
+ static char C = 'c';
+ static Character CHAR = 'C';
+ static long L = -12345678910l;
+ static Long LONG = 9876543210l;
+ static int I = 42;
+ static Integer INT = -49;
+ static boolean BO = true;
+ static Boolean BOOL = false;
+ static short S = 13;
+ static Short SHORT = -17;
+ static byte BY = -3;
+ static Byte BYTE = 12;
+ static float F = 4.789f;
+ static Float FLOAT = -0.000006f;
+ static double D = 6545745.6734654563;
+ static Double DOUBLE = -4323.7645676574;
+
+ public static void run(java.util.List log) {
+ runGeneral(log);
+ runCharacter(log);
+ runIntegral(log);
+ runBigInt(log);
+ runFloating(log);
+ runBigFloat(log);
+ runDate(log);
+ }
+ public static void runGeneral(java.util.List log) {
+ \{genFragments(Category.GENERAL)}
+ }
+ public static void runCharacter(java.util.List log) {
+ \{genFragments(Category.CHARACTER)}
+ }
+ public static void runIntegral(java.util.List log) {
+ \{genFragments(Category.INTEGRAL)}
+ }
+ public static void runBigInt(java.util.List log) {
+ \{genFragments(Category.BIG_INT)}
+ }
+ public static void runFloating(java.util.List log) {
+ \{genFragments(Category.FLOATING)}
+ }
+ public static void runBigFloat(java.util.List log) {
+ \{genFragments(Category.BIG_FLOAT)}
+ }
+ public static void runDate(java.util.List log) {
+ \{genFragments(Category.DATE)}
+ }
+ static void test(String fmt, String format, String expression, Object value, java.util.List log) {
+ var formatted = String.format(java.util.Locale.US, format, value);
+ if (!fmt.equals(formatted)) {
+ log.add(" format: '%s' expression: '%s' value: '%s' expected: '%s' found: '%s'".formatted(format, expression, value, formatted, fmt));
+ }
+ }
+ }
+ """;
+ }
+
+ public static void main(String... args) throws Exception {
+ var log = new LinkedList();
+ new StringTemplateTest().compile().getMethod("run", List.class).invoke(null, log);
+ if (!log.isEmpty()) {
+ log.forEach(System.out::println);
+ throw new AssertionError(STR."failed \{log.size()} tests");
+ }
+ }
+}
diff --git a/test/langtools/jdk/jshell/CompletenessTest.java b/test/langtools/jdk/jshell/CompletenessTest.java
index 7748f1ee848..79d81965689 100644
--- a/test/langtools/jdk/jshell/CompletenessTest.java
+++ b/test/langtools/jdk/jshell/CompletenessTest.java
@@ -89,7 +89,8 @@ public class CompletenessTest extends KullaTesting {
"record.any",
"record()",
"record(1)",
- "record.length()"
+ "record.length()",
+ "\"\\{0}\""
};
static final String[] complete_with_semi = new String[] {
@@ -232,7 +233,12 @@ public class CompletenessTest extends KullaTesting {
};
static final String[] unknown = new String[] {
- "new ;"
+ "new ;",
+ "\"",
+ "\"\\",
+ "\"\\{",
+ "\"\\{0",
+ "\"\\{0}",
};
static final Map statusToCases = new HashMap<>();
@@ -369,6 +375,7 @@ public class CompletenessTest extends KullaTesting {
public void testTextBlocks() {
assertStatus("\"\"\"", DEFINITELY_INCOMPLETE, null);
assertStatus("\"\"\"broken", DEFINITELY_INCOMPLETE, null);
+ assertStatus("\"\"\"\n", DEFINITELY_INCOMPLETE, null);
assertStatus("\"\"\"\ntext", DEFINITELY_INCOMPLETE, null);
assertStatus("\"\"\"\ntext\"\"", DEFINITELY_INCOMPLETE, "\"\"\"\ntext\"\"\"");
assertStatus("\"\"\"\ntext\"\"\"", COMPLETE, "\"\"\"\ntext\"\"\"");
@@ -376,6 +383,10 @@ public class CompletenessTest extends KullaTesting {
assertStatus("\"\"\"\ntext\\\"\"\"", DEFINITELY_INCOMPLETE, null);
assertStatus("\"\"\"\ntext\\\"\"\"\\\"\"\"", DEFINITELY_INCOMPLETE, null);
assertStatus("\"\"\"\ntext\\\"\"\"\\\"\"\"\"\"\"", COMPLETE, "\"\"\"\ntext\\\"\"\"\\\"\"\"\"\"\"");
+ assertStatus("\"\"\"\n\\", DEFINITELY_INCOMPLETE, null);
+ assertStatus("\"\"\"\n\\{", DEFINITELY_INCOMPLETE, null);
+ assertStatus("\"\"\"\n\\{0", DEFINITELY_INCOMPLETE, null);
+ assertStatus("\"\"\"\n\\{0}", DEFINITELY_INCOMPLETE, null);
}
public void testMiscSource() {
diff --git a/test/langtools/tools/javac/api/TestJavacTaskScanner.java b/test/langtools/tools/javac/api/TestJavacTaskScanner.java
index e302247bd68..0d09a81cd70 100644
--- a/test/langtools/tools/javac/api/TestJavacTaskScanner.java
+++ b/test/langtools/tools/javac/api/TestJavacTaskScanner.java
@@ -103,7 +103,7 @@ public class TestJavacTaskScanner extends ToolTester {
check(numTokens, "#Tokens", 1054);
check(numParseTypeElements, "#parseTypeElements", 180);
- check(numAllMembers, "#allMembers", 52);
+ check(numAllMembers, "#allMembers", 64);
}
void check(int value, String name, int expected) {
diff --git a/test/langtools/tools/javac/diags/examples/StringTemplate.java b/test/langtools/tools/javac/diags/examples/StringTemplate.java
new file mode 100644
index 00000000000..4f6c0f89192
--- /dev/null
+++ b/test/langtools/tools/javac/diags/examples/StringTemplate.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+ // key: compiler.misc.feature.string.templates
+ // key: compiler.warn.preview.feature.use.plural
+ // options: --enable-preview -source 21 -Xlint:preview
+
+class StringTemplate {
+ String m() {
+ int x = 10, y = 20;
+ return STR."\{x} + \{y} = \{x + y}";
+ }
+}
diff --git a/test/langtools/tools/javac/diags/examples/StringTemplateNoProcessor.java b/test/langtools/tools/javac/diags/examples/StringTemplateNoProcessor.java
new file mode 100644
index 00000000000..87df7de0164
--- /dev/null
+++ b/test/langtools/tools/javac/diags/examples/StringTemplateNoProcessor.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+ // key: compiler.err.processor.missing.from.string.template.expression
+ // key: compiler.misc.feature.string.templates
+ // key: compiler.warn.preview.feature.use.plural
+ // options: --enable-preview -source 21 -Xlint:preview
+
+class StringTemplateNoProcessor {
+ String m() {
+ int x = 10, y = 20;
+ return "\{x} + \{y} = \{x + y}";
+ }
+}
diff --git a/test/langtools/tools/javac/diags/examples/StringTemplateNotProcessor.java b/test/langtools/tools/javac/diags/examples/StringTemplateNotProcessor.java
new file mode 100644
index 00000000000..1e9472c87f9
--- /dev/null
+++ b/test/langtools/tools/javac/diags/examples/StringTemplateNotProcessor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+ // key: compiler.note.preview.filename
+ // key: compiler.note.preview.recompile
+ // key: compiler.err.not.a.processor.type
+ // options: --enable-preview -source 21
+
+import java.lang.*;
+
+class StringTemplateNotProcessor {
+ String m() {
+ String processor = "";
+ int x = 10, y = 20;
+ return processor."\{x} + \{y} = \{x + y}";
+ }
+}
diff --git a/test/langtools/tools/javac/diags/examples/StringTemplateRawProcessor.java b/test/langtools/tools/javac/diags/examples/StringTemplateRawProcessor.java
new file mode 100644
index 00000000000..1d1cb41acd3
--- /dev/null
+++ b/test/langtools/tools/javac/diags/examples/StringTemplateRawProcessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+ // key: compiler.note.preview.filename
+ // key: compiler.note.preview.recompile
+ // key: compiler.misc.unexpected.ret.val
+ // key: compiler.err.prob.found.req
+ // key: compiler.err.processor.type.cannot.be.a.raw.type
+ // options: --enable-preview -source 21
+
+import java.lang.*;
+import java.lang.StringTemplate.Processor;
+
+class StringTemplateRawProcessor {
+ void m() {
+ Processor processor = ts -> ts.interpolate();
+ try {
+ int x = 10, y = 20;
+ return processor."\{x} + \{y} = \{x + y}";
+ } catch (Throwable x) {
+ throw new RuntimeException(x);
+ }
+ }
+}
+
diff --git a/test/langtools/tools/javac/diags/examples/StringTemplateUnclosedString.java b/test/langtools/tools/javac/diags/examples/StringTemplateUnclosedString.java
new file mode 100644
index 00000000000..008d0dc311e
--- /dev/null
+++ b/test/langtools/tools/javac/diags/examples/StringTemplateUnclosedString.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+ // key: compiler.note.preview.filename
+ // key: compiler.note.preview.recompile
+ // key: compiler.err.unclosed.str.lit
+ // key: compiler.err.string.template.is.not.well.formed
+ // options: --enable-preview -source 21
+
+import java.lang.*;
+
+class StringTemplateUnclosedString {
+ String m() {
+ int x = 10;
+ return STR."\{x";
+ }
+}
diff --git a/test/langtools/tools/javac/diags/examples/StringTemplateUnclosedTextBlock.java b/test/langtools/tools/javac/diags/examples/StringTemplateUnclosedTextBlock.java
new file mode 100644
index 00000000000..9c0f9c495ea
--- /dev/null
+++ b/test/langtools/tools/javac/diags/examples/StringTemplateUnclosedTextBlock.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+ // key: compiler.note.preview.filename
+ // key: compiler.note.preview.recompile
+ // key: compiler.err.unclosed.text.block
+ // key: compiler.err.text.block.template.is.not.well.formed
+ // key: compiler.err.premature.eof
+ // options: --enable-preview -source 21
+
+import java.lang.*;
+
+class StringTemplateUnclosedTextBlock {
+ String m() {
+ int x = 10;
+ return STR."""
+ aaa
+ \{x
+ """
+ ;
+ }
+}
diff --git a/test/langtools/tools/javac/parser/JavacParserTest.java b/test/langtools/tools/javac/parser/JavacParserTest.java
index 1a48d31ba88..6b4a95b4c2a 100644
--- a/test/langtools/tools/javac/parser/JavacParserTest.java
+++ b/test/langtools/tools/javac/parser/JavacParserTest.java
@@ -87,6 +87,7 @@ import com.sun.source.tree.DefaultCaseLabelTree;
import com.sun.source.tree.ModuleTree;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.api.JavacTaskPool;
+import com.sun.tools.javac.api.JavacTaskPool.Worker;
import java.util.Objects;
public class JavacParserTest extends TestCase {
@@ -1912,6 +1913,52 @@ public class JavacParserTest extends TestCase {
}.scan(cut, null);
}
+ @Test
+ void testStringTemplate1() throws IOException {
+ String code = """
+ package test;
+ public class Test {
+ Test(int a) {
+ String s = "prefix \\{a} suffix";
+ }
+ }
+ """;
+
+ JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, null,
+ null, null, Arrays.asList(new MyFileObject(code)));
+ CompilationUnitTree cut = ct.parse().iterator().next();
+ ClassTree clazz = (ClassTree) cut.getTypeDecls().get(0);
+ MethodTree constr = (MethodTree) clazz.getMembers().get(0);
+ VariableTree decl = (VariableTree) constr.getBody().getStatements().get(0);
+ SourcePositions sp = Trees.instance(ct).getSourcePositions();
+ int initStart = (int) sp.getStartPosition(cut, decl.getInitializer());
+ int initEnd = (int) sp.getEndPosition(cut, decl.getInitializer());
+ assertEquals("correct templated String span expected", code.substring(initStart, initEnd), "\"prefix \\{a} suffix\"");
+ }
+
+ @Test
+ void testStringTemplate2() throws IOException {
+ String code = """
+ package test;
+ public class Test {
+ Test(int a) {
+ String s = STR."prefix \\{a} suffix";
+ }
+ }
+ """;
+
+ JavacTaskImpl ct = (JavacTaskImpl) tool.getTask(null, fm, null,
+ null, null, Arrays.asList(new MyFileObject(code)));
+ CompilationUnitTree cut = ct.parse().iterator().next();
+ ClassTree clazz = (ClassTree) cut.getTypeDecls().get(0);
+ MethodTree constr = (MethodTree) clazz.getMembers().get(0);
+ VariableTree decl = (VariableTree) constr.getBody().getStatements().get(0);
+ SourcePositions sp = Trees.instance(ct).getSourcePositions();
+ int initStart = (int) sp.getStartPosition(cut, decl.getInitializer());
+ int initEnd = (int) sp.getEndPosition(cut, decl.getInitializer());
+ assertEquals("correct templated String span expected", code.substring(initStart, initEnd), "STR.\"prefix \\{a} suffix\"");
+ }
+
@Test //JDK-8293897
void testImplicitFinalInTryWithResources() throws IOException {
String code = """
@@ -1993,6 +2040,63 @@ public class JavacParserTest extends TestCase {
}.scan(cut, null);
}
+ @Test
+ void testIncompleteStringTemplate() throws IOException {
+ String template = "\"\\{o.toString()}\"";
+ String prefix = """
+ package t;
+ class Test {
+ void test(Object o) {
+ String s = STR.""";
+
+ Worker verifyParseable = task -> {
+ try {
+ task.parse().iterator().next();
+ return null;
+ } catch (IOException ex) {
+ throw new AssertionError(ex);
+ }
+ };
+ JavacTaskPool pool = new JavacTaskPool(1);
+ DiagnosticListener dl = d -> {};
+ List options = List.of("--enable-preview",
+ "-source", System.getProperty("java.specification.version"));
+ for (int i = 0; i < template.length(); i++) {
+ pool.getTask(null, fm, dl, options,
+ null, Arrays.asList(new MyFileObject(prefix + template.substring(0, i))),
+ verifyParseable
+ );
+ }
+ for (int i = 0; i < template.length() - 1; i++) {
+ pool.getTask(null, fm, dl, options,
+ null, Arrays.asList(new MyFileObject(prefix + template.substring(0, i) + "\"")),
+ verifyParseable);
+ }
+ String incomplete = prefix + "\"\\{o.";
+ pool.getTask(null, fm, dl, options,
+ null, Arrays.asList(new MyFileObject(incomplete)), task -> {
+ try {
+ CompilationUnitTree cut = task.parse().iterator().next();
+ String result = cut.toString().replaceAll("\\R", "\n");
+ System.out.println("RESULT\n" + result);
+ assertEquals("incorrect AST",
+ result,
+ """
+ package t;
+ \n\
+ class Test {
+ \n\
+ void test(Object o) {
+ String s = STR.;
+ }
+ }""");
+ return null;
+ } catch (IOException ex) {
+ throw new AssertionError(ex);
+ }
+ });
+ }
+
@Test //JDK-8295401
void testModuleInfoProvidesRecovery() throws IOException {
String code = """
diff --git a/test/langtools/tools/javac/template/Basic.java b/test/langtools/tools/javac/template/Basic.java
new file mode 100644
index 00000000000..d6d36de18ca
--- /dev/null
+++ b/test/langtools/tools/javac/template/Basic.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/*
+ * @test
+ * @bug 0000000
+ * @summary Exercise javac handing of templated strings.
+ * @library /tools/lib
+ * @modules jdk.compiler/com.sun.tools.javac.api
+ * jdk.compiler/com.sun.tools.javac.main
+ * @build toolbox.ToolBox toolbox.JavacTask
+ * @run main Basic
+ */
+
+
+import toolbox.JavacTask;
+import toolbox.JavaTask;
+import toolbox.Task;
+import toolbox.ToolBox;
+
+public class Basic {
+ private static ToolBox TOOLBOX = new ToolBox();
+ private static final String JAVA_VERSION = System.getProperty("java.specification.version");
+
+ public static void main(String... arg) {
+ primitivesTest();
+ missingPartsTest();
+ expressionsTest();
+ invalidExpressionsTest();
+ processorTest();
+ }
+
+ /*
+ * Primitive types test.
+ */
+ static void primitivesTest() {
+ for (String type : new String[] {
+ "byte",
+ "short",
+ "int",
+ "long",
+ "float",
+ "double"
+ }) {
+ compPass(type + " x = 10; " + type + " y = 20; StringTemplate result = RAW.\"\\{x} + \\{y} = \\{x + y}\";");
+ }
+ }
+
+ /*
+ * Missing parts test.
+ */
+ static void missingPartsTest() {
+ compFail("""
+ int x = 10;
+ StringTemplate result = RAW."\\{x";
+ """);
+ compFail("""
+ int x = 10;
+ StringTemplate result = RAW."\\{{x}";
+ """);
+ compFail("""
+ int x = 10;
+ StringTemplate result = RAW."\\{x + }";
+ """);
+ compFail("""
+ int x = 10;
+ StringTemplate result = RAW."\\{ * x }";
+ """);
+ compFail("""
+ int x = 10;
+ StringTemplate result = RAW."\\{ (x + x }";
+ """);
+ }
+
+ /*
+ * Expressions test.
+ */
+ static void expressionsTest() {
+ compPass("""
+ int x = 10;
+ int[] y = new int[] { 10, 20, 30 };
+ StringTemplate result1 = RAW."\\{x + 1}";
+ StringTemplate result2 = RAW."\\{x + x}";
+ StringTemplate result3 = RAW."\\{x - x}";
+ StringTemplate result4 = RAW."\\{x * x}";
+ StringTemplate result5 = RAW."\\{x / x}";
+ StringTemplate result6 = RAW."\\{x % x}";
+ StringTemplate result7 = RAW."\\{x + (x + x)}";
+ StringTemplate result8 = RAW."\\{y[x - 9]}";
+ StringTemplate result9 = RAW."\\{System.out}";
+ StringTemplate result10 = RAW.\"""
+ \\{ "a string" }
+ \""";
+ """);
+ compPass("""
+ StringTemplate result = RAW.\"""
+ \\{
+ new Collection() {
+ @Override public int size() { return 0; }
+ @Override public boolean isEmpty() { return false; }
+ @Override public boolean contains(Object o) { return false; }
+ @Override public Iterator iterator() { return null; }
+ @Override public Object[] toArray() { return new Object[0]; }
+ @Override public