//===- llvm/unittests/Frontend/OpenMPDecompositionTest.cpp ----------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Frontend/OpenMP/ClauseT.h"
#include "llvm/Frontend/OpenMP/ConstructDecompositionT.h"
#include "llvm/Frontend/OpenMP/OMP.h"
#include "gtest/gtest.h"

#include <iterator>
#include <optional>
#include <sstream>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>

// The actual tests start at comment "--- Test" below.

// Create simple instantiations of all clauses to allow manual construction
// of clauses, and implement emitting of a directive with clauses to a string.
//
// The tests then follow the pattern
// 1. Create a list of clauses.
// 2. Pass them, together with a construct, to the decomposition class.
// 3. Extract individual resulting leaf constructs with clauses applied
//    to them.
// 4. Convert them to strings and compare with expected outputs.

namespace omp {
struct TypeTy {}; // placeholder
struct ExprTy {}; // placeholder
using IdTy = std::string;
} // namespace omp

namespace tomp::type {
template <> struct ObjectT<omp::IdTy, omp::ExprTy> {
  const omp::IdTy &id() const { return name; }
  const std::optional<omp::ExprTy> ref() const { return omp::ExprTy{}; }

  omp::IdTy name;
};
} // namespace tomp::type

namespace omp {
template <typename ElemTy> using List = tomp::type::ListT<ElemTy>;

using Object = tomp::ObjectT<IdTy, ExprTy>;

namespace clause {
using DefinedOperator = tomp::type::DefinedOperatorT<IdTy, ExprTy>;
using ProcedureDesignator = tomp::type::ProcedureDesignatorT<IdTy, ExprTy>;
using ReductionOperator = tomp::type::ReductionIdentifierT<IdTy, ExprTy>;

using AcqRel = tomp::clause::AcqRelT<TypeTy, IdTy, ExprTy>;
using Acquire = tomp::clause::AcquireT<TypeTy, IdTy, ExprTy>;
using AdjustArgs = tomp::clause::AdjustArgsT<TypeTy, IdTy, ExprTy>;
using Affinity = tomp::clause::AffinityT<TypeTy, IdTy, ExprTy>;
using Aligned = tomp::clause::AlignedT<TypeTy, IdTy, ExprTy>;
using Align = tomp::clause::AlignT<TypeTy, IdTy, ExprTy>;
using Allocate = tomp::clause::AllocateT<TypeTy, IdTy, ExprTy>;
using Allocator = tomp::clause::AllocatorT<TypeTy, IdTy, ExprTy>;
using AppendArgs = tomp::clause::AppendArgsT<TypeTy, IdTy, ExprTy>;
using AtomicDefaultMemOrder =
    tomp::clause::AtomicDefaultMemOrderT<TypeTy, IdTy, ExprTy>;
using At = tomp::clause::AtT<TypeTy, IdTy, ExprTy>;
using Bind = tomp::clause::BindT<TypeTy, IdTy, ExprTy>;
using Capture = tomp::clause::CaptureT<TypeTy, IdTy, ExprTy>;
using Collapse = tomp::clause::CollapseT<TypeTy, IdTy, ExprTy>;
using Compare = tomp::clause::CompareT<TypeTy, IdTy, ExprTy>;
using Copyin = tomp::clause::CopyinT<TypeTy, IdTy, ExprTy>;
using Copyprivate = tomp::clause::CopyprivateT<TypeTy, IdTy, ExprTy>;
using Defaultmap = tomp::clause::DefaultmapT<TypeTy, IdTy, ExprTy>;
using Default = tomp::clause::DefaultT<TypeTy, IdTy, ExprTy>;
using Depend = tomp::clause::DependT<TypeTy, IdTy, ExprTy>;
using Destroy = tomp::clause::DestroyT<TypeTy, IdTy, ExprTy>;
using Detach = tomp::clause::DetachT<TypeTy, IdTy, ExprTy>;
using Device = tomp::clause::DeviceT<TypeTy, IdTy, ExprTy>;
using DeviceType = tomp::clause::DeviceTypeT<TypeTy, IdTy, ExprTy>;
using DistSchedule = tomp::clause::DistScheduleT<TypeTy, IdTy, ExprTy>;
using Doacross = tomp::clause::DoacrossT<TypeTy, IdTy, ExprTy>;
using DynamicAllocators =
    tomp::clause::DynamicAllocatorsT<TypeTy, IdTy, ExprTy>;
using Enter = tomp::clause::EnterT<TypeTy, IdTy, ExprTy>;
using Exclusive = tomp::clause::ExclusiveT<TypeTy, IdTy, ExprTy>;
using Fail = tomp::clause::FailT<TypeTy, IdTy, ExprTy>;
using Filter = tomp::clause::FilterT<TypeTy, IdTy, ExprTy>;
using Final = tomp::clause::FinalT<TypeTy, IdTy, ExprTy>;
using Firstprivate = tomp::clause::FirstprivateT<TypeTy, IdTy, ExprTy>;
using From = tomp::clause::FromT<TypeTy, IdTy, ExprTy>;
using Full = tomp::clause::FullT<TypeTy, IdTy, ExprTy>;
using Grainsize = tomp::clause::GrainsizeT<TypeTy, IdTy, ExprTy>;
using HasDeviceAddr = tomp::clause::HasDeviceAddrT<TypeTy, IdTy, ExprTy>;
using Hint = tomp::clause::HintT<TypeTy, IdTy, ExprTy>;
using If = tomp::clause::IfT<TypeTy, IdTy, ExprTy>;
using Inbranch = tomp::clause::InbranchT<TypeTy, IdTy, ExprTy>;
using Inclusive = tomp::clause::InclusiveT<TypeTy, IdTy, ExprTy>;
using Indirect = tomp::clause::IndirectT<TypeTy, IdTy, ExprTy>;
using Init = tomp::clause::InitT<TypeTy, IdTy, ExprTy>;
using InReduction = tomp::clause::InReductionT<TypeTy, IdTy, ExprTy>;
using IsDevicePtr = tomp::clause::IsDevicePtrT<TypeTy, IdTy, ExprTy>;
using Lastprivate = tomp::clause::LastprivateT<TypeTy, IdTy, ExprTy>;
using Linear = tomp::clause::LinearT<TypeTy, IdTy, ExprTy>;
using Link = tomp::clause::LinkT<TypeTy, IdTy, ExprTy>;
using Map = tomp::clause::MapT<TypeTy, IdTy, ExprTy>;
using Match = tomp::clause::MatchT<TypeTy, IdTy, ExprTy>;
using Mergeable = tomp::clause::MergeableT<TypeTy, IdTy, ExprTy>;
using Message = tomp::clause::MessageT<TypeTy, IdTy, ExprTy>;
using Nocontext = tomp::clause::NocontextT<TypeTy, IdTy, ExprTy>;
using Nogroup = tomp::clause::NogroupT<TypeTy, IdTy, ExprTy>;
using Nontemporal = tomp::clause::NontemporalT<TypeTy, IdTy, ExprTy>;
using Notinbranch = tomp::clause::NotinbranchT<TypeTy, IdTy, ExprTy>;
using Novariants = tomp::clause::NovariantsT<TypeTy, IdTy, ExprTy>;
using Nowait = tomp::clause::NowaitT<TypeTy, IdTy, ExprTy>;
using NumTasks = tomp::clause::NumTasksT<TypeTy, IdTy, ExprTy>;
using NumTeams = tomp::clause::NumTeamsT<TypeTy, IdTy, ExprTy>;
using NumThreads = tomp::clause::NumThreadsT<TypeTy, IdTy, ExprTy>;
using OmpxAttribute = tomp::clause::OmpxAttributeT<TypeTy, IdTy, ExprTy>;
using OmpxBare = tomp::clause::OmpxBareT<TypeTy, IdTy, ExprTy>;
using OmpxDynCgroupMem = tomp::clause::OmpxDynCgroupMemT<TypeTy, IdTy, ExprTy>;
using Ordered = tomp::clause::OrderedT<TypeTy, IdTy, ExprTy>;
using Order = tomp::clause::OrderT<TypeTy, IdTy, ExprTy>;
using Partial = tomp::clause::PartialT<TypeTy, IdTy, ExprTy>;
using Priority = tomp::clause::PriorityT<TypeTy, IdTy, ExprTy>;
using Private = tomp::clause::PrivateT<TypeTy, IdTy, ExprTy>;
using ProcBind = tomp::clause::ProcBindT<TypeTy, IdTy, ExprTy>;
using Read = tomp::clause::ReadT<TypeTy, IdTy, ExprTy>;
using Reduction = tomp::clause::ReductionT<TypeTy, IdTy, ExprTy>;
using Relaxed = tomp::clause::RelaxedT<TypeTy, IdTy, ExprTy>;
using Release = tomp::clause::ReleaseT<TypeTy, IdTy, ExprTy>;
using ReverseOffload = tomp::clause::ReverseOffloadT<TypeTy, IdTy, ExprTy>;
using Safelen = tomp::clause::SafelenT<TypeTy, IdTy, ExprTy>;
using Schedule = tomp::clause::ScheduleT<TypeTy, IdTy, ExprTy>;
using SeqCst = tomp::clause::SeqCstT<TypeTy, IdTy, ExprTy>;
using Severity = tomp::clause::SeverityT<TypeTy, IdTy, ExprTy>;
using Shared = tomp::clause::SharedT<TypeTy, IdTy, ExprTy>;
using Simdlen = tomp::clause::SimdlenT<TypeTy, IdTy, ExprTy>;
using Simd = tomp::clause::SimdT<TypeTy, IdTy, ExprTy>;
using Sizes = tomp::clause::SizesT<TypeTy, IdTy, ExprTy>;
using TaskReduction = tomp::clause::TaskReductionT<TypeTy, IdTy, ExprTy>;
using ThreadLimit = tomp::clause::ThreadLimitT<TypeTy, IdTy, ExprTy>;
using Threads = tomp::clause::ThreadsT<TypeTy, IdTy, ExprTy>;
using To = tomp::clause::ToT<TypeTy, IdTy, ExprTy>;
using UnifiedAddress = tomp::clause::UnifiedAddressT<TypeTy, IdTy, ExprTy>;
using UnifiedSharedMemory =
    tomp::clause::UnifiedSharedMemoryT<TypeTy, IdTy, ExprTy>;
using Uniform = tomp::clause::UniformT<TypeTy, IdTy, ExprTy>;
using Unknown = tomp::clause::UnknownT<TypeTy, IdTy, ExprTy>;
using Untied = tomp::clause::UntiedT<TypeTy, IdTy, ExprTy>;
using Update = tomp::clause::UpdateT<TypeTy, IdTy, ExprTy>;
using UseDeviceAddr = tomp::clause::UseDeviceAddrT<TypeTy, IdTy, ExprTy>;
using UseDevicePtr = tomp::clause::UseDevicePtrT<TypeTy, IdTy, ExprTy>;
using UsesAllocators = tomp::clause::UsesAllocatorsT<TypeTy, IdTy, ExprTy>;
using Use = tomp::clause::UseT<TypeTy, IdTy, ExprTy>;
using Weak = tomp::clause::WeakT<TypeTy, IdTy, ExprTy>;
using When = tomp::clause::WhenT<TypeTy, IdTy, ExprTy>;
using Write = tomp::clause::WriteT<TypeTy, IdTy, ExprTy>;
} // namespace clause

struct Helper {
  std::optional<Object> getBaseObject(const Object &object) {
    return std::nullopt;
  }
  std::optional<Object> getLoopIterVar() { return std::nullopt; }
};

using Clause = tomp::ClauseT<TypeTy, IdTy, ExprTy>;
using ConstructDecomposition = tomp::ConstructDecompositionT<Clause, Helper>;
using DirectiveWithClauses = tomp::DirectiveWithClauses<Clause>;
} // namespace omp

