#include "Utils/AArch64SMEAttributes.h"
#include "llvm/AsmParser/Parser.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/SourceMgr.h"

#include "gtest/gtest.h"

using namespace llvm;
using SA = SMEAttrs;

std::unique_ptr<Module> parseIR(const char *IR) {
  static LLVMContext C;
  SMDiagnostic Err;
  return parseAssemblyString(IR, Err, C);
}

TEST(SMEAttributes, Constructors) {
  LLVMContext Context;

  ASSERT_TRUE(SA(*parseIR("declare void @foo()")->getFunction("foo"))
                  .hasNonStreamingInterfaceAndBody());

  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_pstate_sm_body\"")
                      ->getFunction("foo"))
                  .hasNonStreamingInterface());

  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_pstate_sm_enabled\"")
                      ->getFunction("foo"))
                  .hasStreamingInterface());

  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_pstate_sm_body\"")
                      ->getFunction("foo"))
                  .hasStreamingBody());

  ASSERT_TRUE(
      SA(*parseIR("declare void @foo() \"aarch64_pstate_sm_compatible\"")
              ->getFunction("foo"))
          .hasStreamingCompatibleInterface());

  ASSERT_TRUE(
      SA(*parseIR("declare void @foo() \"aarch64_in_za\"")->getFunction("foo"))
          .isInZA());
  ASSERT_TRUE(
      SA(*parseIR("declare void @foo() \"aarch64_out_za\"")->getFunction("foo"))
          .isOutZA());
  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_inout_za\"")
                      ->getFunction("foo"))
                  .isInOutZA());
  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_preserves_za\"")
                      ->getFunction("foo"))
                  .isPreservesZA());
  ASSERT_TRUE(
      SA(*parseIR("declare void @foo() \"aarch64_new_za\"")->getFunction("foo"))
          .isNewZA());

  ASSERT_TRUE(
      SA(*parseIR("declare void @foo() \"aarch64_in_zt0\"")->getFunction("foo"))
          .isInZT0());
  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_out_zt0\"")
                      ->getFunction("foo"))
                  .isOutZT0());
  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_inout_zt0\"")
                      ->getFunction("foo"))
                  .isInOutZT0());
  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_preserves_zt0\"")
                      ->getFunction("foo"))
                  .isPreservesZT0());
  ASSERT_TRUE(SA(*parseIR("declare void @foo() \"aarch64_new_zt0\"")
                      ->getFunction("foo"))
                  .isNewZT0());

  // Invalid combinations.
  EXPECT_DEBUG_DEATH(SA(SA::SM_Enabled | SA::SM_Compatible),
                     "SM_Enabled and SM_Compatible are mutually exclusive");

  // Test that the set() methods equally check validity.
  EXPECT_DEBUG_DEATH(SA(SA::SM_Enabled).set(SA::SM_Compatible),
                     "SM_Enabled and SM_Compatible are mutually exclusive");
  EXPECT_DEBUG_DEATH(SA(SA::SM_Compatible).set(SA::SM_Enabled),
                     "SM_Enabled and SM_Compatible are mutually exclusive");
}

