# SPEC CPU 2017
# https://www.spec.org/cpu2017/Docs/

include(External)
include(CopyDir)
include(Host)

# Search for SPEC CPU 2017 root directory.
llvm_externals_find(TEST_SUITE_SPEC2017_ROOT "speccpu2017" "SPEC CPU2017")
if (NOT TEST_SUITE_SPEC2017_ROOT)
  return ()
endif ()

# Print warnings once only, even if included multiple times.
if (NOT TARGET speccpu2017_dummy)

  file(READ ${TEST_SUITE_SPEC2017_ROOT}/version.txt VERSION)
  if (VERSION VERSION_LESS 1.0.1)
    message(WARNING
      "Expected SPEC2017 version 1.0.1 or newer, found ${VERSION}")
  endif ()

  # SPEC supports three "run types": ref, train and test.
  set(_available_run_types test train ref)
  if (NOT TEST_SUITE_RUN_TYPE IN_LIST _available_run_types)
    message(FATAL_ERROR
      "TEST_SUITE_RUN_TYPE must be 'train', 'test' or 'ref' for SPEC")
  endif()

  add_custom_target(speccpu2017_dummy)
endif ()


# Set the variables an common compile flags for a SPEC CPU 2017 benchmark.
#
# SPEED/RATE Kind of benchmark suite
#
# ORIGIN     Allows the reuse of sourec files, input data, and reference
#            output from another benchmark.
macro (speccpu2017_benchmark)
  cmake_parse_arguments(_arg "SPEED;RATE" "ORIGIN" "" ${ARGN})

  # If BENCHMARK is set, another benchmark inherits from this benchark.
  # The relevant variables are already set in this case.
  if (NOT DEFINED BENCHMARK)

    get_filename_component(BENCHMARK "${CMAKE_CURRENT_SOURCE_DIR}" NAME)
    string(SUBSTRING ${BENCHMARK} 0 3 BENCHMARK_NO)
    set(PROG ${BENCHMARK})

    if (_arg_SPEED)
      set(BENCHMARK_SUITE_TYPE speed)
      set(SPEED ON)
      set(SUFFIX s)
    elseif (_arg_RATE)
      set(BENCHMARK_SUITE_TYPE rate)
      set(RATE ON)
      set(SUFFIX r)
    else ()
      message(FATAL_ERROR "Must define the benchmark type (RATE or SPEED)")
    endif ()

    if (DEFINED _arg_ORIGIN)
      set(ORIGIN ${_arg_ORIGIN})
    else ()
      set(ORIGIN ${BENCHMARK})
    endif ()

    set(BENCHMARK_DIR "${TEST_SUITE_SPEC2017_ROOT}/benchspec/CPU/${BENCHMARK}")
    set(ORIGIN_DIR "${TEST_SUITE_SPEC2017_ROOT}/benchspec/CPU/${ORIGIN}")


    set(SRC_DIR "${ORIGIN_DIR}/src")

    set(DATA_DIR "${BENCHMARK_DIR}/data")
    if (NOT EXISTS "${DATA_DIR}")
      set(DATA_DIR "${ORIGIN_DIR}/data")
    endif ()

    set(DATA_all_DIR "${DATA_DIR}/all")
    set(DATA_test_DIR "${DATA_DIR}/test")
    set(DATA_train_DIR "${DATA_DIR}/train")

    set(DATA_ref_DIR "${DATA_DIR}/refrate")
    if (SPEED AND EXISTS "${DATA_DIR}/refspeed")
      set(DATA_ref_DIR "${DATA_DIR}/refspeed")
    endif ()

    set(INPUT_all_DIR "${DATA_all_DIR}/input")
    set(OUTPUT_all_DIR "${DATA_all_DIR}/output")

    set(INPUT_test_DIR "${DATA_test_DIR}/input")
    set(OUTPUT_test_DIR "${DATA_test_DIR}/output")

    set(INPUT_train_DIR "${DATA_train_DIR}/input")
    set(OUTPUT_train_DIR "${DATA_train_DIR}/output")

    set(INPUT_ref_DIR "${DATA_ref_DIR}/input")
    set(OUTPUT_ref_DIR "${DATA_ref_DIR}/output")

    # Create benchmark working directories.
    foreach (_run_type IN LISTS TEST_SUITE_RUN_TYPE)
      set(RUN_${_run_type}_DIR "${CMAKE_CURRENT_BINARY_DIR}/run_${_run_type}")
      file(MAKE_DIRECTORY ${RUN_${_run_type}_DIR})
    endforeach ()


    # Mandatory flags
    add_definitions(-DSPEC -DSPEC_CPU -DNDEBUG)

    if (RATE)
      # rate benchmarks never use parallelism
      add_definitions(-DSPEC_AUTO_SUPPRESS_OPENMP)
    endif ()

    # Portability flags
    if (ENDIAN STREQUAL "little")
      add_definitions(-DSPEC_AUTO_BYTEORDER=0x12345678)
    elseif (ENDIAN STREQUAL "big")
      add_definitions(-DSPEC_AUTO_BYTEORDER=0x87654321)
    endif ()

    check_type_size("long long" SIZEOF_LONG_LONG)
    check_type_size("long" SIZEOF_LONG)
    check_type_size("int" SIZEOF_INT)
    if (CMAKE_SIZEOF_VOID_P EQUAL 4 AND SIZEOF_LONG_LONG EQUAL 8 AND SIZEOF_LONG EQUAL 4 AND SIZEOF_INT EQUAL 4)
      add_definitions(-DSPEC_ILP32)
    elseif (CMAKE_SIZEOF_VOID_P EQUAL 8 AND SIZEOF_LONG_LONG EQUAL 8 AND SIZEOF_LONG EQUAL 4 AND SIZEOF_INT EQUAL 4)
      add_definitions(-DSPEC_P64)
    elseif (CMAKE_SIZEOF_VOID_P EQUAL 8 AND SIZEOF_LONG_LONG EQUAL 8 AND SIZEOF_LONG EQUAL 8 AND SIZEOF_INT EQUAL 4)
      add_definitions(-DSPEC_LP64)
    elseif (CMAKE_SIZEOF_VOID_P EQUAL 8 AND SIZEOF_LONG_LONG EQUAL 8 AND SIZEOF_LONG EQUAL 8 AND SIZEOF_INT EQUAL 8)
      add_definitions(-DSPEC_ILP64)
    else ()
      message(FATAL_ERROR "SPEC CPU 2017 unsupported data model (supported: ILP32/LLP64/LP64/ILP64)")
    endif ()

    if (TARGET_OS STREQUAL "Linux")
      add_definitions(-DSPEC_LINUX) # 526.blender_r
    endif ()

    if(ARCH STREQUAL "x86" AND TARGET_OS STREQUAL "Linux")
      if (CMAKE_SIZEOF_VOID_P EQUAL 8)
        # Linux x86_64
        add_definitions(-DSPEC_LINUX_X64) # perlbench
      elseif (CMAKE_SIZEOF_VOID_P EQUAL 4)
        # Linux x86
        add_definitions(-DSPEC_ILP32)
        add_definitions(-D_FILE_OFFSET_BITS=64)
        add_definitions(-DSPEC_LINUX_I32) # perlbench
      endif ()
    elseif (ARCH STREQUAL "AArch64" AND TARGET_OS STREQUAL "Linux" AND CMAKE_SIZEOF_VOID_P EQUAL 8)
      # Linux ARM
      add_definitions(-DSPEC_LINUX_AARCH64)
    elseif (ARCH STREQUAL "x86" AND TARGET_OS STREQUAL "Windows")
      # Windows x86/x64
    else ()
      message("ARCH: ${ARCH}")
      message("TARGET_OS: ${TARGET_OS}")
      message("CMAKE_SIZEOF_VOID_P: ${CMAKE_SIZEOF_VOID_P}")
      message(FATAL_ERROR
        "Don't know portability flags for SPEC CPU 2017 on this platform")
    endif ()

    # No OpenMP for the moment, even for the _s suites.
    add_definitions(-DSPEC_SUPPRESS_OPENMP)

  endif ()
