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 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 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 m) { + removeStaleReferences(); + for (Entry 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 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 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 T[] toArray(T[] a) { return null; } + @Override public boolean add(String s) { return false; } + @Override public boolean remove(Object o) { return false; } + @Override public boolean containsAll(Collection c) { return false; } + @Override public boolean addAll(Collection c) { return false; } + @Override public boolean removeAll(Collection c) { return false; } + @Override public boolean retainAll(Collection c) { return false; } + @Override public void clear() { } + } + } + \"""; + """); + } + + /* + * Invalid expressions test. + */ + static void invalidExpressionsTest() { + compFail(""" + int x = 10; + StringTemplate result = RAW."\\{ (x == x }"; + """); + compFail(""" + int x = 10; + StringTemplate result = RAW."\\{ true ? : x - 1 }"; + """); + compFail(""" + String result = RAW."\\{ 'a }"; + """); + compFail(""" + int x = 10; + StringTemplate result = RAW."\\{ Math.min(, x - 1) }"; + """); + compFail(""" + int x = 10; + StringTemplate result = RAW."\\{ \\tx }"; + """); + } + + /* + * Processor test. + */ + static void processorTest() { + compPass(""" + int x = 10, y = 20; + String string = STR."\\{x} + \\{y} = \\{x + y}"; + """); + compFail(""" + int x = 10, y = 20; + String processor = "abc"; + String string = processor."\\{x} + \\{y} = \\{x + y}"; + """); + compFail(""" + int x = 10, y = 20; + long processor = 100; + String string = processor."\\{x} + \\{y} = \\{x + y}"; + """); + } + + /* + * Test source for successful compile. + */ + static void compPass(String code) { + String source = """ + import java.lang.*; + import java.util.*; + import static java.lang.StringTemplate.RAW; + public class TEST { + public static void main(String... arg) { + """ + + code.indent(8) + + """ + } + } + """; + String output = new JavacTask(TOOLBOX) + .sources(source) + .classpath(".") + .options("-encoding", "utf8", "--enable-preview", "-source", JAVA_VERSION) + .run() + .writeAll() + .getOutput(Task.OutputKind.DIRECT); + + if (output.contains("compiler.err")) { + throw new RuntimeException("Error detected"); + } + } + + /* + * Test source for unsuccessful compile and specific error. + */ + static void compFail(String code) { + String source = """ + import java.lang.*; + import java.util.*; + import static java.lang.StringTemplate.RAW; + public class TEST { + public static void main(String... arg) { + """ + + code.indent(8) + + """ + } + } + """; + String errors = new JavacTask(TOOLBOX) + .sources(source) + .classpath(".") + .options("-XDrawDiagnostics", "-encoding", "utf8", "--enable-preview", "-source", JAVA_VERSION) + .run(Task.Expect.FAIL) + .writeAll() + .getOutput(Task.OutputKind.DIRECT); + + if (!errors.contains("compiler.err")) { + throw new RuntimeException("No error detected"); + } + } +} diff --git a/test/langtools/tools/javac/template/TreeScannerTest.java b/test/langtools/tools/javac/template/TreeScannerTest.java new file mode 100644 index 00000000000..ffd7112ab47 --- /dev/null +++ b/test/langtools/tools/javac/template/TreeScannerTest.java @@ -0,0 +1,107 @@ +/* + * 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 Verify proper behavior of TreeScanner w.r.t. templated Strings + * @modules jdk.compiler + */ + +import java.io.*; +import java.util.*; +import javax.tools.*; +import com.sun.source.tree.*; +import com.sun.source.util.*; +import java.net.URI; +import java.net.URISyntaxException; + +public class TreeScannerTest { + private static final String JAVA_VERSION = System.getProperty("java.specification.version"); + + public static void main(String... args) throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + String code = """ + public class Test { + private void test(int a) { + String s1 = TEST."p\\{a}s"; + String s2 = "p\\{a}s"; + } + } + """; + JavacTask task = (JavacTask) compiler.getTask(null, null, null, + List.of("--enable-preview", "-source", JAVA_VERSION), null, List.of(new TestJFO(code))); + StringBuilder output = new StringBuilder(); + TreeScanner checker = new TreeScanner() { + private boolean log; + + @Override + public Void visitStringTemplate(StringTemplateTree node, Void p) { + boolean prevLog = log; + try { + log = true; + return super.visitStringTemplate(node, p); + } finally { + log = prevLog; + } + } + + @Override + public Void scan(Tree tree, Void p) { + if (log) { + output.append("("); + output.append(tree != null ? tree.getKind() : "null"); + try { + return super.scan(tree, p); + } finally { + output.append(")"); + } + } else { + return super.scan(tree, p); + } + } + + }; + + checker.scan(task.parse(), null); + + String expected = "(IDENTIFIER)(IDENTIFIER)(null)(IDENTIFIER)"; + if (!expected.equals(output.toString())) { + throw new AssertionError("expected output not found, found: " + output); + } + } + + private static final class TestJFO extends SimpleJavaFileObject { + private final String code; + + public TestJFO(String code) throws URISyntaxException, IOException { + super(new URI("mem://Test.java"), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return code; + } + + } +} diff --git a/test/langtools/tools/javac/tree/TreeKindTest.java b/test/langtools/tools/javac/tree/TreeKindTest.java index 15b4ae37bbe..626288af168 100644 --- a/test/langtools/tools/javac/tree/TreeKindTest.java +++ b/test/langtools/tools/javac/tree/TreeKindTest.java @@ -123,6 +123,10 @@ public class TreeKindTest { ok = ok & verify(k, i, i == OpensTree.class); break; + case TEMPLATE: + ok = ok & verify(k, i, i == StringTemplateTree.class); + break; + case OTHER: ok = ok & verify(k, i, i == null); break; diff --git a/test/micro/org/openjdk/bench/java/lang/StringTemplateFMT.java b/test/micro/org/openjdk/bench/java/lang/StringTemplateFMT.java new file mode 100644 index 00000000000..bd730f9ef87 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/StringTemplateFMT.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. + * + * 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 org.openjdk.bench.java.lang; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +import static java.util.FormatProcessor.FMT; + +/* + * This benchmark measures StringTemplate.FMT FormatProcessor performance; + * exactly mirroring {@link org.openjdk.bench.java.lang.StringFormat} benchmark + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 3, jvmArgsAppend = "--enable-preview") +public class StringTemplateFMT { + + public String s = "str"; + public int i = 17; + + @Benchmark + public String stringFormat() { + return FMT."%s\{s}"; + } + + @Benchmark + public String stringIntFormat() { + return FMT."%s\{s} %d\{i}"; + } + + @Benchmark + public String widthStringFormat() { + return FMT."%3s\{s}"; + } + + @Benchmark + public String widthStringIntFormat() { + return FMT."%3s\{s} %d\{i}"; + } + + @Benchmark + public String complexFormat() { + return FMT."%3s\{s} %10d\{i} %4S\{s} %04X\{i} %4S\{s} %04X\{i} %4S\{s} %04X\{i}"; + } +}