struct StringifyClause {
  static std::string join(const omp::List<std::string> &Strings) {
    std::stringstream Stream;
    for (const auto &[Index, String] : llvm::enumerate(Strings)) {
      if (Index != 0)
        Stream << ", ";
      Stream << String;
    }
    return Stream.str();
  }

  static std::string to_str(llvm::omp::Directive D) {
    return getOpenMPDirectiveName(D).str();
  }
  static std::string to_str(llvm::omp::Clause C) {
    return getOpenMPClauseName(C).str();
  }
  static std::string to_str(const omp::TypeTy &Type) { return "type"; }
  static std::string to_str(const omp::ExprTy &Expr) { return "expr"; }
  static std::string to_str(const omp::Object &Obj) { return Obj.id(); }

  template <typename U>
  static std::enable_if_t<std::is_enum_v<llvm::remove_cvref_t<U>>, std::string>
  to_str(U &&Item) {
    return std::to_string(llvm::to_underlying(Item));
  }

  template <typename U> static std::string to_str(const omp::List<U> &Items) {
    omp::List<std::string> Names;
    llvm::transform(Items, std::back_inserter(Names),
                    [](auto &&S) { return to_str(S); });
    return "(" + join(Names) + ")";
  }

  template <typename U>
  static std::string to_str(const std::optional<U> &Item) {
    if (Item)
      return to_str(*Item);
    return "";
  }

