| // Copyright (c) 2022, 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.optimize.string; |
| |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.ir.analysis.type.TypeElement; |
| import com.android.tools.r8.ir.code.IRCode; |
| import com.android.tools.r8.ir.code.Instruction; |
| import com.android.tools.r8.ir.code.InstructionListIterator; |
| import com.android.tools.r8.ir.code.InvokeDirect; |
| import com.android.tools.r8.ir.code.InvokeMethodWithReceiver; |
| import com.android.tools.r8.ir.code.InvokeVirtual; |
| import com.android.tools.r8.ir.code.Value; |
| import com.google.common.collect.ImmutableList; |
| |
| /** StringBuilderAction defines an interface for updating the IR code based on optimizations. */ |
| public interface StringBuilderAction { |
| |
| void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle); |
| |
| default boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() { |
| return false; |
| } |
| |
| default boolean isReplaceArgumentByStringConcat() { |
| return false; |
| } |
| |
| default ReplaceArgumentByStringConcat asReplaceArgumentByStringConcat() { |
| return null; |
| } |
| |
| /** The RemoveStringBuilderAction will simply remove the instruction completely. */ |
| class RemoveStringBuilderAction implements StringBuilderAction { |
| |
| private static final RemoveStringBuilderAction INSTANCE = new RemoveStringBuilderAction(); |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| removeStringBuilderInstruction(iterator, instruction, oracle); |
| } |
| |
| static RemoveStringBuilderAction getInstance() { |
| return INSTANCE; |
| } |
| |
| @Override |
| public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() { |
| return true; |
| } |
| } |
| |
| /** |
| * ReplaceByConstantString will replace a toString() call on StringBuilder with a constant string. |
| */ |
| class ReplaceByConstantString implements StringBuilderAction { |
| |
| private final String replacement; |
| |
| ReplaceByConstantString(String replacement) { |
| this.replacement = replacement; |
| } |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| assert oracle.isToString(instruction, instruction.getFirstOperand()); |
| iterator.replaceCurrentInstructionWithConstString(appView, code, replacement); |
| } |
| } |
| |
| /** |
| * AppendWithNewConstantString will change the current instruction to be an append with a constant |
| * string. If the current instruction is init the instruction will be changed to an init taking a |
| * string as argument. |
| */ |
| class AppendWithNewConstantString implements StringBuilderAction { |
| |
| private final String replacement; |
| |
| AppendWithNewConstantString(String replacement) { |
| this.replacement = replacement; |
| } |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| Instruction previous = iterator.previous(); |
| InvokeMethodWithReceiver invoke = previous.asInvokeMethodWithReceiver(); |
| assert invoke != null; |
| Value value = insertStringConstantInstruction(appView, code, iterator, invoke, replacement); |
| iterator.next(); |
| DexMethod invokedMethod = invoke.getInvokedMethod(); |
| if (invoke.isInvokeConstructor(appView.dexItemFactory())) { |
| iterator.replaceCurrentInstruction( |
| InvokeDirect.builder() |
| .setArguments(ImmutableList.of(invoke.getReceiver(), value)) |
| .setMethod( |
| getConstructorWithStringParameter(invokedMethod, appView.dexItemFactory())) |
| .setOutValue(invoke.outValue()) |
| .build()); |
| } else if (!isAppendWithString(invokedMethod, appView.dexItemFactory())) { |
| iterator.replaceCurrentInstruction( |
| InvokeVirtual.builder() |
| .setArguments(ImmutableList.of(invoke.getReceiver(), value)) |
| .setMethod(getAppendWithStringParameter(invokedMethod, appView.dexItemFactory())) |
| .setOutValue(invoke.outValue()) |
| .build()); |
| } else { |
| invoke.replaceValue(1, value); |
| } |
| } |
| |
| @Override |
| public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() { |
| return true; |
| } |
| |
| private boolean isAppendWithString(DexMethod method, DexItemFactory factory) { |
| return factory.stringBufferMethods.isAppendStringMethod(method) |
| || factory.stringBuilderMethods.isAppendStringMethod(method); |
| } |
| |
| private DexMethod getAppendWithStringParameter( |
| DexMethod invokedMethod, DexItemFactory factory) { |
| if (invokedMethod.getHolderType() == factory.stringBufferType) { |
| return factory.stringBufferMethods.appendString; |
| } else { |
| assert invokedMethod.getHolderType() == factory.stringBuilderType; |
| return factory.stringBuilderMethods.appendString; |
| } |
| } |
| } |
| |
| class ReplaceByExistingString implements StringBuilderAction { |
| |
| private final Value existingString; |
| |
| public ReplaceByExistingString(Value existingString) { |
| this.existingString = existingString; |
| } |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| instruction.outValue().replaceUsers(existingString); |
| iterator.removeOrReplaceByDebugLocalRead(); |
| } |
| |
| @Override |
| public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() { |
| return true; |
| } |
| } |
| |
| class ReplaceByStringConcat implements StringBuilderAction { |
| |
| private final Value first; |
| private final Value second; |
| |
| private final String newConstant; |
| |
| private ReplaceByStringConcat(Value first, Value second, String newConstant) { |
| assert first != null || newConstant != null; |
| assert second != null || newConstant != null; |
| this.first = first; |
| this.second = second; |
| this.newConstant = newConstant; |
| } |
| |
| public static ReplaceByStringConcat replaceByValues(Value first, Value second) { |
| return new ReplaceByStringConcat(first, second, null); |
| } |
| |
| public static ReplaceByStringConcat replaceByNewConstantConcatValue( |
| String newConstant, Value second) { |
| return new ReplaceByStringConcat(null, second, newConstant); |
| } |
| |
| public static ReplaceByStringConcat replaceByValueConcatNewConstant( |
| Value first, String newConstant) { |
| return new ReplaceByStringConcat(first, null, newConstant); |
| } |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| Value constString = null; |
| if (newConstant != null) { |
| Instruction previous = iterator.previous(); |
| constString = |
| insertStringConstantInstruction(appView, code, iterator, previous, newConstant); |
| iterator.next(); |
| } |
| assert first != null || constString != null; |
| assert second != null || constString != null; |
| // To ensure that we do not fail narrowing when evaluating String.concat, we mark the type |
| // as maybe null. |
| iterator.replaceCurrentInstruction( |
| InvokeVirtual.builder() |
| .setFreshOutValue( |
| code, TypeElement.stringClassType(appView), instruction.getLocalInfo()) |
| .setMethod(appView.dexItemFactory().stringMembers.concat) |
| .setArguments( |
| ImmutableList.of( |
| first != null ? first : constString, second != null ? second : constString)) |
| .build()); |
| } |
| } |
| |
| class ReplaceArgumentByExistingString implements StringBuilderAction { |
| |
| private final Value string; |
| |
| public ReplaceArgumentByExistingString(Value string) { |
| this.string = string; |
| } |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| instruction.replaceValue(1, string); |
| } |
| |
| @Override |
| public boolean isAllowedToBeOverwrittenByRemoveStringBuilderAction() { |
| return true; |
| } |
| } |
| |
| class ReplaceArgumentByStringConcat implements StringBuilderAction { |
| |
| private final Value first; |
| private final Value second; |
| private final String newConstant; |
| private final Value outValue; |
| private boolean removeInstruction; |
| |
| private ReplaceArgumentByStringConcat( |
| Value first, Value second, String newConstant, Value outValue) { |
| assert first != null || newConstant != null; |
| assert second != null || newConstant != null; |
| this.first = first; |
| this.second = second; |
| this.newConstant = newConstant; |
| this.outValue = outValue; |
| } |
| |
| public static ReplaceArgumentByStringConcat replaceByValues( |
| Value first, Value second, Value outValue) { |
| return new ReplaceArgumentByStringConcat(first, second, null, outValue); |
| } |
| |
| public static ReplaceArgumentByStringConcat replaceByNewConstantConcatValue( |
| String newConstant, Value second, Value outValue) { |
| return new ReplaceArgumentByStringConcat(null, second, newConstant, outValue); |
| } |
| |
| public static ReplaceArgumentByStringConcat replaceByValueConcatNewConstant( |
| Value first, String newConstant, Value outValue) { |
| return new ReplaceArgumentByStringConcat(first, null, newConstant, outValue); |
| } |
| |
| public void setRemoveInstruction() { |
| removeInstruction = true; |
| } |
| |
| @Override |
| public void perform( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| StringBuilderOracle oracle) { |
| assert instruction.isInvokeMethod(); |
| assert instruction.inValues().size() == 2; |
| Instruction previous = iterator.previous(); |
| assert previous == instruction; |
| Value constString = null; |
| if (newConstant != null) { |
| constString = |
| insertStringConstantInstruction(appView, code, iterator, previous, newConstant); |
| } |
| assert first != null || constString != null; |
| assert second != null || constString != null; |
| InvokeVirtual stringConcat = |
| InvokeVirtual.builder() |
| .setMethod(appView.dexItemFactory().stringMembers.concat) |
| .setOutValue(outValue) |
| .setArguments( |
| ImmutableList.of( |
| first != null ? first : constString, second != null ? second : constString)) |
| .setPosition(instruction.getPosition()) |
| .build(); |
| iterator.add(stringConcat); |
| Instruction next = iterator.next(); |
| assert next == instruction; |
| if (removeInstruction) { |
| removeStringBuilderInstruction(iterator, instruction, oracle); |
| } else { |
| instruction.replaceValue(1, outValue); |
| } |
| } |
| |
| @Override |
| public boolean isReplaceArgumentByStringConcat() { |
| return true; |
| } |
| |
| @Override |
| public ReplaceArgumentByStringConcat asReplaceArgumentByStringConcat() { |
| return this; |
| } |
| } |
| |
| static DexMethod getConstructorWithStringParameter( |
| DexMethod invokedMethod, DexItemFactory factory) { |
| if (invokedMethod.getHolderType() == factory.stringBufferType) { |
| return factory.stringBufferMethods.stringConstructor; |
| } else { |
| assert invokedMethod.getHolderType() == factory.stringBuilderType; |
| return factory.stringBuilderMethods.stringConstructor; |
| } |
| } |
| |
| static Value insertStringConstantInstruction( |
| AppView<?> appView, |
| IRCode code, |
| InstructionListIterator iterator, |
| Instruction instruction, |
| String newString) { |
| // If the block has catch handlers, inserting a constant string in the same block as the |
| // append violates our block representation in DEX since constant string is throwing. If the |
| // append is in a block with catch handlers, we simply insert a new constant string in the |
| // entry block after all arguments. |
| Value value; |
| if (!instruction.getBlock().hasCatchHandlers()) { |
| value = |
| iterator.insertConstStringInstruction( |
| appView, code, appView.dexItemFactory().createString(newString)); |
| } else { |
| InstructionListIterator stringInsertIterator = code.entryBlock().listIterator(code); |
| while (stringInsertIterator.hasNext()) { |
| Instruction next = stringInsertIterator.next(); |
| if (!next.isArgument()) { |
| stringInsertIterator.previous(); |
| break; |
| } |
| } |
| value = |
| stringInsertIterator.insertConstStringInstruction( |
| appView, code, appView.dexItemFactory().createString(newString)); |
| } |
| return value; |
| } |
| |
| static void removeStringBuilderInstruction( |
| InstructionListIterator iterator, Instruction instruction, StringBuilderOracle oracle) { |
| assert oracle.isModeledStringBuilderInstruction( |
| instruction, |
| value -> |
| value.getType().isClassType() |
| && oracle.isStringBuilderType(value.getType().asClassType().getClassType())); |
| if (oracle.isAppend(instruction) && instruction.outValue() != null) { |
| // Append will return the string builder instance. Before removing, ensure that |
| // all users of the output values uses the receiver. |
| instruction.outValue().replaceUsers(instruction.getFirstOperand()); |
| } |
| iterator.removeOrReplaceByDebugLocalRead(); |
| } |
| } |