| // Copyright (c) 2016, 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; | |
| import static org.junit.Assert.assertEquals; | |
| import static org.junit.Assert.assertSame; | |
| import static org.junit.Assert.assertTrue; | |
| import com.android.tools.r8.graph.AppInfo; | |
| import com.android.tools.r8.graph.AppView; | |
| import com.android.tools.r8.graph.DexApplication; | |
| import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; | |
| import com.android.tools.r8.ir.code.Add; | |
| import com.android.tools.r8.ir.code.BasicBlock; | |
| import com.android.tools.r8.ir.code.ConstNumber; | |
| 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.NumericType; | |
| import com.android.tools.r8.ir.code.Position; | |
| import com.android.tools.r8.ir.code.Value; | |
| import com.android.tools.r8.smali.SmaliBuilder; | |
| import com.android.tools.r8.smali.SmaliBuilder.MethodSignature; | |
| import com.android.tools.r8.utils.InternalOptions; | |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; | |
| import com.google.common.collect.ImmutableList; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import org.junit.Test; | |
| public class SplitBlockTest extends IrInjectionTestBase { | |
| private TestApplication codeWithoutCatchHandlers() { | |
| SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); | |
| String returnType = "int"; | |
| List<String> parameters = ImmutableList.of("int", "int"); | |
| MethodSignature signature = builder.addStaticMethod( | |
| returnType, | |
| DEFAULT_METHOD_NAME, | |
| parameters, | |
| 0, | |
| " add-int p0, p0, p0", | |
| " sub-int p1, p1, p0", | |
| " mul-int p0, p0, p1", | |
| " return p0" | |
| ); | |
| builder.addMainMethod( | |
| 3, | |
| " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", | |
| " const/4 v1, 1", | |
| " const/4 v2, 5", | |
| " invoke-static { v1, v2 }, LTest;->method(II)I", | |
| " move-result v1", | |
| " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V", | |
| " return-void" | |
| ); | |
| InternalOptions options = new InternalOptions(); | |
| DexApplication application = buildApplication(builder, options); | |
| AppView<?> appView = AppView.createForD8(new AppInfo(application), options); | |
| // Return the processed method for inspection. | |
| MethodSubject methodSubject = getMethodSubject(application, signature); | |
| return new TestApplication(appView, methodSubject); | |
| } | |
| @Test | |
| public void noCatchHandlers() throws Exception { | |
| final int initialBlockCount = 1; | |
| final int argumentInstructions = 2; | |
| final int firstBlockInstructions = 6; | |
| // Try split between all non-argument instructions in the first block. | |
| for (int i = argumentInstructions; i < firstBlockInstructions; i++) { | |
| TestApplication test = codeWithoutCatchHandlers(); | |
| IRCode code = test.code; | |
| assertEquals(initialBlockCount, code.blocks.size()); | |
| BasicBlock block = code.entryBlock(); | |
| int instructionCount = block.getInstructions().size(); | |
| assertEquals(firstBlockInstructions, instructionCount); | |
| assertEquals(argumentInstructions, test.countArgumentInstructions()); | |
| assertEquals(firstBlockInstructions, block.getInstructions().size()); | |
| assertTrue(!block.getInstructions().get(i).isArgument()); | |
| InstructionListIterator iterator = test.listIteratorAt(block, i); | |
| BasicBlock newBlock = iterator.split(code); | |
| assertTrue(code.isConsistentSSA()); | |
| assertEquals(initialBlockCount + 1, code.blocks.size()); | |
| assertEquals(i + 1, code.entryBlock().getInstructions().size()); | |
| assertEquals(instructionCount - i, code.blocks.get(1).getInstructions().size()); | |
| assertSame(newBlock, code.blocks.get(1)); | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals("6", result); | |
| } | |
| } | |
| @Test | |
| public void noCatchHandlersSplitThree() throws Exception { | |
| final int initialBlockCount = 1; | |
| final int argumentInstructions = 2; | |
| final int firstBlockInstructions = 6; | |
| // Try split out all non-argument instructions in the first block. | |
| for (int i = argumentInstructions; i < firstBlockInstructions - 1; i++) { | |
| TestApplication test = codeWithoutCatchHandlers(); | |
| IRCode code = test.code; | |
| assertEquals(initialBlockCount, code.blocks.size()); | |
| BasicBlock block = code.entryBlock(); | |
| int instructionCount = block.getInstructions().size(); | |
| assertEquals(firstBlockInstructions, instructionCount); | |
| assertEquals(argumentInstructions, test.countArgumentInstructions()); | |
| assertEquals(firstBlockInstructions, block.getInstructions().size()); | |
| assertTrue(!block.getInstructions().get(i).isArgument()); | |
| InstructionListIterator iterator = test.listIteratorAt(block, i); | |
| BasicBlock newBlock = iterator.split(code, 1); | |
| assertTrue(code.isConsistentSSA()); | |
| assertEquals(initialBlockCount + 2, code.blocks.size()); | |
| assertEquals(i + 1, code.entryBlock().getInstructions().size()); | |
| assertEquals(2, code.blocks.get(1).getInstructions().size()); | |
| assertEquals(instructionCount - i - 1, code.blocks.get(2).getInstructions().size()); | |
| assertSame(newBlock, code.blocks.get(1)); | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals("6", result); | |
| } | |
| } | |
| private TestApplication codeWithCatchHandlers(boolean shouldThrow, boolean twoGuards) { | |
| SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); | |
| String secondGuard = twoGuards ? | |
| " .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch" : " "; | |
| String returnType = "int"; | |
| List<String> parameters = ImmutableList.of("int", "int"); | |
| MethodSignature signature = builder.addStaticMethod( | |
| returnType, | |
| DEFAULT_METHOD_NAME, | |
| parameters, | |
| 0, | |
| " :try_start", | |
| " add-int p0, p0, p0", | |
| " add-int p1, p1, p1", | |
| " div-int p0, p0, p1", | |
| " :try_end", | |
| " .catch Ljava/lang/ArithmeticException; {:try_start .. :try_end} :catch", | |
| secondGuard, | |
| " :return", | |
| " return p0", | |
| " :catch", | |
| " const/4 p0, -1", | |
| " goto :return" | |
| ); | |
| builder.addMainMethod( | |
| 3, | |
| " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", | |
| " const/4 v1, 2", | |
| " const/4 v2, " + (shouldThrow ? "0" : "1"), | |
| " invoke-static { v1, v2 }, LTest;->method(II)I", | |
| " move-result v1", | |
| " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V", | |
| " return-void" | |
| ); | |
| InternalOptions options = new InternalOptions(); | |
| DexApplication application = buildApplication(builder, options); | |
| AppView<?> appView = AppView.createForD8(new AppInfo(application), options); | |
| // Return the processed method for inspection. | |
| MethodSubject methodSubject = getMethodSubject(application, signature); | |
| return new TestApplication(appView, methodSubject); | |
| } | |
| private void hasCatchandlerIfThrowing(BasicBlock block) { | |
| boolean throwing = false; | |
| for (Instruction instruction : block.getInstructions()) { | |
| throwing |= instruction.instructionTypeCanThrow(); | |
| } | |
| assertEquals(throwing, block.hasCatchHandlers()); | |
| } | |
| private void runCatchHandlerTest(boolean codeThrows, boolean twoGuards) throws Exception { | |
| final int secondBlockInstructions = 4; | |
| final int initialBlockCount = twoGuards ? 7 : 6; | |
| // Try split between all instructions in second block. | |
| for (int i = 1; i < secondBlockInstructions; i++) { | |
| TestApplication test = codeWithCatchHandlers(codeThrows, twoGuards); | |
| IRCode code = test.code; | |
| assertEquals(initialBlockCount, code.blocks.size()); | |
| BasicBlock block = code.blocks.get(1); | |
| int instructionCount = block.getInstructions().size(); | |
| assertEquals(secondBlockInstructions, instructionCount); | |
| InstructionListIterator iterator = test.listIteratorAt(block, i); | |
| BasicBlock newBlock = iterator.split(code); | |
| assertTrue(code.isConsistentSSA()); | |
| assertEquals(initialBlockCount + 1, code.blocks.size()); | |
| assertEquals(i + 1, code.blocks.get(1).getInstructions().size()); | |
| assertEquals(instructionCount - i, newBlock.getInstructions().size()); | |
| assertSame(newBlock, code.blocks.get(2)); | |
| code.blocks.forEach(this::hasCatchandlerIfThrowing); | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals(codeThrows ? "-1" : "2", result); | |
| } | |
| } | |
| @Test | |
| public void catchHandlers() throws Exception { | |
| runCatchHandlerTest(false, false); | |
| runCatchHandlerTest(true, false); | |
| runCatchHandlerTest(false, true); | |
| runCatchHandlerTest(true, true); | |
| } | |
| private void runCatchHandlerSplitThreeTest(boolean codeThrows, boolean twoGuards) | |
| throws Exception { | |
| final int secondBlockInstructions = 4; | |
| final int initialBlockCount = twoGuards ? 7 : 6; | |
| // Try split out all instructions in second block. | |
| for (int i = 1; i < secondBlockInstructions - 1; i++) { | |
| TestApplication test = codeWithCatchHandlers(codeThrows, twoGuards); | |
| IRCode code = test.code; | |
| assertEquals(initialBlockCount, code.blocks.size()); | |
| BasicBlock block = code.blocks.get(1); | |
| int instructionCount = block.getInstructions().size(); | |
| assertEquals(secondBlockInstructions, instructionCount); | |
| InstructionListIterator iterator = test.listIteratorAt(block, i); | |
| BasicBlock newBlock = iterator.split(code, 1); | |
| assertTrue(code.isConsistentSSA()); | |
| assertEquals(initialBlockCount + 2, code.blocks.size()); | |
| assertEquals(i + 1, code.blocks.get(1).getInstructions().size()); | |
| assertEquals(2, newBlock.getInstructions().size()); | |
| assertEquals(instructionCount - i - 1, code.blocks.get(3).getInstructions().size()); | |
| assertSame(newBlock, code.blocks.get(2)); | |
| code.blocks.forEach(this::hasCatchandlerIfThrowing); | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals(codeThrows ? "-1" : "2", result); | |
| } | |
| } | |
| @Test | |
| public void catchHandlersSplitThree() throws Exception { | |
| runCatchHandlerSplitThreeTest(false, false); | |
| runCatchHandlerSplitThreeTest(true, false); | |
| runCatchHandlerSplitThreeTest(false, true); | |
| runCatchHandlerSplitThreeTest(true, true); | |
| } | |
| private TestApplication codeWithIf(boolean hitTrueBranch) { | |
| SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); | |
| String returnType = "int"; | |
| List<String> parameters = ImmutableList.of("int", "int"); | |
| MethodSignature signature = builder.addStaticMethod( | |
| returnType, | |
| DEFAULT_METHOD_NAME, | |
| parameters, | |
| 0, | |
| " if-eq p0, p1, :eq", | |
| " const/4 p0, 1", | |
| " return p0", | |
| " :eq", | |
| " const/4 p0, 0", | |
| " return p0" | |
| ); | |
| builder.addMainMethod( | |
| 3, | |
| " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", | |
| " const/4 v1, 2", | |
| " const/4 v2, " + (hitTrueBranch ? "2" : "3"), | |
| " invoke-static { v1, v2 }, LTest;->method(II)I", | |
| " move-result v1", | |
| " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V", | |
| " return-void" | |
| ); | |
| InternalOptions options = new InternalOptions(); | |
| DexApplication application = buildApplication(builder, options); | |
| AppView<?> appView = AppView.createForD8(new AppInfo(application), options); | |
| // Return the processed method for inspection. | |
| MethodSubject methodSubject = getMethodSubject(application, signature); | |
| return new TestApplication(appView, methodSubject); | |
| } | |
| private void runWithIfTest(boolean hitTrueBranch) throws Exception { | |
| final int initialBlockCount = 3; | |
| final int argumentInstructions = 2; | |
| final int firstBlockInstructions = 3; | |
| // Try split between all non-argument instructions in the first block. | |
| for (int i = argumentInstructions; i < firstBlockInstructions; i++) { | |
| TestApplication test = codeWithIf(hitTrueBranch); | |
| IRCode code = test.code; | |
| assertEquals(initialBlockCount, code.blocks.size()); | |
| BasicBlock block = code.entryBlock(); | |
| int instructionCount = block.getInstructions().size(); | |
| assertEquals(firstBlockInstructions, instructionCount); | |
| assertEquals(argumentInstructions, test.countArgumentInstructions()); | |
| assertEquals(firstBlockInstructions, block.getInstructions().size()); | |
| assertTrue(!block.getInstructions().get(i).isArgument()); | |
| InstructionListIterator iterator = test.listIteratorAt(block, i); | |
| BasicBlock newBlock = iterator.split(code); | |
| assertTrue(code.isConsistentSSA()); | |
| assertEquals(initialBlockCount + 1, code.blocks.size()); | |
| assertEquals(i + 1, code.entryBlock().getInstructions().size()); | |
| assertEquals(instructionCount - i, newBlock.getInstructions().size()); | |
| assertSame(newBlock, code.blocks.get(1)); | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals(hitTrueBranch ? "0" : "1", result); | |
| } | |
| } | |
| @Test | |
| public void withIf() throws Exception { | |
| runWithIfTest(false); | |
| runWithIfTest(true); | |
| } | |
| private void splitBeforeReturn(boolean hitTrueBranch) throws Exception { | |
| TestApplication test = codeWithIf(hitTrueBranch); | |
| IRCode code = test.code; | |
| // Locate the exit blocks and split before the return. | |
| List<BasicBlock> exitBlocks = new ArrayList<>(code.computeNormalExitBlocks()); | |
| for (BasicBlock originalReturnBlock : exitBlocks) { | |
| InstructionListIterator iterator = | |
| originalReturnBlock.listIterator(code, originalReturnBlock.getInstructions().size()); | |
| Instruction ret = iterator.previous(); | |
| assert ret.isReturn(); | |
| BasicBlock newReturnBlock = iterator.split(code); | |
| // Modify the code to make the inserted block add the constant 10 to the original return | |
| // value. | |
| Value newConstValue = | |
| new Value(test.valueNumberGenerator.next(), TypeLatticeElement.INT, null); | |
| Value newReturnValue = | |
| new Value(test.valueNumberGenerator.next(), TypeLatticeElement.INT, null); | |
| Value oldReturnValue = newReturnBlock.iterator().next().asReturn().returnValue(); | |
| newReturnBlock.iterator().next().asReturn().returnValue().replaceUsers(newReturnValue); | |
| Instruction constInstruction = new ConstNumber(newConstValue, 10); | |
| Instruction addInstruction = | |
| new Add(NumericType.INT, newReturnValue, oldReturnValue, newConstValue); | |
| iterator.previous(); | |
| iterator.add(constInstruction); | |
| iterator.add(addInstruction); | |
| addInstruction.setPosition(Position.none()); | |
| constInstruction.setPosition(Position.none()); | |
| } | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals(hitTrueBranch ? "10" : "11", result); | |
| } | |
| @Test | |
| public void splitBeforeReturn() throws Exception { | |
| splitBeforeReturn(false); | |
| splitBeforeReturn(true); | |
| } | |
| private TestApplication codeWithSwitch(boolean hitCase) { | |
| SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); | |
| String returnType = "int"; | |
| List<String> parameters = ImmutableList.of("int"); | |
| MethodSignature signature = builder.addStaticMethod( | |
| returnType, | |
| DEFAULT_METHOD_NAME, | |
| parameters, | |
| 0, | |
| " packed-switch p0, :packed_switch_data", | |
| " const/4 p0, 0x5", | |
| " goto :return", | |
| " :case_0", | |
| " const/4 p0, 0x2", | |
| " goto :return", | |
| " :case_1", | |
| " const/4 p0, 0x3", | |
| " :return", | |
| " return p0", | |
| " :packed_switch_data", | |
| " .packed-switch 0x0", | |
| " :case_0", | |
| " :case_1", | |
| " .end packed-switch" | |
| ); | |
| builder.addMainMethod( | |
| 2, | |
| " sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", | |
| " const/4 v1, " + (hitCase ? "1" : "2"), | |
| " invoke-static { v1 }, LTest;->method(I)I", | |
| " move-result v1", | |
| " invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->print(I)V", | |
| " return-void" | |
| ); | |
| InternalOptions options = new InternalOptions(); | |
| DexApplication application = buildApplication(builder, options); | |
| AppView<?> appView = AppView.createForD8(new AppInfo(application), options); | |
| // Return the processed method for inspection. | |
| MethodSubject methodSubject = getMethodSubject(application, signature); | |
| return new TestApplication(appView, methodSubject); | |
| } | |
| private void runWithSwitchTest(boolean hitCase) throws Exception { | |
| final int initialBlockCount = 5; | |
| final int argumentInstructions = 1; | |
| final int firstBlockInstructions = 2; | |
| // Try split between all non-argument instructions in the first block. | |
| for (int i = argumentInstructions; i < firstBlockInstructions; i++) { | |
| TestApplication test = codeWithSwitch(hitCase); | |
| IRCode code = test.code; | |
| assertEquals(initialBlockCount, code.blocks.size()); | |
| BasicBlock block = code.entryBlock(); | |
| int instructionCount = block.getInstructions().size(); | |
| assertEquals(firstBlockInstructions, instructionCount); | |
| assertEquals(argumentInstructions, test.countArgumentInstructions()); | |
| assertEquals(firstBlockInstructions, block.getInstructions().size()); | |
| assertTrue(!block.getInstructions().get(i).isArgument()); | |
| InstructionListIterator iterator = test.listIteratorAt(block, i); | |
| BasicBlock newBlock = iterator.split(code); | |
| assertTrue(code.isConsistentSSA()); | |
| assertEquals(initialBlockCount + 1, code.blocks.size()); | |
| assertEquals(i + 1, code.entryBlock().getInstructions().size()); | |
| assertEquals(instructionCount - i, newBlock.getInstructions().size()); | |
| assertSame(newBlock, code.blocks.get(1)); | |
| // Run code and check result (code in the test object is updated). | |
| String result = test.run(); | |
| assertEquals(hitCase ? "3" : "5", result); | |
| } | |
| } | |
| @Test | |
| public void withSwitch() throws Exception { | |
| runWithSwitchTest(false); | |
| runWithSwitchTest(true); | |
| } | |
| } |