  template <typename... Us, size_t... Is>
  static std::string to_str(const std::tuple<Us...> &Tuple,
                            std::index_sequence<Is...>) {
    omp::List<std::string> Strings;
    (Strings.push_back(to_str(std::get<Is>(Tuple))), ...);
    return "(" + join(Strings) + ")";
  }

  template <typename U>
  static std::enable_if_t<llvm::remove_cvref_t<U>::EmptyTrait::value,
                          std::string>
  to_str(U &&Item) {
    return "";
  }

  template <typename U>
  static std::enable_if_t<llvm::remove_cvref_t<U>::IncompleteTrait::value,
                          std::string>
  to_str(U &&Item) {
    return "";
  }

  template <typename U>
  static std::enable_if_t<llvm::remove_cvref_t<U>::WrapperTrait::value,
                          std::string>
  to_str(U &&Item) {
    // For a wrapper, stringify the wrappee, and only add parentheses if
    // there aren't any already.
    std::string Str = to_str(Item.v);
    if (!Str.empty()) {
      if (Str.front() == '(' && Str.back() == ')')
        return Str;
    }
    return "(" + to_str(Item.v) + ")";
  }

  template <typename U>
  static std::enable_if_t<llvm::remove_cvref_t<U>::TupleTrait::value,
                          std::string>
  to_str(U &&Item) {
    constexpr size_t TupleSize =
        std::tuple_size_v<llvm::remove_cvref_t<decltype(Item.t)>>;
    return to_str(Item.t, std::make_index_sequence<TupleSize>{});
  }

