//===- AsyncRuntimeRefCounting.cpp - Async Runtime Ref Counting -----------===//
//
// Part of the LLVM Project, 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
//
//===----------------------------------------------------------------------===//
//
// This file implements automatic reference counting for Async runtime
// operations and types.
//
//===----------------------------------------------------------------------===//

#include "mlir/Dialect/Async/Passes.h"

#include "mlir/Analysis/Liveness.h"
#include "mlir/Dialect/Async/IR/Async.h"
#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "llvm/ADT/SmallSet.h"

namespace mlir {
#define GEN_PASS_DEF_ASYNCRUNTIMEREFCOUNTINGPASS
#define GEN_PASS_DEF_ASYNCRUNTIMEPOLICYBASEDREFCOUNTINGPASS
#include "mlir/Dialect/Async/Passes.h.inc"
} // namespace mlir

#define DEBUG_TYPE "async-runtime-ref-counting"

using namespace mlir;
using namespace mlir::async;

//===----------------------------------------------------------------------===//
// Utility functions shared by reference counting passes.
//===----------------------------------------------------------------------===//

// Drop the reference count immediately if the value has no uses.
static LogicalResult dropRefIfNoUses(Value value, unsigned count = 1) {
  if (!value.getUses().empty())
    return failure();

  OpBuilder b(value.getContext());

  // Set insertion point after the operation producing a value, or at the
  // beginning of the block if the value defined by the block argument.
  if (Operation *op = value.getDefiningOp())
    b.setInsertionPointAfter(op);
  else
    b.setInsertionPointToStart(value.getParentBlock());

  RuntimeDropRefOp::create(b, value.getLoc(), value, b.getI64IntegerAttr(1));
  return success();
}

// Calls `addRefCounting` for every reference counted value defined by the
// operation `op` (block arguments and values defined in nested regions).
static LogicalResult walkReferenceCountedValues(
    Operation *op, llvm::function_ref<LogicalResult(Value)> addRefCounting) {
  // Check that we do not have high level async operations in the IR because
  // otherwise reference counting will produce incorrect results after high
  // level async operations will be lowered to `async.runtime`
  WalkResult checkNoAsyncWalk = op->walk([&](Operation *op) -> WalkResult {
    if (!isa<ExecuteOp, AwaitOp, AwaitAllOp, YieldOp>(op))
      return WalkResult::advance();

    return op->emitError()
           << "async operations must be lowered to async runtime operations";
  });

  if (checkNoAsyncWalk.wasInterrupted())
    return failure();

  // Add reference counting to block arguments.
  WalkResult blockWalk = op->walk([&](Block *block) -> WalkResult {
    for (BlockArgument arg : block->getArguments())
      if (isRefCounted(arg.getType()))
        if (failed(addRefCounting(arg)))
          return WalkResult::interrupt();

    return WalkResult::advance();
  });

  if (blockWalk.wasInterrupted())
    return failure();

  // Add reference counting to operation results.
  WalkResult opWalk = op->walk([&](Operation *op) -> WalkResult {
    for (unsigned i = 0; i < op->getNumResults(); ++i)
      if (isRefCounted(op->getResultTypes()[i]))
        if (failed(addRefCounting(op->getResult(i))))
          return WalkResult::interrupt();

    return WalkResult::advance();
  });

  if (opWalk.wasInterrupted())
    return failure();

  return success();
}

//===----------------------------------------------------------------------===//
// Automatic reference counting based on the liveness analysis.
//===----------------------------------------------------------------------===//