TEST(SMEAttributes, Basics) {
  // Test PSTATE.SM interfaces.
  ASSERT_TRUE(SA(SA::Normal).hasNonStreamingInterfaceAndBody());
  ASSERT_TRUE(SA(SA::SM_Enabled).hasStreamingInterface());
  ASSERT_TRUE(SA(SA::SM_Body).hasStreamingBody());
  ASSERT_TRUE(SA(SA::SM_Body).hasNonStreamingInterface());
  ASSERT_FALSE(SA(SA::SM_Body).hasNonStreamingInterfaceAndBody());
  ASSERT_FALSE(SA(SA::SM_Body).hasStreamingInterface());
  ASSERT_TRUE(SA(SA::SM_Compatible).hasStreamingCompatibleInterface());
  ASSERT_TRUE(
      SA(SA::SM_Compatible | SA::SM_Body).hasStreamingCompatibleInterface());
  ASSERT_TRUE(SA(SA::SM_Compatible | SA::SM_Body).hasStreamingBody());
  ASSERT_FALSE(SA(SA::SM_Compatible | SA::SM_Body).hasNonStreamingInterface());

  // Test ZA State interfaces
  SA ZA_In = SA(SA::encodeZAState(SA::StateValue::In));
  ASSERT_TRUE(ZA_In.isInZA());
  ASSERT_FALSE(ZA_In.isOutZA());
  ASSERT_FALSE(ZA_In.isInOutZA());
  ASSERT_FALSE(ZA_In.isPreservesZA());
  ASSERT_FALSE(ZA_In.isNewZA());
  ASSERT_TRUE(ZA_In.sharesZA());
  ASSERT_TRUE(ZA_In.hasZAState());
  ASSERT_TRUE(ZA_In.hasSharedZAInterface());
  ASSERT_FALSE(ZA_In.hasPrivateZAInterface());

  SA ZA_Out = SA(SA::encodeZAState(SA::StateValue::Out));
  ASSERT_TRUE(ZA_Out.isOutZA());
  ASSERT_FALSE(ZA_Out.isInZA());
  ASSERT_FALSE(ZA_Out.isInOutZA());
  ASSERT_FALSE(ZA_Out.isPreservesZA());
  ASSERT_FALSE(ZA_Out.isNewZA());
  ASSERT_TRUE(ZA_Out.sharesZA());
  ASSERT_TRUE(ZA_Out.hasZAState());
  ASSERT_TRUE(ZA_Out.hasSharedZAInterface());
  ASSERT_FALSE(ZA_Out.hasPrivateZAInterface());

  SA ZA_InOut = SA(SA::encodeZAState(SA::StateValue::InOut));
  ASSERT_TRUE(ZA_InOut.isInOutZA());
  ASSERT_FALSE(ZA_InOut.isInZA());
  ASSERT_FALSE(ZA_InOut.isOutZA());
  ASSERT_FALSE(ZA_InOut.isPreservesZA());
  ASSERT_FALSE(ZA_InOut.isNewZA());
  ASSERT_TRUE(ZA_InOut.sharesZA());
  ASSERT_TRUE(ZA_InOut.hasZAState());
  ASSERT_TRUE(ZA_InOut.hasSharedZAInterface());
  ASSERT_FALSE(ZA_InOut.hasPrivateZAInterface());

  SA ZA_Preserved = SA(SA::encodeZAState(SA::StateValue::Preserved));
  ASSERT_TRUE(ZA_Preserved.isPreservesZA());
  ASSERT_FALSE(ZA_Preserved.isInZA());
  ASSERT_FALSE(ZA_Preserved.isOutZA());
  ASSERT_FALSE(ZA_Preserved.isInOutZA());
  ASSERT_FALSE(ZA_Preserved.isNewZA());
  ASSERT_TRUE(ZA_Preserved.sharesZA());
  ASSERT_TRUE(ZA_Preserved.hasZAState());
  ASSERT_TRUE(ZA_Preserved.hasSharedZAInterface());
  ASSERT_FALSE(ZA_Preserved.hasPrivateZAInterface());

  SA ZA_New = SA(SA::encodeZAState(SA::StateValue::New));
  ASSERT_TRUE(ZA_New.isNewZA());
  ASSERT_FALSE(ZA_New.isInZA());
  ASSERT_FALSE(ZA_New.isOutZA());
  ASSERT_FALSE(ZA_New.isInOutZA());
  ASSERT_FALSE(ZA_New.isPreservesZA());
  ASSERT_FALSE(ZA_New.sharesZA());
  ASSERT_TRUE(ZA_New.hasZAState());
  ASSERT_FALSE(ZA_New.hasSharedZAInterface());
  ASSERT_TRUE(ZA_New.hasPrivateZAInterface());

  ASSERT_FALSE(SA(SA::Normal).isInZA());
  ASSERT_FALSE(SA(SA::Normal).isOutZA());
  ASSERT_FALSE(SA(SA::Normal).isInOutZA());
  ASSERT_FALSE(SA(SA::Normal).isPreservesZA());
  ASSERT_FALSE(SA(SA::Normal).isNewZA());
  ASSERT_FALSE(SA(SA::Normal).sharesZA());
  ASSERT_FALSE(SA(SA::Normal).hasZAState());

  // Test ZT0 State interfaces
  SA ZT0_In = SA(SA::encodeZT0State(SA::StateValue::In));
  ASSERT_TRUE(ZT0_In.isInZT0());
  ASSERT_FALSE(ZT0_In.isOutZT0());
  ASSERT_FALSE(ZT0_In.isInOutZT0());
  ASSERT_FALSE(ZT0_In.isPreservesZT0());
  ASSERT_FALSE(ZT0_In.isNewZT0());
  ASSERT_TRUE(ZT0_In.sharesZT0());
  ASSERT_TRUE(ZT0_In.hasZT0State());
  ASSERT_TRUE(ZT0_In.hasSharedZAInterface());
  ASSERT_FALSE(ZT0_In.hasPrivateZAInterface());

  SA ZT0_Out = SA(SA::encodeZT0State(SA::StateValue::Out));
  ASSERT_TRUE(ZT0_Out.isOutZT0());
  ASSERT_FALSE(ZT0_Out.isInZT0());
  ASSERT_FALSE(ZT0_Out.isInOutZT0());
  ASSERT_FALSE(ZT0_Out.isPreservesZT0());
  ASSERT_FALSE(ZT0_Out.isNewZT0());
  ASSERT_TRUE(ZT0_Out.sharesZT0());
  ASSERT_TRUE(ZT0_Out.hasZT0State());
  ASSERT_TRUE(ZT0_Out.hasSharedZAInterface());
  ASSERT_FALSE(ZT0_Out.hasPrivateZAInterface());

  SA ZT0_InOut = SA(SA::encodeZT0State(SA::StateValue::InOut));
  ASSERT_TRUE(ZT0_InOut.isInOutZT0());
  ASSERT_FALSE(ZT0_InOut.isInZT0());
  ASSERT_FALSE(ZT0_InOut.isOutZT0());
  ASSERT_FALSE(ZT0_InOut.isPreservesZT0());
  ASSERT_FALSE(ZT0_InOut.isNewZT0());
  ASSERT_TRUE(ZT0_InOut.sharesZT0());
  ASSERT_TRUE(ZT0_InOut.hasZT0State());
  ASSERT_TRUE(ZT0_InOut.hasSharedZAInterface());
  ASSERT_FALSE(ZT0_InOut.hasPrivateZAInterface());

  SA ZT0_Preserved = SA(SA::encodeZT0State(SA::StateValue::Preserved));
  ASSERT_TRUE(ZT0_Preserved.isPreservesZT0());
  ASSERT_FALSE(ZT0_Preserved.isInZT0());
  ASSERT_FALSE(ZT0_Preserved.isOutZT0());
  ASSERT_FALSE(ZT0_Preserved.isInOutZT0());
  ASSERT_FALSE(ZT0_Preserved.isNewZT0());
  ASSERT_TRUE(ZT0_Preserved.sharesZT0());
  ASSERT_TRUE(ZT0_Preserved.hasZT0State());
  ASSERT_TRUE(ZT0_Preserved.hasSharedZAInterface());
  ASSERT_FALSE(ZT0_Preserved.hasPrivateZAInterface());

  SA ZT0_New = SA(SA::encodeZT0State(SA::StateValue::New));
  ASSERT_TRUE(ZT0_New.isNewZT0());
  ASSERT_FALSE(ZT0_New.isInZT0());
  ASSERT_FALSE(ZT0_New.isOutZT0());
  ASSERT_FALSE(ZT0_New.isInOutZT0());
  ASSERT_FALSE(ZT0_New.isPreservesZT0());
  ASSERT_FALSE(ZT0_New.sharesZT0());
  ASSERT_TRUE(ZT0_New.hasZT0State());
  ASSERT_FALSE(ZT0_New.hasSharedZAInterface());
  ASSERT_TRUE(ZT0_New.hasPrivateZAInterface());

  ASSERT_FALSE(SA(SA::Normal).isInZT0());
  ASSERT_FALSE(SA(SA::Normal).isOutZT0());
  ASSERT_FALSE(SA(SA::Normal).isInOutZT0());
  ASSERT_FALSE(SA(SA::Normal).isPreservesZT0());
  ASSERT_FALSE(SA(SA::Normal).isNewZT0());
  ASSERT_FALSE(SA(SA::Normal).sharesZT0());
  ASSERT_FALSE(SA(SA::Normal).hasZT0State());
}

