//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

// REQUIRES: target={{aarch64-.+}}
// UNSUPPORTED: target={{.*-windows.*}}

#include <alloca.h>
#include <libunwind.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__APPLE__)
#include <sys/sysctl.h>
#endif
#if defined(_LIBUNWIND_HAVE_GETAUXVAL) || defined(_LIBUNWIND_HAVE_ELF_AUX_INFO)
#include <sys/auxv.h>
#endif

// Basic test of unwinding with SME lazy saves. This tests libunwind disables ZA
// (and commits a lazy save of ZA) before resuming from unwinding.

// Note: This test requires SME (and is setup to pass on targets without SME).
#if defined(__APPLE__)
static bool checkHasSME() {
  int has_sme = 0;
  size_t size = sizeof(has_sme);
  if (sysctlbyname("hw.optional.arm.FEAT_SME", &has_sme, &size, NULL, 0))
    return false;
  return has_sme != 0;
}
#elif defined(_LIBUNWIND_HAVE_GETAUXVAL)
static bool checkHasSME() {
  constexpr int hwcap2_sme = (1 << 23);
  unsigned long hwcap2 = getauxval(AT_HWCAP2);
  return (hwcap2 & hwcap2_sme) != 0;
}
#elif defined(_LIBUNWIND_HAVE_ELF_AUX_INFO)
static bool checkHasSME() {
  constexpr int hwcap2_sme = (1 << 23);
  unsigned long hwcap2 = 0;
  elf_aux_info(AT_HWCAP2, &hwcap2, sizeof(hwcap2));
  return (hwcap2 & hwcap2_sme) != 0;
}
#else
static bool checkHasSME() {
  // TODO: Support other platforms.
  return false;
}
#endif

struct TPIDR2Block {
  void *za_save_buffer;
  uint64_t num_save_slices;
};

__attribute__((noinline)) void private_za() {
  // Note: Lazy save active on entry to function.
  unw_context_t context;
  unw_cursor_t cursor;

  unw_getcontext(&context);
  unw_init_local(&cursor, &context);
  unw_step(&cursor);
  unw_resume(&cursor);
}

bool isZAOn() {
  register uint64_t svcr asm("x20");
  asm(".inst 0xd53b4254" : "=r"(svcr));
  return (svcr & 0b10) != 0;
}

__attribute__((noinline)) void za_function_with_lazy_save() {
  register uint64_t tmp asm("x8");

  // SMSTART ZA (should zero ZA)
  asm(".inst 0xd503457f");

  // RDSVL x8, #1 (read streaming vector length)
  asm(".inst 0x04bf5828" : "=r"(tmp));

  // Allocate and fill ZA save buffer with 0xAA.
  size_t buffer_size = tmp * tmp;
  uint8_t *za_save_buffer = (uint8_t *)alloca(buffer_size);
  memset(za_save_buffer, 0xAA, buffer_size);

  TPIDR2Block block = {za_save_buffer, tmp};
  tmp = reinterpret_cast<uint64_t>(&block);

  // MRS TPIDR2_EL0, x8 (setup lazy save of ZA)
  asm(".inst 0xd51bd0a8" ::"r"(tmp));

  // ZA should be on before unwinding.
  if (!isZAOn()) {
    fprintf(stderr, __FILE__ ": fail (ZA not on before call)\n");
    abort();
  } else {
    fprintf(stderr, __FILE__ ": pass (ZA on before call)\n");
  }

  private_za();

  // ZA should be off after unwinding.
  if (isZAOn()) {
    fprintf(stderr, __FILE__ ": fail (ZA on after unwinding)\n");
    abort();
  } else {
    fprintf(stderr, __FILE__ ": pass (ZA off after unwinding)\n");
  }

  // MRS x8, TPIDR2_EL0 (read TPIDR2_EL0)
  asm(".inst 0xd53bd0a8" : "=r"(tmp));
  // ZA should have been saved (TPIDR2_EL0 zero).
  if (tmp != 0) {
    fprintf(stderr, __FILE__ ": fail (TPIDR2_EL0 non-null after unwinding)\n");
    abort();
  } else {
    fprintf(stderr, __FILE__ ": pass (TPIDR2_EL0 null after unwinding)\n");
  }

  // ZA (all zero) should have been saved to the buffer.
  for (unsigned i = 0; i < buffer_size; ++i) {
    if (za_save_buffer[i] != 0) {
      fprintf(stderr,
              __FILE__ ": fail (za_save_buffer non-zero after unwinding)\n");
      abort();
    }
  }
  fprintf(stderr, __FILE__ ": pass (za_save_buffer zero'd after unwinding)\n");
}

int main(int, char **) {
  if (!checkHasSME()) {
    fprintf(stderr, __FILE__ ": pass (no SME support)\n");
    return 0; // Pass (SME is required for this test to run).
  }
  za_function_with_lazy_save();
  return 0;
}