endmacro()


# Reuse the CMakeLists.txt of another benchmark.
macro(speccpu2017_inherit _origin_path)
  include("${_origin_path}/CMakeLists.txt")
endmacro ()


# Add include directories relative to SRC_DIR.
macro (speccpu2017_add_include_dirs)
  foreach(_dirname ${ARGN})
    get_filename_component(_absdirname "${_dirname}" ABSOLUTE BASE_DIR ${SRC_DIR})
    include_directories("${_absdirname}")
  endforeach()
endmacro ()


# Add a "RUN:" line.
#
# RUN_TYPE   (test,train or ref)
#            Only run if this TEST_SUITE_RUN_TYPE is is selected.
#
# SUITE_TYPE (rate or speed)
#            Only run in the _r or _s benchmark suites.
#
# WORKDIR    Working dir for the executable to run in.
#            "input" means the dataset source directory. Does not require
#                    copying the input data to the rundir, but the benchmark
#                    must not write data there.
#            If not defined, the run_{run_type} directory is chosen.
#
# STDOUT     Write the benchmark's stdout into this file in the rundir.
#
# STDERR     Write the benchmark's stderr into this file in the rundir.
#
# ARGN       Benchmark's command line arguments
macro (speccpu2017_run_test)
  cmake_parse_arguments(_arg
    "" "RUN_TYPE;SUITE_TYPE;WORKDIR;STDOUT;STDERR" "" ${ARGN})

  if ((NOT DEFINED _arg_SUITE_TYPE) OR
      (BENCHMARK_SUITE_TYPE IN_LIST _arg_SUITE_TYPE))
    if ((NOT DEFINED _arg_RUN_TYPE) OR
        (_arg_RUN_TYPE IN_LIST TEST_SUITE_RUN_TYPE))

      set(_stdout)
      if (DEFINED _arg_STDOUT)
        set(_stdout > "${RUN_${_arg_RUN_TYPE}_DIR}/${_arg_STDOUT}")
      endif ()

      set(_stderr)
      if (DEFINED _arg_STDERR)
        set(_stderr 2> "${RUN_${_arg_RUN_TYPE}_DIR}/${_arg_STDERR}")
      endif ()

      set(_executable)
      if (NOT DEFINED _arg_WORKDIR)
        set(_workdir "${RUN_${_arg_RUN_TYPE}_DIR}")

        # perlbench, xalancbmk need to be invoked with relative paths
        # (SPEC made modifications that prepend another path to find the rundir)
        file(RELATIVE_PATH _executable
          "${_workdir}" "${CMAKE_CURRENT_BINARY_DIR}/${PROG}")
        set (_executable EXECUTABLE "${_executable}")
      elseif (_arg_WORKDIR STREQUAL "input")
        set(_workdir "${INPUT_${_arg_RUN_TYPE}_DIR}")
      else ()
        set(_workdir "${_arg_WORKDIR}")
      endif ()

      llvm_test_run(
        ${_arg_UNPARSED_ARGUMENTS} ${_stdout} ${_stderr}
        RUN_TYPE ${_arg_RUN_TYPE}
        WORKDIR "${_workdir}"
        ${_executable}
      )
    endif ()
  endif ()