TEST(SMEAttributes, Transitions) {
  // Normal -> Normal
  ASSERT_FALSE(SA(SA::Normal).requiresSMChange(SA(SA::Normal)));
  ASSERT_FALSE(SA(SA::Normal).requiresPreservingZT0(SA(SA::Normal)));
  ASSERT_FALSE(SA(SA::Normal).requiresDisablingZABeforeCall(SA(SA::Normal)));
  ASSERT_FALSE(SA(SA::Normal).requiresEnablingZAAfterCall(SA(SA::Normal)));
  // Normal -> Normal + LocallyStreaming
  ASSERT_FALSE(SA(SA::Normal).requiresSMChange(SA(SA::Normal | SA::SM_Body)));

  // Normal -> Streaming
  ASSERT_TRUE(SA(SA::Normal).requiresSMChange(SA(SA::SM_Enabled)));
  // Normal -> Streaming + LocallyStreaming
  ASSERT_TRUE(
      SA(SA::Normal).requiresSMChange(SA(SA::SM_Enabled | SA::SM_Body)));

  // Normal -> Streaming-compatible
  ASSERT_FALSE(SA(SA::Normal).requiresSMChange(SA(SA::SM_Compatible)));
  // Normal -> Streaming-compatible + LocallyStreaming
  ASSERT_FALSE(
      SA(SA::Normal).requiresSMChange(SA(SA::SM_Compatible | SA::SM_Body)));

  // Streaming -> Normal
  ASSERT_TRUE(SA(SA::SM_Enabled).requiresSMChange(SA(SA::Normal)));
  // Streaming -> Normal + LocallyStreaming
  ASSERT_TRUE(
      SA(SA::SM_Enabled).requiresSMChange(SA(SA::Normal | SA::SM_Body)));

  // Streaming -> Streaming
  ASSERT_FALSE(SA(SA::SM_Enabled).requiresSMChange(SA(SA::SM_Enabled)));
  // Streaming -> Streaming + LocallyStreaming
  ASSERT_FALSE(
      SA(SA::SM_Enabled).requiresSMChange(SA(SA::SM_Enabled | SA::SM_Body)));

  // Streaming -> Streaming-compatible
  ASSERT_FALSE(SA(SA::SM_Enabled).requiresSMChange(SA(SA::SM_Compatible)));
  // Streaming -> Streaming-compatible + LocallyStreaming
  ASSERT_FALSE(
      SA(SA::SM_Enabled).requiresSMChange(SA(SA::SM_Compatible | SA::SM_Body)));

  // Streaming-compatible -> Normal
  ASSERT_TRUE(SA(SA::SM_Compatible).requiresSMChange(SA(SA::Normal)));
  ASSERT_TRUE(
      SA(SA::SM_Compatible).requiresSMChange(SA(SA::Normal | SA::SM_Body)));

  // Streaming-compatible -> Streaming
  ASSERT_TRUE(SA(SA::SM_Compatible).requiresSMChange(SA(SA::SM_Enabled)));
  // Streaming-compatible -> Streaming + LocallyStreaming
  ASSERT_TRUE(
      SA(SA::SM_Compatible).requiresSMChange(SA(SA::SM_Enabled | SA::SM_Body)));

  // Streaming-compatible -> Streaming-compatible
  ASSERT_FALSE(SA(SA::SM_Compatible).requiresSMChange(SA(SA::SM_Compatible)));
  // Streaming-compatible -> Streaming-compatible + LocallyStreaming
  ASSERT_FALSE(SA(SA::SM_Compatible)
                   .requiresSMChange(SA(SA::SM_Compatible | SA::SM_Body)));

  SA Private_ZA = SA(SA::Normal);
  SA ZA_Shared = SA(SA::encodeZAState(SA::StateValue::In));
  SA ZT0_Shared = SA(SA::encodeZT0State(SA::StateValue::In));
  SA ZA_ZT0_Shared = SA(SA::encodeZAState(SA::StateValue::In) |
                        SA::encodeZT0State(SA::StateValue::In));

  // Shared ZA -> Private ZA Interface
  ASSERT_FALSE(ZA_Shared.requiresDisablingZABeforeCall(Private_ZA));
  ASSERT_TRUE(ZA_Shared.requiresEnablingZAAfterCall(Private_ZA));

  // Shared ZT0 -> Private ZA Interface
  ASSERT_TRUE(ZT0_Shared.requiresDisablingZABeforeCall(Private_ZA));
  ASSERT_TRUE(ZT0_Shared.requiresPreservingZT0(Private_ZA));
  ASSERT_TRUE(ZT0_Shared.requiresEnablingZAAfterCall(Private_ZA));

  // Shared ZA & ZT0 -> Private ZA Interface
  ASSERT_FALSE(ZA_ZT0_Shared.requiresDisablingZABeforeCall(Private_ZA));
  ASSERT_TRUE(ZA_ZT0_Shared.requiresPreservingZT0(Private_ZA));
  ASSERT_TRUE(ZA_ZT0_Shared.requiresEnablingZAAfterCall(Private_ZA));

  // Shared ZA -> Shared ZA Interface
  ASSERT_FALSE(ZA_Shared.requiresDisablingZABeforeCall(ZT0_Shared));
  ASSERT_FALSE(ZA_Shared.requiresEnablingZAAfterCall(ZT0_Shared));

  // Shared ZT0 -> Shared ZA Interface
  ASSERT_FALSE(ZT0_Shared.requiresDisablingZABeforeCall(ZT0_Shared));
  ASSERT_FALSE(ZT0_Shared.requiresPreservingZT0(ZT0_Shared));
  ASSERT_FALSE(ZT0_Shared.requiresEnablingZAAfterCall(ZT0_Shared));

  // Shared ZA & ZT0 -> Shared ZA Interface
  ASSERT_FALSE(ZA_ZT0_Shared.requiresDisablingZABeforeCall(ZT0_Shared));
  ASSERT_FALSE(ZA_ZT0_Shared.requiresPreservingZT0(ZT0_Shared));
  ASSERT_FALSE(ZA_ZT0_Shared.requiresEnablingZAAfterCall(ZT0_Shared));
}
