// Copyright (c) 2020 Vasyl Teliman
//
// 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.

#include "source/fuzz/transformation_propagate_instruction_up.h"

#include "source/fuzz/fuzzer_util.h"
#include "source/fuzz/instruction_descriptor.h"

namespace spvtools {
namespace fuzz {
namespace {

uint32_t GetResultIdFromLabelId(const opt::Instruction& phi_inst,
                                uint32_t label_id) {
  assert(phi_inst.opcode() == SpvOpPhi && "|phi_inst| is not an OpPhi");

  for (uint32_t i = 1; i < phi_inst.NumInOperands(); i += 2) {
    if (phi_inst.GetSingleWordInOperand(i) == label_id) {
      return phi_inst.GetSingleWordInOperand(i - 1);
    }
  }

  return 0;
}

bool ContainsPointers(const opt::analysis::Type& type) {
  switch (type.kind()) {
    case opt::analysis::Type::kPointer:
      return true;
    case opt::analysis::Type::kStruct:
      return std::any_of(type.AsStruct()->element_types().begin(),
                         type.AsStruct()->element_types().end(),
                         [](const opt::analysis::Type* element_type) {
                           return ContainsPointers(*element_type);
                         });
    default:
      return false;
  }
}

bool HasValidDependencies(opt::IRContext* ir_context, opt::Instruction* inst) {
  const auto* inst_block = ir_context->get_instr_block(inst);
  assert(inst_block &&
         "This function shouldn't be applied to global instructions or function"
         "parameters");

  for (uint32_t i = 0; i < inst->NumInOperands(); ++i) {
    const auto& operand = inst->GetInOperand(i);
    if (operand.type != SPV_OPERAND_TYPE_ID) {
      // Consider only <id> operands.
      continue;
    }

    auto* dependency = ir_context->get_def_use_mgr()->GetDef(operand.words[0]);
    assert(dependency && "Operand has invalid id");

    if (ir_context->get_instr_block(dependency) == inst_block &&
        dependency->opcode() != SpvOpPhi) {
      // |dependency| is "valid" if it's an OpPhi from the same basic block or
      // an instruction from a different basic block.
      return false;
    }
  }

  return true;
}

}  // namespace

TransformationPropagateInstructionUp::TransformationPropagateInstructionUp(
    const protobufs::TransformationPropagateInstructionUp& message)
    : message_(message) {}

TransformationPropagateInstructionUp::TransformationPropagateInstructionUp(
    uint32_t block_id,
    const std::map<uint32_t, uint32_t>& predecessor_id_to_fresh_id) {
  message_.set_block_id(block_id);
  *message_.mutable_predecessor_id_to_fresh_id() =
      fuzzerutil::MapToRepeatedUInt32Pair(predecessor_id_to_fresh_id);
}

bool TransformationPropagateInstructionUp::IsApplicable(
    opt::IRContext* ir_context, const TransformationContext& /*unused*/) const {
  // Check that we can apply this transformation to the |block_id|.
  if (!IsApplicableToBlock(ir_context, message_.block_id())) {
    return false;
  }

  const auto predecessor_id_to_fresh_id = fuzzerutil::RepeatedUInt32PairToMap(
      message_.predecessor_id_to_fresh_id());
  for (auto id : ir_context->cfg()->preds(message_.block_id())) {
    // Each predecessor must have a fresh id in the |predecessor_id_to_fresh_id|
    // map.
    if (!predecessor_id_to_fresh_id.count(id)) {
      return false;
    }
  }

  std::vector<uint32_t> maybe_fresh_ids;
  maybe_fresh_ids.reserve(predecessor_id_to_fresh_id.size());
  for (const auto& entry : predecessor_id_to_fresh_id) {
    maybe_fresh_ids.push_back(entry.second);
  }

  // All ids must be unique and fresh.
  return !fuzzerutil::HasDuplicates(maybe_fresh_ids) &&
         std::all_of(maybe_fresh_ids.begin(), maybe_fresh_ids.end(),
                     [ir_context](uint32_t id) {
                       return fuzzerutil::IsFreshId(ir_context, id);
                     });
}

void TransformationPropagateInstructionUp::Apply(
    opt::IRContext* ir_context, TransformationContext* /*unused*/) const {
  auto* inst = GetInstructionToPropagate(ir_context, message_.block_id());
  assert(inst &&
         "The block must have at least one supported instruction to propagate");
  assert(inst->result_id() && inst->type_id() &&
         "|inst| must have a result id and a type id");

  opt::Instruction::OperandList op_phi_operands;
  const auto predecessor_id_to_fresh_id = fuzzerutil::RepeatedUInt32PairToMap(
      message_.predecessor_id_to_fresh_id());
  std::unordered_set<uint32_t> visited_predecessors;
  for (auto predecessor_id : ir_context->cfg()->preds(message_.block_id())) {
    // A block can have multiple identical predecessors.
    if (visited_predecessors.count(predecessor_id)) {
      continue;
    }

    visited_predecessors.insert(predecessor_id);

    auto new_result_id = predecessor_id_to_fresh_id.at(predecessor_id);

    // Compute InOperands for the OpPhi instruction to be inserted later.
    op_phi_operands.push_back({SPV_OPERAND_TYPE_ID, {new_result_id}});
    op_phi_operands.push_back({SPV_OPERAND_TYPE_ID, {predecessor_id}});

    // Create a clone of the |inst| to be inserted into the |predecessor_id|.
    std::unique_ptr<opt::Instruction> clone(inst->Clone(ir_context));
    clone->SetResultId(new_result_id);

    fuzzerutil::UpdateModuleIdBound(ir_context, new_result_id);

    // Adjust |clone|'s operands to account for possible dependencies on OpPhi
    // instructions from the same basic block.
    for (uint32_t i = 0; i < clone->NumInOperands(); ++i) {
      auto& operand = clone->GetInOperand(i);
      if (operand.type != SPV_OPERAND_TYPE_ID) {
        // Consider only ids.
        continue;
      }

      const auto* dependency_inst =
          ir_context->get_def_use_mgr()->GetDef(operand.words[0]);
      assert(dependency_inst && "|clone| depends on an invalid id");

      if (ir_context->get_instr_block(dependency_inst->result_id()) !=
          ir_context->cfg()->block(message_.block_id())) {
        // We don't need to adjust anything if |dependency_inst| is from a
        // different block, a global instruction or a function parameter.
        continue;
      }

      assert(dependency_inst->opcode() == SpvOpPhi &&
             "Propagated instruction can depend only on OpPhis from the same "
             "basic block or instructions from different basic blocks");

      auto new_id = GetResultIdFromLabelId(*dependency_inst, predecessor_id);
      assert(new_id && "OpPhi instruction is missing a predecessor");
      operand.words[0] = new_id;
    }

    auto* insert_before_inst = fuzzerutil::GetLastInsertBeforeInstruction(
        ir_context, predecessor_id, clone->opcode());
    assert(insert_before_inst && "Can't insert |clone| into |predecessor_id");

    insert_before_inst->InsertBefore(std::move(clone));
  }

  // Insert an OpPhi instruction into the basic block of |inst|.
  ir_context->get_instr_block(inst)->begin()->InsertBefore(
      MakeUnique<opt::Instruction>(ir_context, SpvOpPhi, inst->type_id(),
                                   inst->result_id(),
                                   std::move(op_phi_operands)));

  // Remove |inst| from the basic block.
  ir_context->KillInst(inst);

  // We have changed the module so most analyzes are now invalid.
  ir_context->InvalidateAnalysesExceptFor(
      opt::IRContext::Analysis::kAnalysisNone);
}

protobufs::Transformation TransformationPropagateInstructionUp::ToMessage()
    const {
  protobufs::Transformation result;
  *result.mutable_propagate_instruction_up() = message_;
  return result;
}

bool TransformationPropagateInstructionUp::IsOpcodeSupported(SpvOp opcode) {
  // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3605):
  //  We only support "simple" instructions that don't work with memory.
  //  We should extend this so that we support the ones that modify the memory
  //  too.
  switch (opcode) {
    case SpvOpUndef:
    case SpvOpAccessChain:
    case SpvOpInBoundsAccessChain:
    case SpvOpArrayLength:
    case SpvOpVectorExtractDynamic:
    case SpvOpVectorInsertDynamic:
    case SpvOpVectorShuffle:
    case SpvOpCompositeConstruct:
    case SpvOpCompositeExtract:
    case SpvOpCompositeInsert:
    case SpvOpCopyObject:
    case SpvOpTranspose:
    case SpvOpConvertFToU:
    case SpvOpConvertFToS:
    case SpvOpConvertSToF:
    case SpvOpConvertUToF:
    case SpvOpUConvert:
    case SpvOpSConvert:
    case SpvOpFConvert:
    case SpvOpQuantizeToF16:
    case SpvOpSatConvertSToU:
    case SpvOpSatConvertUToS:
    case SpvOpBitcast:
    case SpvOpSNegate:
    case SpvOpFNegate:
    case SpvOpIAdd:
    case SpvOpFAdd:
    case SpvOpISub:
    case SpvOpFSub:
    case SpvOpIMul:
    case SpvOpFMul:
    case SpvOpUDiv:
    case SpvOpSDiv:
    case SpvOpFDiv:
    case SpvOpUMod:
    case SpvOpSRem:
    case SpvOpSMod:
    case SpvOpFRem:
    case SpvOpFMod:
    case SpvOpVectorTimesScalar:
    case SpvOpMatrixTimesScalar:
    case SpvOpVectorTimesMatrix:
    case SpvOpMatrixTimesVector:
    case SpvOpMatrixTimesMatrix:
    case SpvOpOuterProduct:
    case SpvOpDot:
    case SpvOpIAddCarry:
    case SpvOpISubBorrow:
    case SpvOpUMulExtended:
    case SpvOpSMulExtended:
    case SpvOpAny:
    case SpvOpAll:
    case SpvOpIsNan:
    case SpvOpIsInf:
    case SpvOpIsFinite:
    case SpvOpIsNormal:
    case SpvOpSignBitSet:
    case SpvOpLessOrGreater:
    case SpvOpOrdered:
    case SpvOpUnordered:
    case SpvOpLogicalEqual:
    case SpvOpLogicalNotEqual:
    case SpvOpLogicalOr:
    case SpvOpLogicalAnd:
    case SpvOpLogicalNot:
    case SpvOpSelect:
    case SpvOpIEqual:
    case SpvOpINotEqual:
    case SpvOpUGreaterThan:
    case SpvOpSGreaterThan:
    case SpvOpUGreaterThanEqual:
    case SpvOpSGreaterThanEqual:
    case SpvOpULessThan:
    case SpvOpSLessThan:
    case SpvOpULessThanEqual:
    case SpvOpSLessThanEqual:
    case SpvOpFOrdEqual:
    case SpvOpFUnordEqual:
    case SpvOpFOrdNotEqual:
    case SpvOpFUnordNotEqual:
    case SpvOpFOrdLessThan:
    case SpvOpFUnordLessThan:
    case SpvOpFOrdGreaterThan:
    case SpvOpFUnordGreaterThan:
    case SpvOpFOrdLessThanEqual:
    case SpvOpFUnordLessThanEqual:
    case SpvOpFOrdGreaterThanEqual:
    case SpvOpFUnordGreaterThanEqual:
    case SpvOpShiftRightLogical:
    case SpvOpShiftRightArithmetic:
    case SpvOpShiftLeftLogical:
    case SpvOpBitwiseOr:
    case SpvOpBitwiseXor:
    case SpvOpBitwiseAnd:
    case SpvOpNot:
    case SpvOpBitFieldInsert:
    case SpvOpBitFieldSExtract:
    case SpvOpBitFieldUExtract:
    case SpvOpBitReverse:
    case SpvOpBitCount:
    case SpvOpCopyLogical:
    case SpvOpPtrEqual:
    case SpvOpPtrNotEqual:
      return true;
    default:
      return false;
  }
}

opt::Instruction*
TransformationPropagateInstructionUp::GetInstructionToPropagate(
    opt::IRContext* ir_context, uint32_t block_id) {
  auto* block = ir_context->cfg()->block(block_id);
  assert(block && "|block_id| is invalid");

  for (auto& inst : *block) {
    // We look for the first instruction in the block that satisfies the
    // following rules:
    // - it's not an OpPhi
    // - it must be supported by this transformation
    // - it may depend only on instructions from different basic blocks or on
    //   OpPhi instructions from the same basic block.
    if (inst.opcode() == SpvOpPhi || !IsOpcodeSupported(inst.opcode()) ||
        !inst.type_id() || !inst.result_id()) {
      continue;
    }

    const auto* inst_type = ir_context->get_type_mgr()->GetType(inst.type_id());
    assert(inst_type && "|inst| has invalid type");

    if (inst_type->AsSampledImage()) {
      // OpTypeSampledImage cannot be used as an argument to OpPhi instructions,
      // thus we cannot support this type.
      continue;
    }

    if (!ir_context->get_feature_mgr()->HasCapability(
            SpvCapabilityVariablePointersStorageBuffer) &&
        ContainsPointers(*inst_type)) {
      // OpPhi supports pointer operands only with VariablePointers or
      // VariablePointersStorageBuffer capabilities.
      //
      // Note that VariablePointers capability implicitly declares
      // VariablePointersStorageBuffer capability.
      continue;
    }

    if (!HasValidDependencies(ir_context, &inst)) {
      continue;
    }

    return &inst;
  }

  return nullptr;
}

bool TransformationPropagateInstructionUp::IsApplicableToBlock(
    opt::IRContext* ir_context, uint32_t block_id) {
  // Check that |block_id| is valid.
  const auto* label_inst = ir_context->get_def_use_mgr()->GetDef(block_id);
  if (!label_inst || label_inst->opcode() != SpvOpLabel) {
    return false;
  }

  // Check that |block| has predecessors.
  const auto& predecessors = ir_context->cfg()->preds(block_id);
  if (predecessors.empty()) {
    return false;
  }

  // The block must contain an instruction to propagate.
  const auto* inst_to_propagate =
      GetInstructionToPropagate(ir_context, block_id);
  if (!inst_to_propagate) {
    return false;
  }

  // We should be able to insert |inst_to_propagate| into every predecessor of
  // |block|.
  return std::all_of(predecessors.begin(), predecessors.end(),
                     [ir_context, inst_to_propagate](uint32_t predecessor_id) {
                       return fuzzerutil::GetLastInsertBeforeInstruction(
                                  ir_context, predecessor_id,
                                  inst_to_propagate->opcode()) != nullptr;
                     });
}

std::unordered_set<uint32_t> TransformationPropagateInstructionUp::GetFreshIds()
    const {
  std::unordered_set<uint32_t> result;
  for (auto& pair : message_.predecessor_id_to_fresh_id()) {
    result.insert(pair.second());
  }
  return result;
}

}  // namespace fuzz
}  // namespace spvtools
