From 3c93b789020f5a7044c6ede141473dd443e9ff8a Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 9 May 2016 18:13:00 +0300 Subject: [PATCH] Implementing loop inversion --- .../java/org/teavm/common/GraphUtils.java | 13 +- .../org/teavm/common/MutableGraphNode.java | 6 +- .../model/util/InstructionVariableMapper.java | 43 +++- .../teavm/model/util/RegisterAllocator.java | 21 +- .../optimization/LoopInvariantMotion.java | 21 +- .../teavm/optimization/LoopInversionImpl.java | 196 ++++++++++++------ .../org/teavm/jso/impl/JSClassProcessor.java | 44 ++-- .../optimizations/LoopInversionTest.java | 36 ++++ 8 files changed, 229 insertions(+), 151 deletions(-) create mode 100644 tests/src/test/java/org/teavm/optimizations/LoopInversionTest.java diff --git a/core/src/main/java/org/teavm/common/GraphUtils.java b/core/src/main/java/org/teavm/common/GraphUtils.java index 67bbbaa02..c9fdee2f1 100644 --- a/core/src/main/java/org/teavm/common/GraphUtils.java +++ b/core/src/main/java/org/teavm/common/GraphUtils.java @@ -240,8 +240,7 @@ public final class GraphUtils { int[] set = new int[items.length]; int sz = 0; int last = -1; - for (int i = 0; i < items.length; ++i) { - int item = items[i]; + for (int item : items) { if (item != last) { set[sz++] = item; last = item; @@ -252,14 +251,4 @@ public final class GraphUtils { } return set; } - - public static Graph invert(Graph graph) { - GraphBuilder graphBuilder = new GraphBuilder(graph.size()); - for (int node = 0; node < graph.size(); ++node) { - for (int pred : graph.incomingEdges(node)) { - graphBuilder.addEdge(node, pred); - } - } - return graphBuilder.build(); - } } diff --git a/core/src/main/java/org/teavm/common/MutableGraphNode.java b/core/src/main/java/org/teavm/common/MutableGraphNode.java index 981af1848..29e9a6640 100644 --- a/core/src/main/java/org/teavm/common/MutableGraphNode.java +++ b/core/src/main/java/org/teavm/common/MutableGraphNode.java @@ -22,14 +22,14 @@ import java.util.*; * @author Alexey Andreev */ public class MutableGraphNode { - int tag; - Map edges = new HashMap<>(); + private int tag; + final Map edges = new LinkedHashMap<>(); public MutableGraphNode(int tag) { this.tag = tag; } - public MutableGraphEdge connect(MutableGraphNode other) { + private MutableGraphEdge connect(MutableGraphNode other) { MutableGraphEdge edge = edges.get(other); if (edge == null) { edge = new MutableGraphEdge(); diff --git a/core/src/main/java/org/teavm/model/util/InstructionVariableMapper.java b/core/src/main/java/org/teavm/model/util/InstructionVariableMapper.java index 4297c5131..3da89e137 100644 --- a/core/src/main/java/org/teavm/model/util/InstructionVariableMapper.java +++ b/core/src/main/java/org/teavm/model/util/InstructionVariableMapper.java @@ -15,16 +15,43 @@ */ package org.teavm.model.util; +import java.util.function.Function; +import org.teavm.model.BasicBlock; +import org.teavm.model.Incoming; +import org.teavm.model.Instruction; import org.teavm.model.InvokeDynamicInstruction; +import org.teavm.model.Phi; +import org.teavm.model.TryCatchBlock; import org.teavm.model.Variable; import org.teavm.model.instructions.*; -/** - * - * @author Alexey Andreev - */ -public abstract class InstructionVariableMapper implements InstructionVisitor { - protected abstract Variable map(Variable var); +public class InstructionVariableMapper implements InstructionVisitor { + private final Function f; + + public InstructionVariableMapper(Function f) { + this.f = f; + } + + public void apply(BasicBlock block) { + for (Instruction insn : block.getInstructions()) { + insn.acceptVisitor(this); + } + for (Phi phi : block.getPhis()) { + phi.setReceiver(map(phi.getReceiver())); + for (Incoming incoming : phi.getIncomings()) { + incoming.setValue(map(incoming.getValue())); + } + } + for (TryCatchBlock tryCatch : block.getTryCatchBlocks()) { + if (tryCatch.getExceptionVariable() != null) { + tryCatch.setExceptionVariable(map(tryCatch.getExceptionVariable())); + } + } + } + + private Variable map(Variable var) { + return f.apply(var); + } @Override public void visit(EmptyInstruction insn) { @@ -252,8 +279,4 @@ public abstract class InstructionVariableMapper implements InstructionVisitor { public void visit(MonitorExitInstruction insn) { insn.setObjectRef(map(insn.getObjectRef())); } - - - - } diff --git a/core/src/main/java/org/teavm/model/util/RegisterAllocator.java b/core/src/main/java/org/teavm/model/util/RegisterAllocator.java index 59b9b8604..2b407d02e 100644 --- a/core/src/main/java/org/teavm/model/util/RegisterAllocator.java +++ b/core/src/main/java/org/teavm/model/util/RegisterAllocator.java @@ -218,26 +218,11 @@ public class RegisterAllocator { } private void renameVariables(final Program program, final int[] varMap) { - InstructionVariableMapper mapper = new InstructionVariableMapper() { - @Override protected Variable map(Variable var) { - return program.variableAt(varMap[var.getIndex()]); - } - }; + InstructionVariableMapper mapper = new InstructionVariableMapper(var -> + program.variableAt(varMap[var.getIndex()])); for (int i = 0; i < program.basicBlockCount(); ++i) { BasicBlock block = program.basicBlockAt(i); - for (Instruction insn : block.getInstructions()) { - insn.acceptVisitor(mapper); - } - for (Phi phi : block.getPhis()) { - phi.setReceiver(program.variableAt(varMap[phi.getReceiver().getIndex()])); - for (Incoming incoming : phi.getIncomings()) { - incoming.setValue(program.variableAt(varMap[incoming.getValue().getIndex()])); - } - } - for (TryCatchBlock tryCatch : block.getTryCatchBlocks()) { - tryCatch.setExceptionVariable(program.variableAt( - varMap[tryCatch.getExceptionVariable().getIndex()])); - } + mapper.apply(block); } String[][] originalNames = new String[program.variableCount()][]; for (int i = 0; i < program.variableCount(); ++i) { diff --git a/core/src/main/java/org/teavm/optimization/LoopInvariantMotion.java b/core/src/main/java/org/teavm/optimization/LoopInvariantMotion.java index 9d8fc7c68..bf38d7890 100644 --- a/core/src/main/java/org/teavm/optimization/LoopInvariantMotion.java +++ b/core/src/main/java/org/teavm/optimization/LoopInvariantMotion.java @@ -29,7 +29,6 @@ import org.teavm.model.util.*; */ public class LoopInvariantMotion implements MethodOptimization { private int[] preheaders; - private Instruction[] constantInstructions; private LoopGraph graph; private DominatorTree dom; private Program program; @@ -45,7 +44,7 @@ public class LoopInvariantMotion implements MethodOptimization { IntegerStack stack = new IntegerStack(graph.size()); int[] defLocation = new int[program.variableCount()]; Arrays.fill(defLocation, -1); - constantInstructions = new Instruction[program.variableCount()]; + Instruction[] constantInstructions = new Instruction[program.variableCount()]; for (int i = 0; i <= method.parameterCount(); ++i) { defLocation[i] = 0; } @@ -126,7 +125,8 @@ public class LoopInvariantMotion implements MethodOptimization { } } if (variableMap != null) { - insn.acceptVisitor(new VariableMapperImpl(variableMap)); + Variable[] currentVariableMap = variableMap; + insn.acceptVisitor(new InstructionVariableMapper(var -> currentVariableMap[var.getIndex()])); } newInstructions.add(insn); preheaderInstructions.addAll(preheaderInstructions.size() - 1, newInstructions); @@ -208,21 +208,8 @@ public class LoopInvariantMotion implements MethodOptimization { return preheader.getIndex(); } - private static class VariableMapperImpl extends InstructionVariableMapper { - private Variable[] map; - - public VariableMapperImpl(Variable[] map) { - this.map = map; - } - - @Override - protected Variable map(Variable var) { - return map[var.getIndex()]; - } - } - private static class InstructionAnalyzer implements InstructionVisitor { - public boolean canMove; + boolean canMove; public boolean constant; @Override diff --git a/core/src/main/java/org/teavm/optimization/LoopInversionImpl.java b/core/src/main/java/org/teavm/optimization/LoopInversionImpl.java index 7211dbd65..b228f906b 100644 --- a/core/src/main/java/org/teavm/optimization/LoopInversionImpl.java +++ b/core/src/main/java/org/teavm/optimization/LoopInversionImpl.java @@ -67,14 +67,18 @@ import org.teavm.model.util.ProgramUtils; * ``` * * where `condition` is a part of loop that has exits and `body` has no exits. - * More formally, we define *condition end* as a node that postdominates all loop exits. - * Therefore, *condition* is a set of nodes of the loop that are postdominated by condition end and - * all remaining nodes are *body*. + * More formally, we define *body start candidate* as a node which 1) dominates all of the "tails" (i.e. nodes + * that have edges to loop header), 2) does not dominate loop exits. *Body start* is a body start candidate + * that is not dominates by some other body start candidate. If body start does not exits, loop is + * not inversible. + * + * Therefore, *body* is a set of nodes of the loop that are dominated by body start and + * all remaining nodes are *condition*. */ class LoopInversionImpl { - private Program program; + private final Program program; private Graph cfg; - private DominatorTree pdom; + private DominatorTree dom; private boolean postponed; LoopInversionImpl(Program program) { @@ -85,7 +89,7 @@ class LoopInversionImpl { do { cfg = ProgramUtils.buildControlFlowGraph(program); LoopGraph loopGraph = new LoopGraph(cfg); - pdom = GraphUtils.buildDominatorTree(GraphUtils.invert(cfg)); + dom = GraphUtils.buildDominatorTree(cfg); List loops = getLoopsWithExits(loopGraph); postponed = false; @@ -147,11 +151,12 @@ class LoopInversionImpl { final IntSet nodes = new IntOpenHashSet(); final IntSet nodesAndCopies = new IntOpenHashSet(); final IntSet exits = new IntOpenHashSet(); - int conditionEnd; + int bodyStart; int copyStart; int headCopy; final IntIntMap copiedVars = new IntIntOpenHashMap(); final IntIntMap copiedNodes = new IntIntOpenHashMap(); + final IntIntMap varDefinitionPoints = new IntIntOpenHashMap(); boolean shouldSkip; LoopWithExits(int head, LoopWithExits parent) { @@ -175,8 +180,7 @@ class LoopInversionImpl { return false; } - findCondition(); - if (conditionEnd < 0 || !canInvert()) { + if (!findCondition() || bodyStart < 0) { return false; } @@ -187,37 +191,31 @@ class LoopInversionImpl { removeInternalPhiInputsFromCondition(); removeExternalPhiInputsFromConditionCopy(); putNewPhis(); + adjustOutputPhis(); return true; } - private void findCondition() { - IntSet endNodes = new IntOpenHashSet(program.basicBlockCount()); - for (int exit : exits.toArray()) { - for (int successor : cfg.outgoingEdges(exit)) { - if (nodes.contains(successor) && successor != head) { - endNodes.add(successor); - } + private boolean findCondition() { + IntSet tailNodes = new IntOpenHashSet(program.basicBlockCount()); + for (int tailCandidate : cfg.incomingEdges(head)) { + if (nodes.contains(tailCandidate)) { + tailNodes.add(tailCandidate); } } - conditionEnd = pdom.commonDominatorOf(endNodes.toArray()); - } - /** - * We can't invert loop if condition has back edges. Indeed, back edges from `if` statement - * must point inside loop, which makes CFG irreducible. - */ - private boolean canInvert() { - for (int node : nodes.toArray()) { - if (pdom.dominates(conditionEnd, node)) { - for (int successor : cfg.outgoingEdges(node)) { - if (successor == head) { - return false; - } - } + bodyStart = dom.commonDominatorOf(tailNodes.toArray()); + int candidate = bodyStart; + while (bodyStart != head) { + int currentCandidate = candidate; + if (Arrays.stream(exits.toArray()).anyMatch(exit -> dom.dominates(currentCandidate, exit))) { + break; } + bodyStart = candidate; + candidate = dom.immediateDominatorOf(candidate); } - return true; + + return candidate != bodyStart; } private void collectNodesToCopy() { @@ -225,7 +223,7 @@ class LoopInversionImpl { Arrays.sort(nodes); for (int node : nodes) { nodesAndCopies.add(node); - if (pdom.dominates(conditionEnd, node)) { + if (node == head || (node != bodyStart && !dom.dominates(bodyStart, node))) { int copy = program.createBasicBlock().getIndex(); if (head == node) { headCopy = copy; @@ -245,10 +243,12 @@ class LoopInversionImpl { insn.acceptVisitor(definitionExtractor); for (Variable var : definitionExtractor.getDefinedVariables()) { varsToCopy.add(var.getIndex()); + varDefinitionPoints.put(var.getIndex(), node); } } for (Phi phi : block.getPhis()) { varsToCopy.add(phi.getReceiver().getIndex()); + varDefinitionPoints.put(phi.getReceiver().getIndex(), node); } } @@ -260,12 +260,8 @@ class LoopInversionImpl { } private void copyCondition() { - InstructionVariableMapper variableMapper = new InstructionVariableMapper() { - @Override - protected Variable map(Variable var) { - return program.variableAt(copiedVars.getOrDefault(var.getIndex(), var.getIndex())); - } - }; + InstructionVariableMapper variableMapper = new InstructionVariableMapper(var -> + program.variableAt(copiedVars.getOrDefault(var.getIndex(), var.getIndex()))); BasicBlockMapper blockMapper = new BasicBlockMapper() { @Override protected BasicBlock map(BasicBlock block) { @@ -298,7 +294,7 @@ class LoopInversionImpl { incomingCopy.setValue(program.variableAt(copiedVars.getOrDefault(value, value))); phiCopy.getIncomings().add(incomingCopy); } - targetBlock.getPhis().add(phi); + targetBlock.getPhis().add(phiCopy); } for (TryCatchBlock tryCatch : sourceBlock.getTryCatchBlocks()) { @@ -358,55 +354,137 @@ class LoopInversionImpl { BasicBlock block = program.basicBlockAt(headCopy); for (Phi phi : block.getPhis()) { List incomings = phi.getIncomings(); - for (int i = 0; i < incomings.size(); ++i) { - Incoming incoming = incomings.get(i); - if (!nodesAndCopies.contains(incoming.getSource().getIndex())) { - incomings.remove(i--); + List newIncomings = new ArrayList<>(incomings.size()); + for (Incoming incoming : incomings) { + if (nodesAndCopies.contains(incoming.getSource().getIndex())) { + newIncomings.add(incoming); + } else { + for (int exit : exits.toArray()) { + Incoming newIncoming = new Incoming(); + newIncoming.setValue(incoming.getValue()); + newIncoming.setSource(program.basicBlockAt(exit)); + newIncomings.add(newIncoming); + } } } + incomings.clear(); + incomings.addAll(newIncomings); } } /** - * Variables defined in condition should be converted to phis in a new loop head (i.e. condition end). + * Variables defined in condition should be converted to phis in a new loop head (i.e. body start). * Every reference to variable from old condition must be replaced by reference to corresponding phi. */ private void putNewPhis() { - BasicBlock head = program.basicBlockAt(conditionEnd); + BasicBlock head = program.basicBlockAt(bodyStart); IntIntMap phiMap = new IntIntOpenHashMap(); int[] vars = copiedVars.keys().toArray(); Arrays.sort(vars); + List phisToAdd = new ArrayList<>(); for (int var : vars) { + int varCopy = copiedVars.get(var); + Phi phi = new Phi(); phi.setReceiver(program.createVariable()); phiMap.put(var, phi.getReceiver().getIndex()); - head.getPhis().add(phi); + phisToAdd.add(phi); + + for (int source : cfg.incomingEdges(bodyStart)) { + if (!nodes.contains(source)) { + continue; + } - for (int source : cfg.incomingEdges(conditionEnd)) { - int inputVar = copiedNodes.containsKey(source) ? var : copiedVars.get(var); Incoming incoming = new Incoming(); - incoming.setValue(program.variableAt(inputVar)); + incoming.setValue(program.variableAt(var)); incoming.setSource(program.basicBlockAt(source)); phi.getIncomings().add(incoming); + + incoming = new Incoming(); + incoming.setValue(program.variableAt(varCopy)); + incoming.setSource(program.basicBlockAt(copiedNodes.get(source))); + phi.getIncomings().add(incoming); } } - InstructionVariableMapper mapper = new InstructionVariableMapper() { - @Override - protected Variable map(Variable var) { - int index = var.getIndex(); - return program.variableAt(phiMap.getOrDefault(index, index)); - } - }; + InstructionVariableMapper mapper = new InstructionVariableMapper(var -> { + int index = var.getIndex(); + return program.variableAt(phiMap.getOrDefault(index, index)); + }); for (int node : nodes.toArray()) { - if (copiedNodes.containsKey(node)) { + if (!copiedNodes.containsKey(node)) { BasicBlock block = program.basicBlockAt(node); - for (Instruction instruction : block.getInstructions()) { - instruction.acceptVisitor(mapper); + mapper.apply(block); + } + } + + head.getPhis().addAll(phisToAdd); + } + + private void adjustOutputPhis() { + IntIntMap phiMap = new IntIntOpenHashMap(); + class PhiToAdd { + private final Phi phi; + private final BasicBlock target; + private PhiToAdd(Phi phi, BasicBlock target) { + this.phi = phi; + this.target = target; + } + } + List phis = new ArrayList<>(); + + int[] vars = copiedVars.keys().toArray(); + Arrays.sort(vars); + int[] exits = this.exits.toArray(); + Arrays.sort(exits); + + for (int exit : exits) { + for (int var : vars) { + int definedAt = varDefinitionPoints.get(var); + if (!dom.dominates(definedAt, exit)) { + continue; + } + + int varCopy = copiedVars.get(var); + int copiedAt = copiedNodes.get(definedAt); + for (int successor : cfg.outgoingEdges(exit)) { + if (nodes.contains(successor)) { + continue; + } + + Phi phi = new Phi(); + phi.setReceiver(program.createVariable()); + + Incoming originalInput = new Incoming(); + originalInput.setSource(program.basicBlockAt(definedAt)); + originalInput.setValue(program.variableAt(var)); + phi.getIncomings().add(originalInput); + + Incoming copyInput = new Incoming(); + copyInput.setSource(program.basicBlockAt(copiedAt)); + copyInput.setValue(program.variableAt(varCopy)); + phi.getIncomings().add(copyInput); + + phis.add(new PhiToAdd(phi, program.basicBlockAt(successor))); + phiMap.put(var, phi.getReceiver().getIndex()); } } } + + InstructionVariableMapper mapper = new InstructionVariableMapper(var -> { + int index = var.getIndex(); + return program.variableAt(phiMap.getOrDefault(index, index)); + }); + for (int i = 0; i < cfg.size(); ++i) { + if (!nodes.contains(i)) { + mapper.apply(program.basicBlockAt(i)); + } + } + + for (PhiToAdd phiToAdd : phis) { + phiToAdd.target.getPhis().add(phiToAdd.phi); + } } } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java index 1d8379e78..14b5575de 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java @@ -54,16 +54,13 @@ import org.teavm.model.ClassReader; import org.teavm.model.ClassReaderSource; import org.teavm.model.ElementModifier; import org.teavm.model.FieldHolder; -import org.teavm.model.Incoming; import org.teavm.model.Instruction; import org.teavm.model.InstructionLocation; import org.teavm.model.MethodDescriptor; import org.teavm.model.MethodHolder; import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; -import org.teavm.model.Phi; import org.teavm.model.Program; -import org.teavm.model.TryCatchBlock; import org.teavm.model.ValueType; import org.teavm.model.Variable; import org.teavm.model.instructions.AssignInstruction; @@ -82,15 +79,15 @@ import org.teavm.model.util.ProgramUtils; * @author Alexey Andreev */ class JSClassProcessor { - private ClassReaderSource classSource; - private JSBodyRepository repository; - private JavaInvocationProcessor javaInvocationProcessor; + private final ClassReaderSource classSource; + private final JSBodyRepository repository; + private final JavaInvocationProcessor javaInvocationProcessor; private Program program; - private List replacement = new ArrayList<>(); - private JSTypeHelper typeHelper; - private Diagnostics diagnostics; + private final List replacement = new ArrayList<>(); + private final JSTypeHelper typeHelper; + private final Diagnostics diagnostics; private int methodIndexGenerator; - private Map overridenMethodCache = new HashMap<>(); + private final Map overridenMethodCache = new HashMap<>(); public JSClassProcessor(ClassReaderSource classSource, JSBodyRepository repository, Diagnostics diagnostics) { this.classSource = classSource; @@ -170,32 +167,15 @@ class JSClassProcessor { callerMethod.getModifiers().add(ElementModifier.STATIC); final Program program = ProgramUtils.copy(method.getProgram()); program.createVariable(); - InstructionVariableMapper variableMapper = new InstructionVariableMapper() { - @Override protected Variable map(Variable var) { - return program.variableAt(var.getIndex() + 1); - } - }; + InstructionVariableMapper variableMapper = new InstructionVariableMapper(var -> + program.variableAt(var.getIndex() + 1)); for (int i = program.variableCount() - 1; i > 0; --i) { program.variableAt(i).getDebugNames().addAll(program.variableAt(i - 1).getDebugNames()); program.variableAt(i - 1).getDebugNames().clear(); } for (int i = 0; i < program.basicBlockCount(); ++i) { BasicBlock block = program.basicBlockAt(i); - for (Instruction insn : block.getInstructions()) { - insn.acceptVisitor(variableMapper); - } - for (Phi phi : block.getPhis()) { - phi.setReceiver(program.variableAt(phi.getReceiver().getIndex() + 1)); - for (Incoming incoming : phi.getIncomings()) { - incoming.setValue(program.variableAt(incoming.getValue().getIndex() + 1)); - } - } - for (TryCatchBlock tryCatch : block.getTryCatchBlocks()) { - if (tryCatch.getExceptionVariable() != null) { - tryCatch.setExceptionVariable(program.variableAt( - tryCatch.getExceptionVariable().getIndex() + 1)); - } - } + variableMapper.apply(block); } callerMethod.setProgram(program); ModelUtils.copyAnnotations(method.getAnnotations(), callerMethod.getAnnotations()); @@ -277,7 +257,7 @@ class JSClassProcessor { } } - static ValueType[] getStaticSignature(MethodReference method) { + private static ValueType[] getStaticSignature(MethodReference method) { ValueType[] signature = method.getSignature(); ValueType[] staticSignature = new ValueType[signature.length + 1]; for (int i = 0; i < signature.length; ++i) { @@ -287,7 +267,7 @@ class JSClassProcessor { return staticSignature; } - public void processProgram(MethodHolder methodToProcess) { + void processProgram(MethodHolder methodToProcess) { program = methodToProcess.getProgram(); for (int i = 0; i < program.basicBlockCount(); ++i) { BasicBlock block = program.basicBlockAt(i); diff --git a/tests/src/test/java/org/teavm/optimizations/LoopInversionTest.java b/tests/src/test/java/org/teavm/optimizations/LoopInversionTest.java new file mode 100644 index 000000000..dada88ac6 --- /dev/null +++ b/tests/src/test/java/org/teavm/optimizations/LoopInversionTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.optimizations; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class LoopInversionTest { + @Test + public void respectsLoopOutput() { + int a = 0; + int b = 1; + for (int i = 0; i < 10; ++i) { + int c = a + b; + a = b; + b = c; + } + assertEquals(55, a); + } +}