// Copyright (c) 2019, the Dart 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.

#ifndef RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_
#define RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_

#include <memory>
#include <type_traits>
#include <utility>
#include <vector>

#include "include/dart_api.h"

#include "platform/allocation.h"
#include "vm/compiler/backend/flow_graph.h"
#include "vm/compiler/backend/il.h"
#include "vm/compiler/backend/inliner.h"
#include "vm/compiler/compiler_pass.h"
#include "vm/compiler/compiler_state.h"
#include "vm/compiler/jit/compiler.h"
#include "vm/unit_test.h"

// The helpers in this file make it easier to write C++ unit tests which assert
// that Dart code gets turned into certain IR.
//
// Here is an example on how to use it:
//
//     ISOLATE_UNIT_TEST_CASE(MyIRTest) {
//       const char* script = R"(
//           void foo() { ... }
//           void main() { foo(); }
//       )";
//
//       // Load the script and exercise the code once.
//       const auto& lib = Library::Handle(LoadTestScript(script);
//
//       // Cause the code to be exercised once (to populate ICData).
//       Invoke(lib, "main");
//
//       // Look up the function.
//       const auto& function = Function::Handle(GetFunction(lib, "foo"));
//
//       // Run the JIT compilation pipeline with two passes.
//       TestPipeline pipeline(function);
//       FlowGraph* graph = pipeline.RunJITPasses("ComputeSSA,TypePropagation");
//
//       ...
//     }
//
namespace dart {

class FlowGraph;
class Function;
class Library;

LibraryPtr LoadTestScript(const char* script,
                          Dart_NativeEntryResolver resolver = nullptr,
                          const char* lib_uri = RESOLVED_USER_TEST_URI);

NOT_IN_PRODUCT(LibraryPtr ReloadTestScript(const char* script));

FunctionPtr GetFunction(const Library& lib, const char* name);
ClassPtr GetClass(const Library& lib, const char* name);
TypeParameterPtr GetClassTypeParameter(const Class& klass, intptr_t index);
TypeParameterPtr GetFunctionTypeParameter(const Function& fun, intptr_t index);

ObjectPtr Invoke(const Library& lib, const char* name);

InstructionsPtr BuildInstructions(
    std::function<void(compiler::Assembler* assembler)> fun);

class TestPipeline : public ValueObject {
 public:
  TestPipeline(const Function& function,
               CompilerPass::PipelineMode mode,
               bool is_optimizing = true)
      : thread_(Thread::Current()),
        compiler_state_(thread_,
                        mode == CompilerPass::PipelineMode::kAOT,
                        is_optimizing,
                        CompilerState::ShouldTrace(function)),
        hierarchy_info_(thread_),
        mode_(mode),
        flow_graph_(nullptr),
        function_(function),
        parsed_function_(nullptr) {}
  ~TestPipeline() { delete pass_state_; }

  TestPipeline(CompilerPass::PipelineMode mode, std::function<FlowGraph*()> fn)
      : thread_(Thread::Current()),
        compiler_state_(thread_,
                        mode == CompilerPass::PipelineMode::kAOT,
                        /*is_optimizing=*/true,
                        CompilerTracing::kOff),
        hierarchy_info_(thread_),
        mode_(mode),
        flow_graph_(fn()),
        function_(flow_graph_->function()),
        parsed_function_(
            const_cast<ParsedFunction*>(&flow_graph_->parsed_function())) {}

  // As a side-effect this will populate
  //   - [ic_data_array_]
  //   - [parsed_function_]
  //   - [pass_state_]
  //   - [flow_graph_]
  FlowGraph* RunPasses(std::initializer_list<CompilerPass::Id> passes);

  void RunAdditionalPasses(std::initializer_list<CompilerPass::Id> passes);

  void RunForcedOptimizedAfterSSAPasses();

  void CompileGraphAndAttachFunction();

