--- /dev/null 2015-11-23 01:27:16.425045507 +0300 +++ new/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java 2015-11-26 01:38:41.289625128 +0300 @@ -0,0 +1,1790 @@ +/* + * Copyright (c) 2015, 2015, 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.invoke; + +import jdk.internal.org.objectweb.asm.ClassWriter; +import jdk.internal.org.objectweb.asm.Label; +import jdk.internal.org.objectweb.asm.MethodVisitor; +import jdk.internal.org.objectweb.asm.Opcodes; +import sun.misc.Unsafe; + +import java.lang.invoke.MethodHandles.Lookup; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import static jdk.internal.org.objectweb.asm.Opcodes.*; + +/** + *

Methods to facilitate the creation of String concatenation methods, that + * can be used to efficiently concatenate a known number of arguments of known + * types, possibly after type adaptation and partial evaluation of arguments. + * These methods are typically used as bootstrap methods for {@code + * invokedynamic} call sites, to support the string concatenation + * feature of the Java Programming Language. + * + *

Indirect access to the behavior specified by the provided {@code + * MethodHandle} proceeds in order through two phases: + * + *

    + *
  1. Linkage occurs when the methods in this class are invoked. + * They take as arguments a method type describing the concatenated arguments + * count and types, and optionally the String recipe, plus the + * constants that participate in the String concatenation. The details on + * accepted recipe shapes are described further below. Linkage may involve + * dynamically loading a new class that implements the expected concatenation + * behavior. The {@code CallSite} holds the {@code MethodHandle} pointing to the + * exact concatenation method. The concatenation methods may be shared among + * different {@code CallSite}s, e.g. if linkage methods produce them as pure + * functions.
  2. + * + *
  3. Invocation occurs when a generated concatenation method is + * invoked with the exact dynamic arguments. This may occur many times for a + * single concatenation method. The method referenced by the behavior {@code + * MethodHandle} is invoked with the static arguments and any additional dynamic + * arguments provided on invocation, as if by {@link MethodHandle#invoke(Object...)}.
  4. + *
+ * + *

This class provides two forms of linkage methods: a simple version + * ({@link #makeConcat(java.lang.invoke.MethodHandles.Lookup, String, + * MethodType)}) using only the dynamic arguments, and an advanced version + * ({@link #makeConcatWithConstants(java.lang.invoke.MethodHandles.Lookup, + * String, MethodType, String, Object...)} using the advanced forms of capturing + * the constant arguments. The advanced strategy can produce marginally better + * invocation bytecode, at the expense of exploding the number of shapes of + * string concatenation methods present at runtime, because those shapes would + * include constant static arguments as well. + * + * @author Aleksey Shipilev + * @author Remi Forax + * @author Peter Levart + * + * @apiNote

The recommended way to use these methods is to collect all + * concatenation arguments into dynamic arguments for the {@code invokedynamic} + * method with a bootstrap method pointing to one of the linkage methods in this + * class. A second recommended method is to move known constants from dynamic + * arguments to static arguments, produce the recipe with the order in which to + * process dynamic and static arguments. + * + *

There is a JVM limit (classfile structural constraint): no method + * can call with more than 255 slots. This limits the number of static and + * dynamic arguments one can pass to bootstrap method. Since there are potential + * concatenation strategies that use {@code MethodHandle} combinators, we need + * to reserve a few empty slots on the parameter lists to to capture the + * temporal results. This is why bootstrap methods in this factory do not accept + * more than 200 argument slots. Users requiring more than 200 argument slots in + * concatenation are expected to split the large concatenation in smaller + * expressions. + */ +public class StringConcatFactory { + + /** + * Tag used to demarcate an ordinary argument. + */ + private static final char TAG_ARG = '\u0001'; + + /** + * Tag used to demarcate a constant. + */ + private static final char TAG_CONST = '\u0002'; + + /** + * Maximum number of argument slots in String Concat call. + * + * 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. + */ + private static final int MAX_INDY_CONCAT_ARG_SLOTS = 200; + + /** + * Concatenation strategy to use. See {@link Strategy} for possible options. + * This option is controllable with -Djava.lang.invoke.stringConcat JDK option. + */ + private static final Strategy STRATEGY; + + /** + * Default strategy to use for concatenation. + */ + private static final Strategy DEFAULT_STRATEGY = Strategy.BC_SB_SIZED; + + private enum Strategy { + /** + * Bytecode generator, calling into {@link java.lang.StringBuilder}. + */ + BC_SB, + + /** + * Bytecode generator, calling into {@link java.lang.StringBuilder}; + * but trying to estimate the required storage. + */ + BC_SB_SIZED, + + /** + * Bytecode generator, calling into {@link java.lang.StringBuilder}; + * but computing the required storage exactly. + */ + BC_SB_SIZED_EXACT, + + /** + * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}. + * This strategy also tries to estimate the required storage. + */ + MH_SB_SIZED, + + /** + * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}. + * This strategy also estimate the required storage exactly. + */ + MH_SB_SIZED_EXACT, + + /** + * MethodHandle-based generator, that constructs its own byte[] array from + * the arguments. It computes the required storage exactly. + */ + MH_INLINE_SIZED_EXACT + } + + /** + * Enables debugging: this may print debugging messages, perform additional (non-neutral for performance) + * checks, etc. + */ + private static final boolean DEBUG; + + /** + * Enables caching of strategy stubs. This may improve the linkage time by reusing the generated + * code, at the expense of contaminating the profiles. + */ + private static final boolean CACHE_ENABLE; + + private static final ConcurrentMap CACHE; + + static { + // Poke the privileged block once, taking everything we need: + final Object[] values = new Object[3]; + AccessController.doPrivileged((PrivilegedAction) () -> { + values[0] = System.getProperty("java.lang.invoke.stringConcat"); + values[1] = Boolean.getBoolean("java.lang.invoke.stringConcat.cache"); + values[2] = Boolean.getBoolean("java.lang.invoke.stringConcat.debug"); + return null; + }); + + final String strategy = (String) values[0]; + CACHE_ENABLE = (Boolean) values[1]; + DEBUG = (Boolean) values[2]; + + STRATEGY = (strategy == null) ? DEFAULT_STRATEGY : Strategy.valueOf(strategy); + CACHE = CACHE_ENABLE ? new ConcurrentHashMap<>() : null; + } + + private static final class Key { + final MethodType mt; + final Recipe recipe; + + public Key(MethodType mt, Recipe recipe) { + this.mt = mt; + this.recipe = recipe; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (!mt.equals(key.mt)) return false; + if (!recipe.equals(key.recipe)) return false; + return true; + } + + @Override + public int hashCode() { + int result = mt.hashCode(); + result = 31 * result + recipe.hashCode(); + return result; + } + } + + /** + * Parses the recipe string, and produces the traversable collection of + * {@link java.lang.invoke.StringConcatFactory.RecipeElement}-s for generator + * strategies. Notably, this class parses out the constants from the recipe + * and from other static arguments. + */ + private static class Recipe { + private final List elements; + private final List elementsRev; + + public Recipe(String src, Object[] constants) { + List el = new ArrayList<>(); + + int constC = 0; + int argC = 0; + + StringBuilder acc = new StringBuilder(); + + for (int i = 0; i < src.length(); i++) { + char c = src.charAt(i); + + if (c == TAG_CONST || c == TAG_ARG) { + // Detected a special tag, flush all accumulated characters + // as a constant first: + if (acc.length() > 0) { + el.add(new RecipeElement(acc.toString())); + acc = new StringBuilder(); + } + if (c == TAG_CONST) { + Object cnst = constants[constC++]; + el.add(new RecipeElement(cnst)); + } + if (c == TAG_ARG) { + el.add(new RecipeElement(argC++)); + } + } else { + // Not a special characters, this is a constant embedded into + // the recipe itself. + acc.append(c); + } + } + + // Flush the remaining characters as constant: + if (acc.length() > 0) { + el.add(new RecipeElement(acc.toString())); + } + + elements = new ArrayList<>(el); + Collections.reverse(el); + elementsRev = el; + } + + public Collection getElements() { + return elements; + } + + public Collection getElementsReversed() { + return elementsRev; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Recipe recipe = (Recipe) o; + return elements.equals(recipe.elements); + } + + @Override + public int hashCode() { + return elements.hashCode(); + } + } + + private static class RecipeElement { + private final Object value; + private final int argPos; + private final Tag tag; + + public RecipeElement(Object cnst) { + this.value = cnst; + this.argPos = -1; + this.tag = Tag.CONST; + } + + public RecipeElement(int arg) { + this.value = null; + this.argPos = arg; + this.tag = Tag.ARG; + } + + public Object getValue() { + assert (tag == Tag.CONST); + return value; + } + + public int getArgPos() { + assert (tag == Tag.ARG); + return argPos; + } + + public Tag getTag() { + return tag; + } + } + + private enum Tag { + CONST, + ARG, + } + + /** + * Facilitates the creation of optimized String concatenation methods, that + * can be used to efficiently concatenate a known number of arguments of + * known types, possibly after type adaptation and partial evaluation of + * arguments. Typically used as a bootstrap method for {@code + * invokedynamic} call sites, to support the string concatenation + * feature of the Java Programming Language. + * + *

When the target of the {@code CallSite} returned from this method is + * invoked, it returns the result of String concatenation, taking all + * function arguments passed to the linkage method as inputs for + * concatenation. The target signature is given by {@code concatType}. + * The arguments are concatenated as per requirements stated in JLS 15.18.1 + * "String Concatenation Operator +". Notably, the inputs are converted as + * per JLS 5.1.11 "String Conversion", and combined from left to right. + * + *

Assume the linkage arguments are as follows: + * + *

    + *
  • {@code concatType}, describing the {@code CallSite} signature
  • + *
+ * + *

Then the following linkage invariants must hold: + * + *

    + *
  • The parameter types in {@code concatType} describe only the concatenation + * arguments (i.e. there is no receiver, the method is static)
  • + * + *
  • The parameter count in {@code concatType} is less than or equal to 200
  • + * + *
  • The return type in {@code concatType} is assignable from {@link java.lang.String}
  • + *
+ * + * @param lookup Represents a lookup context with the accessibility + * privileges of the caller. When used with {@code + * invokedynamic}, this is stacked automatically by the VM. + * This argument has no meaning for this linkage method. + * @param name The name of the method to implement. This name is + * arbitrary, and has no meaning for this linkage method. + * When used with {@code invokedynamic}, this is provided by + * the {@code NameAndType} of the {@code InvokeDynamic} + * structure and is stacked automatically by the VM. + * @param concatType The expected signature of the {@code CallSite}. The + * parameter types represent the types of concatenation + * arguments; the return type is always assignable from {@link + * java.lang.String}. When used with {@code invokedynamic}, + * this is provided by the {@code NameAndType} of the {@code + * InvokeDynamic} structure and is stacked automatically by + * the VM. + * @return a CallSite whose target can be used to perform String + * concatenation for the given {@code concatType} signature. + * @throws StringConcatException If any of the linkage invariants described + * here are violated + * @throws NullPointerException If any of the incoming arguments is null; + * This will never happen when a bootstrap method + * is called with invokedynamic. + */ + public static CallSite makeConcat(MethodHandles.Lookup lookup, + String name, + MethodType concatType) throws StringConcatException { + if (DEBUG) { + System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType); + } + + return doStringConcat(lookup, name, concatType, true, null); + } + + /** + * Facilitates the creation of optimized String concatenation methods, that + * can be used to efficiently concatenate a known number of arguments of + * known types, possibly after type adaptation and partial evaluation of + * arguments. Typically used as a bootstrap method for {@code + * invokedynamic} call sites, to support the string concatenation + * feature of the Java Programming Language. + * + *

When the target of the {@code CallSite} returned from this method is + * invoked, it returns the result of String concatenation, taking all + * function arguments and constants passed to the linkage method as inputs for + * concatenation. The target signature is given by {@code concatType}, and + * does not include constants. The arguments are concatenated as per requirements + * stated in JLS 15.18.1 "String Concatenation Operator +". Notably, the inputs + * are converted as per JLS 5.1.11 "String Conversion", and combined from left + * to right. + * + *

The concatenation recipe is a String description for the way to + * construct a concatenated String from the arguments and constants. The + * recipe is processed from left to right, and each character represents an + * input to concatenation. Recipe characters mean: + * + *

    + * + *
  • \1 (Unicode point 0001): an ordinary argument. This + * input is passed through dynamic argument, and is provided during the + * concatenation method invocation. This input can be null.
  • + * + *
  • \2 (Unicode point 0002): a constant. This input passed + * through static bootstrap argument. This constant can be any value + * representable in constant pool. If necessary, the factory would call + * {@code toString} to perform a one-time String conversion.
  • + * + *
  • Any other char value: a single character constant.
  • + *
+ * + *

Assume the linkage arguments are as follows: + * + *

    + *
  • {@code concatType}, describing the {@code CallSite} signature
  • + *
  • {@code recipe}, describing the String recipe
  • + *
  • {@code constants}, the vararg array of constants
  • + *
+ * + *

Then the following linkage invariants must hold: + * + *

    + *
  • The parameter types in {@code concatType} describe only the + * concatenation arguments (i.e. there is no receiver, the method + * is static)
  • + * + *
  • The parameter count in {@code concatType} is less than or equal to + * 200
  • + * + *
  • The parameter count in {@code concatType} equals to number of \1 tags + * in {@code recipe}
  • + * + *
  • The return type in {@code concatType} is assignable + * from {@link java.lang.String}, and matches the return type of the + * returned {@link MethodHandle}
  • + * + *
  • The number of elements in {@code constants} equals to number of \2 + * tags in {@code recipe}
  • + * + *
  • Every element in {@code constants} is not null.
  • + *
+ * + * @param lookup Represents a lookup context with the accessibility + * privileges of the caller. When used with {@code + * invokedynamic}, this is stacked automatically by the + * VM. This argument has no meaning for this linkage method. + * @param name The name of the method to implement. This name is + * arbitrary, and has no meaning for this linkage method. + * When used with {@code invokedynamic}, this is provided + * by the {@code NameAndType} of the {@code InvokeDynamic} + * structure and is stacked automatically by the VM. + * @param concatType The expected signature of the {@code CallSite}. The + * parameter types represent the types of concatenation + * arguments; the return type is always assignable from {@link + * java.lang.String}. When used with {@code + * invokedynamic}, this is provided by the {@code + * NameAndType} of the {@code InvokeDynamic} structure and + * is stacked automatically by the VM. + * @param recipe Concatenation recipe, described above. + * @param constants A vararg parameter representing the constants passed to + * the linkage method. + * @return a CallSite whose target can be used to perform String + * concatenation for the given {@code concatType} signature. + * @throws StringConcatException If any of the linkage invariants described + * here are violated + * @throws NullPointerException If any of the incoming arguments is null; + * This will never happen when a bootstrap method + * is called with invokedynamic. + * @apiNote Code generators have three distinct ways to process a constant + * string operand S in a string concatenation expression. First, S can be + * materialized as a reference (using ldc) and passed as an ordinary argument + * (recipe '\1'). Or, S can be stored in the constant pool and passed as a + * constant (recipe '\2') . Finally, if S contains neither of the recipe + * tag characters ('\1', '\2') then S can be interpolated into the recipe + * itself, causing its characters to be inserted into the result. + */ + public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup, + String name, + MethodType concatType, + String recipe, + Object... constants) throws StringConcatException { + if (DEBUG) { + System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType + ", {" + recipe + "}, " + Arrays.toString(constants)); + } + + return doStringConcat(lookup, name, concatType, false, recipe, constants); + } + + private static CallSite doStringConcat(MethodHandles.Lookup caller, + String name, + MethodType concatType, + boolean generateRecipe, + String recipe, + Object... constants) throws StringConcatException { + Objects.requireNonNull(caller, "Lookup is null"); + Objects.requireNonNull(name, "Name is null"); + Objects.requireNonNull(concatType, "Concat type is null"); + Objects.requireNonNull(constants, "Constants are null"); + + for (Object o : constants) { + if (o == null) { + throw new StringConcatException("Cannot accept null constants"); + } + } + + if (generateRecipe) { + // Mock the recipe to reuse the concat generator code + StringBuilder sb = new StringBuilder(); + for (int c = 0; c < concatType.parameterCount(); c++) { + sb.append(TAG_ARG); + } + recipe = sb.toString(); + } else { + Objects.requireNonNull(recipe, "Recipe is null"); + } + + int cCount = 0; + int oCount = 0; + for (int i = 0; i < recipe.length(); i++) { + char c = recipe.charAt(i); + if (c == TAG_CONST) cCount++; + if (c == TAG_ARG) oCount++; + } + + if (concatType.parameterCount() > MAX_INDY_CONCAT_ARG_SLOTS) { + throw new StringConcatException("Too many concat argument slots: " + + concatType.parameterCount() + + ", can only accept " + + MAX_INDY_CONCAT_ARG_SLOTS); + } + + if (oCount != concatType.parameterCount()) { + throw new StringConcatException( + "Mismatched number of concat arguments: recipe wants " + + oCount + + " arguments, but signature provides " + + concatType.parameterCount()); + } + + if (cCount != constants.length) { + throw new StringConcatException( + "Mismatched number of concat constants: recipe wants " + + cCount + + " constants, but only " + + constants.length + + " are passed"); + } + + if (!concatType.returnType().isAssignableFrom(String.class)) { + throw new StringConcatException( + "The return type should be compatible with String, but it is " + + concatType.returnType()); + } + + + MethodType mt = adaptType(concatType); + + Recipe rec = new Recipe(recipe, constants); + + Key key = new Key(mt, rec); + MethodHandle mh = CACHE_ENABLE ? CACHE.get(key) : null; + if (mh == null) { + try { + mh = generate(caller, mt, rec); + } catch (Throwable t) { + if (t instanceof StringConcatException) { + throw (StringConcatException)t; + } else { + throw new StringConcatException("Generator failed", t); + } + } + if (CACHE_ENABLE) { + CACHE.put(key, mh); + } + } + + return new ConstantCallSite(mh.asType(concatType)); + } + + + /** + * Adapt method type to an API we are going to use. + * + * This strips the concrete classes from the signatures, thus preventing + * class leakage when we cache the concatenation stubs. + * + * @param args actual argument types + * @return argument types the strategy is going to use + */ + private static MethodType adaptType(MethodType args) { + Class[] ptypes = args.parameterArray(); + boolean changed = false; + for (int i = 0; i < ptypes.length; i++) { + Class ptype = ptypes[i]; + if (!ptype.isPrimitive() && + ptype != String.class && + ptype != Object.class) { // truncate to Object + ptypes[i] = Object.class; + changed = true; + } + // else other primitives or String or Object (unchanged) + } + return changed + ? MethodType.methodType(args.returnType(), ptypes) + : args; + } + + private static MethodHandle generate(Lookup lookup, MethodType mt, Recipe recipe) throws Throwable { + switch (STRATEGY) { + case BC_SB: + return BytecodeStringBuilderStrategy.generate(lookup, mt, recipe, Mode.DEFAULT); + case BC_SB_SIZED: + return BytecodeStringBuilderStrategy.generate(lookup, mt, recipe, Mode.SIZED); + case BC_SB_SIZED_EXACT: + return BytecodeStringBuilderStrategy.generate(lookup, mt, recipe, Mode.SIZED_EXACT); + case MH_SB_SIZED: + return MethodHandleStringBuilderStrategy.generate(lookup, mt, recipe, Mode.SIZED); + case MH_SB_SIZED_EXACT: + return MethodHandleStringBuilderStrategy.generate(lookup, mt, recipe, Mode.SIZED_EXACT); + case MH_INLINE_SIZED_EXACT: + return MethodHandleInlineCopyStrategy.generate(lookup, mt, recipe); + default: + throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented"); + } + } + + private enum Mode { + DEFAULT(false, false), + SIZED(true, false), + SIZED_EXACT(true, true), + ; + + boolean sized, exact; + + Mode(boolean sized, boolean exact) { + this.sized = sized; + this.exact = exact; + } + + boolean isSized() { + return sized; + } + + boolean isExact() { + return exact; + } + } + + /** + * Bytecode StringBuilder strategy. + * + *

This strategy operates in three modes, gated by {@link Mode}. + * + *

{@link Strategy#BC_SB}: "bytecode StringBuilder". + * + *

This strategy spins up the bytecode that has the same StringBuilder + * chain javac would otherwise emit. This strategy uses only the public API, + * and comes as the baseline for the current JDK behavior. On other words, + * this strategy moves the javac generated bytecode to runtime. The + * generated bytecode is loaded via Unsafe.defineAnonymousClass, but with + * the caller class coming from the BSM -- in other words, the protection + * guarantees are inherited from the method where invokedynamic was + * originally called. This means, among other things, that the bytecode is + * verified for all non-JDK uses. + * + *

{@link Strategy#BC_SB_SIZED}: "bytecode StringBuilder, but + * sized". + * + *

This strategy acts similarly to {@link Strategy#BC_SB}, but it also + * tries to guess the capacity required for StringBuilder to accept all + * arguments without resizing. This strategy only makes an educated guess: + * it only guesses the space required for known types (e.g. primitives and + * Strings), but does not otherwise convert arguments. Therefore, the + * capacity estimate may be wrong, and StringBuilder may have to + * transparently resize or trim when doing the actual concatenation. While + * this does not constitute a correctness issue (in the end, that what BC_SB + * has to do anyway), this does pose a potential performance problem. + * + *

{@link Strategy#BC_SB_SIZED_EXACT}: "bytecode StringBuilder, but + * sized exactly". + * + *

This strategy improves on @link Strategy#BC_SB_SIZED}, by first + * converting all arguments to String in order to get the exact capacity + * StringBuilder should have. The conversion is done via the public + * String.valueOf and/or Object.toString methods, and does not touch any + * private String API. + */ + private static final class BytecodeStringBuilderStrategy { + static final Unsafe UNSAFE = Unsafe.getUnsafe(); + static final int CLASSFILE_VERSION = 52; + static final String NAME_FACTORY = "concat"; + static final String CLASS_NAME = "java/lang/String$Concat"; + + private BytecodeStringBuilderStrategy() { + // no instantiation + } + + private static MethodHandle generate(MethodHandles.Lookup lookup, MethodType args, Recipe recipe, Mode mode) throws Exception { + Class targetClass = lookup.lookupClass(); + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES); + + cw.visit(CLASSFILE_VERSION, + ACC_SUPER + ACC_PUBLIC + ACC_FINAL + ACC_SYNTHETIC, + CLASS_NAME, + null, + "java/lang/Object", + null + ); + + MethodVisitor mv = cw.visitMethod( + ACC_PUBLIC + ACC_STATIC + ACC_FINAL, + NAME_FACTORY, + args.toMethodDescriptorString(), + null, + null); + + mv.visitAnnotation("Ljava/lang/invoke/ForceInline;", true); + mv.visitCode(); + + Class[] arr = args.parameterArray(); + boolean[] guaranteedNonNull = new boolean[arr.length]; + + if (mode.isExact()) { + /* + In exact mode, we need to convert all arguments to their String representations, + as this allows to compute their String sizes exactly. We cannot use private + methods for primitives in here, therefore we need to convert those as well. + + We also record what arguments are guaranteed to be non-null as the result + of the conversion. String.valueOf does the null checks for us. The only + corner case to take care of is String.valueOf(Object) returning null itself. + + Also, if any conversion happened, then the slot indices in the incoming + arguments are not equal to the final local maps. The only case this may break + is when converting 2-slot long/double argument to 1-slot String. Therefore, + we get away with tracking modified offset, since no conversion can overwrite + the upcoming the argument. + */ + + int off = 0; + int modOff = 0; + for (int c = 0; c < arr.length; c++) { + Class cl = arr[c]; + if (cl == String.class) { + if (off != modOff) { + mv.visitIntInsn(getLoadOpcode(cl), off); + mv.visitIntInsn(ASTORE, modOff); + } + } else { + mv.visitIntInsn(getLoadOpcode(cl), off); + mv.visitMethodInsn( + INVOKESTATIC, + "java/lang/String", + "valueOf", + getStringValueOfDesc(cl), + false + ); + mv.visitIntInsn(ASTORE, modOff); + arr[c] = String.class; + guaranteedNonNull[c] = cl.isPrimitive(); + } + off += getParameterSize(cl); + modOff += getParameterSize(String.class); + } + } + + if (mode.isSized()) { + /* + When operating in sized mode (this includes exact mode), it makes sense to make + StringBuilder append chains look familiar to OptimizeStringConcat. For that, we + need to do null-checks early, not make the append chain shape simpler. + */ + + int off = 0; + for (RecipeElement el : recipe.getElements()) { + switch (el.getTag()) { + case CONST: { + // Guaranteed non-null, no null check required. + break; + } + case ARG: { + // Null-checks are needed only for String arguments, and when a previous stage + // did not do implicit null-checks. If an String is null, we eagerly replace it + // with "null" constant. Note, we omit Objects here, because we don't call + // .length() on them down below. + int ac = el.getArgPos(); + Class cl = arr[ac]; + if (cl == String.class && !guaranteedNonNull[ac]) { + Label l0 = new Label(); + mv.visitIntInsn(ALOAD, off); + mv.visitJumpInsn(IFNONNULL, l0); + mv.visitLdcInsn("null"); + mv.visitIntInsn(ASTORE, off); + mv.visitLabel(l0); + } + off += getParameterSize(cl); + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + } + } + + // Prepare StringBuilder instance + mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); + mv.visitInsn(DUP); + + if (mode.isSized()) { + /* + Sized mode requires us to walk through the arguments, and estimate the final length. + In exact mode, this will operate on Strings only. This code would accumulate the + final length on stack. + */ + int len = 0; + int off = 0; + + mv.visitInsn(ICONST_0); + + for (RecipeElement el : recipe.getElements()) { + switch (el.getTag()) { + case CONST: { + Object cnst = el.getValue(); + len += cnst.toString().length(); + break; + } + case ARG: { + /* + If an argument is String, then we can call .length() on it. Sized/Exact modes have + converted arguments for us. If an argument is primitive, we can provide a guess + for its String representation size. + */ + Class cl = arr[el.getArgPos()]; + if (cl == String.class) { + mv.visitIntInsn(ALOAD, off); + mv.visitMethodInsn( + INVOKEVIRTUAL, + "java/lang/String", + "length", + "()I", + false + ); + mv.visitInsn(IADD); + } else if (cl.isPrimitive()) { + len += estimateSize(cl); + } + off += getParameterSize(cl); + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + } + + // Constants have non-zero length, mix in + if (len > 0) { + iconst(mv, len); + mv.visitInsn(IADD); + } + + mv.visitMethodInsn( + INVOKESPECIAL, + "java/lang/StringBuilder", + "", + "(I)V", + false + ); + } else { + mv.visitMethodInsn( + INVOKESPECIAL, + "java/lang/StringBuilder", + "", + "()V", + false + ); + } + + // At this point, we have a blank StringBuilder on stack, fill it in with .append calls. + { + int off = 0; + for (RecipeElement el : recipe.getElements()) { + String desc; + switch (el.getTag()) { + case CONST: { + Object cnst = el.getValue(); + mv.visitLdcInsn(cnst); + desc = getSBAppendDesc(cnst.getClass()); + break; + } + case ARG: { + Class cl = arr[el.getArgPos()]; + mv.visitVarInsn(getLoadOpcode(cl), off); + off += getParameterSize(cl); + desc = getSBAppendDesc(cl); + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + mv.visitMethodInsn( + INVOKEVIRTUAL, + "java/lang/StringBuilder", + "append", + desc, + false + ); + } + } + + if (DEBUG && mode.isExact()) { + /* + Exactness checks compare the final StringBuilder.capacity() with a resulting + String.length(). If these values disagree, that means StringBuilder had to perform + storage trimming, which defeats the purpose of exact strategies. + */ + + mv.visitInsn(DUP); + + mv.visitMethodInsn( + INVOKEVIRTUAL, + "java/lang/StringBuilder", + "capacity", + "()I", + false + ); + + mv.visitIntInsn(ISTORE, 0); + + mv.visitMethodInsn( + INVOKEVIRTUAL, + "java/lang/StringBuilder", + "toString", + "()Ljava/lang/String;", + false + ); + + mv.visitInsn(DUP); + + mv.visitMethodInsn( + INVOKEVIRTUAL, + "java/lang/String", + "length", + "()I", + false + ); + + mv.visitIntInsn(ILOAD, 0); + + Label l0 = new Label(); + mv.visitJumpInsn(IF_ICMPEQ, l0); + + mv.visitTypeInsn(NEW, "java/lang/AssertionError"); + mv.visitInsn(DUP); + mv.visitLdcInsn("Failed exactness check"); + mv.visitMethodInsn(INVOKESPECIAL, + "java/lang/AssertionError", + "", + "(Ljava/lang/Object;)V", + false); + mv.visitInsn(ATHROW); + + mv.visitLabel(l0); + } else { + mv.visitMethodInsn( + INVOKEVIRTUAL, + "java/lang/StringBuilder", + "toString", + "()Ljava/lang/String;", + false + ); + } + + mv.visitInsn(ARETURN); + + mv.visitMaxs(-1, -1); + mv.visitEnd(); + cw.visitEnd(); + + final byte[] classBytes = cw.toByteArray(); + final Class innerClass = UNSAFE.defineAnonymousClass(targetClass, classBytes, null); + + try { + UNSAFE.ensureClassInitialized(innerClass); + return lookup.findStatic(innerClass, NAME_FACTORY, args); + } catch (ReflectiveOperationException e) { + throw new StringConcatException("Exception finding constructor", e); + } + } + + private static String getSBAppendDesc(Class cl) { + if (cl.isPrimitive()) { + if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) { + return "(I)Ljava/lang/StringBuilder;"; + } else if (cl == Boolean.TYPE) { + return "(Z)Ljava/lang/StringBuilder;"; + } else if (cl == Character.TYPE) { + return "(C)Ljava/lang/StringBuilder;"; + } else if (cl == Double.TYPE) { + return "(D)Ljava/lang/StringBuilder;"; + } else if (cl == Float.TYPE) { + return "(F)Ljava/lang/StringBuilder;"; + } else if (cl == Long.TYPE) { + return "(J)Ljava/lang/StringBuilder;"; + } else { + throw new IllegalStateException("Unhandled primitive StringBuilder.append: " + cl); + } + } else if (cl == String.class) { + return "(Ljava/lang/String;)Ljava/lang/StringBuilder;"; + } else { + return "(Ljava/lang/Object;)Ljava/lang/StringBuilder;"; + } + } + + private static String getStringValueOfDesc(Class cl) { + if (cl.isPrimitive()) { + if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) { + return "(I)Ljava/lang/String;"; + } else if (cl == Boolean.TYPE) { + return "(Z)Ljava/lang/String;"; + } else if (cl == Character.TYPE) { + return "(C)Ljava/lang/String;"; + } else if (cl == Double.TYPE) { + return "(D)Ljava/lang/String;"; + } else if (cl == Float.TYPE) { + return "(F)Ljava/lang/String;"; + } else if (cl == Long.TYPE) { + return "(J)Ljava/lang/String;"; + } else { + throw new IllegalStateException("Unhandled String.valueOf: " + cl); + } + } else if (cl == String.class) { + return "(Ljava/lang/String;)Ljava/lang/String;"; + } else { + return "(Ljava/lang/Object;)Ljava/lang/String;"; + } + } + + /** + * The following method is copied from + * org.objectweb.asm.commons.InstructionAdapter. Part of ASM: a very small + * and fast Java bytecode manipulation framework. + * Copyright (c) 2000-2005 INRIA, France Telecom All rights reserved. + */ + private static void iconst(MethodVisitor mv, final int cst) { + if (cst >= -1 && cst <= 5) { + mv.visitInsn(Opcodes.ICONST_0 + cst); + } else if (cst >= Byte.MIN_VALUE && cst <= Byte.MAX_VALUE) { + mv.visitIntInsn(Opcodes.BIPUSH, cst); + } else if (cst >= Short.MIN_VALUE && cst <= Short.MAX_VALUE) { + mv.visitIntInsn(Opcodes.SIPUSH, cst); + } else { + mv.visitLdcInsn(cst); + } + } + + private static int getLoadOpcode(Class c) { + if (c == Void.TYPE) { + throw new InternalError("Unexpected void type of load opcode"); + } + return ILOAD + getOpcodeOffset(c); + } + + private static int getOpcodeOffset(Class c) { + if (c.isPrimitive()) { + if (c == Long.TYPE) { + return 1; + } else if (c == Float.TYPE) { + return 2; + } else if (c == Double.TYPE) { + return 3; + } + return 0; + } else { + return 4; + } + } + + private static int getParameterSize(Class c) { + if (c == Void.TYPE) { + return 0; + } else if (c == Long.TYPE || c == Double.TYPE) { + return 2; + } + return 1; + } + } + + /** + * MethodHandle StringBuilder strategy. + * + *

This strategy operates in two modes, gated by {@link Mode}. + * + *

{@link Strategy#MH_SB_SIZED}: "MethodHandles StringBuilder, + * sized". + * + *

This strategy avoids spinning up the bytecode by building the + * computation on MethodHandle combinators. The computation is built with + * public MethodHandle APIs, resolved from a public Lookup sequence, and + * ends up calling the public StringBuilder API. Therefore, this strategy + * does not use any private API at all, even the Unsafe.defineAnonymousClass, + * since everything is handled under cover by java.lang.invoke APIs. + * + *

{@link Strategy#MH_SB_SIZED_EXACT}: "MethodHandles StringBuilder, + * sized exactly". + * + *

This strategy improves on @link Strategy#MH_SB_SIZED}, by first + * converting all arguments to String in order to get the exact capacity + * StringBuilder should have. The conversion is done via the public + * String.valueOf and/or Object.toString methods, and does not touch any + * private String API. + */ + private static final class MethodHandleStringBuilderStrategy { + + private MethodHandleStringBuilderStrategy() { + // no instantiation + } + + private static MethodHandle generate(Lookup lookup, MethodType mt, Recipe recipe, Mode mode) throws Exception { + int pc = mt.parameterCount(); + + Class[] ptypes = mt.parameterArray(); + MethodHandle[] filters = new MethodHandle[ptypes.length]; + for (int i = 0; i < ptypes.length; i++) { + MethodHandle filter; + switch (mode) { + case SIZED: + // In sized mode, we convert all references and floats/doubles + // to String: there is no specialization for different + // classes in StringBuilder API, and it will convert to + // String internally anyhow. + filter = Stringifiers.forMost(ptypes[i]); + break; + case SIZED_EXACT: + // In exact mode, we convert everything to String: + // this helps to compute the storage exactly. + filter = Stringifiers.forAny(ptypes[i]); + break; + default: + throw new StringConcatException("Not supported"); + } + if (filter != null) { + filters[i] = filter; + ptypes[i] = filter.type().returnType(); + } + } + + List> ptypesList = Arrays.asList(ptypes); + MethodHandle[] lengthers = new MethodHandle[pc]; + + // Figure out lengths: constants' lengths can be deduced on the spot. + // All reference arguments were filtered to String in the combinators below, so we can + // call the usual String.length(). Primitive values string sizes can be estimated. + int initial = 0; + for (RecipeElement el : recipe.getElements()) { + switch (el.getTag()) { + case CONST: { + Object cnst = el.getValue(); + initial += cnst.toString().length(); + break; + } + case ARG: { + final int i = el.getArgPos(); + Class type = ptypesList.get(i); + if (type.isPrimitive()) { + MethodHandle est = MethodHandles.constant(int.class, estimateSize(type)); + est = MethodHandles.dropArguments(est, 0, type); + lengthers[i] = est; + } else { + lengthers[i] = STRING_LENGTH; + } + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + } + + // Create (StringBuilder, ) shape for appending: + MethodHandle builder = MethodHandles.dropArguments(MethodHandles.identity(StringBuilder.class), 1, ptypesList); + + // Compose append calls. This is done in reverse because the application order is + // reverse as well. + for (RecipeElement el : recipe.getElementsReversed()) { + MethodHandle appender; + switch (el.getTag()) { + case CONST: { + Object constant = el.getValue(); + MethodHandle mh = appender(adaptToStringBuilder(constant.getClass())); + appender = MethodHandles.insertArguments(mh, 1, constant); + break; + } + case ARG: { + int ac = el.getArgPos(); + appender = appender(ptypesList.get(ac)); + + // Insert dummy arguments to match the prefix in the signature. + // The actual appender argument will be the ac-ith argument. + if (ac != 0) { + appender = MethodHandles.dropArguments(appender, 1, ptypesList.subList(0, ac)); + } + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + builder = MethodHandles.foldArguments(builder, appender); + } + + // Build the sub-tree that adds the sizes and produces a StringBuilder: + + // a) Start with the reducer that accepts all arguments, plus one + // slot for the initial value. Inject the initial value right away. + // This produces ()int shape: + MethodHandle sum = getReducerFor(pc + 1); + MethodHandle adder = MethodHandles.insertArguments(sum, 0, initial); + + // b) Apply lengthers to transform arguments to lengths, producing ()int + adder = MethodHandles.filterArguments(adder, 0, lengthers); + + // c) Instantiate StringBuilder ()int -> ()StringBuilder + MethodHandle newBuilder = MethodHandles.filterReturnValue(adder, NEW_STRING_BUILDER); + + // d) Fold in StringBuilder constructor, this produces ()StringBuilder + MethodHandle mh = MethodHandles.foldArguments(builder, newBuilder); + + // Convert non-primitive arguments to Strings + mh = MethodHandles.filterArguments(mh, 0, filters); + + // Convert ()StringBuilder to ()String + if (DEBUG && mode.isExact()) { + mh = MethodHandles.filterReturnValue(mh, BUILDER_TO_STRING_CHECKED); + } else { + mh = MethodHandles.filterReturnValue(mh, BUILDER_TO_STRING); + } + + return mh; + } + + private static MethodHandle getReducerFor(int cnt) { + return SUMMERS.computeIfAbsent(cnt, SUMMER); + } + + private static MethodHandle appender(Class appendType) { + MethodHandle appender = lookupVirtual(MethodHandles.publicLookup(), StringBuilder.class, "append", + StringBuilder.class, adaptToStringBuilder(appendType)); + + // appenders should return void, this would not modify the target signature during folding + MethodType nt = MethodType.methodType(void.class, StringBuilder.class, appendType); + return appender.asType(nt); + } + + private static String toStringChecked(StringBuilder sb) { + String s = sb.toString(); + if (s.length() != sb.capacity()) { + throw new AssertionError("Exactness check failed: result length = " + s.length() + ", buffer capacity = " + sb.capacity()); + } + return s; + } + + private static int sum(int v1, int v2) { + return v1 + v2; + } + + private static int sum(int v1, int v2, int v3) { + return v1 + v2 + v3; + } + + private static int sum(int v1, int v2, int v3, int v4) { + return v1 + v2 + v3 + v4; + } + + private static int sum(int v1, int v2, int v3, int v4, int v5) { + return v1 + v2 + v3 + v4 + v5; + } + + private static int sum(int v1, int v2, int v3, int v4, int v5, int v6) { + return v1 + v2 + v3 + v4 + v5 + v6; + } + + private static int sum(int v1, int v2, int v3, int v4, int v5, int v6, int v7) { + return v1 + v2 + v3 + v4 + v5 + v6 + v7; + } + + private static int sum(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8) { + return v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8; + } + + private static int sum(int initial, int[] vs) { + int sum = initial; + for (int v : vs) { + sum += v; + } + return sum; + } + + private static final ConcurrentMap SUMMERS; + + // This one is deliberately non-lambdified to optimize startup time: + private static final Function SUMMER = new Function() { + @Override + public MethodHandle apply(Integer cnt) { + if (cnt == 1) { + return MethodHandles.identity(int.class); + } else if (cnt <= 8) { + // Variable-arity collectors are not as efficient as small-count methods, + // unroll some initial sizes. + Class[] cls = new Class[cnt]; + for (int i = 0; i < cnt; i++) { + cls[i] = int.class; + } + return lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleStringBuilderStrategy.class, "sum", int.class, cls); + } else { + return lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleStringBuilderStrategy.class, "sum", int.class, int.class, int[].class) + .asCollector(int[].class, cnt - 1); + } + } + }; + + private static final MethodHandle NEW_STRING_BUILDER, STRING_LENGTH, BUILDER_TO_STRING, BUILDER_TO_STRING_CHECKED; + + static { + SUMMERS = new ConcurrentHashMap<>(); + Lookup publicLookup = MethodHandles.publicLookup(); + NEW_STRING_BUILDER = lookupConstructor(publicLookup, StringBuilder.class, int.class); + STRING_LENGTH = lookupVirtual(publicLookup, String.class, "length", int.class); + BUILDER_TO_STRING = lookupVirtual(publicLookup, StringBuilder.class, "toString", String.class); + if (DEBUG) { + BUILDER_TO_STRING_CHECKED = lookupStatic(MethodHandles.Lookup.IMPL_LOOKUP, + MethodHandleStringBuilderStrategy.class, "toStringChecked", String.class, StringBuilder.class); + } else { + BUILDER_TO_STRING_CHECKED = null; + } + } + + } + + + /** + *

{@link Strategy#MH_INLINE_SIZED_EXACT}: "MethodHandles inline, + * sized exactly". + * + *

This strategy replicates what StringBuilders are doing: it builds the + * byte[] array on its own and passes that byte[] array to String + * constructor. This strategy requires access to some private APIs in JDK, + * most notably, the read-only Integer/Long.stringSize methods that measure + * the character length of the integers, and the private String constructor + * that accepts byte[] arrays without copying. While this strategy assumes a + * particular implementation details for String, this opens the door for + * building a very optimal concatenation sequence. This is the only strategy + * that requires porting if there are private JDK changes occur. + */ + private static final class MethodHandleInlineCopyStrategy { + + private MethodHandleInlineCopyStrategy() { + // no instantiation + } + + static MethodHandle generate(Lookup lookup, MethodType mt, Recipe recipe) throws Throwable { + + // Create filters and obtain filtered parameter types. Filters would be used in the beginning + // to convert the incoming arguments into the arguments we can process (e.g. Objects -> Strings). + // The filtered argument type list is used all over in the combinators below. + Class[] ptypes = mt.parameterArray(); + MethodHandle[] filters = null; + for (int i = 0; i < ptypes.length; i++) { + MethodHandle filter = Stringifiers.forMost(ptypes[i]); + if (filter != null) { + if (filters == null) { + filters = new MethodHandle[ptypes.length]; + } + filters[i] = filter; + ptypes[i] = filter.type().returnType(); + } + } + List> ptypesList = Arrays.asList(ptypes); + + // Start building the combinator tree. The tree "starts" with ()String, and "finishes" + // with the (int, byte[], byte)String in String helper. The combinators are assembled bottom-up, + // which makes the code arguably hard to read. + + // Drop all remaining parameter types, leave only helper arguments: + MethodHandle mh; + + mh = MethodHandles.dropArguments(NEW_STRING, 2, ptypes); + mh = MethodHandles.dropArguments(mh, 0, int.class); + + // In debug mode, check that remaining index is zero. + if (DEBUG) { + mh = MethodHandles.filterArgument(mh, 0, CHECK_INDEX); + } + + // Mix in prependers. This happens when (int, byte[], byte) = (index, storage, coder) is already + // known from the combinators below. We are assembling the string backwards, so "index" is the + // *ending* index. + for (RecipeElement el : recipe.getElements()) { + MethodHandle prepender; + switch (el.getTag()) { + case CONST: { + Object cnst = el.getValue(); + prepender = MethodHandles.insertArguments(prepender(cnst.getClass()), 3, cnst); + break; + } + case ARG: { + int pos = el.getArgPos(); + prepender = selectArgument(prepender(ptypesList.get(pos)), 3, ptypesList, pos); + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + + // Remove "old" index from arguments + mh = MethodHandles.dropArguments(mh, 1, int.class); + + // Do the prepend, and put "new" index at index 0 + mh = MethodHandles.foldArguments(mh, prepender); + } + + // Prepare the argument list for prepending. The tree below would instantiate + // the storage byte[] into argument 0, so we need to swap "storage" and "index". + // The index at this point equals to "size", and resides at argument 1. + { + MethodType nmt = mh.type() + .changeParameterType(0, byte[].class) + .changeParameterType(1, int.class); + mh = MethodHandles.permuteArguments(mh, nmt, swap10(nmt.parameterCount())); + } + + // Fold in byte[] instantiation at argument 0. + MethodHandle combiner = MethodHandles.dropArguments(NEW_ARRAY, 2, ptypesList); + mh = MethodHandles.foldArguments(mh, combiner); + + // Start combining length and coder mixers. + // + // Length is easy: constant lengths can be computed on the spot, and all non-constant + // shapes have been either converted to Strings, or explicit methods for getting the + // string length out of primitives are provided. + // + // Coders are more interesting. Only Object, String and char arguments (and constants) + // can have non-Latin1 encoding. It is easier to blindly convert constants to String, + // and deduce the coder from there. Arguments would be either converted to Strings + // during the initial filtering, or handled by primitive specializations in CODER_MIXERS. + // + // The method handle shape after all length and coder mixers is: + // (int, byte, )String = ("index", "coder", ) + byte initialCoder = 0; // initial coder + int initialLen = 0; // initial length, in characters + for (RecipeElement el : recipe.getElements()) { + switch (el.getTag()) { + case CONST: { + Object constant = el.getValue(); + String s = constant.toString(); + initialCoder = (byte) coderMixer(String.class).invoke(initialCoder, s); + initialLen += s.length(); + break; + } + case ARG: { + int ac = el.getArgPos(); + + Class argClass = ptypesList.get(ac); + MethodHandle lm = selectArgument(lengthMixer(argClass), 1, ptypesList, ac); + lm = MethodHandles.dropArguments(lm, 0, byte.class); // (*) + lm = MethodHandles.dropArguments(lm, 2, byte.class); + + MethodHandle cm = selectArgument(coderMixer(argClass), 1, ptypesList, ac); + cm = MethodHandles.dropArguments(cm, 0, int.class); // (**) + + // Read this bottom up: + + // 4. Drop old index and coder, producing ("new-index", "new-coder", ) + mh = MethodHandles.dropArguments(mh, 2, int.class, byte.class); + + // 3. Compute "new-index", producing ("new-index", "new-coder", "old-index", "old-coder", ) + // Length mixer ignores both "new-coder" and "old-coder" due to dropArguments above (*) + mh = MethodHandles.foldArguments(mh, lm); + + // 2. Compute "new-coder", producing ("new-coder", "old-index", "old-coder", ) + // Coder mixer ignores the "old-index" arg due to dropArguments above (**) + mh = MethodHandles.foldArguments(mh, cm); + + // 1. The mh shape here is ("old-index", "old-coder", ) + break; + } + default: + throw new StringConcatException("Unhandled tag: " + el.getTag()); + } + } + + // Insert initial lengths and coders here. + // The method handle shape here is (). + mh = MethodHandles.insertArguments(mh, 0, initialLen, initialCoder); + + // Apply filters, converting the arguments: + if (filters != null) { + mh = MethodHandles.filterArguments(mh, 0, filters); + } + + return mh; + } + + private static int[] swap10(int count) { + int[] perm = new int[count]; + perm[0] = 1; + perm[1] = 0; + for (int i = 2; i < count; i++) { + perm[i] = i; + } + return perm; + } + + // (...prefix..., parameter[pos])R -> (...prefix..., ...parameters...)R + private static MethodHandle selectArgument(MethodHandle mh, int prefix, List> ptypes, int pos) { + if (pos == 0) { + return MethodHandles.dropArguments(mh, prefix + 1, ptypes.subList(1, ptypes.size())); + } else if (pos == ptypes.size() - 1) { + return MethodHandles.dropArguments(mh, prefix, ptypes.subList(0, ptypes.size() - 1)); + } else { // 0 < pos < ptypes.size() - 1 + return MethodHandles.dropArguments( + MethodHandles.dropArguments(mh, prefix, ptypes.subList(0, pos)), + prefix + 1 + pos, ptypes.subList(pos + 1, ptypes.size())); + } + } + + @ForceInline + private static byte[] newArray(int length, byte coder) { + return new byte[length << coder]; + } + + @ForceInline + private static int checkIndex(int index) { + if (index != 0) { + throw new AssertionError("Exactness check failed: " + index + " characters left in the buffer."); + } + return index; + } + + private static MethodHandle prepender(Class cl) { + return PREPENDERS.computeIfAbsent(cl, PREPEND); + } + + private static MethodHandle coderMixer(Class cl) { + return CODER_MIXERS.computeIfAbsent(cl, CODER_MIX); + } + + private static MethodHandle lengthMixer(Class cl) { + return LENGTH_MIXERS.computeIfAbsent(cl, LENGTH_MIX); + } + + // This one is deliberately non-lambdified to optimize startup time: + private static final Function, MethodHandle> PREPEND = new Function, MethodHandle>() { + @Override + public MethodHandle apply(Class c) { + return lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "prepend", int.class, int.class, byte[].class, byte.class, c); + } + }; + + // This one is deliberately non-lambdified to optimize startup time: + private static final Function, MethodHandle> CODER_MIX = new Function, MethodHandle>() { + @Override + public MethodHandle apply(Class c) { + return lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "mixCoder", byte.class, byte.class, c); + } + }; + + // This one is deliberately non-lambdified to optimize startup time: + private static final Function, MethodHandle> LENGTH_MIX = new Function, MethodHandle>() { + @Override + public MethodHandle apply(Class c) { + return lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "mixLen", int.class, int.class, c); + } + }; + + private static final MethodHandle NEW_STRING; + private static final MethodHandle CHECK_INDEX; + private static final MethodHandle NEW_ARRAY; + private static final ConcurrentMap, MethodHandle> PREPENDERS; + private static final ConcurrentMap, MethodHandle> LENGTH_MIXERS; + private static final ConcurrentMap, MethodHandle> CODER_MIXERS; + private static Class STRING_HELPER; + + static { + try { + STRING_HELPER = Class.forName("java.lang.StringConcatHelper"); + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + + PREPENDERS = new ConcurrentHashMap<>(); + LENGTH_MIXERS = new ConcurrentHashMap<>(); + CODER_MIXERS = new ConcurrentHashMap<>(); + + NEW_STRING = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "newString", String.class, byte[].class, byte.class); + NEW_ARRAY = lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleInlineCopyStrategy.class, "newArray", byte[].class, int.class, byte.class); + + if (DEBUG) { + CHECK_INDEX = lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleInlineCopyStrategy.class, "checkIndex", int.class, int.class); + } else { + CHECK_INDEX = null; + } + } + } + + /** + * Public gateways to public "stringify" methods. These methods have the form String apply(T obj), and normally + * delegate to {@code String.valueOf}, depending on argument's type. + */ + private static final class Stringifiers { + private Stringifiers() { + // no instantiation + } + + // This one is deliberately non-lambdified to optimize startup time: + private static final Function, MethodHandle> MOST = new Function, MethodHandle>() { + @Override + public MethodHandle apply(Class cl) { + MethodHandle mhObject = lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, Object.class); + + // We need the additional conversion here, because String.valueOf(Object) may return null. + // String conversion rules in Java state we need to produce "null" String in this case. + // It can be easily done with applying valueOf the second time. + MethodHandle mhObjectNoNulls = MethodHandles.filterReturnValue(mhObject, + mhObject.asType(MethodType.methodType(String.class, String.class))); + + if (cl == String.class) { + return mhObject; + } else if (cl == float.class) { + return lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, float.class); + } else if (cl == double.class) { + return lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, double.class); + } else if (!cl.isPrimitive()) { + return mhObjectNoNulls; + } + + return null; + } + }; + + // This one is deliberately non-lambdified to optimize startup time: + private static final Function, MethodHandle> ANY = new Function, MethodHandle>() { + @Override + public MethodHandle apply(Class cl) { + MethodHandle mh = MOST.apply(cl); + if (mh != null) { + return mh; + } + + if (cl == byte.class || cl == short.class || cl == int.class) { + return lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, int.class); + } else if (cl == boolean.class) { + return lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, boolean.class); + } else if (cl == char.class) { + return lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, char.class); + } else if (cl == long.class) { + return lookupStatic(Lookup.PUBLIC_LOOKUP, String.class, "valueOf", String.class, long.class); + } else { + throw new IllegalStateException("Unknown class: " + cl); + } + } + }; + + private static final ConcurrentMap, MethodHandle> STRINGIFIERS_MOST = new ConcurrentHashMap<>(); + private static final ConcurrentMap, MethodHandle> STRINGIFIERS_ANY = new ConcurrentHashMap<>(); + + /** + * 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 + */ + static MethodHandle forMost(Class t) { + return STRINGIFIERS_MOST.computeIfAbsent(t, MOST); + } + + /** + * Returns a stringifier for any type. Never returns null. + * + * @param t class to stringify + * @return stringifier + */ + static MethodHandle forAny(Class t) { + return STRINGIFIERS_ANY.computeIfAbsent(t, ANY); + } + } + + /* ------------------------------- Common utilities ------------------------------------ */ + + private static MethodHandle lookupStatic(Lookup lookup, Class refc, String name, Class rtype, Class... ptypes) { + try { + return lookup.findStatic(refc, name, MethodType.methodType(rtype, ptypes)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupVirtual(Lookup lookup, Class refc, String name, Class rtype, Class... ptypes) { + try { + return lookup.findVirtual(refc, name, MethodType.methodType(rtype, ptypes)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupConstructor(Lookup lookup, Class refc, Class ptypes) { + try { + return lookup.findConstructor(refc, MethodType.methodType(void.class, ptypes)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static int estimateSize(Class cl) { + if (cl == Integer.TYPE) { + return 11; // "-2147483648" + } else if (cl == Boolean.TYPE) { + return 5; // "false" + } else if (cl == Byte.TYPE) { + return 4; // "-128" + } else if (cl == Character.TYPE) { + return 1; // duh + } else if (cl == Short.TYPE) { + return 6; // "-32768" + } else if (cl == Double.TYPE) { + return 26; // apparently, no larger than this, see FloatingDecimal.BinaryToASCIIBuffer.buffer + } else if (cl == Float.TYPE) { + return 26; // apparently, no larger than this, see FloatingDecimal.BinaryToASCIIBuffer.buffer + } else if (cl == Long.TYPE) { + return 20; // "-9223372036854775808" + } else { + throw new IllegalArgumentException("Cannot estimate the size for " + cl); + } + } + + private static Class adaptToStringBuilder(Class c) { + if (c.isPrimitive()) { + if (c == Byte.TYPE || c == Short.TYPE) { + return int.class; + } + } else { + if (c != String.class) { + return Object.class; + } + } + return c; + } + + private StringConcatFactory() { + // no instantiation + } + +}