// 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.DexApplication; import com.android.tools.r8.graph.DexEncodedMethod; 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.ConstType; 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.MoveType; import com.android.tools.r8.ir.code.NumericType; import com.android.tools.r8.ir.code.Value; import com.android.tools.r8.ir.code.ValueNumberGenerator; import com.android.tools.r8.smali.SmaliTestBase; import com.android.tools.r8.utils.InternalOptions; import com.google.common.collect.ImmutableList; import java.util.List; import org.junit.Test; public class SplitBlockTest extends SmaliTestBase { TestApplication codeWithoutCatchHandlers() { SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); String returnType = "int"; List 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); // Return the processed method for inspection. ValueNumberGenerator valueNumberGenerator = new ValueNumberGenerator(); DexEncodedMethod method = getMethod(application, signature); IRCode code = method.buildIR(valueNumberGenerator, new InternalOptions()); return new TestApplication(application, method, code, valueNumberGenerator, options); } @Test public void noCatchHandlers() { 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.blocks.get(0); 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.blocks.get(0).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() { 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.blocks.get(0); 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(1, code); assertTrue(code.isConsistentSSA()); assertEquals(initialBlockCount + 2, code.blocks.size()); assertEquals(i + 1, code.blocks.get(0).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); } } 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 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); // Return the processed method for inspection. ValueNumberGenerator valueNumberGenerator = new ValueNumberGenerator(); DexEncodedMethod method = getMethod(application, signature); IRCode code = method.buildIR(valueNumberGenerator, new InternalOptions()); return new TestApplication(application, method, code, valueNumberGenerator, options); } public void hasCatchandlerIfThrowing(BasicBlock block) { boolean throwing = false; for (Instruction instruction : block.getInstructions()) { throwing |= instruction.instructionTypeCanThrow(); } assertEquals(throwing, block.hasCatchHandlers()); } public void runCatchHandlerTest(boolean codeThrows, boolean twoGuards) { final int secondBlockInstructions = 4; final int initialBlockCount = 5; // 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() { runCatchHandlerTest(false, false); runCatchHandlerTest(true, false); runCatchHandlerTest(false, true); runCatchHandlerTest(true, true); } public void runCatchHandlerSplitThreeTest(boolean codeThrows, boolean twoGuards) { final int secondBlockInstructions = 4; final int initialBlockCount = 5; // 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(1, code); 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() { runCatchHandlerSplitThreeTest(false, false); runCatchHandlerSplitThreeTest(true, false); runCatchHandlerSplitThreeTest(false, true); runCatchHandlerSplitThreeTest(true, true); } TestApplication codeWithIf(boolean hitTrueBranch) { SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); String returnType = "int"; List 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); // Return the processed method for inspection. ValueNumberGenerator valueNumberGenerator = new ValueNumberGenerator(); DexEncodedMethod method = getMethod(application, signature); IRCode code = method.buildIR(valueNumberGenerator, new InternalOptions()); return new TestApplication(application, method, code, valueNumberGenerator, options); } public void runWithIfTest(boolean hitTrueBranch) { final int initialBlockCount = 4; 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.blocks.get(0); 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.blocks.get(0).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() { runWithIfTest(false); runWithIfTest(true); } public void splitBeforeReturn(boolean hitTrueBranch) { TestApplication test = codeWithIf(hitTrueBranch); IRCode code = test.code; // Locate the exit block and split before the return (the first instruction in the block). BasicBlock originalReturnBlock = code.getNormalExitBlock(); BasicBlock newReturnBlock = originalReturnBlock.listIterator().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(), MoveType.SINGLE, null); Value newReturnValue = new Value(test.valueNumberGenerator.next(), MoveType.SINGLE, null); Value oldReturnValue = newReturnBlock.listIterator().next().asReturn().returnValue(); newReturnBlock.listIterator().next().asReturn().returnValue().replaceUsers(newReturnValue); Instruction constInstruction = new ConstNumber(ConstType.INT, newConstValue, 10); Instruction addInstruction = new Add( NumericType.INT, newReturnValue, oldReturnValue, newConstValue); InstructionListIterator iterator = originalReturnBlock.listIterator(); iterator.add(constInstruction); iterator.add(addInstruction); // 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() { splitBeforeReturn(false); splitBeforeReturn(true); } TestApplication codeWithSwitch(boolean hitCase) { SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME); String returnType = "int"; List 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); // Return the processed method for inspection. ValueNumberGenerator valueNumberGenerator = new ValueNumberGenerator(); DexEncodedMethod method = getMethod(application, signature); IRCode code = method.buildIR(valueNumberGenerator, new InternalOptions()); return new TestApplication(application, method, code, valueNumberGenerator, options); } public void runWithSwitchTest(boolean hitCase) { 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.blocks.get(0); 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.blocks.get(0).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() { runWithSwitchTest(false); runWithSwitchTest(true); } }