namespace {

class AsyncRuntimeRefCountingPass
    : public impl::AsyncRuntimeRefCountingPassBase<
          AsyncRuntimeRefCountingPass> {
public:
  AsyncRuntimeRefCountingPass() = default;
  void runOnOperation() override;

private:
  /// Adds an automatic reference counting to the `value`.
  ///
  /// All values (token, group or value) are semantically created with a
  /// reference count of +1 and it is the responsibility of the async value user
  /// to place the `add_ref` and `drop_ref` operations to ensure that the value
  /// is destroyed after the last use.
  ///
  /// The function returns failure if it can't deduce the locations where
  /// to place the reference counting operations.
  ///
  /// Async values "semantically created" when:
  ///   1. Operation returns async result (e.g. `async.runtime.create`)
  ///   2. Async value passed in as a block argument (or function argument,
  ///      because function arguments are just entry block arguments)
  ///
  /// Passing async value as a function argument (or block argument) does not
  /// really mean that a new async value is created, it only means that the
  /// caller of a function transfered ownership of `+1` reference to the callee.
  /// It is convenient to think that from the callee perspective async value was
  /// "created" with `+1` reference by the block argument.
  ///
  /// Automatic reference counting algorithm outline:
  ///
  /// #1 Insert `drop_ref` operations after last use of the `value`.
  /// #2 Insert `add_ref` operations before functions calls with reference
  ///    counted `value` operand (newly created `+1` reference will be
  ///    transferred to the callee).
  /// #3 Verify that divergent control flow does not lead to leaked reference
  ///    counted objects.
  ///
  /// Async runtime reference counting optimization pass will optimize away
  /// some of the redundant `add_ref` and `drop_ref` operations inserted by this
  /// strategy (see `async-runtime-ref-counting-opt`).
  LogicalResult addAutomaticRefCounting(Value value);

  /// (#1) Adds the `drop_ref` operation after the last use of the `value`
  /// relying on the liveness analysis.
  ///
  /// If the `value` is in the block `liveIn` set and it is not in the block
  /// `liveOut` set, it means that it "dies" in the block. We find the last
  /// use of the value in such block and:
  ///
  ///   1. If the last user is a `ReturnLike` operation we do nothing, because
  ///      it forwards the ownership to the caller.
  ///   2. Otherwise we add a `drop_ref` operation immediately after the last
  ///      use.
  LogicalResult addDropRefAfterLastUse(Value value);

  /// (#2) Adds the `add_ref` operation before the function call taking `value`
  /// operand to ensure that the value passed to the function entry block
  /// has a `+1` reference count.
  LogicalResult addAddRefBeforeFunctionCall(Value value);

  /// (#3) Adds the `drop_ref` operation to account for successor blocks with
  /// divergent `liveIn` property: `value` is not in the `liveIn` set of all
  /// successor blocks.
  ///
  /// Example:
  ///
  ///   ^entry:
  ///     %token = async.runtime.create : !async.token
  ///     cf.cond_br %cond, ^bb1, ^bb2
  ///   ^bb1:
  ///     async.runtime.await %token
  ///     async.runtime.drop_ref %token
  ///     cf.br ^bb2
  ///   ^bb2:
  ///     return
  ///
  /// In this example ^bb2 does not have `value` in the `liveIn` set, so we have
  /// to branch into a special "reference counting block" from the ^entry that
  /// will have a `drop_ref` operation, and then branch into the ^bb2.
  ///
  /// After transformation:
  ///
  ///   ^entry:
  ///     %token = async.runtime.create : !async.token
  ///     cf.cond_br %cond, ^bb1, ^reference_counting
  ///   ^bb1:
  ///     async.runtime.await %token
  ///     async.runtime.drop_ref %token
  ///     cf.br ^bb2
  ///   ^reference_counting:
  ///     async.runtime.drop_ref %token
  ///     cf.br ^bb2
  ///   ^bb2:
  ///     return
  ///
  /// An exception to this rule are blocks with `async.coro.suspend` terminator,
  /// because in Async to LLVM lowering it is guaranteed that the control flow
  /// will jump into the resume block, and then follow into the cleanup and
  /// suspend blocks.
  ///
  /// Example:
  ///
  ///  ^entry(%value: !async.value<f32>):
  ///     async.runtime.await_and_resume %value, %hdl : !async.value<f32>
  ///     async.coro.suspend %ret, ^suspend, ^resume, ^cleanup
  ///   ^resume:
  ///     %0 = async.runtime.load %value
  ///     cf.br ^cleanup
  ///   ^cleanup:
  ///     ...
  ///   ^suspend:
  ///     ...
  ///
  /// Although cleanup and suspend blocks do not have the `value` in the
  /// `liveIn` set, it is guaranteed that execution will eventually continue in
  /// the resume block (we never explicitly destroy coroutines).
  LogicalResult addDropRefInDivergentLivenessSuccessor(Value value);
};

} // namespace

