blob: 607b08841231fcb922c26909547bca3cc958b540 [file] [log] [blame]
// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.desugar.stringconcat;
import com.android.tools.r8.cf.code.CfConstString;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfInvoke;
import com.android.tools.r8.cf.code.CfInvokeDynamic;
import com.android.tools.r8.cf.code.CfLoad;
import com.android.tools.r8.cf.code.CfNew;
import com.android.tools.r8.cf.code.CfStackInstruction;
import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
import com.android.tools.r8.cf.code.CfStore;
import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexCallSite;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexItemFactory.StringBuildingMethods;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
import com.android.tools.r8.graph.DexValue;
import com.android.tools.r8.graph.DexValue.DexValueString;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
import com.android.tools.r8.ir.desugar.FreshLocalProvider;
import com.android.tools.r8.ir.desugar.LocalStackAllocator;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.IteratorUtils;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.objectweb.asm.Opcodes;
/** String concatenation desugaring rewriter. */
public class StringConcatInstructionDesugaring implements CfInstructionDesugaring {
private final DexItemFactory factory;
private final StringBuildingMethods stringBuilderMethods;
private final Map<DexType, DexMethod> paramTypeToAppendMethod = new IdentityHashMap<>();
public StringConcatInstructionDesugaring(AppView<?> appView) {
this.factory = appView.dexItemFactory();
this.stringBuilderMethods = factory.stringBuilderMethods;
// Mapping of type parameters to methods of StringBuilder.
paramTypeToAppendMethod.put(factory.booleanType, stringBuilderMethods.appendBoolean);
paramTypeToAppendMethod.put(factory.charType, stringBuilderMethods.appendChar);
paramTypeToAppendMethod.put(factory.byteType, stringBuilderMethods.appendInt);
paramTypeToAppendMethod.put(factory.shortType, stringBuilderMethods.appendInt);
paramTypeToAppendMethod.put(factory.intType, stringBuilderMethods.appendInt);
paramTypeToAppendMethod.put(factory.longType, stringBuilderMethods.appendLong);
paramTypeToAppendMethod.put(factory.floatType, stringBuilderMethods.appendFloat);
paramTypeToAppendMethod.put(factory.doubleType, stringBuilderMethods.appendDouble);
paramTypeToAppendMethod.put(factory.stringType, stringBuilderMethods.appendString);
}
@Override
public Collection<CfInstruction> desugarInstruction(
CfInstruction instruction,
FreshLocalProvider freshLocalProvider,
LocalStackAllocator localStackAllocator,
CfInstructionDesugaringEventConsumer eventConsumer,
ProgramMethod context,
MethodProcessingContext methodProcessingContext,
DexItemFactory dexItemFactory) {
if (instruction.isInvokeDynamic()) {
// We are interested in bootstrap methods StringConcatFactory::makeConcat
// and StringConcatFactory::makeConcatWthConstants, both are static.
CfInvokeDynamic invoke = instruction.asInvokeDynamic();
DexCallSite callSite = invoke.getCallSite();
if (callSite.bootstrapMethod.type.isInvokeStatic()) {
DexMethod bootstrapMethod = callSite.bootstrapMethod.asMethod();
if (bootstrapMethod == factory.stringConcatFactoryMembers.makeConcat) {
return desugarMakeConcat(invoke, freshLocalProvider, localStackAllocator);
}
if (bootstrapMethod == factory.stringConcatFactoryMembers.makeConcatWithConstants) {
return desugarMakeConcatWithConstants(
invoke, freshLocalProvider, localStackAllocator, context);
}
}
}
return null;
}
private Collection<CfInstruction> desugarMakeConcat(
CfInvokeDynamic invoke,
FreshLocalProvider freshLocalProvider,
LocalStackAllocator localStackAllocator) {
DexProto proto = invoke.getCallSite().methodProto;
DexType[] parameters = proto.parameters.values;
// Collect chunks.
ConcatBuilder builder = new ConcatBuilder();
for (DexType parameter : parameters) {
ValueType valueType = ValueType.fromDexType(parameter);
builder.addChunk(
new ArgumentChunk(
paramTypeToAppendMethod.getOrDefault(parameter, stringBuilderMethods.appendObject),
freshLocalProvider.getFreshLocal(valueType.requiredRegisters())));
}
// Desugar the instruction.
return builder.desugar(localStackAllocator);
}
private Collection<CfInstruction> desugarMakeConcatWithConstants(
CfInvokeDynamic invoke,
FreshLocalProvider freshLocalProvider,
LocalStackAllocator localStackAllocator,
ProgramMethod context) {
DexCallSite callSite = invoke.getCallSite();
DexProto proto = callSite.methodProto;
DexTypeList parameters = proto.getParameters();
List<DexValue> bootstrapArgs = callSite.bootstrapArgs;
// Get `recipe` string.
if (bootstrapArgs.isEmpty()) {
throw error(context, "bootstrap method misses `recipe` argument");
}
// Extract recipe.
DexValueString recipeValue = bootstrapArgs.get(0).asDexValueString();
if (recipeValue == null) {
throw error(context, "bootstrap method argument `recipe` must be a string");
}
String recipe = recipeValue.getValue().toString();
// Constant arguments to `recipe`.
List<DexValue> constantArguments = new ArrayList<>();
for (int i = 1; i < bootstrapArgs.size(); i++) {
constantArguments.add(bootstrapArgs.get(i));
}
// Collect chunks and patch the instruction.
ConcatBuilder builder = new ConcatBuilder();
StringBuilder acc = new StringBuilder();
int length = recipe.length();
Iterator<DexValue> constantArgumentsIterator = constantArguments.iterator();
Iterator<DexType> parameterIterator = parameters.iterator();
for (int i = 0; i < length; i++) {
char c = recipe.charAt(i);
if (c == '\u0001') {
// Reference to an argument, so we need to flush the accumulated string.
if (acc.length() > 0) {
DexString stringConstant = factory.createString(acc.toString());
builder.addChunk(
new ConstantChunk(paramTypeToAppendMethod.get(factory.stringType), stringConstant));
acc.setLength(0);
}
if (!parameterIterator.hasNext()) {
throw error(context, "too many argument references in `recipe`");
}
DexType parameter = parameterIterator.next();
ValueType valueType = ValueType.fromDexType(parameter);
builder.addChunk(
new ArgumentChunk(
paramTypeToAppendMethod.getOrDefault(parameter, stringBuilderMethods.appendObject),
freshLocalProvider.getFreshLocal(valueType.requiredRegisters())));
} else if (c == '\u0002') {
// Reference to a constant. Since it's a constant we just convert it to string and append to
// `acc`, this way we will avoid calling toString() on every call.
if (!constantArgumentsIterator.hasNext()) {
throw error(context, "too many constant references in `recipe`");
}
acc.append(convertToString(constantArgumentsIterator.next(), context));
} else {
acc.append(c);
}
}
if (parameterIterator.hasNext()) {
throw error(
context,
"too few argument references in `recipe`, "
+ "expected "
+ parameters.size()
+ ", referenced: "
+ (parameters.size() - IteratorUtils.countRemaining(parameterIterator)));
}
if (constantArgumentsIterator.hasNext()) {
throw error(
context,
"too few constant references in `recipe`, "
+ "expected "
+ constantArguments.size()
+ ", referenced: "
+ (constantArguments.size()
- IteratorUtils.countRemaining(constantArgumentsIterator)));
}
// Final part.
if (acc.length() > 0) {
DexString stringConstant = factory.createString(acc.toString());
builder.addChunk(
new ConstantChunk(paramTypeToAppendMethod.get(factory.stringType), stringConstant));
}
// Desugar the instruction.
return builder.desugar(localStackAllocator);
}
@Override
public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
return isStringConcatInvoke(instruction, factory);
}
public static boolean isStringConcatInvoke(CfInstruction instruction, DexItemFactory factory) {
CfInvokeDynamic invoke = instruction.asInvokeDynamic();
if (invoke == null) {
return false;
}
// We are interested in bootstrap methods StringConcatFactory::makeConcat
// and StringConcatFactory::makeConcatWithConstants, both are static.
DexCallSite callSite = invoke.getCallSite();
if (callSite.bootstrapMethod.type.isInvokeStatic()) {
DexMethod bootstrapMethod = callSite.bootstrapMethod.asMethod();
return bootstrapMethod == factory.stringConcatFactoryMembers.makeConcat
|| bootstrapMethod == factory.stringConcatFactoryMembers.makeConcatWithConstants;
}
return false;
}
private static String convertToString(DexValue value, ProgramMethod context) {
if (value.isDexValueString()) {
return value.asDexValueString().getValue().toString();
}
throw error(
context,
"const arg referenced from `recipe` is not supported: " + value.getClass().getName());
}
private final class ConcatBuilder {
private final List<Chunk> chunks = new ArrayList<>();
private ArgumentChunk biggestArgumentChunk = null;
private ConstantChunk firstConstantChunk = null;
private int argumentChunksStackSize = 0;
ConcatBuilder() {}
void addChunk(ArgumentChunk chunk) {
chunks.add(chunk);
argumentChunksStackSize += chunk.getValueType().requiredRegisters();
if (biggestArgumentChunk == null
|| chunk.getValueType().requiredRegisters()
> biggestArgumentChunk.getValueType().requiredRegisters()) {
biggestArgumentChunk = chunk;
}
}
void addChunk(ConstantChunk chunk) {
chunks.add(chunk);
if (firstConstantChunk == null) {
firstConstantChunk = chunk;
}
}
/**
* Patch current `invoke-custom` instruction with:
*
* <pre>
* prologue:
* | new-instance v0, StringBuilder
* | invoke-direct {v0}, void StringBuilder.<init>()
*
* populate each chunk:
* | (optional) load the constant, e.g.: const-string v1, ""
* | invoke-virtual {v0, v1}, StringBuilder StringBuilder.append([type])
*
* epilogue:
* | invoke-virtual {v0}, String StringBuilder.toString()
*
* </pre>
*/
final Collection<CfInstruction> desugar(LocalStackAllocator localStackAllocator) {
Deque<CfInstruction> replacement = new ArrayDeque<>();
for (Chunk chunk : chunks) {
if (chunk.isArgumentChunk()) {
ArgumentChunk argumentChunk = chunk.asArgumentChunk();
replacement.addFirst(
new CfStore(argumentChunk.getValueType(), argumentChunk.getVariableIndex()));
}
}
replacement.add(new CfNew(factory.stringBuilderType));
replacement.add(new CfStackInstruction(Opcode.Dup));
replacement.add(
new CfInvoke(Opcodes.INVOKESPECIAL, stringBuilderMethods.defaultConstructor, false));
for (Chunk chunk : chunks) {
if (chunk.isArgumentChunk()) {
ArgumentChunk argumentChunk = chunk.asArgumentChunk();
replacement.add(
new CfLoad(argumentChunk.getValueType(), argumentChunk.getVariableIndex()));
} else {
assert chunk.isConstantChunk();
replacement.add(new CfConstString(chunk.asConstantChunk().getStringConstant()));
}
replacement.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, chunk.method, false));
}
replacement.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, stringBuilderMethods.toString, false));
// Coming into the original invoke-dynamic instruction, we have N arguments on the stack. We
// then pop the N arguments from the stack, allocate a new-instance on the stack, and dup it,
// to initialize the instance. We then one-by-one load the arguments and call append(). We
// therefore need a local stack of size 3 if there is a wide argument, and otherwise a local
// stack of size 2.
int maxLocalStackSizeAfterStores =
2
+ BooleanUtils.intValue(
biggestArgumentChunk != null
&& biggestArgumentChunk.getValueType().requiredRegisters() == 2);
if (maxLocalStackSizeAfterStores > argumentChunksStackSize) {
localStackAllocator.allocateLocalStack(
maxLocalStackSizeAfterStores - argumentChunksStackSize);
}
return replacement;
}
}
private abstract static class Chunk {
private final DexMethod method;
Chunk(DexMethod method) {
this.method = method;
}
public DexMethod getMethod() {
return method;
}
public ValueType getValueType() {
assert method.getProto().getArity() == 1;
return ValueType.fromDexType(method.getParameter(0));
}
public boolean isArgumentChunk() {
return false;
}
public ArgumentChunk asArgumentChunk() {
return null;
}
public boolean isConstantChunk() {
return false;
}
public ConstantChunk asConstantChunk() {
return null;
}
}
private static final class ArgumentChunk extends Chunk {
private final int variableIndex;
ArgumentChunk(DexMethod method, int variableIndex) {
super(method);
this.variableIndex = variableIndex;
}
public int getVariableIndex() {
return variableIndex;
}
@Override
public boolean isArgumentChunk() {
return true;
}
@Override
public ArgumentChunk asArgumentChunk() {
return this;
}
}
private static final class ConstantChunk extends Chunk {
private final DexString stringConstant;
ConstantChunk(DexMethod method, DexString stringConstant) {
super(method);
this.stringConstant = stringConstant;
}
public DexString getStringConstant() {
return stringConstant;
}
@Override
public boolean isConstantChunk() {
return true;
}
@Override
public ConstantChunk asConstantChunk() {
return this;
}
}
private static CompilationError error(ProgramMethod context, String message) {
return new CompilationError(
"String concatenation desugaring error (method: "
+ context.toSourceString()
+ "): "
+ message);
}
}