| // Copyright (c) 2017, 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.graph; | 
 |  | 
 | import com.android.tools.r8.dex.Constants; | 
 | import com.android.tools.r8.graph.DexDebugEvent.StartLocal; | 
 | import com.android.tools.r8.ir.code.Argument; | 
 | import com.android.tools.r8.ir.code.DebugLocalsChange; | 
 | import com.android.tools.r8.ir.code.DebugPosition; | 
 | import com.android.tools.r8.ir.code.Instruction; | 
 | import com.android.tools.r8.ir.code.MoveException; | 
 | import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap; | 
 | import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; | 
 | import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry; | 
 | import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps; | 
 | import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; | 
 | import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap; | 
 | import java.util.ArrayList; | 
 | import java.util.List; | 
 |  | 
 | /** | 
 |  * Builder for constructing a list of debug events suitable for DexDebugInfo. | 
 |  * | 
 |  * This builder is intended to be very pedantic and ensure a well-formed structure of the resulting | 
 |  * event stream. | 
 |  */ | 
 | public class DexDebugEventBuilder { | 
 |  | 
 |   private static final int NO_PC_INFO = -1; | 
 |   private static final int NO_LINE_INFO = -1; | 
 |  | 
 |   private final DexEncodedMethod method; | 
 |   private final DexItemFactory factory; | 
 |  | 
 |   // In order list of non-this argument locals. | 
 |   private ArrayList<DebugLocalInfo> arguments; | 
 |  | 
 |   // Mapping from register to the last known local in that register (See DBG_RESTART_LOCAL). | 
 |   private Int2ReferenceMap<DebugLocalInfo> lastKnownLocals; | 
 |  | 
 |   // Mapping from register to local for currently open/visible locals. | 
 |   private Int2ReferenceMap<DebugLocalInfo> pendingLocals = null; | 
 |  | 
 |   // Conservative pending-state of locals to avoid some equality checks on locals. | 
 |   // pendingLocalChanges == true ==> localsEqual(emittedLocals, pendingLocals). | 
 |   private boolean pendingLocalChanges = false; | 
 |  | 
 |   // State of pc, line, file and locals in the emitted event stream. | 
 |   private int emittedPc = NO_PC_INFO; | 
 |   private int emittedLine = NO_LINE_INFO; | 
 |   private DexString emittedFile = null; | 
 |   private Int2ReferenceMap<DebugLocalInfo> emittedLocals; | 
 |  | 
 |   // Emitted events. | 
 |   private final List<DexDebugEvent> events = new ArrayList<>(); | 
 |  | 
 |   // Initial known line for the method. | 
 |   private int startLine = NO_LINE_INFO; | 
 |  | 
 |   public DexDebugEventBuilder(DexEncodedMethod method, DexItemFactory factory) { | 
 |     this.method = method; | 
 |     this.factory = factory; | 
 |   } | 
 |  | 
 |   /** Add events at pc for instruction. */ | 
 |   public void add(int pc, Instruction instruction) { | 
 |     // Initialize locals state on block entry. | 
 |     if (instruction.getBlock().entry() == instruction) { | 
 |       updateBlockEntry(instruction); | 
 |     } | 
 |     assert pendingLocals != null; | 
 |  | 
 |     if (instruction.isDebugPosition()) { | 
 |       emitDebugPosition(pc, instruction.asDebugPosition()); | 
 |     } else if (instruction.isMoveException()) { | 
 |       MoveException move = instruction.asMoveException(); | 
 |       if (move.getPosition() != null) { | 
 |         emitDebugPosition(pc, move.getPosition()); | 
 |       } | 
 |     } else if (instruction.isArgument()) { | 
 |       startArgument(instruction.asArgument()); | 
 |     } else if (instruction.isDebugLocalsChange()) { | 
 |       updateLocals(instruction.asDebugLocalsChange()); | 
 |     } else if (instruction.getBlock().exit() == instruction) { | 
 |       // If this is the end of the block clear out the pending state. | 
 |       pendingLocals = null; | 
 |       pendingLocalChanges = false; | 
 |     } else if (pc != emittedPc && !instruction.isDebugLocalRead()) { | 
 |       // For non-exit / pc-advancing instructions emit any pending changes once possible. | 
 |       emitLocalChanges(pc); | 
 |     } | 
 |   } | 
 |  | 
 |   /** Build the resulting DexDebugInfo object. */ | 
 |   public DexDebugInfo build() { | 
 |     assert pendingLocals == null; | 
 |     assert !pendingLocalChanges; | 
 |     if (startLine == NO_LINE_INFO) { | 
 |       return null; | 
 |     } | 
 |     DexString[] params = new DexString[method.method.getArity()]; | 
 |     if (arguments != null) { | 
 |       assert params.length == arguments.size(); | 
 |       for (int i = 0; i < arguments.size(); i++) { | 
 |         DebugLocalInfo local = arguments.get(i); | 
 |         params[i] = (local == null || local.signature != null) ? null : local.name; | 
 |       } | 
 |     } | 
 |     return new DexDebugInfo(startLine, params, events.toArray(new DexDebugEvent[events.size()])); | 
 |   } | 
 |  | 
 |   private void updateBlockEntry(Instruction instruction) { | 
 |     assert pendingLocals == null; | 
 |     assert !pendingLocalChanges; | 
 |     Int2ReferenceMap<DebugLocalInfo> locals = instruction.getBlock().getLocalsAtEntry(); | 
 |     if (locals == null) { | 
 |       pendingLocals = Int2ReferenceMaps.emptyMap(); | 
 |     } else { | 
 |       pendingLocals = new Int2ReferenceOpenHashMap<>(locals); | 
 |       pendingLocalChanges = true; | 
 |     } | 
 |     if (emittedLocals == null) { | 
 |       initialize(locals); | 
 |     } | 
 |   } | 
 |  | 
 |   private void initialize(Int2ReferenceMap<DebugLocalInfo> locals) { | 
 |     assert arguments == null; | 
 |     assert emittedLocals == null; | 
 |     assert lastKnownLocals == null; | 
 |     assert startLine == NO_LINE_INFO; | 
 |     if (locals == null) { | 
 |       emittedLocals = Int2ReferenceMaps.emptyMap(); | 
 |       lastKnownLocals = Int2ReferenceMaps.emptyMap(); | 
 |       return; | 
 |     } | 
 |     // Implicitly open all unparameterized arguments. | 
 |     emittedLocals = new Int2ReferenceOpenHashMap<>(); | 
 |     for (Entry<DebugLocalInfo> entry : locals.int2ReferenceEntrySet()) { | 
 |       if (entry.getValue().signature == null) { | 
 |         emittedLocals.put(entry.getIntKey(), entry.getValue()); | 
 |       } | 
 |     } | 
 |     lastKnownLocals = new Int2ReferenceOpenHashMap<>(emittedLocals); | 
 |   } | 
 |  | 
 |   private void startArgument(Argument argument) { | 
 |     if (arguments == null) { | 
 |       arguments = new ArrayList<>(method.method.getArity()); | 
 |     } | 
 |     if (!argument.outValue().isThis()) { | 
 |       arguments.add(argument.getLocalInfo()); | 
 |     } | 
 |   } | 
 |  | 
 |   private void updateLocals(DebugLocalsChange change) { | 
 |     pendingLocalChanges = true; | 
 |     for (Entry<DebugLocalInfo> end : change.getEnding().int2ReferenceEntrySet()) { | 
 |       assert pendingLocals.get(end.getIntKey()) == end.getValue(); | 
 |       pendingLocals.remove(end.getIntKey()); | 
 |     } | 
 |     for (Entry<DebugLocalInfo> start : change.getStarting().int2ReferenceEntrySet()) { | 
 |       assert !pendingLocals.containsKey(start.getIntKey()); | 
 |       pendingLocals.put(start.getIntKey(), start.getValue()); | 
 |     } | 
 |   } | 
 |  | 
 |   private boolean localsChanged() { | 
 |     if (!pendingLocalChanges) { | 
 |       return false; | 
 |     } | 
 |     pendingLocalChanges = !localsEqual(emittedLocals, pendingLocals); | 
 |     return pendingLocalChanges; | 
 |   } | 
 |  | 
 |   private void emitDebugPosition(int pc, DebugPosition position) { | 
 |     emitDebugPosition(pc, position.line, position.file); | 
 |   } | 
 |  | 
 |   private void emitDebugPosition(int pc, int line, DexString file) { | 
 |     if (emittedPc == pc) { | 
 |       // The pc is unchanged then this must be the same position as the previous and we do nothing. | 
 |       // If this assert fails, we have likely forgotten to insert a "nop" instruction between two | 
 |       // successive yet distinct debug positions. | 
 |       assert emittedLine == line && emittedFile == file; | 
 |       return; | 
 |     } | 
 |     boolean changedLocals = localsChanged(); | 
 |     if (!changedLocals && emittedLine == line && emittedFile == file) { | 
 |       return; | 
 |     } | 
 |     if (startLine == NO_LINE_INFO) { | 
 |       assert emittedLine == NO_LINE_INFO; | 
 |       startLine = line; | 
 |       emittedLine = line; | 
 |     } | 
 |     emitAdvancementEvents(emittedPc, emittedLine, emittedFile, pc, line, file, events, factory); | 
 |     emittedPc = pc; | 
 |     emittedLine = line; | 
 |     emittedFile = file; | 
 |     if (changedLocals) { | 
 |       emitLocalChangeEvents(emittedLocals, pendingLocals, lastKnownLocals, events, factory); | 
 |       assert localsEqual(emittedLocals, pendingLocals); | 
 |     } | 
 |     pendingLocalChanges = false; | 
 |   } | 
 |  | 
 |   private void emitLocalChanges(int pc) { | 
 |     // If pc advanced since the locals changed and locals indeed have changed, emit the changes. | 
 |     if (localsChanged()) { | 
 |       emitAdvancementEvents( | 
 |           emittedPc, emittedLine, emittedFile, pc, emittedLine, emittedFile, events, factory); | 
 |       emittedPc = pc; | 
 |       emitLocalChangeEvents(emittedLocals, pendingLocals, lastKnownLocals, events, factory); | 
 |       pendingLocalChanges = false; | 
 |       assert localsEqual(emittedLocals, pendingLocals); | 
 |     } | 
 |   } | 
 |  | 
 |   private static void emitAdvancementEvents( | 
 |       int previousPc, | 
 |       int previousLine, | 
 |       DexString previousFile, | 
 |       int nextPc, | 
 |       int nextLine, | 
 |       DexString nextFile, | 
 |       List<DexDebugEvent> events, | 
 |       DexItemFactory factory) { | 
 |     assert previousPc != nextPc; | 
 |     int pcDelta = previousPc == NO_PC_INFO ? nextPc : nextPc - previousPc; | 
 |     int lineDelta = nextLine == NO_LINE_INFO ? 0 : nextLine - previousLine; | 
 |     assert pcDelta >= 0; | 
 |     if (nextFile != previousFile) { | 
 |       events.add(factory.createSetFile(nextFile)); | 
 |     } | 
 |     if (lineDelta < Constants.DBG_LINE_BASE | 
 |         || lineDelta - Constants.DBG_LINE_BASE >= Constants.DBG_LINE_RANGE) { | 
 |       events.add(factory.createAdvanceLine(lineDelta)); | 
 |       // TODO(herhut): To be super clever, encode only the part that is above limit. | 
 |       lineDelta = 0; | 
 |     } | 
 |     if (pcDelta >= Constants.DBG_ADDRESS_RANGE) { | 
 |       events.add(factory.createAdvancePC(pcDelta)); | 
 |       pcDelta = 0; | 
 |     } | 
 |     // TODO(herhut): Maybe only write this one if needed (would differ from DEX). | 
 |     int specialOpcode = | 
 |         0x0a + (lineDelta - Constants.DBG_LINE_BASE) + Constants.DBG_LINE_RANGE * pcDelta; | 
 |     assert specialOpcode >= 0x0a; | 
 |     assert specialOpcode <= 0xff; | 
 |     events.add(factory.createDefault(specialOpcode)); | 
 |   } | 
 |  | 
 |   public static void emitLocalChangeEvents( | 
 |       Int2ReferenceMap<DebugLocalInfo> previousLocals, | 
 |       Int2ReferenceMap<DebugLocalInfo> nextLocals, | 
 |       Int2ReferenceMap<DebugLocalInfo> lastKnownLocals, | 
 |       List<DexDebugEvent> events, | 
 |       DexItemFactory factory) { | 
 |     Int2ReferenceSortedMap<DebugLocalInfo> ending = new Int2ReferenceAVLTreeMap<>(); | 
 |     Int2ReferenceSortedMap<DebugLocalInfo> starting = new Int2ReferenceAVLTreeMap<>(); | 
 |     for (Entry<DebugLocalInfo> entry : previousLocals.int2ReferenceEntrySet()) { | 
 |       int register = entry.getIntKey(); | 
 |       DebugLocalInfo local = entry.getValue(); | 
 |       if (nextLocals.get(register) != local) { | 
 |         ending.put(register, local); | 
 |       } | 
 |     } | 
 |     for (Entry<DebugLocalInfo> entry : nextLocals.int2ReferenceEntrySet()) { | 
 |       int register = entry.getIntKey(); | 
 |       DebugLocalInfo local = entry.getValue(); | 
 |       if (previousLocals.get(register) != local) { | 
 |         starting.put(register, local); | 
 |       } | 
 |     } | 
 |     assert !ending.isEmpty() || !starting.isEmpty(); | 
 |     for (Entry<DebugLocalInfo> end : ending.int2ReferenceEntrySet()) { | 
 |       int register = end.getIntKey(); | 
 |       if (!starting.containsKey(register)) { | 
 |         previousLocals.remove(register); | 
 |         events.add(factory.createEndLocal(register)); | 
 |       } | 
 |     } | 
 |     for (Entry<DebugLocalInfo> start : starting.int2ReferenceEntrySet()) { | 
 |       int register = start.getIntKey(); | 
 |       DebugLocalInfo local = start.getValue(); | 
 |       previousLocals.put(register, local); | 
 |       if (lastKnownLocals.get(register) == local) { | 
 |         events.add(factory.createRestartLocal(register)); | 
 |       } else { | 
 |         events.add(new StartLocal(register, local)); | 
 |         lastKnownLocals.put(register, local); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   private static boolean localsEqual( | 
 |       Int2ReferenceMap<DebugLocalInfo> locals1, Int2ReferenceMap<DebugLocalInfo> locals2) { | 
 |     if (locals1 == locals2) { | 
 |       return true; | 
 |     } | 
 |     if (locals1.size() != locals2.size()) { | 
 |       return false; | 
 |     } | 
 |     for (Int2ReferenceMap.Entry<DebugLocalInfo> entry : locals1.int2ReferenceEntrySet()) { | 
 |       if (locals2.get(entry.getIntKey()) != entry.getValue()) { | 
 |         return false; | 
 |       } | 
 |     } | 
 |     return true; | 
 |   } | 
 | } |