  template <typename U>
  static std::enable_if_t<llvm::remove_cvref_t<U>::UnionTrait::value,
                          std::string>
  to_str(U &&Item) {
    return std::visit([](auto &&S) { return to_str(S); }, Item.u);
  }

  StringifyClause(const omp::Clause &C)
      // Rely on content stringification to emit enclosing parentheses.
      : Str(to_str(C.id) + to_str(C)) {}

  std::string Str;
};

std::string stringify(const omp::DirectiveWithClauses &DWC) {
  std::stringstream Stream;

  Stream << getOpenMPDirectiveName(DWC.id).str();
  for (const omp::Clause &C : DWC.clauses)
    Stream << ' ' << StringifyClause(C).Str;

  return Stream.str();
}

// --- Tests ----------------------------------------------------------

namespace red {
// Make it easier to construct reduction operators from built-in intrinsics.
omp::clause::ReductionOperator
makeOp(omp::clause::DefinedOperator::IntrinsicOperator Op) {
  return omp::clause::ReductionOperator{omp::clause::DefinedOperator{Op}};
}
} // namespace red

namespace {
using namespace llvm::omp;

class OpenMPDecompositionTest : public testing::Test {
protected:
  void SetUp() override {}
  void TearDown() override {}

  omp::Helper Helper;
  uint32_t AnyVersion = 999;
};

// PRIVATE
// [5.2:111:5-7]
// Directives: distribute, do, for, loop, parallel, scope, sections, simd,
// single, target, task, taskloop, teams
//
// [5.2:340:1-2]
// (1) The effect of the 1 private clause is as if it is applied only to the
// innermost leaf construct that permits it.
TEST_F(OpenMPDecompositionTest, Private1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_private, omp::clause::Private{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel");            // (1)
  ASSERT_EQ(Dir1, "sections private(x)"); // (1)
}

TEST_F(OpenMPDecompositionTest, Private2) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_private, omp::clause::Private{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_masked,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel private(x)"); // (1)
  ASSERT_EQ(Dir1, "masked");              // (1)
}

// FIRSTPRIVATE
// [5.2:112:5-7]
// Directives: distribute, do, for, parallel, scope, sections, single, target,
// task, taskloop, teams
//
// [5.2:340:3-20]
// (3) The effect of the firstprivate clause is as if it is applied to one or
// more leaf constructs as follows:
//  (5) To the distribute construct if it is among the constituent constructs;
//  (6) To the teams construct if it is among the constituent constructs and the
//      distribute construct is not;
//  (8) To a worksharing construct that accepts the clause if one is among the
//      constituent constructs;
//  (9) To the taskloop construct if it is among the constituent constructs;
// (10) To the parallel construct if it is among the constituent constructs and
//      neither a taskloop construct nor a worksharing construct that accepts
//      the clause is among them;
// (12) To the target construct if it is among the constituent constructs and
//      the same list item neither appears in a lastprivate clause nor is the
//      base variable or base pointer of a list item that appears in a map
//      clause.
//
// (15) If the parallel construct is among the constituent constructs and the
// effect is not as if the firstprivate clause is applied to it by the above
// rules, then the effect is as if the shared clause with the same list item is
// applied to the parallel construct.
// (17) If the teams construct is among the constituent constructs and the
// effect is not as if the firstprivate clause is applied to it by the above
// rules, then the effect is as if the shared clause with the same list item is
// applied to the teams construct.
TEST_F(OpenMPDecompositionTest, Firstprivate1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel shared(x)");       // (10), (15)
  ASSERT_EQ(Dir1, "sections firstprivate(x)"); // (8)
}

TEST_F(OpenMPDecompositionTest, Firstprivate2) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_target_teams_distribute, Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "target firstprivate(x)");     // (12)
  ASSERT_EQ(Dir1, "teams shared(x)");            // (6), (17)
  ASSERT_EQ(Dir2, "distribute firstprivate(x)"); // (5)
}

TEST_F(OpenMPDecompositionTest, Firstprivate3) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
      {OMPC_lastprivate, omp::clause::Lastprivate{{std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_target_teams_distribute, Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "target map(2, , , , (x))"); // (12), (27)
  ASSERT_EQ(Dir1, "teams shared(x)");          // (6), (17)
  ASSERT_EQ(Dir2, "distribute firstprivate(x) lastprivate(, (x))"); // (5), (21)
}

TEST_F(OpenMPDecompositionTest, Firstprivate4) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_target_teams,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "target firstprivate(x)"); // (12)
  ASSERT_EQ(Dir1, "teams firstprivate(x)");  // (6)
}

