// Copyright 2019 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "iree/compiler/Dialect/VM/Analysis/RegisterAllocation.h"

#include <algorithm>
#include <utility>

#include "iree/compiler/Dialect/Util/IR/UtilTypes.h"
#include "iree/compiler/Dialect/VM/Analysis/LinearScan/LiveIntervals.h"
#include "iree/compiler/Dialect/VM/Analysis/LinearScan/RegisterBank.h"
#include "iree/compiler/Dialect/VM/IR/VMOps.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/FormatVariadic.h"
#include "mlir/IR/Attributes.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/Dominance.h"

namespace mlir::iree_compiler {

static Attribute getStrArrayAttr(Builder &builder,
                                 ArrayRef<std::string> values) {
  return builder.getStrArrayAttr(llvm::map_to_vector<8>(
      values, [](const std::string &value) { return StringRef(value); }));
}

// static
LogicalResult RegisterAllocation::annotateIR(IREE::VM::FuncOp funcOp) {
  RegisterAllocation registerAllocation;
  if (failed(registerAllocation.recalculate(funcOp))) {
    funcOp.emitOpError() << "failed to allocate registers for function";
    return failure();
  }

  Builder builder(funcOp.getContext());
  for (auto &block : funcOp.getBlocks()) {
    SmallVector<std::string, 8> blockRegStrs;
    blockRegStrs.reserve(block.getNumArguments());
    for (auto blockArg : block.getArguments()) {
      auto reg = registerAllocation.map_[blockArg];
      blockRegStrs.push_back(reg.toString());
    }
    block.front().setAttr("block_registers",
                          getStrArrayAttr(builder, blockRegStrs));

    for (auto &op : block.getOperations()) {
      // Emit operand registers with MOVE bits for ref operands.
      if (op.getNumOperands() > 0) {
        SmallVector<std::string, 8> operandRegStrs;
        operandRegStrs.reserve(op.getNumOperands());

        // For branch operations, use remapSuccessorRegisters to get the
        // correct MOVE bits for branch arguments.
        // Build a map from operand index to the correct register with MOVE bit.
        llvm::DenseMap<unsigned, Register> branchOperandRegs;
        if (auto branchOp = dyn_cast<BranchOpInterface>(&op)) {
          for (unsigned i = 0; i < op.getNumSuccessors(); ++i) {
            auto srcDstRegs =
                registerAllocation.remapSuccessorRegisters(&op, i);
            auto succOperands =
                branchOp.getSuccessorOperands(i).getForwardedOperands();
            if (succOperands.empty()) {
              continue;
            }
            unsigned baseIdx = succOperands.getBeginOperandIndex();
            // remapSuccessorRegisters only returns pairs where src != dst.
            // For display, we need ALL operands with correct MOVE bits.
            // Re-compute with the same logic as remapSuccessorRegisters.
            (void)srcDstRegs; // Unused - we recompute for display.

            // Build lastOccurrence map: only the last occurrence of a ref value
            // in this successor's operand list can get MOVE (matches
            // remapSuccessorRegisters logic).
            llvm::DenseMap<Value, unsigned> lastOccurrence;
            for (auto [idx, operand] : llvm::enumerate(succOperands)) {
              if (isa<IREE::VM::RefType>(operand.getType())) {
                lastOccurrence[operand] = idx;
              }
            }

            for (auto [localIdx, operand] : llvm::enumerate(succOperands)) {
              unsigned globalIdx = baseIdx + localIdx;
              auto srcReg = registerAllocation.mapToRegister(operand);
              // Apply same MOVE logic as remapSuccessorRegisters.
              if (srcReg.isRef()) {
                bool isLastOccurrence =
                    (lastOccurrence.lookup(operand) == localIdx);
                bool isLastUse =
                    registerAllocation.liveness_.isLastRealValueUse(
                        operand, &op, globalIdx);
                if (isLastOccurrence && isLastUse) {
                  srcReg.setMove(true);
                }
              }
              branchOperandRegs[globalIdx] = srcReg;
            }
          }
        }

        for (auto &operand : op.getOpOperands()) {
          unsigned idx = operand.getOperandNumber();
          Register reg;
          if (branchOperandRegs.count(idx)) {
            // Use the pre-computed register with correct MOVE bit.
            reg = branchOperandRegs[idx];
          } else {
            // Use standard mapUseToRegister for non-branch operands.
            reg = registerAllocation.mapUseToRegister(operand.get(), &op, idx);
          }
          operandRegStrs.push_back(reg.toString());
        }
        op.setAttr("operand_registers",
                   getStrArrayAttr(builder, operandRegStrs));
      }
      if (op.getNumResults() == 0) {
        continue;
      }
      SmallVector<std::string, 8> regStrs;
      regStrs.reserve(op.getNumResults());
      for (auto result : op.getResults()) {
        auto reg = registerAllocation.map_[result];
        regStrs.push_back(reg.toString());
      }
      op.setAttr("result_registers", getStrArrayAttr(builder, regStrs));
    }

    Operation *terminatorOp = block.getTerminator();
    if (terminatorOp->getNumSuccessors() > 0) {
      SmallVector<Attribute, 2> successorAttrs;
      successorAttrs.reserve(terminatorOp->getNumSuccessors());
      for (int i = 0; i < terminatorOp->getNumSuccessors(); ++i) {
        auto srcDstRegs =
            registerAllocation.remapSuccessorRegisters(terminatorOp, i);
        SmallVector<std::string, 8> remappingStrs;
        for (auto &srcDstReg : srcDstRegs) {
          remappingStrs.push_back(llvm::formatv("{}->{}",
                                                srcDstReg.first.toString(),
                                                srcDstReg.second.toString())
                                      .str());
        }
        successorAttrs.push_back(getStrArrayAttr(builder, remappingStrs));
      }
      terminatorOp->setAttr("remap_registers",
                            builder.getArrayAttr(successorAttrs));
    }
  }

  return success();
}

// Sorts blocks in dominance order such that the entry block is first and
// all of the following blocks are dominated only by blocks that have come
// before them in the list. This ensures that we always know all registers for
// block live-in values as we walk the blocks.
static SmallVector<Block *, 8>
sortBlocksInDominanceOrder(IREE::VM::FuncOp funcOp) {
  if (funcOp.getBlocks().size() == 1) {
    // Dominance info cannot be computed for regions with one block.
    return {&funcOp.getBlocks().front()};
  }

  DominanceInfo dominanceInfo(funcOp);
  llvm::SmallSetVector<Block *, 8> unmarkedBlocks;
  for (auto &block : funcOp.getBlocks()) {
    unmarkedBlocks.insert(&block);
  }
  llvm::SmallSetVector<Block *, 8> markedBlocks;
  std::function<void(Block *)> visit = [&](Block *block) {
    if (markedBlocks.contains(block)) {
      return;
    }
    for (auto *childBlock : dominanceInfo.getNode(block)->children()) {
      visit(childBlock->getBlock());
    }
    markedBlocks.insert(block);
  };
  while (!unmarkedBlocks.empty()) {
    visit(unmarkedBlocks.pop_back_val());
  }
  auto orderedBlocks = markedBlocks.takeVector();
  std::reverse(orderedBlocks.begin(), orderedBlocks.end());
  return orderedBlocks;
}

// Runs linear scan allocation for a single register bank.
// Processes intervals matching isRefBank, allocating from bank into active
// list. Updates map_ and intervalAllocations_ for each allocated interval.
LogicalResult RegisterAllocation::runLinearScan(
    IREE::VM::FuncOp funcOp, const LiveIntervals &liveIntervals,
    IREE::VM::RegisterBank &bank, SmallVectorImpl<const LiveInterval *> &active,
    const llvm::DenseMap<Value, Value> &coalesceSource, bool isRefBank,
    int &maxOrdinal) {

  // Helper to expire old intervals.
  // Use < (not <=) so intervals ending at currentStart are NOT expired yet.
  // This prevents the bug where multiple block args with the same start
  // position get assigned the same register.
  auto expireOldIntervals = [&](uint32_t currentStart) {
    while (!active.empty() && active.front()->end < currentStart) {
      const LiveInterval *expired = active.front();
      size_t byteWidth = isRefBank ? 0 : expired->byteWidth;
      bank.release(intervalAllocations_[expired].assigned, byteWidth);
      active.erase(active.begin());
    }
  };

  // Helper to insert into active list (maintain sorted by end).
  auto insertActive = [&](const LiveInterval *interval) {
    auto it =
        std::lower_bound(active.begin(), active.end(), interval,
                         [](const LiveInterval *a, const LiveInterval *b) {
                           return a->end < b->end;
                         });
    active.insert(it, interval);
  };

  for (const LiveInterval *interval : liveIntervals.getSortedByStart()) {
    // Filter by bank type.
    if (interval->isRef != isRefBank) {
      continue;
    }

    // Skip already-assigned values (entry block args).
    if (map_.count(interval->value)) {
      continue;
    }

    // Expire intervals that ended before this one starts.
    expireOldIntervals(interval->start);

    size_t byteWidth = isRefBank ? 0 : interval->byteWidth;

    // Try coalescing: reuse register from a value whose interval ends exactly
    // where ours begins.
    int ordinal = -1;
    auto coalesceIt = coalesceSource.find(interval->value);
    if (coalesceIt != coalesceSource.end()) {
      auto srcIt = map_.find(coalesceIt->second);
      if (srcIt != map_.end()) {
        int preferredOrdinal = srcIt->second.ordinal();
        if (bank.allocateAt(preferredOrdinal, byteWidth)) {
          ordinal = preferredOrdinal;
        } else {
          // The source register may still be "active" (not expired due to <
          // check). Find and expire it to complete the hand-off.
          for (auto it = active.begin(); it != active.end(); ++it) {
            if (intervalAllocations_[*it].assigned == preferredOrdinal &&
                (*it)->end == interval->start) {
              size_t expiredByteWidth = isRefBank ? 0 : (*it)->byteWidth;
              bank.release(intervalAllocations_[*it].assigned,
                           expiredByteWidth);
              active.erase(it);
              if (bank.allocateAt(preferredOrdinal, byteWidth)) {
                ordinal = preferredOrdinal;
              }
              break;
            }
          }
        }
      }
    }

    // Fall back to first-fit allocation.
    if (ordinal < 0) {
      auto allocated = bank.allocate(byteWidth);
      if (!allocated) {
        return funcOp.emitError()
               << "register allocation failed (" << (isRefBank ? "ref" : "int")
               << " bank overflow)";
      }
      ordinal = *allocated;
    }

    // Store assignment in map.
    intervalAllocations_[interval].assigned = ordinal;
    if (isRefBank) {
      map_[interval->value] = Register::getRef(interval->value.getType(),
                                               ordinal, /*isMove=*/false);
    } else {
      map_[interval->value] =
          Register::getValue(interval->value.getType(), ordinal);
    }

    // Add to active set.
    insertActive(interval);
  }

  maxOrdinal = bank.getMaxUsed();
  return success();
}

// Linear scan allocator implementation.
// Uses LiveIntervals for global register lifetime tracking and enables
// cross-block register reuse for both integer and reference registers.
LogicalResult RegisterAllocation::recalculate(IREE::VM::FuncOp funcOp) {
  map_.clear();
  intervalAllocations_.clear();
  dominanceInfo_.reset();
  maxI32RegisterOrdinal_ = -1;
  maxRefRegisterOrdinal_ = -1;
  scratchI32RegisterCount_ = 0;
  scratchRefRegisterCount_ = 0;

  // Build live intervals for global allocation.
  LiveIntervals liveIntervals;
  if (failed(liveIntervals.build(funcOp))) {
    return funcOp.emitError() << "failed to build live intervals";
  }

  // Also compute liveness for MOVE bit computation (still needed).
  if (failed(liveness_.recalculate(funcOp))) {
    return funcOp.emitError()
           << "failed to calculate required liveness information";
  }

  // Compute dominance info for back-edge detection in MOVE bit logic.
  // Only needed for functions with more than one block.
  if (funcOp.getBlocks().size() > 1) {
    dominanceInfo_.emplace(funcOp);
  }

  // Global register banks (single instance for entire function).
  IREE::VM::RegisterBank intBank(Register::kInt32RegisterCount);
  IREE::VM::RegisterBank refBank(Register::kRefRegisterCount);

  // ===== PHASE A: Pre-allocate entry block args (monotonic, ABI) =====
  // Also set the assigned field in the interval so it can be properly expired.
  Block *entryBlock = &funcOp.getBlocks().front();
  for (auto blockArg : entryBlock->getArguments()) {
    Type type = blockArg.getType();
    if (isa<IREE::VM::RefType>(type)) {
      // Entry block ref args: monotonic allocation for ABI stability.
      auto ordinal = refBank.allocate(0, /*fromEnd=*/true);
      if (!ordinal) {
        return funcOp.emitError()
               << "register allocation failed for entry block arg "
               << blockArg.getArgNumber() << " (ref bank overflow)";
      }
      map_[blockArg] = Register::getRef(type, *ordinal, /*isMove=*/false);
      // Set assigned in interval for expiration tracking.
      if (const LiveInterval *interval = liveIntervals.getInterval(blockArg)) {
        intervalAllocations_[interval].assigned = *ordinal;
      }
    } else if (type.isIntOrFloat()) {
      // Entry block int args: monotonic allocation for ABI stability.
      size_t byteWidth = IREE::Util::getRoundedElementByteWidth(type);
      auto ordinal = intBank.allocate(byteWidth, /*fromEnd=*/true);
      if (!ordinal) {
        return funcOp.emitError()
               << "register allocation failed for entry block arg "
               << blockArg.getArgNumber() << " (int bank overflow)";
      }
      map_[blockArg] = Register::getValue(type, *ordinal);
      // Set assigned in interval for expiration tracking.
      if (const LiveInterval *interval = liveIntervals.getInterval(blockArg)) {
        intervalAllocations_[interval].assigned = *ordinal;
      }
    }
  }

  // ===== PHASE B: Compute coalescing candidates =====
  // Find values that could potentially share registers via hand-off.
  // A value V can inherit register from value U if:
  //   1. U's interval ends exactly where V's begins (hand-off point)
  //   2. U and V have compatible types (same register bank)
  //
  // We store U (the source) for each V (the dest), and resolve to register
  // ordinals during allocation when U has been assigned.
  llvm::DenseMap<Value, Value> coalesceSource;

  auto recordCoalesceCandidate = [&](Value dest, Value src) {
    if (dest.getType() != src.getType()) {
      return;
    }
    auto srcInterval = liveIntervals.getInterval(src);
    auto destInterval = liveIntervals.getInterval(dest);
    if (!srcInterval || !destInterval) {
      return;
    }
    // Only coalesce if intervals meet exactly (hand-off).
    if (srcInterval->end != destInterval->start) {
      return;
    }
    coalesceSource[dest] = src;
  };

  // Scan all operations for coalescing opportunities.
  for (auto *block : liveIntervals.getBlockOrder()) {
    // Block arguments can coalesce with branch operands from predecessors.
    for (auto *pred : block->getPredecessors()) {
      auto branchOp = dyn_cast<BranchOpInterface>(pred->getTerminator());
      if (!branchOp) {
        continue;
      }
      for (unsigned succIdx = 0;
           succIdx < pred->getTerminator()->getNumSuccessors(); ++succIdx) {
        if (pred->getTerminator()->getSuccessor(succIdx) != block) {
          continue;
        }
        OperandRange operands =
            branchOp.getSuccessorOperands(succIdx).getForwardedOperands();
        for (auto [idx, operand] : llvm::enumerate(operands)) {
          if (idx >= block->getNumArguments()) {
            break;
          }
          recordCoalesceCandidate(block->getArgument(idx), operand);
        }
      }
    }

    // Operation results can coalesce with operands whose intervals end here.
    for (auto &op : block->getOperations()) {
      for (Value result : op.getResults()) {
        for (Value operand : op.getOperands()) {
          recordCoalesceCandidate(result, operand);
        }
      }
    }
  }

  // ===== PHASE C+D: Linear scan for both register banks =====
  // Active intervals sorted by end position (one list per bank).
  SmallVector<const LiveInterval *> activeInt;
  SmallVector<const LiveInterval *> activeRef;

  // Helper to seed active list with pre-allocated entry block arg intervals.
  // This allows their registers to be expired and reused.
  auto seedActive = [&](SmallVectorImpl<const LiveInterval *> &active,
                        auto typeCheck) {
    for (auto blockArg : entryBlock->getArguments()) {
      if (typeCheck(blockArg.getType())) {
        if (const LiveInterval *interval =
                liveIntervals.getInterval(blockArg)) {
          auto it = std::lower_bound(
              active.begin(), active.end(), interval,
              [](const LiveInterval *a, const LiveInterval *b) {
                return a->end < b->end;
              });
          active.insert(it, interval);
        }
      }
    }
  };
  seedActive(activeInt, [](Type t) { return t.isIntOrFloat(); });
  seedActive(activeRef, [](Type t) { return isa<IREE::VM::RefType>(t); });

  // Run linear scan for each bank.
  if (failed(runLinearScan(funcOp, liveIntervals, intBank, activeInt,
                           coalesceSource, /*isRefBank=*/false,
                           maxI32RegisterOrdinal_))) {
    return failure();
  }
  if (failed(runLinearScan(funcOp, liveIntervals, refBank, activeRef,
                           coalesceSource, /*isRefBank=*/true,
                           maxRefRegisterOrdinal_))) {
    return failure();
  }

  // Verify bounds.
  if (maxI32RegisterOrdinal_ > Register::kInt32RegisterCount ||
      maxRefRegisterOrdinal_ > Register::kRefRegisterCount) {
    return funcOp.emitError() << "function overflows stack register banks; "
                                 "spilling to memory not yet implemented";
  }

  // Identify discards that can be elided because their operands were released
  // via MOVE on preceding operations.
  computeElidableDiscards(funcOp);

  return success();
}

void RegisterAllocation::computeElidableDiscards(IREE::VM::FuncOp funcOp) {
  discardOperandElidability_.clear();

  // For each discard operand, check if it was released via MOVE on a preceding
  // operation. An operand is elidable when:
  // 1. The value has a "real" use (non-discard) before this discard in the
  //    same block, AND
  // 2. That real use actually got MOVE set (isLastRealValueUse is true and
  //    the op implements RefMoveInterface)
  //
  // Values that only have the discard as their use in a block (e.g., coming
  // from block arguments or definitions in predecessors with no real use
  // before the discard) cannot be elided - the discard is the only thing
  // releasing them.
  for (auto &block : funcOp.getBlocks()) {
    for (auto &op : block.getOperations()) {
      auto discardOp = dyn_cast<IREE::VM::DiscardRefsOp>(&op);
      if (!discardOp) {
        continue;
      }

      SmallVector<bool> operandElidability;
      for (Value ref : discardOp.getRefs()) {
        // Check if there's a preceding real use in this block that got MOVE.
        bool hasPrecedingMoveUse = false;
        for (auto it = Block::iterator(discardOp); it != block.begin();) {
          --it;
          Operation *precedingOp = &*it;
          for (OpOperand &operand : precedingOp->getOpOperands()) {
            if (operand.get() == ref &&
                !isa<IREE::VM::DiscardRefsOp>(precedingOp)) {
              // Found a real use before the discard.
              // Check if MOVE was actually set on this use.
              if (liveness_.isLastRealValueUse(ref, precedingOp,
                                               operand.getOperandNumber())) {
                // Also check if the op implements RefMoveInterface and says
                // this operand is movable.
                if (auto moveOp =
                        dyn_cast<IREE::VM::RefMoveInterface>(precedingOp)) {
                  if (moveOp.isRefOperandMovable(operand.getOperandNumber())) {
                    hasPrecedingMoveUse = true;
                  }
                }
              }
              break;
            }
          }
          if (hasPrecedingMoveUse) {
            break;
          }
        }
        operandElidability.push_back(hasPrecedingMoveUse);
      }
      discardOperandElidability_[discardOp] = std::move(operandElidability);
    }
  }
}

Register RegisterAllocation::mapToRegister(Value value) const {
  auto it = map_.find(value);
  assert(it != map_.end());
  return it->getSecond();
}

Register RegisterAllocation::mapUseToRegister(Value value, Operation *useOp,
                                              int operandIndex) {
  auto reg = mapToRegister(value);
  if (reg.isRef() && liveness_.isLastRealValueUse(value, useOp, operandIndex)) {
    // Set MOVE bit when this is the last real use of the value. Only set MOVE
    // if the op implements RefMoveInterface and says this operand is movable.
    // This prevents setting MOVE on ops that don't honor it in the runtime.
    if (auto moveOp = dyn_cast<IREE::VM::RefMoveInterface>(useOp)) {
      if (moveOp.isRefOperandMovable(operandIndex)) {
        reg.setMove(true);
      }
    }
  }
  return reg;
}

// A feedback arc set containing the minimal list of cycle-causing edges.
// https://en.wikipedia.org/wiki/Feedback_arc_set
struct FeedbackArcSet {
  using NodeID = Register;
  using Edge = std::pair<NodeID, NodeID>;

