elements = new ArrayList<>();
int cCount = 0;
int oCount = 0;
StringBuilder acc = new StringBuilder();
for (int i = 0; i < recipe.length(); i++) {
char c = recipe.charAt(i);
if (c == TAG_CONST) {
if (cCount == constants.length) {
// Not enough constants
throw constantMismatch(constants, cCount);
}
// Accumulate constant args along with any constants encoded
// into the recipe
acc.append(constants[cCount++]);
} else if (c == TAG_ARG) {
// Flush any accumulated characters into a constant
if (acc.length() > 0) {
elements.add(acc.toString());
acc.setLength(0);
}
elements.add(null);
oCount++;
} else {
// Not a special character, this is a constant embedded into
// the recipe itself.
acc.append(c);
}
}
// Flush the remaining characters as constant:
if (acc.length() > 0) {
elements.add(acc.toString());
}
if (oCount != concatType.parameterCount()) {
throw argumentMismatch(concatType, oCount);
}
if (cCount < constants.length) {
throw constantMismatch(constants, cCount);
}
return elements;
}
private static StringConcatException argumentMismatch(MethodType concatType,
int oCount) {
return new StringConcatException(
"Mismatched number of concat arguments: recipe wants " +
oCount +
" arguments, but signature provides " +
concatType.parameterCount());
}
private static StringConcatException constantMismatch(Object[] constants,
int cCount) {
return new StringConcatException(
"Mismatched number of concat constants: recipe wants " +
cCount +
" constants, but only " +
constants.length +
" are passed");
}
/**
* 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 private String constructor that accepts byte[] arrays
* without copying.
*/
private static MethodHandle generateMHInlineCopy(MethodType mt, List elements) {
// Fast-path unary concatenations
if (elements.size() == 1) {
String s0 = elements.get(0);
if (s0 == null) {
return unaryConcat(mt.parameterType(0));
} else {
return MethodHandles.insertArguments(unaryConcat(Object.class), 0, s0);
}
}
// Fast-path binary concatenations
if (elements.size() == 2) {
// Two arguments
String s0 = elements.get(0);
String s1 = elements.get(1);
if (mt.parameterCount() == 2 &&
!mt.parameterType(0).isPrimitive() &&
!mt.parameterType(1).isPrimitive() &&
s0 == null &&
s1 == null) {
return simpleConcat();
} else if (mt.parameterCount() == 1) {
// One argument, one constant
String constant;
int constIdx;
if (s1 == null) {
constant = s0;
constIdx = 0;
} else {
constant = s1;
constIdx = 1;
}
if (constant.isEmpty()) {
return unaryConcat(mt.parameterType(0));
} else if (!mt.parameterType(0).isPrimitive()) {
// Non-primitive argument
return MethodHandles.insertArguments(simpleConcat(), constIdx, constant);
}
}
// else... fall-through to slow-path
}
// 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.erase().parameterArray();
MethodHandle[] filters = null;
for (int i = 0; i < ptypes.length; i++) {
MethodHandle filter = stringifierFor(ptypes[i]);
if (filter != null) {
if (filters == null) {
filters = new MethodHandle[ptypes.length];
}
filters[i] = filter;
ptypes[i] = String.class;
}
}
// Start building the combinator tree. The tree "starts" with ()String, and "finishes"
// with the (byte[], long)String shape to invoke newString in StringConcatHelper. 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 = MethodHandles.dropArguments(newString(), 2, ptypes);
long initialLengthCoder = INITIAL_CODER;
// Mix in prependers. This happens when (byte[], long) = (storage, indexCoder) is already
// known from the combinators below. We are assembling the string backwards, so the index coded
// into indexCoder is the *ending* index.
// We need one prepender per argument, but also need to fold in constants. We do so by greedily
// create prependers that fold in surrounding constants into the argument prepender. This reduces
// the number of unique MH combinator tree shapes we'll create in an application.
String constant = null;
int pos = 0;
for (String el : elements) {
// Do the prepend, and put "new" index at index 1
if (el != null) {
// Constant element
// Eagerly update the initialLengthCoder value
initialLengthCoder = JLA.stringConcatMix(initialLengthCoder, el);
// Save the constant and fold it either into the next
// argument prepender, or into the newArray combinator
assert (constant == null);
constant = el;
} else {
// Add prepender, along with any prefix constant
mh = MethodHandles.filterArgumentsWithCombiner(
mh, 1,
prepender(constant, ptypes[pos]),
1, 0, // indexCoder, storage
2 + pos // selected argument
);
constant = null;
pos++;
}
}
// Fold in byte[] instantiation at argument 0
MethodHandle newArrayCombinator;
if (constant != null) {
// newArray variant that deals with prepending the trailing constant
//
// initialLengthCoder has been adjusted to have the correct coder
// and length already, but to avoid binding an extra variable to
// the method handle we now adjust the length to be correct for the
// first prepender above, while adjusting for the missing length of
// the constant in StringConcatHelper
initialLengthCoder -= constant.length();
newArrayCombinator = newArrayWithSuffix(constant);
} else {
newArrayCombinator = newArray();
}
mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArrayCombinator,
1 // index
);
// 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 specializations in MIXERS.
//
// The method handle shape before all mixers are combined in is:
// (long, )String = ("indexCoder", )
//
// We will bind the initialLengthCoder value to the last mixer (the one that will be
// executed first), then fold that in. This leaves the shape after all mixers are
// combined in as:
// ()String = ()
pos = -1;
MethodHandle mix = null;
for (String el : elements) {
// Constants already handled in the code above
if (el == null) {
if (pos >= 0) {
// Compute new "index" in-place using old value plus the appropriate argument.
mh = MethodHandles.filterArgumentsWithCombiner(mh, 0, mix,
0, // old-index
1 + pos // selected argument
);
}
Class> argClass = ptypes[++pos];
mix = mixer(argClass);
}
}
// Insert the initialLengthCoder value into the final mixer, then
// fold that into the base method handle
if (pos >= 0) {
mix = MethodHandles.insertArguments(mix, 0, initialLengthCoder);
mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, mix,
1 + pos // selected argument
);
} else {
// No mixer (constants only concat), insert initialLengthCoder directly
mh = MethodHandles.insertArguments(mh, 0, initialLengthCoder);
}
// The method handle shape here is ().
// Apply filters, converting the arguments:
if (filters != null) {
mh = MethodHandles.filterArguments(mh, 0, filters);
}
return mh;
}
private static MethodHandle prepender(String prefix, Class> cl) {
if (prefix == null) {
return NULL_PREPENDERS.computeIfAbsent(cl, NULL_PREPEND);
}
return MethodHandles.insertArguments(
PREPENDERS.computeIfAbsent(cl, PREPEND), 3, prefix);
}
private static MethodHandle mixer(Class> cl) {
return MIXERS.computeIfAbsent(cl, MIX);
}
// These are deliberately not lambdas to optimize startup time:
private static final Function, MethodHandle> PREPEND = new Function<>() {
@Override
public MethodHandle apply(Class> c) {
MethodHandle prepend = JLA.stringConcatHelper("prepend",
methodType(long.class, long.class, byte[].class,
Wrapper.asPrimitiveType(c), String.class));
return prepend.rebind();
}
};
private static final Function, MethodHandle> NULL_PREPEND = new Function<>() {
@Override
public MethodHandle apply(Class> c) {
return MethodHandles.insertArguments(
PREPENDERS.computeIfAbsent(c, PREPEND), 3, (String)null);
}
};
private static final Function, MethodHandle> MIX = new Function<>() {
@Override
public MethodHandle apply(Class> c) {
MethodHandle mix = JLA.stringConcatHelper("mix",
methodType(long.class, long.class, Wrapper.asPrimitiveType(c)));
return mix.rebind();
}
};
private @Stable static MethodHandle SIMPLE_CONCAT;
private static MethodHandle simpleConcat() {
MethodHandle mh = SIMPLE_CONCAT;
if (mh == null) {
MethodHandle simpleConcat = JLA.stringConcatHelper("simpleConcat",
methodType(String.class, Object.class, Object.class));
SIMPLE_CONCAT = mh = simpleConcat.rebind();
}
return mh;
}
private @Stable static MethodHandle NEW_STRING;
private static MethodHandle newString() {
MethodHandle mh = NEW_STRING;
if (mh == null) {
MethodHandle newString = JLA.stringConcatHelper("newString",
methodType(String.class, byte[].class, long.class));
NEW_STRING = mh = newString.rebind();
}
return mh;
}
private @Stable static MethodHandle NEW_ARRAY_SUFFIX;
private static MethodHandle newArrayWithSuffix(String suffix) {
MethodHandle mh = NEW_ARRAY_SUFFIX;
if (mh == null) {
MethodHandle newArrayWithSuffix = JLA.stringConcatHelper("newArrayWithSuffix",
methodType(byte[].class, String.class, long.class));
NEW_ARRAY_SUFFIX = mh = newArrayWithSuffix.rebind();
}
return MethodHandles.insertArguments(mh, 0, suffix);
}
private @Stable static MethodHandle NEW_ARRAY;
private static MethodHandle newArray() {
MethodHandle mh = NEW_ARRAY;
if (mh == null) {
NEW_ARRAY = mh =
JLA.stringConcatHelper("newArray", methodType(byte[].class, long.class));
}
return mh;
}
/**
* 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 @Stable static MethodHandle OBJECT_STRINGIFIER;
private static MethodHandle objectStringifier() {
MethodHandle mh = OBJECT_STRINGIFIER;
if (mh == null) {
OBJECT_STRINGIFIER = mh = JLA.stringConcatHelper("stringOf",
methodType(String.class, Object.class));
}
return mh;
}
private @Stable static MethodHandle FLOAT_STRINGIFIER;
private static MethodHandle floatStringifier() {
MethodHandle mh = FLOAT_STRINGIFIER;
if (mh == null) {
FLOAT_STRINGIFIER = mh =
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, float.class);
}
return mh;
}
private @Stable static MethodHandle DOUBLE_STRINGIFIER;
private static MethodHandle doubleStringifier() {
MethodHandle mh = DOUBLE_STRINGIFIER;
if (mh == null) {
DOUBLE_STRINGIFIER = mh =
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, double.class);
}
return mh;
}
private @Stable static MethodHandle INT_STRINGIFIER;
private static MethodHandle intStringifier() {
MethodHandle mh = INT_STRINGIFIER;
if (mh == null) {
INT_STRINGIFIER = mh =
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, int.class);
}
return mh;
}
private @Stable static MethodHandle LONG_STRINGIFIER;
private static MethodHandle longStringifier() {
MethodHandle mh = LONG_STRINGIFIER;
if (mh == null) {
LONG_STRINGIFIER = mh =
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, long.class);
}
return mh;
}
private @Stable static MethodHandle CHAR_STRINGIFIER;
private static MethodHandle charStringifier() {
MethodHandle mh = CHAR_STRINGIFIER;
if (mh == null) {
CHAR_STRINGIFIER = mh =
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, char.class);
}
return mh;
}
private @Stable static MethodHandle BOOLEAN_STRINGIFIER;
private static MethodHandle booleanStringifier() {
MethodHandle mh = BOOLEAN_STRINGIFIER;
if (mh == null) {
BOOLEAN_STRINGIFIER = mh =
lookupStatic(MethodHandles.publicLookup(), String.class, "valueOf", String.class, boolean.class);
}
return mh;
}
private @Stable static MethodHandle NEW_STRINGIFIER;
private static MethodHandle newStringifier() {
MethodHandle mh = NEW_STRINGIFIER;
if (mh == null) {
NEW_STRINGIFIER = mh = JLA.stringConcatHelper("newStringOf",
methodType(String.class, Object.class));
}
return mh;
}
private static MethodHandle unaryConcat(Class> cl) {
if (!cl.isPrimitive()) {
return newStringifier();
} else if (cl == int.class || cl == short.class || cl == byte.class) {
return intStringifier();
} else if (cl == long.class) {
return longStringifier();
} else if (cl == char.class) {
return charStringifier();
} else if (cl == boolean.class) {
return booleanStringifier();
} else if (cl == float.class) {
return floatStringifier();
} else if (cl == double.class) {
return doubleStringifier();
} else {
throw new InternalError("Unhandled type for unary concatenation: " + cl);
}
}
private static final ConcurrentMap, MethodHandle> PREPENDERS;
private static final ConcurrentMap, MethodHandle> NULL_PREPENDERS;
private static final ConcurrentMap, MethodHandle> MIXERS;
private static final long INITIAL_CODER;
static {
INITIAL_CODER = JLA.stringConcatInitialCoder();
PREPENDERS = new ConcurrentHashMap<>();
NULL_PREPENDERS = new ConcurrentHashMap<>();
MIXERS = 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
*/
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 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 StringConcatFactory() {
// no instantiation
}
}