TEST_F(OpenMPDecompositionTest, Firstprivate5) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_parallel_masked_taskloop, Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "parallel shared(x)"); // (10)
  ASSERT_EQ(Dir1, "masked");
  ASSERT_EQ(Dir2, "taskloop firstprivate(x)"); // (9)
}

TEST_F(OpenMPDecompositionTest, Firstprivate6) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_masked,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel firstprivate(x)"); // (10)
  ASSERT_EQ(Dir1, "masked");
}

TEST_F(OpenMPDecompositionTest, Firstprivate7) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  // Composite constructs are still decomposed.
  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_teams_distribute,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "teams shared(x)");            // (17)
  ASSERT_EQ(Dir1, "distribute firstprivate(x)"); // (5)
}

// LASTPRIVATE
// [5.2:115:7-8]
// Directives: distribute, do, for, loop, sections, simd, taskloop
//
// [5.2:340:21-30]
// (21) The effect of the lastprivate clause is as if it is applied to all leaf
// constructs that permit the clause.
// (22) If the parallel construct is among the constituent constructs and the
// list item is not also specified in the firstprivate clause, then the effect
// of the lastprivate clause is as if the shared clause with the same list item
// is applied to the parallel construct.
// (24) If the teams construct is among the constituent constructs and the list
// item is not also specified in the firstprivate clause, then the effect of the
// lastprivate clause is as if the shared clause with the same list item is
// applied to the teams construct.
// (27) If the target construct is among the constituent constructs and the list
// item is not the base variable or base pointer of a list item that appears in
// a map clause, the effect of the lastprivate clause is as if the same list
// item appears in a map clause with a map-type of tofrom.
TEST_F(OpenMPDecompositionTest, Lastprivate1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_lastprivate, omp::clause::Lastprivate{{std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel shared(x)");          // (21), (22)
  ASSERT_EQ(Dir1, "sections lastprivate(, (x))"); // (21)
}

TEST_F(OpenMPDecompositionTest, Lastprivate2) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_lastprivate, omp::clause::Lastprivate{{std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_teams_distribute,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "teams shared(x)");               // (21), (25)
  ASSERT_EQ(Dir1, "distribute lastprivate(, (x))"); // (21)
}

TEST_F(OpenMPDecompositionTest, Lastprivate3) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_lastprivate, omp::clause::Lastprivate{{std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_target_parallel_do,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "target map(2, , , , (x))"); // (21), (27)
  ASSERT_EQ(Dir1, "parallel shared(x)");       // (22)
  ASSERT_EQ(Dir2, "do lastprivate(, (x))");    // (21)
}

// SHARED
// [5.2:110:5-6]
// Directives: parallel, task, taskloop, teams
//
// [5.2:340:31-32]
// (31) The effect of the shared, default, thread_limit, or order clause is as
// if it is applied to all leaf constructs that permit the clause.
TEST_F(OpenMPDecompositionTest, Shared1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_shared, omp::clause::Shared{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_parallel_masked_taskloop, Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "parallel shared(x)"); // (31)
  ASSERT_EQ(Dir1, "masked");             // (31)
  ASSERT_EQ(Dir2, "taskloop shared(x)"); // (31)
}

// DEFAULT
// [5.2:109:5-6]
// Directives: parallel, task, taskloop, teams
//
// [5.2:340:31-32]
// (31) The effect of the shared, default, thread_limit, or order clause is as
// if it is applied to all leaf constructs that permit the clause.
TEST_F(OpenMPDecompositionTest, Default1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_default,
       omp::clause::Default{
           omp::clause::Default::DataSharingAttribute::Firstprivate}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_parallel_masked_taskloop, Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "parallel default(0)"); // (31)
  ASSERT_EQ(Dir1, "masked");              // (31)
  ASSERT_EQ(Dir2, "taskloop default(0)"); // (31)
}

// THREAD_LIMIT
// [5.2:277:14-15]
// Directives: target, teams
//
// [5.2:340:31-32]
// (31) The effect of the shared, default, thread_limit, or order clause is as
// if it is applied to all leaf constructs that permit the clause.
TEST_F(OpenMPDecompositionTest, ThreadLimit1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_thread_limit, omp::clause::ThreadLimit{omp::ExprTy{}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_target_teams_distribute, Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "target thread_limit(expr)"); // (31)
  ASSERT_EQ(Dir1, "teams thread_limit(expr)");  // (31)
  ASSERT_EQ(Dir2, "distribute");                // (31)
}

// ORDER
// [5.2:234:3-4]
// Directives: distribute, do, for, loop, simd
//
// [5.2:340:31-32]
// (31) The effect of the shared, default, thread_limit, or order clause is as
// if it is applied to all leaf constructs that permit the clause.
TEST_F(OpenMPDecompositionTest, Order1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_order,
       omp::clause::Order{{omp::clause::Order::OrderModifier::Unconstrained,
                           omp::clause::Order::Ordering::Concurrent}}},
  };

  omp::ConstructDecomposition Dec(
      AnyVersion, Helper, OMPD_target_teams_distribute_parallel_for_simd,
      Clauses);
  ASSERT_EQ(Dec.output.size(), 6u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  std::string Dir3 = stringify(Dec.output[3]);
  std::string Dir4 = stringify(Dec.output[4]);
  std::string Dir5 = stringify(Dec.output[5]);
  ASSERT_EQ(Dir0, "target");                 // (31)
  ASSERT_EQ(Dir1, "teams");                  // (31)
  ASSERT_EQ(Dir2, "distribute order(1, 0)"); // (31)
  ASSERT_EQ(Dir3, "parallel");               // (31)
  ASSERT_EQ(Dir4, "for order(1, 0)");        // (31)
  ASSERT_EQ(Dir5, "simd order(1, 0)");       // (31)
}