endmacro ()


# Compare an image file to a reference image.
macro(speccpu2017_validate_image _imgfile _cmpfile _outfile)
  cmake_parse_arguments(_carg "" "RUN_TYPE" "SUITE_TYPE" ${ARGN})

  set(VALIDATOR imagevalidate_${BENCHMARK_NO})
  if (NOT TARGET ${VALIDATOR}-host)
    file(GLOB_RECURSE _validator_sources "${SRC_DIR}/image_validator/*.c")
    llvm_add_host_executable(
      ${VALIDATOR}-host ${VALIDATOR} ${_validator_sources}
      LDFLAGS -lm
      CPPFLAGS -DSPEC
    )
  endif ()

  if ((NOT DEFINED _carg_SUITE_TYPE) OR (${BENCHMARK_SUITE_TYPE} IN_LIST _carg_SUITE_TYPE))
    get_filename_component(_basename "${_imgfile}" NAME_WE)
    get_filename_component(_ext "${_imgfile}" EXT)
    llvm_test_verify(
      cd "${RUN_${_carg_RUN_TYPE}_DIR}" &&
      "${CMAKE_CURRENT_BINARY_DIR}/${VALIDATOR}" ${_carg_UNPARSED_ARGUMENTS}
        "${_imgfile}" "${DATA_${_carg_RUN_TYPE}_DIR}/compare/${_cmpfile}"
        > ${RUN_${_carg_RUN_TYPE}_DIR}/${_outfile}
      RUN_TYPE ${_carg_RUN_TYPE}
    )
  endif ()