 private:
  Thread* thread_;
  CompilerState compiler_state_;
  HierarchyInfo hierarchy_info_;
  CompilerPass::PipelineMode mode_;
  ZoneGrowableArray<const ICData*>* ic_data_array_ = nullptr;
  CompilerPassState* pass_state_ = nullptr;
  FlowGraph* flow_graph_;
  const Function& function_;
  ParsedFunction* parsed_function_;
};

// Match opcodes used for [ILMatcher], see below.
enum MatchOpCode {
// Emit a match and match-and-move code for every instruction.
#define DEFINE_MATCH_OPCODES(Instruction, _)                                   \
  kMatch##Instruction, kMatchAndMove##Instruction,                             \
      kMatchAndMoveOptional##Instruction,
  FOR_EACH_CONCRETE_INSTRUCTION(DEFINE_MATCH_OPCODES)
      FOR_EACH_ABSTRACT_INSTRUCTION(DEFINE_MATCH_OPCODES)
#undef DEFINE_MATCH_OPCODES

  // Matches a branch and moves left.
  kMatchAndMoveBranchTrue,

  // Matches a branch and moves right.
  kMatchAndMoveBranchFalse,

  // Is ignored.
  kNop,

  // Moves forward across any instruction.
  kMoveAny,

  // Moves over all parallel moves.
  kMoveParallelMoves,

  // Moves forward until the next match code matches.
  kMoveGlob,

  // Moves over any DebugStepChecks.
  kMoveDebugStepChecks,

  // Invalid match opcode used as default [insert_before] argument to TryMatch
  // to signal that no insertions should occur.
  kInvalidMatchOpCode,
};

// Match codes used for [ILMatcher], see below.
class MatchCode {
 public:
  MatchCode(MatchOpCode opcode)  // NOLINT
      : opcode_(opcode), capture_(nullptr) {}