// ALLOCATE
// [5.2:178:7-9]
// Directives: allocators, distribute, do, for, parallel, scope, sections,
// single, target, task, taskgroup, taskloop, teams
//
// [5.2:340:33-35]
// (33) The effect of the allocate clause is as if it is applied to all leaf
// constructs that permit the clause and to which a data-sharing attribute
// clause that may create a private copy of the same list item is applied.
TEST_F(OpenMPDecompositionTest, Allocate1) {
  omp::Object x{"x"};

  // Allocate + firstprivate
  omp::List<omp::Clause> Clauses{
      {OMPC_allocate, omp::clause::Allocate{{std::nullopt, std::nullopt, {x}}}},
      {OMPC_firstprivate, omp::clause::Firstprivate{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel shared(x)");                         // (33)
  ASSERT_EQ(Dir1, "sections firstprivate(x) allocate(, , (x))"); // (33)
}

TEST_F(OpenMPDecompositionTest, Allocate2) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  // Allocate + in_reduction
  omp::List<omp::Clause> Clauses{
      {OMPC_allocate, omp::clause::Allocate{{std::nullopt, std::nullopt, {x}}}},
      {OMPC_in_reduction, omp::clause::InReduction{{{Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_target_parallel,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "target in_reduction((3), (x)) allocate(, , (x))"); // (33)
  ASSERT_EQ(Dir1, "parallel");                                        // (33)
}

TEST_F(OpenMPDecompositionTest, Allocate3) {
  omp::Object x{"x"};

  // Allocate + linear
  omp::List<omp::Clause> Clauses{
      {OMPC_allocate, omp::clause::Allocate{{std::nullopt, std::nullopt, {x}}}},
      {OMPC_linear,
       omp::clause::Linear{{std::nullopt, std::nullopt, std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_for,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  // The "shared" clause is duplicated---this isn't harmful, but it
  // should be fixed eventually.
  ASSERT_EQ(Dir0, "parallel shared(x) shared(x)"); // (33)
  ASSERT_EQ(Dir1, "for linear(, , , (x)) firstprivate(x) lastprivate(, (x)) "
                  "allocate(, , (x))"); // (33)
}

TEST_F(OpenMPDecompositionTest, Allocate4) {
  omp::Object x{"x"};

  // Allocate + lastprivate
  omp::List<omp::Clause> Clauses{
      {OMPC_allocate, omp::clause::Allocate{{std::nullopt, std::nullopt, {x}}}},
      {OMPC_lastprivate, omp::clause::Lastprivate{{std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel shared(x)");                            // (33)
  ASSERT_EQ(Dir1, "sections lastprivate(, (x)) allocate(, , (x))"); // (33)
}

TEST_F(OpenMPDecompositionTest, Allocate5) {
  omp::Object x{"x"};

  // Allocate + private
  omp::List<omp::Clause> Clauses{
      {OMPC_allocate, omp::clause::Allocate{{std::nullopt, std::nullopt, {x}}}},
      {OMPC_private, omp::clause::Private{{x}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel");                              // (33)
  ASSERT_EQ(Dir1, "sections private(x) allocate(, , (x))"); // (33)
}

TEST_F(OpenMPDecompositionTest, Allocate6) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  // Allocate + reduction
  omp::List<omp::Clause> Clauses{
      {OMPC_allocate, omp::clause::Allocate{{std::nullopt, std::nullopt, {x}}}},
      {OMPC_reduction, omp::clause::Reduction{{std::nullopt, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel shared(x)");                               // (33)
  ASSERT_EQ(Dir1, "sections reduction(, (3), (x)) allocate(, , (x))"); // (33)
}

// REDUCTION
// [5.2:134:17-18]
// Directives: do, for, loop, parallel, scope, sections, simd, taskloop, teams
//
// [5.2:340-341:36-13]
// (36) The effect of the reduction clause is as if it is applied to all leaf
// constructs that permit the clause, except for the following constructs:
//  (1) The parallel construct, when combined with the sections,
//      worksharing-loop, loop, or taskloop construct; and
//  (3) The teams construct, when combined with the loop construct.
// (4) For the parallel and teams constructs above, the effect of the reduction
// clause instead is as if each list item or, for any list item that is an array
// item, its corresponding base array or base pointer appears in a shared clause
// for the construct.
// (6) If the task reduction-modifier is specified, the effect is as if it only
// modifies the behavior of the reduction clause on the innermost leaf construct
// that accepts the modifier (see Section 5.5.8).
// (8) If the inscan reduction-modifier is specified, the effect is as if it
// modifies the behavior of the reduction clause on all constructs of the
// combined construct to which the clause is applied and that accept the
// modifier.
// (10) If a list item in a reduction clause on a combined target construct does
// not have the same base variable or base pointer as a list item in a map
// clause on the construct, then the effect is as if the list item in the
// reduction clause appears as a list item in a map clause with a map-type of
// tofrom.
TEST_F(OpenMPDecompositionTest, Reduction1) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{std::nullopt, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_sections,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel shared(x)");             // (36), (1), (4)
  ASSERT_EQ(Dir1, "sections reduction(, (3), (x))"); // (36)
}

TEST_F(OpenMPDecompositionTest, Reduction2) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{std::nullopt, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_parallel_masked,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "parallel reduction(, (3), (x))"); // (36), (1), (4)
  ASSERT_EQ(Dir1, "masked");                         // (36)
}

TEST_F(OpenMPDecompositionTest, Reduction3) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{std::nullopt, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_teams_loop, Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "teams shared(x)");            // (36), (3), (4)
  ASSERT_EQ(Dir1, "loop reduction(, (3), (x))"); // (36)
}

TEST_F(OpenMPDecompositionTest, Reduction4) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{std::nullopt, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_teams_distribute_parallel_for, Clauses);
  ASSERT_EQ(Dec.output.size(), 4u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  std::string Dir3 = stringify(Dec.output[3]);
  ASSERT_EQ(Dir0, "teams reduction(, (3), (x))"); // (36), (3)
  ASSERT_EQ(Dir1, "distribute");                  // (36)
  ASSERT_EQ(Dir2, "parallel shared(x)");          // (36), (1), (4)
  ASSERT_EQ(Dir3, "for reduction(, (3), (x))");   // (36)
}

TEST_F(OpenMPDecompositionTest, Reduction5) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);
  auto TaskMod = omp::clause::Reduction::ReductionModifier::Task;

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{TaskMod, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_teams_distribute_parallel_for, Clauses);
  ASSERT_EQ(Dec.output.size(), 4u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  std::string Dir3 = stringify(Dec.output[3]);
  ASSERT_EQ(Dir0, "teams reduction(, (3), (x))"); // (36), (3), (6)
  ASSERT_EQ(Dir1, "distribute");                  // (36)
  ASSERT_EQ(Dir2, "parallel shared(x)");          // (36), (1), (4)
  ASSERT_EQ(Dir3, "for reduction(2, (3), (x))");  // (36), (6)
}

TEST_F(OpenMPDecompositionTest, Reduction6) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);
  auto InscanMod = omp::clause::Reduction::ReductionModifier::Inscan;

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{InscanMod, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_teams_distribute_parallel_for, Clauses);
  ASSERT_EQ(Dec.output.size(), 4u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  std::string Dir3 = stringify(Dec.output[3]);
  ASSERT_EQ(Dir0, "teams reduction(, (3), (x))"); // (36), (3), (8)
  ASSERT_EQ(Dir1, "distribute");                  // (36)
  ASSERT_EQ(Dir2, "parallel shared(x)");          // (36), (1), (4)
  ASSERT_EQ(Dir3, "for reduction(1, (3), (x))");  // (36), (8)
}

TEST_F(OpenMPDecompositionTest, Reduction7) {
  omp::Object x{"x"};
  auto Add = red::makeOp(omp::clause::DefinedOperator::IntrinsicOperator::Add);

  omp::List<omp::Clause> Clauses{
      {OMPC_reduction, omp::clause::Reduction{{std::nullopt, {Add}, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_target_parallel_do,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);

  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "target map(2, , , , (x))"); // (36), (10)
  ASSERT_EQ(Dir1, "parallel shared(x)");       // (36), (1), (4)
  ASSERT_EQ(Dir2, "do reduction(, (3), (x))"); // (36)
}

// IF
// [5.2:72:7-9]
// Directives: cancel, parallel, simd, target, target data, target enter data,
// target exit data, target update, task, taskloop
//
// [5.2:72:15-18]
// (15) For combined or composite constructs, the if clause only applies to the
// semantics of the construct named in the directive-name-modifier.
// (16) For a combined or composite construct, if no directive-name-modifier is
// specified then the if clause applies to all constituent constructs to which
// an if clause can apply.
TEST_F(OpenMPDecompositionTest, If1) {
  omp::List<omp::Clause> Clauses{
      {OMPC_if,
       omp::clause::If{{llvm::omp::Directive::OMPD_parallel, omp::ExprTy{}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_target_parallel_for_simd, Clauses);
  ASSERT_EQ(Dec.output.size(), 4u);
  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  std::string Dir3 = stringify(Dec.output[3]);
  ASSERT_EQ(Dir0, "target");              // (15)
  ASSERT_EQ(Dir1, "parallel if(, expr)"); // (15)
  ASSERT_EQ(Dir2, "for");                 // (15)
  ASSERT_EQ(Dir3, "simd");                // (15)
}

TEST_F(OpenMPDecompositionTest, If2) {
  omp::List<omp::Clause> Clauses{
      {OMPC_if, omp::clause::If{{std::nullopt, omp::ExprTy{}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper,
                                  OMPD_target_parallel_for_simd, Clauses);
  ASSERT_EQ(Dec.output.size(), 4u);
  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  std::string Dir3 = stringify(Dec.output[3]);
  ASSERT_EQ(Dir0, "target if(, expr)");   // (16)
  ASSERT_EQ(Dir1, "parallel if(, expr)"); // (16)
  ASSERT_EQ(Dir2, "for");                 // (16)
  ASSERT_EQ(Dir3, "simd if(, expr)");     // (16)
}

// LINEAR
// [5.2:118:1-2]
// Directives: declare simd, do, for, simd
//
// [5.2:341:15-22]
// (15.1) The effect of the linear clause is as if it is applied to the
// innermost leaf construct.
// (15.2) Additionally, if the list item is not the iteration variable of a simd
// or worksharing-loop SIMD construct, the effect on the outer leaf constructs
// is as if the list item was specified in firstprivate and lastprivate clauses
// on the combined or composite construct, with the rules specified above
// applied.
// (19) If a list item of the linear clause is the iteration variable of a simd
// or worksharing-loop SIMD construct and it is not declared in the construct,
// the effect on the outer leaf constructs is as if the list item was specified
// in a lastprivate clause on the combined or composite construct with the rules
// specified above applied.
TEST_F(OpenMPDecompositionTest, Linear1) {
  omp::Object x{"x"};

  omp::List<omp::Clause> Clauses{
      {OMPC_linear,
       omp::clause::Linear{{std::nullopt, std::nullopt, std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_for_simd, Clauses);
  ASSERT_EQ(Dec.output.size(), 2u);
  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  ASSERT_EQ(Dir0, "for firstprivate(x) lastprivate(, (x))"); // (15.1), (15.2)
  ASSERT_EQ(Dir1, "simd linear(, , , (x)) lastprivate(, (x))"); // (15.1)
}

// NOWAIT
// [5.2:308:11-13]
// Directives: dispatch, do, for, interop, scope, sections, single, target,
// target enter data, target exit data, target update, taskwait, workshare
//
// [5.2:341:23]
// (23) The effect of the nowait clause is as if it is applied to the outermost
// leaf construct that permits it.
TEST_F(OpenMPDecompositionTest, Nowait1) {
  omp::List<omp::Clause> Clauses{
      {OMPC_nowait, omp::clause::Nowait{}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_target_parallel_for,
                                  Clauses);
  ASSERT_EQ(Dec.output.size(), 3u);
  std::string Dir0 = stringify(Dec.output[0]);
  std::string Dir1 = stringify(Dec.output[1]);
  std::string Dir2 = stringify(Dec.output[2]);
  ASSERT_EQ(Dir0, "target nowait"); // (23)
  ASSERT_EQ(Dir1, "parallel");      // (23)
  ASSERT_EQ(Dir2, "for");           // (23)
}

// ---

// Check that "simd linear(x)" does not fail despite the implied "firstprivate"
// (which "simd" does not allow).
TEST_F(OpenMPDecompositionTest, Misc1) {
  omp::Object x{"x"};
  omp::List<omp::Clause> Clauses{
      {OMPC_linear,
       omp::clause::Linear{{std::nullopt, std::nullopt, std::nullopt, {x}}}},
  };

  omp::ConstructDecomposition Dec(AnyVersion, Helper, OMPD_simd, Clauses);
  ASSERT_EQ(Dec.output.size(), 1u);
  std::string Dir0 = stringify(Dec.output[0]);
  ASSERT_EQ(Dir0, "simd linear(, , , (x)) lastprivate(, (x))");
}
} // namespace