endmacro ()

# Add a "VERIFY:" line that compares all of the benchmark's reference outputs
# with files in the rundir.
macro(speccpu2017_verify_output)
  cmake_parse_arguments(_arg
    "IGNORE_WHITESPACE" "ABSOLUTE_TOLERANCE;RELATIVE_TOLERANCE" "" ${ARGN})

  set(_abstol)
  if (DEFINED _arg_ABSOLUTE_TOLERANCE)
    set(_abstol -a "${_arg_ABSOLUTE_TOLERANCE}")
  endif ()

  set(_reltol)
  if (DEFINED _arg_RELATIVE_TOLERANCE)
    set(_reltol -r "${_arg_RELATIVE_TOLERANCE}")
  endif ()

  set(_ignorewhitespace)
  if (DEFINED _arg_IGNORE_WHITESPACE)
    set(_ignorewhitespace "-i")
  endif ()

  foreach (_runtype IN LISTS TEST_SUITE_RUN_TYPE ITEMS all)
    file(GLOB_RECURSE _reffiles "${OUTPUT_${_runtype}_DIR}/*")
    foreach (_reffile IN LISTS _reffiles)
      file(RELATIVE_PATH _relfile "${OUTPUT_${_runtype}_DIR}" "${_reffile}")
      set(_outfile "${RUN_${_runtype}_DIR}/${_relfile}")
      llvm_test_verify(RUN_TYPE ${_runtype}
        "${FPCMP}" ${_abstol} ${_reltol} ${_ignorewhitespace}
          "${_reffile}" "${_outfile}"
      )
    endforeach ()
  endforeach ()
endmacro()


# Add a SPEC CPU 2017 benchmark.
#
# Must be used after speccpu2017_run_test, speccpu2017_validate_image
# and speccpu2017_verify_output because those add lines to the
# ${BENCHMARK}.test file that is written here.
macro(speccpu2017_add_executable)
  set(_sources ${ARGN})
  if (_sources)
    set(_sources)
    foreach(_filename ${ARGN})
      get_filename_component(_absfilename "${_filename}"
        ABSOLUTE BASE_DIR ${SRC_DIR})
      list(APPEND _sources "${_absfilename}")
    endforeach()
  else ()
    file(GLOB_RECURSE _sources
      ${SRC_DIR}/*.c ${SRC_DIR}/*.cpp ${SRC_DIR}/*.cc ${SRC_DIR}/*.C)
  endif ()

  llvm_test_executable(${PROG} ${_sources})

  if (TARGET ${VALIDATOR}-host)
    add_dependencies(${PROG} ${VALIDATOR}-host)
  endif ()
endmacro()


# Copy the input data to the rundir.
#
# Can often be avoided by either passing an absolute path to the file in the
# input dir, or using the input dir as working directory and specify the output
# file as an absolute path to the rundir.
macro(speccpu2017_prepare_rundir)
  foreach (_runtype IN LISTS TEST_SUITE_RUN_TYPE)
    if (EXISTS "${INPUT_all_DIR}")
      llvm_copy_dir(${PROG} "${RUN_${_runtype}_DIR}" "${INPUT_all_DIR}")
    endif ()
    llvm_copy_dir(${PROG} "${RUN_${_runtype}_DIR}" "${INPUT_${_runtype}_DIR}")
  endforeach ()
endmacro()