LogicalResult AsyncRuntimeRefCountingPass::addDropRefAfterLastUse(Value value) {
  OpBuilder builder(value.getContext());
  Location loc = value.getLoc();

  // Use liveness analysis to find the placement of `drop_ref`operation.
  auto &liveness = getAnalysis<Liveness>();

  // We analyse only the blocks of the region that defines the `value`, and do
  // not check nested blocks attached to operations.
  //
  // By analyzing only the `definingRegion` CFG we potentially loose an
  // opportunity to drop the reference count earlier and can extend the lifetime
  // of reference counted value longer then it is really required.
  //
  // We also assume that all nested regions finish their execution before the
  // completion of the owner operation. The only exception to this rule is
  // `async.execute` operation, and we verify that they are lowered to the
  // `async.runtime` operations before adding automatic reference counting.
  Region *definingRegion = value.getParentRegion();

  // Last users of the `value` inside all blocks where the value dies.
  llvm::SmallPtrSet<Operation *, 4> lastUsers;

  // Find blocks in the `definingRegion` that have users of the `value` (if
  // there are multiple users in the block, which one will be selected is
  // undefined). User operation might be not the actual user of the value, but
  // the operation in the block that has a "real user" in one of the attached
  // regions.
  llvm::DenseMap<Block *, Operation *> usersInTheBlocks;

  for (Operation *user : value.getUsers()) {
    Block *userBlock = user->getBlock();
    Block *ancestor = definingRegion->findAncestorBlockInRegion(*userBlock);
    usersInTheBlocks[ancestor] = ancestor->findAncestorOpInBlock(*user);
    assert(ancestor && "ancestor block must be not null");
    assert(usersInTheBlocks[ancestor] && "ancestor op must be not null");
  }

  // Find blocks where the `value` dies: the value is in `liveIn` set and not
  // in the `liveOut` set. We place `drop_ref` immediately after the last use
  // of the `value` in such regions (after handling few special cases).
  //
  // We do not traverse all the blocks in the `definingRegion`, because the
  // `value` can be in the live in set only if it has users in the block, or it
  // is defined in the block.
  //
  // Values with zero users (only definition) handled explicitly above.
  for (auto &blockAndUser : usersInTheBlocks) {
    Block *block = blockAndUser.getFirst();
    Operation *userInTheBlock = blockAndUser.getSecond();

    const LivenessBlockInfo *blockLiveness = liveness.getLiveness(block);

    // Value must be in the live input set or defined in the block.
    assert(blockLiveness->isLiveIn(value) ||
           blockLiveness->getBlock() == value.getParentBlock());

    // If value is in the live out set, it means it doesn't "die" in the block.
    if (blockLiveness->isLiveOut(value))
      continue;

    // At this point we proved that `value` dies in the `block`. Find the last
    // use of the `value` inside the `block`, this is where it "dies".
    Operation *lastUser = blockLiveness->getEndOperation(value, userInTheBlock);
    assert(lastUsers.count(lastUser) == 0 && "last users must be unique");
    lastUsers.insert(lastUser);
  }

  // Process all the last users of the `value` inside each block where the value
  // dies.
  for (Operation *lastUser : lastUsers) {
    // Return like operations forward reference count.
    if (lastUser->hasTrait<OpTrait::ReturnLike>())
      continue;

    // We can't currently handle other types of terminators.
    if (lastUser->hasTrait<OpTrait::IsTerminator>())
      return lastUser->emitError() << "async reference counting can't handle "
                                      "terminators that are not ReturnLike";

    // Add a drop_ref immediately after the last user.
    builder.setInsertionPointAfter(lastUser);
    RuntimeDropRefOp::create(builder, loc, value, builder.getI64IntegerAttr(1));
  }

  return success();
}