  MatchCode(MatchOpCode opcode, Instruction** capture)
      : opcode_(opcode), capture_(capture) {}

#define DEFINE_TYPED_CONSTRUCTOR(Type, ignored)                                \
  MatchCode(MatchOpCode opcode, Type##Instr** capture)                         \
      : opcode_(opcode), capture_(reinterpret_cast<Instruction**>(capture)) {  \
    RELEASE_ASSERT(opcode == kMatch##Type || opcode == kMatchAndMove##Type);   \
  }
  FOR_EACH_CONCRETE_INSTRUCTION(DEFINE_TYPED_CONSTRUCTOR)
  FOR_EACH_ABSTRACT_INSTRUCTION(DEFINE_TYPED_CONSTRUCTOR)
#undef DEFINE_TYPED_CONSTRUCTOR

  MatchOpCode opcode() { return opcode_; }

 private:
  friend class ILMatcher;

  MatchOpCode opcode_;
  Instruction** capture_;
};

enum class ParallelMovesHandling {
  // Matcher doesn't do anything special with ParallelMove instructions.
  kDefault,
  // All ParallelMove instructions are skipped.
  // This mode is useful when matching a flow graph after the whole
  // compiler pipeline, as it may have ParallelMove instructions
  // at arbitrary architecture-dependent places.
  kSkip,
};

// Used for matching a sequence of IL instructions including capturing support.
//
// Example:
//
//     TargetEntryInstr* entry = ....;
//     BranchInstr* branch = nullptr;
//
//     ILMatcher matcher(flow_graph, entry);
//     if (matcher.TryMatch({ kMoveGlob, {kMatchBranch, &branch}, })) {
//       EXPECT(branch->operation_cid() == kMintCid);
//       ...
//     }
//
// This match will start at [entry], follow any number instructions (including
// [GotoInstr]s until a [BranchInstr] is found).
//
// If the match was successful, this returns `true` and updates the current
// value for the cursor.
class ILMatcher : public ValueObject {
 public:
  ILMatcher(FlowGraph* flow_graph,
            Instruction* cursor,
            bool trace = true,
            ParallelMovesHandling parallel_moves_handling =
                ParallelMovesHandling::kDefault)
      : flow_graph_(flow_graph),
        cursor_(cursor),
        parallel_moves_handling_(parallel_moves_handling),
  // clang-format off
#if !defined(PRODUCT)
        trace_(trace) {}
#else
        trace_(false) {}
#endif
  // clang-format on

  Instruction* value() { return cursor_; }

  // From the current [value] according to match_codes.
  //
  // Returns `true` if the match was successful and cursor has been updated,
  // otherwise returns `false`.
  //
  // If [insert_before] is a valid match opcode, then it will be inserted
  // before each MatchCode in [match_codes] prior to matching.
  bool TryMatch(std::initializer_list<MatchCode> match_codes,
                MatchOpCode insert_before = kInvalidMatchOpCode);

 private:
  Instruction* MatchInternal(std::vector<MatchCode> match_codes,
                             size_t i,
                             Instruction* cursor);

  const char* MatchOpCodeToCString(MatchOpCode code);

  FlowGraph* flow_graph_;
  Instruction* cursor_;
  ParallelMovesHandling parallel_moves_handling_;
  bool trace_;
};

#if !defined(PRODUCT)
#define ENTITY_TOCSTRING(v) ((v)->ToCString())
#else
#define ENTITY_TOCSTRING(v) "<?>"
#endif

// Helper to check various IL and object properties and informative error
// messages if check fails. [entity] should be a pointer to a value.
// [property] should be an expression which can refer to [entity] using
// variable named [it].
// [entity] is expected to have a ToCString() method in non-PRODUCT builds.
#define EXPECT_PROPERTY(entity, property)                                      \
  do {                                                                         \
    auto& it = *entity;                                                        \
    if (!(property)) {                                                         \
      dart::Expect(__FILE__, __LINE__)                                         \
          .Fail("expected " #property " for " #entity " which is %s.\n",       \
                ENTITY_TOCSTRING(entity));                                     \
    }                                                                          \
  } while (0)

class FlowGraphBuilderHelper {
 public:
  explicit FlowGraphBuilderHelper(
      intptr_t num_parameters = 0,
      const std::function<void(const Function&)>& configure_function =
          [](const Function&) {})
      : state_(CompilerState::Current()),
        flow_graph_(MakeDummyGraph(Thread::Current(),
                                   num_parameters,
                                   state_.is_optimizing(),
                                   configure_function)) {
    flow_graph_.CreateCommonConstants();
  }

  TargetEntryInstr* TargetEntry(intptr_t try_index = kInvalidTryIndex) const {
    return new TargetEntryInstr(flow_graph_.allocate_block_id(), try_index,
                                state_.GetNextDeoptId());
  }

  JoinEntryInstr* JoinEntry(intptr_t try_index = kInvalidTryIndex) const {
    return new JoinEntryInstr(flow_graph_.allocate_block_id(), try_index,
                              state_.GetNextDeoptId());
  }

  ConstantInstr* IntConstant(int64_t value,
                             Representation representation = kTagged) const {
    ASSERT(representation == kTagged ||
           RepresentationUtils::IsUnboxedInteger(representation));
    return flow_graph_.GetConstant(
        Integer::Handle(Integer::NewCanonical(value)), representation);
  }

  ConstantInstr* DoubleConstant(double value) {
    return flow_graph_.GetConstant(Double::Handle(Double::NewCanonical(value)));
  }

  // Adds a variable into the scope which would provide inferred argument type
  // for the parameter.
  void AddVariable(const char* name,
                   const AbstractType& static_type,
                   CompileType* inferred_arg_type = nullptr) {
    LocalVariable* v =
        new LocalVariable(TokenPosition::kNoSource, TokenPosition::kNoSource,
                          String::Handle(Symbols::New(Thread::Current(), name)),
                          static_type, LocalVariable::kNoKernelOffset,
                          new CompileType(CompileType::FromAbstractType(
                              static_type, CompileType::kCanBeNull,
                              CompileType::kCannotBeSentinel)),
                          inferred_arg_type);
    v->set_type_check_mode(LocalVariable::kTypeCheckedByCaller);
    flow_graph()->parsed_function().scope()->AddVariable(v);
  }

  enum class IncomingDefKind {
    kImmediate,
    kDelayed,
  };

  class IncomingDef {
   public:
    IncomingDef(BlockEntryInstr* from, Definition* defn)
        : kind_(IncomingDefKind::kImmediate), from_(from), defn_(defn) {}

    template <typename T,
              typename = typename std::enable_if<
                  std::is_base_of<Definition, T>::value>::type>
    IncomingDef(BlockEntryInstr* from, T** defn_source)
        : kind_(IncomingDefKind::kDelayed),
          from_(from),
          defn_source_(reinterpret_cast<Definition**>(defn_source)) {}

    BlockEntryInstr* from() const { return from_; }
    Definition* defn() const {
      return kind_ == IncomingDefKind::kImmediate ? defn_ : *defn_source_;
    }

   private:
    IncomingDefKind kind_;
    BlockEntryInstr* from_;
    union {
      Definition* defn_;
      Definition** defn_source_;
    };
  };

  PhiInstr* Phi(JoinEntryInstr* join,
                std::initializer_list<IncomingDef> incoming) {
    auto phi = new PhiInstr(join, incoming.size());
    for (size_t i = 0; i < incoming.size(); i++) {
      auto input = new Value(flow_graph_.constant_dead());
      phi->SetInputAt(i, input);
      input->definition()->AddInputUse(input);
    }
    for (auto def : incoming) {
      pending_phis_.Add({phi, def});
    }
    return phi;
  }

  void FinishGraph() {
    flow_graph_.DiscoverBlocks();
    GrowableArray<BitVector*> dominance_frontier;
    flow_graph_.ComputeDominators(&dominance_frontier);

    for (auto& pending : pending_phis_) {
      auto join = pending.phi->block();
      EXPECT(pending.phi->InputCount() == join->PredecessorCount());
      auto pred_index = join->IndexOfPredecessor(pending.incoming.from());
      EXPECT(pred_index != -1);
      pending.phi->InputAt(pred_index)->BindTo(pending.incoming.defn());
    }
  }

  FlowGraph* flow_graph() { return &flow_graph_; }

 private:
  static FlowGraph& MakeDummyGraph(
      Thread* thread,
      intptr_t num_parameters,
      bool is_optimizing,
      const std::function<void(const Function&)>& configure_function) {
    const FunctionType& signature =
        FunctionType::ZoneHandle(FunctionType::New());
    signature.set_num_fixed_parameters(num_parameters);
    const Function& func = Function::ZoneHandle(Function::New(
        signature, String::Handle(Symbols::New(thread, "dummy")),
        UntaggedFunction::kRegularFunction,
        /*is_static=*/true,
        /*is_const=*/false,
        /*is_abstract=*/false,
        /*is_external=*/false,
        /*is_native=*/true,
        Class::Handle(thread->isolate_group()->object_store()->object_class()),
        TokenPosition::kNoSource));
    configure_function(func);

    Zone* zone = thread->zone();
    ParsedFunction* parsed_function = new (zone) ParsedFunction(thread, func);

    parsed_function->set_scope(new LocalScope(nullptr, 0, 0));

    auto graph_entry =
        new GraphEntryInstr(*parsed_function, Compiler::kNoOSRDeoptId);

    const intptr_t block_id = 1;  // 0 is GraphEntry.
    graph_entry->set_normal_entry(
        new FunctionEntryInstr(graph_entry, block_id, kInvalidTryIndex,
                               CompilerState::Current().GetNextDeoptId()));
    return *new FlowGraph(*parsed_function, graph_entry, block_id,
                          PrologueInfo{-1, -1},
                          FlowGraph::CompilationModeFrom(is_optimizing));
  }

  CompilerState& state_;
  FlowGraph& flow_graph_;

  struct PendingPhiInput {
    PhiInstr* phi;
    IncomingDef incoming;
  };
  GrowableArray<PendingPhiInput> pending_phis_;
};

template <typename... Args>
ObjectPtr Invoke(const Library& lib, const char* name, const Args&... args) {
  Thread* thread = Thread::Current();
  Dart_Handle api_lib = Api::NewHandle(thread, lib.ptr());
  Dart_Handle argv[] = {Api::NewHandle(thread, args.ptr())...};
  Dart_Handle result;
  {
    TransitionVMToNative transition(thread);
    result =
        Dart_Invoke(api_lib, NewString(name), /*argc=*/sizeof...(Args), argv);
    EXPECT_VALID(result);
  }
  return Api::UnwrapHandle(result);
}

}  // namespace dart

#endif  // RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_
