Build string builder graphs from escape state
Bug: 190489514
Bug: 219455761
Bug: 113859361
Bug: 114002137
Bug: 222437581
Bug: 154899065
Change-Id: Ia823071f8ebc93791a25c8ec594ae5b19d11ff34
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
new file mode 100644
index 0000000..576c156
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderAppendOptimizer.java
@@ -0,0 +1,426 @@
+// 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 static com.android.tools.r8.ir.optimize.string.StringBuilderHelper.canMutate;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createAppendNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createEscapeNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createImplicitToStringNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createInitNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createInspectionNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createMutateNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createNewInstanceNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createOtherStringBuilderNode;
+import static com.android.tools.r8.ir.optimize.string.StringBuilderNode.createToStringNode;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult.SuccessfulDataflowAnalysisResult;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraproceduralDataflowAnalysis;
+import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.EscapeNode;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppend;
+import com.android.tools.r8.ir.optimize.string.StringBuilderNode.LoopNode;
+import com.android.tools.r8.ir.optimize.string.StringBuilderOracle.DefaultStringBuilderOracle;
+import com.android.tools.r8.utils.DepthFirstSearchWorkListBase.StatefulDepthFirstSearchWorkList;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.WorkList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * {@link StringBuilderAppendOptimizer} will try to optimize string builders by joining appends with
+ * constant arguments and materializing toStrings into constant strings to be able to remove entire
+ * StringBuilders (or StringBuffers).
+ *
+ * <p>The algorithm works by producing graphs for all string builders of a method. The graphs follow
+ * the control flow of the method, thus, if a string builder is assigned a value in two blocks from
+ * a certain point, and there is no linear path between the two assignments, they will show up in a
+ * graph as two successor nodes to a common predecessor. StringBuilder graphs are not tracked
+ * through phis, instead a phi and subsequent uses of that phi will also occur as a graph.
+ *
+ * <p>When all graphs are constructed the graphs are optimized by running a munching algorithm on
+ * them.
+ *
+ * <p>Finally, based on all optimizations, the IR is updated to reflect the optimizations.
+ */
+public class StringBuilderAppendOptimizer {
+
+ private final AppView<?> appView;
+ private final StringBuilderOracle oracle;
+ private final IRCode code;
+
+ private StringBuilderAppendOptimizer(AppView<?> appView, IRCode code) {
+ this.appView = appView;
+ this.code = code;
+ oracle = new DefaultStringBuilderOracle(appView.dexItemFactory());
+ }
+
+ public static void run(AppView<?> appView, IRCode code) {
+ new StringBuilderAppendOptimizer(appView, code).run();
+ }
+
+ private void run() {
+ Map<Value, StringBuilderNode> stringBuilderGraphs = computeStringBuilderGraphs();
+ // TODO(b/219455761): Update with actions on the graphs.
+ assert stringBuilderGraphs != null;
+ }
+
+ private static class StringBuilderGraphState {
+
+ private final Map<Value, StringBuilderNode> roots;
+ private final Map<Value, StringBuilderNode> tails;
+ private boolean isPartOfLoop;
+
+ private StringBuilderGraphState(
+ Map<Value, StringBuilderNode> roots, Map<Value, StringBuilderNode> tails) {
+ this.roots = roots;
+ this.tails = tails;
+ }
+ }
+
+ /***
+ * This will compute a collection of graphs from StringBuilders and return a map from value to
+ * the root of the string builder graph.
+ *
+ * The graphs are constructed by doing a DFS traversel and computing all string builder actions
+ * inside a block. The flow here is known to be linear. When backtracking, the linear collection
+ * of nodes (represented by root and tail) are then combined in a way that ensure the control flow
+ * of the method is maintained in the graph.
+ *
+ * To ensure that we correctly compute when a string builder escapes and when it can be mutated
+ * we first compute a {@link StringBuilderEscapeState} by using a flow analysis, enabling us to
+ * answer, for a given instruction, is a string builder value escaping and all escaped values
+ * at the instruction.
+ */
+ private Map<Value, StringBuilderNode> computeStringBuilderGraphs() {
+ StringBuilderEscapeTransferFunction transferFunction =
+ new StringBuilderEscapeTransferFunction(oracle);
+ IntraproceduralDataflowAnalysis<StringBuilderEscapeState> analysis =
+ new IntraproceduralDataflowAnalysis<>(
+ StringBuilderEscapeState.bottom(), code, transferFunction);
+ SuccessfulDataflowAnalysisResult<?, StringBuilderEscapeState> stringBuilderEscapeResult =
+ analysis.run(code.entryBlock()).asSuccessfulAnalysisResult();
+
+ if (stringBuilderEscapeResult == null) {
+ return Collections.emptyMap();
+ }
+
+ TraversalContinuation<Void, StringBuilderGraphState> graphResult =
+ new StatefulDepthFirstSearchWorkList<BasicBlock, StringBuilderGraphState, Void>() {
+
+ @Override
+ @SuppressWarnings("ReturnValueIgnored")
+ protected TraversalContinuation<Void, StringBuilderGraphState> process(
+ DFSNodeWithState<BasicBlock, StringBuilderGraphState> node,
+ Function<BasicBlock, DFSNodeWithState<BasicBlock, StringBuilderGraphState>>
+ childNodeConsumer) {
+ Map<Value, StringBuilderNode> currentRoots = new IdentityHashMap<>();
+ Map<Value, StringBuilderNode> currentTails = new IdentityHashMap<>();
+ BasicBlock block = node.getNode();
+ StringBuilderEscapeState previousState = analysis.computeBlockEntryState(block);
+ TransferFunctionResult<StringBuilderEscapeState> result =
+ transferFunction.applyBlock(block, previousState);
+ if (result.isFailedTransferResult()) {
+ assert false : "Computing the state should never fail";
+ return TraversalContinuation.doBreak();
+ }
+ previousState = result.asAbstractState();
+ for (Phi phi : block.getPhis()) {
+ if (previousState.isLiveStringBuilder(phi)) {
+ visitAllAliasing(
+ phi,
+ previousState,
+ value -> {},
+ alias -> {
+ EscapeNode escapeNode = new EscapeNode();
+ currentRoots.put(alias, escapeNode);
+ currentTails.put(alias, escapeNode);
+ });
+ }
+ }
+ for (Instruction instruction : block.getInstructions()) {
+ result = transferFunction.apply(instruction, previousState);
+ if (result.isFailedTransferResult()) {
+ assert false : "Computing the state should never fail";
+ return TraversalContinuation.doBreak();
+ }
+ previousState = result.asAbstractState();
+ createNodesForInstruction(
+ instruction,
+ previousState,
+ (value, sbNode) -> {
+ StringBuilderNode currentTail = currentTails.get(value);
+ if (currentTail == null) {
+ currentRoots.put(value, sbNode);
+ currentTails.put(value, sbNode);
+ } else if (shouldAddNodeToGraph(currentTail, sbNode)) {
+ currentTail.addSuccessor(sbNode);
+ currentTails.put(value, sbNode);
+ }
+ });
+ }
+ assert currentRoots.keySet().equals(currentTails.keySet());
+ assert previousState.getLiveStringBuilders().containsAll(currentRoots.keySet())
+ : "Seen root that is not a live string builder";
+ node.setState(new StringBuilderGraphState(currentRoots, currentTails));
+ for (BasicBlock successor : block.getSuccessors()) {
+ childNodeConsumer.apply(successor);
+ }
+ return TraversalContinuation.doContinue();
+ }
+
+ private boolean shouldAddNodeToGraph(
+ StringBuilderNode insertedNode, StringBuilderNode newNode) {
+ // No need for multiple mutating nodes or inspecting nodes.
+ if (insertedNode.isMutateNode()) {
+ return !newNode.isMutateNode() && !newNode.isInspectingNode();
+ } else if (insertedNode.isInspectingNode()) {
+ return !newNode.isInspectingNode();
+ }
+ return true;
+ }
+
+ private void createNodesForInstruction(
+ Instruction instruction,
+ StringBuilderEscapeState escapeState,
+ BiConsumer<Value, StringBuilderNode> nodeConsumer) {
+ // Do not build nodes for assume values.
+ if (instruction.isAssume()) {
+ return;
+ }
+ if (oracle.isModeledStringBuilderInstruction(instruction)) {
+ createNodesForStringBuilderInstruction(instruction, escapeState, nodeConsumer);
+ } else {
+ for (Value newEscapedValue : escapeState.getNewlyEscaped()) {
+ visitStringBuilderValues(
+ newEscapedValue,
+ escapeState,
+ nonAlias -> nodeConsumer.accept(nonAlias, createEscapeNode()),
+ alias -> nodeConsumer.accept(alias, createEscapeNode()));
+ }
+ if (canMutate(instruction)) {
+ for (Value escapedStringBuilder : escapeState.getEscaping()) {
+ visitStringBuilderValues(
+ escapedStringBuilder,
+ escapeState,
+ nonAlias -> nodeConsumer.accept(nonAlias, createMutateNode()),
+ alias -> nodeConsumer.accept(alias, createMutateNode()));
+ }
+ }
+ }
+ }
+
+ private void createNodesForStringBuilderInstruction(
+ Instruction instruction,
+ StringBuilderEscapeState escapeState,
+ BiConsumer<Value, StringBuilderNode> nodeConsumer) {
+ if (instruction.isNewInstance()) {
+ Value newInstanceValue = instruction.outValue();
+ assert newInstanceValue != null;
+ nodeConsumer.accept(
+ newInstanceValue, createNewInstanceNode(instruction.asNewInstance()));
+ } else {
+ InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
+ Value receiver = invoke.getReceiver();
+ if (oracle.isInit(instruction)) {
+ InitNode initNode = createInitNode(instruction.asInvokeDirect());
+ initNode.setConstantArgument(oracle.getConstantArgument(instruction));
+ if (invoke.arguments().size() == 2) {
+ Value arg = invoke.getOperand(1);
+ if (oracle.hasStringBuilderType(arg)) {
+ insertImplicitToStringNode(
+ arg, instruction, initNode, escapeState, nodeConsumer);
+ }
+ }
+ visitStringBuilderValues(
+ receiver,
+ escapeState,
+ actual -> nodeConsumer.accept(actual, initNode),
+ escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
+ } else if (oracle.isAppend(instruction)) {
+ AppendNode appendNode = createAppendNode(instruction.asInvokeVirtual());
+ appendNode.setConstantArgument(oracle.getConstantArgument(instruction));
+ Value arg = invoke.getOperand(1).getAliasedValue();
+ if (oracle.hasStringBuilderType(arg)) {
+ insertImplicitToStringNode(
+ arg, instruction, appendNode, escapeState, nodeConsumer);
+ }
+ visitStringBuilderValues(
+ receiver,
+ escapeState,
+ actual -> nodeConsumer.accept(actual, appendNode),
+ escaped -> nodeConsumer.accept(escaped, createMutateNode()));
+ } else if (oracle.isToString(instruction)) {
+ visitStringBuilderValues(
+ receiver,
+ escapeState,
+ actual ->
+ nodeConsumer.accept(
+ actual, createToStringNode(instruction.asInvokeVirtual())),
+ escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
+ } else if (oracle.isInspecting(instruction)) {
+ visitStringBuilderValues(
+ receiver,
+ escapeState,
+ actual -> nodeConsumer.accept(actual, createInspectionNode(instruction)),
+ escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
+ } else {
+ visitStringBuilderValues(
+ receiver,
+ escapeState,
+ actual ->
+ nodeConsumer.accept(actual, createOtherStringBuilderNode(instruction)),
+ escaped ->
+ nodeConsumer.accept(escaped, createOtherStringBuilderNode(instruction)));
+ }
+ }
+ }
+
+ // For tracking propagating constant string builder values through append or init we
+ // build a link between the two graphs.
+ private void insertImplicitToStringNode(
+ Value value,
+ Instruction instruction,
+ InitOrAppend node,
+ StringBuilderEscapeState escapeState,
+ BiConsumer<Value, StringBuilderNode> nodeConsumer) {
+ assert escapeState.isLiveStringBuilder(value);
+ ImplicitToStringNode implicitToStringNode = createImplicitToStringNode(node);
+ visitStringBuilderValues(
+ value,
+ escapeState,
+ actual -> nodeConsumer.accept(actual, implicitToStringNode),
+ escaped -> nodeConsumer.accept(escaped, createInspectionNode(instruction)));
+ node.setImplicitToStringNode(implicitToStringNode);
+ }
+
+ private void visitStringBuilderValues(
+ Value value,
+ StringBuilderEscapeState state,
+ Consumer<Value> actualConsumer,
+ Consumer<Value> aliasAndEscapedConsumer) {
+ assert state.isLiveStringBuilder(value);
+ boolean seenEscaped =
+ visitAllAliasing(value, state, actualConsumer, aliasAndEscapedConsumer);
+ seenEscaped |= visitAllAliases(value, state, aliasAndEscapedConsumer);
+ if (seenEscaped) {
+ for (Value escapingValue : state.getEscaping()) {
+ aliasAndEscapedConsumer.accept(escapingValue);
+ }
+ }
+ }
+
+ private boolean visitAllAliasing(
+ Value value,
+ StringBuilderEscapeState state,
+ Consumer<Value> nonAliasConsumer,
+ Consumer<Value> aliasAndEscapedConsumer) {
+ WorkList<Value> valueWorkList = WorkList.newIdentityWorkList(value);
+ boolean seenUnknownAlias = false;
+ boolean seenEscaped = false;
+ while (valueWorkList.hasNext()) {
+ Value next = valueWorkList.next();
+ seenEscaped |= state.isEscaped(next);
+ Set<Value> aliasing =
+ state.getAliasesToDefinitions().getOrDefault(next, Collections.emptySet());
+ valueWorkList.addIfNotSeen(aliasing);
+ // Check if we have a direct alias such as Assume, CheckCast or StringBuilder.append.
+ if (aliasing.size() != 1 || next.isPhi()) {
+ if (!seenUnknownAlias) {
+ nonAliasConsumer.accept(next);
+ seenUnknownAlias = true;
+ } else {
+ aliasAndEscapedConsumer.accept(next);
+ }
+ }
+ }
+ return seenEscaped;
+ }
+
+ private boolean visitAllAliases(
+ Value value, StringBuilderEscapeState state, Consumer<Value> aliasConsumer) {
+ Map<Value, Set<Value>> escapedDefinitionsToKnown = state.getDefinitionsToAliases();
+ WorkList<Value> valueWorkList =
+ WorkList.newIdentityWorkList(
+ escapedDefinitionsToKnown.getOrDefault(value, Collections.emptySet()));
+ boolean seenEscaped = false;
+ while (valueWorkList.hasNext()) {
+ Value next = valueWorkList.next();
+ seenEscaped |= state.isEscaped(next);
+ if (next.isPhi()) {
+ aliasConsumer.accept(next);
+ }
+ valueWorkList.addIfNotSeen(
+ escapedDefinitionsToKnown.getOrDefault(next, Collections.emptySet()));
+ }
+ return seenEscaped;
+ }
+
+ @Override
+ protected TraversalContinuation<Void, StringBuilderGraphState> joiner(
+ DFSNodeWithState<BasicBlock, StringBuilderGraphState> node,
+ List<DFSNodeWithState<BasicBlock, StringBuilderGraphState>> childStates) {
+ StringBuilderGraphState state = node.getState();
+ for (DFSNodeWithState<BasicBlock, StringBuilderGraphState> childState : childStates) {
+ StringBuilderGraphState childGraphState = childState.getState();
+ childGraphState.roots.forEach(
+ (value, sbNode) -> {
+ StringBuilderNode currentRoot = state.roots.get(value);
+ StringBuilderNode currentTail = state.tails.get(value);
+ if (currentRoot == null) {
+ assert currentTail == null;
+ if (childStates.size() == 1) {
+ state.roots.put(value, sbNode);
+ state.tails.put(value, sbNode);
+ return;
+ }
+ // We are adding a value coming only from a child state and not defined here.
+ // To ensure proper ordering we add a sentinel node. If it turns out there is
+ // only a single reference, we rely on munching to remove it.
+ currentRoot = StringBuilderNode.createSplitReferenceNode();
+ currentTail = currentRoot;
+ state.roots.put(value, currentRoot);
+ state.tails.put(value, currentTail);
+ }
+ assert currentTail != null;
+ // Link next node from successor
+ currentTail.addSuccessor(sbNode);
+ sbNode.addPredecessor(currentTail);
+ });
+ if (childState.seenAndNotProcessed()) {
+ childGraphState.isPartOfLoop = true;
+ }
+ }
+ if (state.isPartOfLoop) {
+ for (Value value : state.roots.keySet()) {
+ LoopNode loopNode = StringBuilderNode.createLoopNode();
+ loopNode.addSuccessor(state.roots.get(value));
+ state.roots.put(value, loopNode);
+ }
+ }
+ return TraversalContinuation.doContinue(state);
+ }
+ }.run(code.entryBlock());
+
+ return graphResult.shouldBreak()
+ ? Collections.emptyMap()
+ : graphResult.asContinue().getValue().roots;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
index 898442f..ff4a62a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderEscapeTransferFunction.java
@@ -53,7 +53,7 @@
public TransferFunctionResult<StringBuilderEscapeState> apply(
Instruction instruction, StringBuilderEscapeState state) {
StringBuilderEscapeState.Builder builder = state.builder();
- boolean isStringBuilderInstruction = oracle.isStringBuilderInstruction(instruction);
+ boolean isStringBuilderInstruction = oracle.isModeledStringBuilderInstruction(instruction);
if (!isStringBuilderInstruction && isEscapingInstructionForInValues(instruction)) {
for (Value inValue : instruction.inValues()) {
if (isLiveStringBuilder(builder, inValue)) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderHelper.java
index a56b3e7..a795308 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderHelper.java
@@ -30,6 +30,12 @@
|| instruction.isCheckCast();
}
+ static boolean canMutate(Instruction instruction) {
+ return instruction.isInvoke()
+ || instruction.isFieldInstruction()
+ || instruction.isNewInstance();
+ }
+
static boolean isInstructionThatIntroducesDefiniteAlias(
Instruction instruction, StringBuilderOracle oracle) {
return instruction.isAssume() || instruction.isCheckCast() || oracle.isAppend(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
new file mode 100644
index 0000000..acd5c47
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderNode.java
@@ -0,0 +1,606 @@
+// 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.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+/**
+ * StringBuilderNode defines a single point where a string builder operation occur or some abstract
+ * state changes. The node can be assembled into a graph by defining predecessors and successors.
+ */
+class StringBuilderNode {
+
+ interface StringBuilderInstruction {
+
+ Instruction getInstruction();
+
+ boolean isStringBuilderInstructionNode();
+
+ StringBuilderInstruction asStringBuilderInstructionNode();
+ }
+
+ interface InitOrAppend extends StringBuilderInstruction {
+
+ boolean hasConstantArgument();
+
+ String getConstantArgument();
+
+ void setConstantArgument(String constantArgument);
+
+ void setImplicitToStringNode(ImplicitToStringNode node);
+
+ ImplicitToStringNode getImplicitToStringNode();
+ }
+
+ private final Set<StringBuilderNode> successors = Sets.newIdentityHashSet();
+ private final Set<StringBuilderNode> predecessors = Sets.newIdentityHashSet();
+
+ // Field uses to ensure that munching will not operate on the same value multiple times. If all
+ // peep holes would look in the same direction, this field could be removed.
+ private boolean isDead = false;
+
+ private StringBuilderNode() {}
+
+ boolean isEscapeNode() {
+ return false;
+ }
+
+ boolean isMutateNode() {
+ return false;
+ }
+
+ boolean isSplitReferenceNode() {
+ return false;
+ }
+
+ boolean isLoopNode() {
+ return false;
+ }
+
+ boolean isNewInstanceNode() {
+ return false;
+ }
+
+ boolean isInitNode() {
+ return false;
+ }
+
+ boolean isAppendNode() {
+ return false;
+ }
+
+ boolean isInitOrAppend() {
+ return false;
+ }
+
+ boolean isToStringNode() {
+ return false;
+ }
+
+ boolean isInspectingNode() {
+ return false;
+ }
+
+ boolean isOtherStringBuilderNode() {
+ return false;
+ }
+
+ boolean isImplicitToStringNode() {
+ return false;
+ }
+
+ boolean isStringBuilderInstructionNode() {
+ return false;
+ }
+
+ NewInstanceNode asNewInstanceNode() {
+ return null;
+ }
+
+ InitNode asInitNode() {
+ return null;
+ }
+
+ AppendNode asAppendNode() {
+ return null;
+ }
+
+ InitOrAppend asInitOrAppend() {
+ return null;
+ }
+
+ ToStringNode asToStringNode() {
+ return null;
+ }
+
+ InspectingNode asInspectingNode() {
+ return null;
+ }
+
+ OtherStringBuilderNode asOtherStringBuilderNode() {
+ return null;
+ }
+
+ ImplicitToStringNode asImplicitToStringNode() {
+ return null;
+ }
+
+ StringBuilderInstruction asStringBuilderInstructionNode() {
+ return null;
+ }
+
+ boolean isDead() {
+ return isDead;
+ }
+
+ boolean hasSingleSuccessor() {
+ return successors.size() == 1;
+ }
+
+ StringBuilderNode getSingleSuccessor() {
+ assert hasSingleSuccessor();
+ return successors.iterator().next();
+ }
+
+ void addSuccessor(StringBuilderNode successor) {
+ successors.add(successor);
+ successor.predecessors.add(this);
+ }
+
+ Set<StringBuilderNode> getSuccessors() {
+ return successors;
+ }
+
+ boolean hasSinglePredecessor() {
+ return predecessors.size() == 1;
+ }
+
+ StringBuilderNode getSinglePredecessor() {
+ assert hasSinglePredecessor();
+ return predecessors.iterator().next();
+ }
+
+ Set<StringBuilderNode> getPredecessors() {
+ return predecessors;
+ }
+
+ void addPredecessor(StringBuilderNode predecessor) {
+ predecessors.add(predecessor);
+ }
+
+ void removeNode() {
+ for (StringBuilderNode successor : this.getSuccessors()) {
+ successor.getPredecessors().remove(this);
+ successor.getPredecessors().addAll(this.getPredecessors());
+ }
+ for (StringBuilderNode predecessor : this.getPredecessors()) {
+ predecessor.getSuccessors().remove(this);
+ predecessor.getSuccessors().addAll(this.getSuccessors());
+ }
+ isDead = true;
+ }
+
+ /** EscapeNode is used to explicitly define an escape point for a StringBuilder. */
+ static class EscapeNode extends StringBuilderNode {
+
+ @Override
+ boolean isEscapeNode() {
+ return true;
+ }
+ }
+
+ /**
+ * MutateNode defines if a string builder could be possibly mutated or inspected. An example could
+ * be:
+ *
+ * <pre>
+ * sb = new StringBuilder();
+ * escape(sb);
+ * sb.append("foo");
+ * canMutate(); <-- This is represented by a MutateNode.
+ * sb.append("bar");
+ * </pre>
+ */
+ static class MutateNode extends StringBuilderNode {
+
+ @Override
+ boolean isMutateNode() {
+ return true;
+ }
+ }
+
+ /**
+ * SplitReferenceNodes are synthetic nodes inserted when a string builder is used in multiple
+ * successor blocks.
+ */
+ static class SplitReferenceNode extends StringBuilderNode {
+
+ @Override
+ boolean isSplitReferenceNode() {
+ return true;
+ }
+ }
+
+ /**
+ * LoopNode is a node indicating that all successor paths are part of a loop. LoopNode's are only
+ * inserted once ensuring there are no loops in a StringBuilderGraph.
+ */
+ static class LoopNode extends StringBuilderNode {
+
+ @Override
+ boolean isLoopNode() {
+ return true;
+ }
+ }
+
+ /** A NewInstanceNode is a new instance of either StringBuilder or StringBuffer. */
+ static class NewInstanceNode extends StringBuilderNode implements StringBuilderInstruction {
+
+ private final NewInstance instruction;
+
+ private NewInstanceNode(NewInstance instruction) {
+ this.instruction = instruction;
+ }
+
+ @Override
+ boolean isNewInstanceNode() {
+ return true;
+ }
+
+ @Override
+ NewInstanceNode asNewInstanceNode() {
+ return this;
+ }
+
+ @Override
+ public Instruction getInstruction() {
+ return instruction;
+ }
+
+ @Override
+ public boolean isStringBuilderInstructionNode() {
+ return true;
+ }
+
+ @Override
+ public StringBuilderInstruction asStringBuilderInstructionNode() {
+ return this;
+ }
+ }
+
+ /** An initNode is where a StringBuilder/StringBuffer is initialized. */
+ static class InitNode extends StringBuilderNode
+ implements InitOrAppend, StringBuilderInstruction {
+
+ private final InvokeDirect instruction;
+ private ImplicitToStringNode implicitToStringNode;
+ private String constantArgument;
+
+ private InitNode(InvokeDirect instruction) {
+ this.instruction = instruction;
+ }
+
+ @Override
+ boolean isInitNode() {
+ return true;
+ }
+
+ @Override
+ boolean isInitOrAppend() {
+ return true;
+ }
+
+ @Override
+ InitNode asInitNode() {
+ return this;
+ }
+
+ @Override
+ InitOrAppend asInitOrAppend() {
+ return this;
+ }
+
+ @Override
+ public Instruction getInstruction() {
+ return instruction;
+ }
+
+ @Override
+ public boolean isStringBuilderInstructionNode() {
+ return true;
+ }
+
+ @Override
+ public StringBuilderInstruction asStringBuilderInstructionNode() {
+ return this;
+ }
+
+ @Override
+ public void setConstantArgument(String constantArgument) {
+ this.constantArgument = constantArgument;
+ }
+
+ @Override
+ public void setImplicitToStringNode(ImplicitToStringNode node) {
+ implicitToStringNode = node;
+ }
+
+ @Override
+ public ImplicitToStringNode getImplicitToStringNode() {
+ return implicitToStringNode;
+ }
+
+ @Override
+ public String getConstantArgument() {
+ return constantArgument;
+ }
+
+ @Override
+ public boolean hasConstantArgument() {
+ return constantArgument != null;
+ }
+ }
+
+ /** AppendNodes are StringBuilder.append or StringBuffer.append. */
+ static class AppendNode extends StringBuilderNode
+ implements InitOrAppend, StringBuilderInstruction {
+
+ private final InvokeVirtual instruction;
+ private ImplicitToStringNode implicitToStringNode;
+ private String constantArgument;
+
+ private AppendNode(InvokeVirtual instruction) {
+ this.instruction = instruction;
+ }
+
+ @Override
+ boolean isAppendNode() {
+ return true;
+ }
+
+ @Override
+ boolean isInitOrAppend() {
+ return true;
+ }
+
+ @Override
+ AppendNode asAppendNode() {
+ return this;
+ }
+
+ @Override
+ InitOrAppend asInitOrAppend() {
+ return this;
+ }
+
+ @Override
+ public Instruction getInstruction() {
+ return instruction;
+ }
+
+ @Override
+ public boolean isStringBuilderInstructionNode() {
+ return true;
+ }
+
+ @Override
+ public StringBuilderInstruction asStringBuilderInstructionNode() {
+ return this;
+ }
+
+ @Override
+ public void setConstantArgument(String constantArgument) {
+ this.constantArgument = constantArgument;
+ }
+
+ @Override
+ public void setImplicitToStringNode(ImplicitToStringNode node) {
+ implicitToStringNode = node;
+ }
+
+ @Override
+ public ImplicitToStringNode getImplicitToStringNode() {
+ return implicitToStringNode;
+ }
+
+ @Override
+ public String getConstantArgument() {
+ return constantArgument;
+ }
+
+ @Override
+ public boolean hasConstantArgument() {
+ return constantArgument != null;
+ }
+ }
+
+ /**
+ * ToStringNodes marked a point where a StringBuilder/StringBuffer is materialized to a string.
+ */
+ static class ToStringNode extends StringBuilderNode implements StringBuilderInstruction {
+
+ private final InvokeVirtual instruction;
+
+ private ToStringNode(InvokeVirtual instruction) {
+ this.instruction = instruction;
+ }
+
+ @Override
+ boolean isToStringNode() {
+ return true;
+ }
+
+ @Override
+ ToStringNode asToStringNode() {
+ return this;
+ }
+
+ @Override
+ public Instruction getInstruction() {
+ return instruction;
+ }
+
+ @Override
+ public boolean isStringBuilderInstructionNode() {
+ return true;
+ }
+
+ @Override
+ public StringBuilderInstruction asStringBuilderInstructionNode() {
+ return this;
+ }
+ }
+
+ /**
+ * InspectingNode is inserted if there is inspection of the capacity of a
+ * StringBuilder/StringBuffer. Special care needs to be taken since we try to ensure that capacity
+ * will be the same after optimizations.
+ */
+ static class InspectingNode extends StringBuilderNode implements StringBuilderInstruction {
+
+ private final Instruction instruction;
+
+ private InspectingNode(Instruction instruction) {
+ this.instruction = instruction;
+ }
+
+ @Override
+ boolean isInspectingNode() {
+ return true;
+ }
+
+ @Override
+ InspectingNode asInspectingNode() {
+ return this;
+ }
+
+ @Override
+ public Instruction getInstruction() {
+ return instruction;
+ }
+
+ @Override
+ public boolean isStringBuilderInstructionNode() {
+ return true;
+ }
+
+ @Override
+ public StringBuilderInstruction asStringBuilderInstructionNode() {
+ return this;
+ }
+ }
+
+ /** OtherStringBuilderNode marks operations on string builders we do not model. */
+ static class OtherStringBuilderNode extends StringBuilderNode
+ implements StringBuilderInstruction {
+
+ private final Instruction instruction;
+
+ private OtherStringBuilderNode(Instruction instruction) {
+ this.instruction = instruction;
+ }
+
+ @Override
+ boolean isOtherStringBuilderNode() {
+ return true;
+ }
+
+ @Override
+ OtherStringBuilderNode asOtherStringBuilderNode() {
+ return this;
+ }
+
+ @Override
+ public Instruction getInstruction() {
+ return instruction;
+ }
+
+ @Override
+ public boolean isStringBuilderInstructionNode() {
+ return true;
+ }
+
+ @Override
+ public StringBuilderInstruction asStringBuilderInstructionNode() {
+ return this;
+ }
+ }
+
+ /**
+ * ImplicitToStringNode are placed a StringBuilder/StringBuffer is appended to another
+ * StringBuilder/StringBuffer.
+ */
+ static class ImplicitToStringNode extends StringBuilderNode {
+
+ private final InitOrAppend initOrAppend;
+
+ ImplicitToStringNode(InitOrAppend initOrAppend) {
+ this.initOrAppend = initOrAppend;
+ }
+
+ public InitOrAppend getInitOrAppend() {
+ return initOrAppend;
+ }
+
+ @Override
+ boolean isImplicitToStringNode() {
+ return true;
+ }
+
+ @Override
+ ImplicitToStringNode asImplicitToStringNode() {
+ return this;
+ }
+ }
+
+ static EscapeNode createEscapeNode() {
+ return new EscapeNode();
+ }
+
+ static MutateNode createMutateNode() {
+ return new MutateNode();
+ }
+
+ static SplitReferenceNode createSplitReferenceNode() {
+ return new SplitReferenceNode();
+ }
+
+ static LoopNode createLoopNode() {
+ return new LoopNode();
+ }
+
+ static NewInstanceNode createNewInstanceNode(NewInstance instruction) {
+ return new NewInstanceNode(instruction);
+ }
+
+ static InitNode createInitNode(InvokeDirect instruction) {
+ return new InitNode(instruction);
+ }
+
+ static AppendNode createAppendNode(InvokeVirtual instruction) {
+ return new AppendNode(instruction);
+ }
+
+ static ToStringNode createToStringNode(InvokeVirtual instruction) {
+ return new ToStringNode(instruction);
+ }
+
+ static InspectingNode createInspectionNode(Instruction instruction) {
+ return new InspectingNode(instruction);
+ }
+
+ static OtherStringBuilderNode createOtherStringBuilderNode(Instruction instruction) {
+ return new OtherStringBuilderNode(instruction);
+ }
+
+ static ImplicitToStringNode createImplicitToStringNode(InitOrAppend otherNode) {
+ return new ImplicitToStringNode(otherNode);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
index 26eb12b..b6c0b4f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOracle.java
@@ -23,7 +23,7 @@
*/
interface StringBuilderOracle {
- boolean isStringBuilderInstruction(Instruction instruction);
+ boolean isModeledStringBuilderInstruction(Instruction instruction);
boolean hasStringBuilderType(Value value);
@@ -50,7 +50,7 @@
}
@Override
- public boolean isStringBuilderInstruction(Instruction instruction) {
+ public boolean isModeledStringBuilderInstruction(Instruction instruction) {
if (instruction.isNewInstance()) {
return isStringBuilderType(instruction.asNewInstance().getType());
} else if (instruction.isInvokeMethod()) {
@@ -157,7 +157,7 @@
@Override
public boolean canObserveStringBuilderCall(Instruction instruction) {
- assert isStringBuilderInstruction(instruction);
+ assert isModeledStringBuilderInstruction(instruction);
if (!instruction.isInvokeMethod()) {
assert false : "Expecting a call to string builder";
return true;