LogicalResult
AsyncRuntimeRefCountingPass::addAddRefBeforeFunctionCall(Value value) {
  OpBuilder builder(value.getContext());
  Location loc = value.getLoc();

  for (Operation *user : value.getUsers()) {
    if (!isa<func::CallOp>(user))
      continue;

    // Add a reference before the function call to pass the value at `+1`
    // reference to the function entry block.
    builder.setInsertionPoint(user);
    RuntimeAddRefOp::create(builder, loc, value, builder.getI64IntegerAttr(1));
  }

  return success();
}

LogicalResult
AsyncRuntimeRefCountingPass::addDropRefInDivergentLivenessSuccessor(
    Value value) {
  using BlockSet = llvm::SmallPtrSet<Block *, 4>;

  OpBuilder builder(value.getContext());

  // If a block has successors with different `liveIn` property of the `value`,
  // record block successors that do not thave the `value` in the `liveIn` set.
  llvm::SmallDenseMap<Block *, BlockSet> divergentLivenessBlocks;

  // Use liveness analysis to find the placement of `drop_ref`operation.
  auto &liveness = getAnalysis<Liveness>();

  // Because we only add `drop_ref` operations to the region that defines the
  // `value` we can only process CFG for the same region.
  Region *definingRegion = value.getParentRegion();

  // Collect blocks with successors with mismatching `liveIn` sets.
  for (Block &block : definingRegion->getBlocks()) {
    const LivenessBlockInfo *blockLiveness = liveness.getLiveness(&block);

    // Skip the block if value is not in the `liveOut` set.
    if (!blockLiveness || !blockLiveness->isLiveOut(value))
      continue;

    BlockSet liveInSuccessors;   // `value` is in `liveIn` set
    BlockSet noLiveInSuccessors; // `value` is not in the `liveIn` set

    // Collect successors that do not have `value` in the `liveIn` set.
    for (Block *successor : block.getSuccessors()) {
      const LivenessBlockInfo *succLiveness = liveness.getLiveness(successor);
      if (succLiveness && succLiveness->isLiveIn(value))
        liveInSuccessors.insert(successor);
      else
        noLiveInSuccessors.insert(successor);
    }

    // Block has successors with different `liveIn` property of the `value`.
    if (!liveInSuccessors.empty() && !noLiveInSuccessors.empty())
      divergentLivenessBlocks.try_emplace(&block, noLiveInSuccessors);
  }

  // Try to insert `dropRef` operations to handle blocks with divergent liveness
  // in successors blocks.
  for (auto kv : divergentLivenessBlocks) {
    Block *block = kv.getFirst();
    BlockSet &successors = kv.getSecond();

    // Coroutine suspension is a special case terminator for wich we do not
    // need to create additional reference counting (see details above).
    Operation *terminator = block->getTerminator();
    if (isa<CoroSuspendOp>(terminator))
      continue;

    // We only support successor blocks with empty block argument list.
    auto hasArgs = [](Block *block) { return !block->getArguments().empty(); };
    if (llvm::any_of(successors, hasArgs))
      return terminator->emitOpError()
             << "successor have different `liveIn` property of the reference "
                "counted value";

    // Make sure that `dropRef` operation is called when branched into the
    // successor block without `value` in the `liveIn` set.
    for (Block *successor : successors) {
      // If successor has a unique predecessor, it is safe to create `dropRef`
      // operations directly in the successor block.
      //
      // Otherwise we need to create a special block for reference counting
      // operations, and branch from it to the original successor block.
      Block *refCountingBlock = nullptr;

      if (successor->getUniquePredecessor() == block) {
        refCountingBlock = successor;
      } else {
        refCountingBlock = &successor->getParent()->emplaceBlock();
        refCountingBlock->moveBefore(successor);
        OpBuilder builder = OpBuilder::atBlockEnd(refCountingBlock);
        cf::BranchOp::create(builder, value.getLoc(), successor);
      }

      OpBuilder builder = OpBuilder::atBlockBegin(refCountingBlock);
      RuntimeDropRefOp::create(builder, value.getLoc(), value,
                               builder.getI64IntegerAttr(1));

      // No need to update the terminator operation.
      if (successor == refCountingBlock)
        continue;

      // Update terminator `successor` block to `refCountingBlock`.
      for (const auto &pair : llvm::enumerate(terminator->getSuccessors()))
        if (pair.value() == successor)
          terminator->setSuccessor(refCountingBlock, pair.index());
    }
  }

  return success();
}