  // Edges making up a DAG (inputEdges - feedbackEdges).
  SmallVector<Edge, 8> acyclicEdges;

  // Edges of the feedback arc set that, if added to acyclicEdges, would cause
  // cycles.
  SmallVector<Edge, 8> feedbackEdges;

  // Computes the FAS of a given directed graph that may contain cycles.
  static FeedbackArcSet compute(ArrayRef<Edge> inputEdges) {
    FeedbackArcSet result;
    if (inputEdges.empty()) {
      return result;
    } else if (inputEdges.size() == 1) {
      result.acyclicEdges.push_back(inputEdges.front());
      return result;
    }

    struct FASNode {
      NodeID id;
      int indegree = 0;
      int outdegree = 0;
    };
    // This should not be modified after creation in this loop. We take pointers
    // to its entries so do not want to invalidate them with reallocation.
    llvm::SmallDenseMap<NodeID, FASNode> nodes;
    for (auto &edge : inputEdges) {
      NodeID sourceID = edge.first.asBaseRegister();
      NodeID sinkID = edge.second.asBaseRegister();
      assert(sourceID != sinkID && "self-cycles not supported");
      if (nodes.count(sourceID) == 0) {
        nodes.insert({sourceID, {sourceID, 0, 0}});
      }
      if (nodes.count(sinkID) == 0) {
        nodes.insert({sinkID, {sinkID, 0, 0}});
      }
    }

    struct FASEdge {
      FASNode *source;
      FASNode *sink;
    };
    int maxOutdegree = 0;
    int maxIndegree = 0;
    SmallVector<FASEdge, 8> edges;
    for (auto &edge : inputEdges) {
      NodeID sourceID = edge.first.asBaseRegister();
      NodeID sinkID = edge.second.asBaseRegister();
      auto &sourceNode = nodes[sourceID];
      ++sourceNode.outdegree;
      maxOutdegree = std::max(maxOutdegree, sourceNode.outdegree);
      auto &sinkNode = nodes[sinkID];
      ++sinkNode.indegree;
      maxIndegree = std::max(maxIndegree, sinkNode.indegree);
      edges.push_back({&sourceNode, &sinkNode});
    }

    std::vector<SmallVector<FASNode *, 2>> buckets;
    buckets.resize(std::max(maxOutdegree, maxIndegree) + 2);
    auto nodeToBucketIndex = [&](FASNode *node) {
      return node->indegree == 0 || node->outdegree == 0
                 ? buckets.size() - 1
                 : std::abs(node->outdegree - node->indegree);
    };
    auto assignBucket = [&](FASNode *node) {
      buckets[nodeToBucketIndex(node)].push_back(node);
    };
    auto removeBucket = [&](FASNode *node) {
      int index = nodeToBucketIndex(node);
      auto it = std::find(buckets[index].begin(), buckets[index].end(), node);
      if (it != buckets[index].end()) {
        buckets[index].erase(it);
      }
    };
    llvm::SmallPtrSet<FASNode *, 8> remainingNodes;
    for (auto &nodeEntry : nodes) {
      assignBucket(&nodeEntry.getSecond());
      remainingNodes.insert(&nodeEntry.getSecond());
    }

    auto removeNode = [&](FASNode *node) {
      SmallVector<FASEdge> inEdges;
      inEdges.reserve(node->indegree);
      SmallVector<FASEdge> outEdges;
      outEdges.reserve(node->outdegree);
      for (auto &edge : edges) {
        if (edge.sink == node) {
          inEdges.push_back(edge);
        }
        if (edge.source == node) {
          outEdges.push_back(edge);
        }
      }
      bool collectInEdges = node->indegree <= node->outdegree;
      bool collectOutEdges = !collectInEdges;

      SmallVector<Edge> results;
      for (auto &edge : inEdges) {
        if (edge.source == node) {
          continue;
        }
        if (collectInEdges) {
          results.push_back({edge.source->id, edge.sink->id});
        }
        removeBucket(edge.source);
        --edge.source->outdegree;
        assert(edge.source->outdegree >= 0 && "outdegree has become negative");
        assignBucket(edge.source);
      }
      for (auto &edge : outEdges) {
        if (edge.sink == node) {
          continue;
        }
        if (collectOutEdges) {
          results.push_back({edge.source->id, edge.sink->id});
        }
        removeBucket(edge.sink);
        --edge.sink->indegree;
        assert(edge.sink->indegree >= 0 && "indegree has become negative");
        assignBucket(edge.sink);
      }

      remainingNodes.erase(node);
      edges.erase(std::remove_if(edges.begin(), edges.end(),
                                 [&](const FASEdge &edge) {
                                   return edge.source == node ||
                                          edge.sink == node;
                                 }),
                  edges.end());
      return results;
    };
    auto ends = buckets.back();
    while (!remainingNodes.empty()) {
      while (!ends.empty()) {
        auto *node = ends.front();
        ends.erase(ends.begin());
        removeNode(node);
      }
      if (remainingNodes.empty()) {
        break;
      }
      for (ssize_t i = buckets.size() - 1; i >= 0; --i) {
        if (buckets[i].empty()) {
          continue;
        }
        auto *bucket = buckets[i].front();
        buckets[i].erase(buckets[i].begin());
        auto feedbackEdges = removeNode(bucket);
        result.feedbackEdges.append(feedbackEdges.begin(), feedbackEdges.end());
        break;
      }
    }

    // Build the DAG of the remaining edges now that we've isolated the ones
    // that cause cycles.
    llvm::SmallSetVector<NodeID, 8> acyclicNodes;
    SmallVector<Edge, 8> acyclicEdges;
    for (auto &inputEdge : inputEdges) {
      auto it = std::find_if(result.feedbackEdges.begin(),
                             result.feedbackEdges.end(), [&](const Edge &edge) {
                               return edge.first == inputEdge.first &&
                                      edge.second == inputEdge.second;
                             });
      if (it == result.feedbackEdges.end()) {
        acyclicEdges.push_back(inputEdge);
        acyclicNodes.insert(inputEdge.first.asBaseRegister());
        acyclicNodes.insert(inputEdge.second.asBaseRegister());
      }
    }

    // Topologically sort the DAG so that we don't overwrite anything.
    llvm::SmallSetVector<NodeID, 8> unmarkedNodes = acyclicNodes;
    llvm::SmallSetVector<NodeID, 8> markedNodes;
    std::function<void(NodeID)> visit = [&](NodeID node) {
      if (markedNodes.contains(node)) {
        return;
      }
      for (auto &edge : acyclicEdges) {
        if (edge.first != node) {
          continue;
        }
        visit(edge.second);
      }
      markedNodes.insert(node);
    };
    while (!unmarkedNodes.empty()) {
      visit(unmarkedNodes.pop_back_val());
    }
    for (auto node : markedNodes.takeVector()) {
      for (auto &edge : acyclicEdges) {
        if (edge.first != node) {
          continue;
        }
        result.acyclicEdges.push_back({edge.first, edge.second});
      }
    }

    return result;
  }
};

SmallVector<std::pair<Register, Register>, 8>
RegisterAllocation::remapSuccessorRegisters(Operation *op, int successorIndex) {
  auto branchOp = cast<BranchOpInterface>(op);
  auto *targetBlock = op->getSuccessor(successorIndex);
  auto targetOperands =
      branchOp.getSuccessorOperands(successorIndex).getForwardedOperands();

  // Get the base operand index for this successor's operands in the branch op.
  // This is needed to query isLastRealValueUse with the correct global index.
  // Note: getBeginOperandIndex() asserts on empty ranges, so we handle that.
  unsigned baseOperandIndex =
      targetOperands.empty() ? 0 : targetOperands.getBeginOperandIndex();

  return remapSuccessorRegisters(op, targetBlock, targetOperands,
                                 baseOperandIndex);
}

SmallVector<std::pair<Register, Register>, 8>
RegisterAllocation::remapSuccessorRegisters(Location loc, Block *targetBlock,
                                            OperandRange targetOperands) {
  // Legacy overload - called without branch op context, so no MOVE bits.
  // This path is only used for testing/annotation where we don't have the op.
  return remapSuccessorRegisters(/*branchOp=*/nullptr, targetBlock,
                                 targetOperands, /*baseOperandIndex=*/0);
}

// Computes register remapping for branch operands from source block to target.
//
// This function handles the MOVE bit logic for ref operands:
// - MOVE transfers ownership (runtime nulls source after copy)
// - No MOVE retains (runtime increments refcount of destination)
//
// MOVE is set when all three conditions are true:
// 1. This is the last occurrence of the ref value in this successor's list
//    (handles same ref appearing multiple times like `^bb(%ref, %ref)`)
// 2. The ref is not live after this branch (checked via isLastRealValueUse)
// 3. The branch op supports MOVE on this operand (RefMoveInterface)
//
// MOVE on back-edges (loops) is CORRECT:
// - Ownership transfers to the next iteration's block argument
// - NOT MOVE would leak: source retains AND destination retains = refcount++
// - At loop exit, source register would be non-null causing spurious release
//
// Returns a list of (src, dst) register pairs that need to be remapped.
// Pairs where src == dst (coalesced) are omitted.
SmallVector<std::pair<Register, Register>, 8>
RegisterAllocation::remapSuccessorRegisters(Operation *branchOp,
                                            Block *targetBlock,
                                            OperandRange targetOperands,
                                            unsigned baseOperandIndex) {
  // Compute the initial directed graph of register movements.
  // This may contain cycles ([reg 0->1], [reg 1->0], ...) that would not be
  // possible to evaluate as a direct remapping.
  SmallVector<std::pair<Register, Register>, 8> srcDstRegs;

  // Build last-occurrence map for ref operands in this successor's list.
  // Only the last occurrence of a value can get MOVE (for multi-use cases like
  // passing same ref to both branches of cond_br).
  llvm::DenseMap<Value, unsigned> lastOccurrence;
  for (auto [idx, operand] : llvm::enumerate(targetOperands)) {
    if (isa<IREE::VM::RefType>(operand.getType())) {
      lastOccurrence[operand] = idx;
    }
  }

  for (auto [idx, operand] : llvm::enumerate(targetOperands)) {
    auto srcReg = mapToRegister(operand);
    BlockArgument targetArg = targetBlock->getArgument(idx);
    auto dstReg = mapToRegister(targetArg);

    // Apply MOVE bit logic for ref registers.
    if (branchOp && srcReg.isRef()) {
      // Check if this is the last occurrence of this value in the operand list.
      bool isLastOccurrence = (lastOccurrence.lookup(operand) == idx);

      // Compute global operand index for liveness query.
      unsigned globalOperandIndex = baseOperandIndex + idx;

      // Check if this is the last real use of the value (not live after branch,
      // not used elsewhere in the function after this point).
      bool isLastUse =
          liveness_.isLastRealValueUse(operand, branchOp, globalOperandIndex);

      // Check if the branch op supports MOVE on this operand.
      bool isMovable = false;
      if (auto moveOp = dyn_cast<IREE::VM::RefMoveInterface>(branchOp)) {
        isMovable = moveOp.isRefOperandMovable(globalOperandIndex);
      }

      // Set MOVE if:
      // 1. This is the last occurrence in this successor's operand list
      // 2. This is the last real use of the value (not live after branch)
      // 3. The branch op supports MOVE on this operand
      // Note: MOVE is correct even on back-edges. The ref ownership transfers
      // to the destination block arg, which is correct for loop-carried values.
      if (isLastOccurrence && isLastUse && isMovable) {
        srcReg.setMove(true);
      }
    }

    if (srcReg != dstReg) {
      srcDstRegs.push_back({srcReg, dstReg});
    }
  }

  // Compute the feedback arc set to determine which edges are the ones inducing
  // cycles, if any. This also provides us a DAG that we can trivially remap
  // without worrying about cycles.
  auto feedbackArcSet = FeedbackArcSet::compute(srcDstRegs);
  assert(feedbackArcSet.acyclicEdges.size() +
                 feedbackArcSet.feedbackEdges.size() ==
             srcDstRegs.size() &&
         "lost an edge during feedback arc set computation");

  // If there's no cycles we can simply use the sorted DAG produced.
  if (feedbackArcSet.feedbackEdges.empty()) {
    return feedbackArcSet.acyclicEdges;
  }

  // The tail registers in each bank is reserved for swapping, when required.
  int localScratchI32RegCount = 0;
  int localScratchRefRegCount = 0;
  for (auto feedbackEdge : feedbackArcSet.feedbackEdges) {
    Register scratchReg;
    if (feedbackEdge.first.isRef()) {
      localScratchRefRegCount += 1;
      scratchReg = Register::getWithSameType(
          feedbackEdge.first, maxRefRegisterOrdinal_ + localScratchRefRegCount);
    } else {
      localScratchI32RegCount += 1;
      scratchReg = Register::getWithSameType(
          feedbackEdge.first, maxI32RegisterOrdinal_ + localScratchI32RegCount);
      // Integer types that use more than one register slot will be emitted
      // as remapping per 4-byte word, so we have to account for the extra
      // temporaries. See BytecodeEncoder:encodeBranch().
      assert(scratchReg.byteWidth() >= 4 && "expected >= i32");
      localScratchI32RegCount += scratchReg.byteWidth() / 4 - 1;
    }
    feedbackArcSet.acyclicEdges.insert(feedbackArcSet.acyclicEdges.begin(),
                                       {feedbackEdge.first, scratchReg});
    // The scratch register's use as source is its last use, so set MOVE.
    // This ensures the ref in the scratch register is released after the copy,
    // preventing leaks when the branch takes an alternate path (e.g., loop
    // exit).
    Register scratchSrc = scratchReg;
    if (scratchSrc.isRef()) {
      scratchSrc.setMove(true);
    }
    feedbackArcSet.acyclicEdges.push_back({scratchSrc, feedbackEdge.second});
  }
  if (localScratchI32RegCount > 0) {
    scratchI32RegisterCount_ =
        std::max(scratchI32RegisterCount_, localScratchI32RegCount);
    assert(getMaxI32RegisterOrdinal() <= Register::kInt32RegisterCount &&
           "spilling i32 regs");
    if (getMaxI32RegisterOrdinal() > Register::kInt32RegisterCount &&
        branchOp) {
      mlir::emitError(branchOp->getLoc())
          << "spilling entire i32 register address space";
    }
  }
  if (localScratchRefRegCount > 0) {
    scratchRefRegisterCount_ =
        std::max(scratchRefRegisterCount_, localScratchRefRegCount);
    assert(getMaxRefRegisterOrdinal() <= Register::kRefRegisterCount &&
           "spilling ref regs");
    if (getMaxRefRegisterOrdinal() > Register::kRefRegisterCount && branchOp) {
      mlir::emitError(branchOp->getLoc())
          << "spilling entire ref register address space";
    }
  }

  return feedbackArcSet.acyclicEdges;
}

} // namespace mlir::iree_compiler