LogicalResult
AsyncRuntimeRefCountingPass::addAutomaticRefCounting(Value value) {
  // Short-circuit reference counting for values without uses.
  if (succeeded(dropRefIfNoUses(value)))
    return success();

  // Add `drop_ref` operations based on the liveness analysis.
  if (failed(addDropRefAfterLastUse(value)))
    return failure();

  // Add `add_ref` operations before function calls.
  if (failed(addAddRefBeforeFunctionCall(value)))
    return failure();

  // Add `drop_ref` operations to successors with divergent `value` liveness.
  if (failed(addDropRefInDivergentLivenessSuccessor(value)))
    return failure();

  return success();
}

void AsyncRuntimeRefCountingPass::runOnOperation() {
  auto functor = [&](Value value) { return addAutomaticRefCounting(value); };
  if (failed(walkReferenceCountedValues(getOperation(), functor)))
    signalPassFailure();
}

//===----------------------------------------------------------------------===//
// Reference counting based on the user defined policy.
//===----------------------------------------------------------------------===//

namespace {

class AsyncRuntimePolicyBasedRefCountingPass
    : public impl::AsyncRuntimePolicyBasedRefCountingPassBase<
          AsyncRuntimePolicyBasedRefCountingPass> {
public:
  AsyncRuntimePolicyBasedRefCountingPass() { initializeDefaultPolicy(); }

  void runOnOperation() override;

private:
  // Adds a reference counting operations for all uses of the `value` according
  // to the reference counting policy.
  LogicalResult addRefCounting(Value value);

  void initializeDefaultPolicy();

  llvm::SmallVector<std::function<FailureOr<int>(OpOperand &)>> policy;
};

} // namespace

LogicalResult
AsyncRuntimePolicyBasedRefCountingPass::addRefCounting(Value value) {
  // Short-circuit reference counting for values without uses.
  if (succeeded(dropRefIfNoUses(value)))
    return success();

  OpBuilder b(value.getContext());

  // Consult the user defined policy for every value use.
  for (OpOperand &operand : value.getUses()) {
    Location loc = operand.getOwner()->getLoc();

    for (auto &func : policy) {
      FailureOr<int> refCount = func(operand);
      if (failed(refCount))
        return failure();

      int cnt = *refCount;

      // Create `add_ref` operation before the operand owner.
      if (cnt > 0) {
        b.setInsertionPoint(operand.getOwner());
        RuntimeAddRefOp::create(b, loc, value, b.getI64IntegerAttr(cnt));
      }

      // Create `drop_ref` operation after the operand owner.
      if (cnt < 0) {
        b.setInsertionPointAfter(operand.getOwner());
        RuntimeDropRefOp::create(b, loc, value, b.getI64IntegerAttr(-cnt));
      }
    }
  }

  return success();
}

void AsyncRuntimePolicyBasedRefCountingPass::initializeDefaultPolicy() {
  policy.push_back([](OpOperand &operand) -> FailureOr<int> {
    Operation *op = operand.getOwner();
    Type type = operand.get().getType();

    bool isToken = isa<TokenType>(type);
    bool isGroup = isa<GroupType>(type);
    bool isValue = isa<ValueType>(type);

    // Drop reference after async token or group error check (coro await).
    if (isa<RuntimeIsErrorOp>(op))
      return (isToken || isGroup) ? -1 : 0;

    // Drop reference after async value load.
    if (isa<RuntimeLoadOp>(op))
      return isValue ? -1 : 0;

    // Drop reference after async token added to the group.
    if (isa<RuntimeAddToGroupOp>(op))
      return isToken ? -1 : 0;

    return 0;
  });
}

void AsyncRuntimePolicyBasedRefCountingPass::runOnOperation() {
  auto functor = [&](Value value) { return addRefCounting(value); };
  if (failed(walkReferenceCountedValues(getOperation(), functor)))
    signalPassFailure();
}
