# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

#######################################################
# Enhanced version of find llvm.
#
# Usage:
#   find_llvm(${USE_LLVM} [${LLVM_VERSION}])
#
# - When USE_LLVM=ON, use auto search
# - When USE_LLVM=/path/to/llvm-config, use corresponding config
# - The optional LLVM_VERSION=<required version> which will find a matching version
#      If LLVM_VERSION=10 then it will find 10.X.X, any version of 10
#      If LLVM_VERSION=10.2 then it will find 10.2.X
#
# Provide variables:
# - LLVM_INCLUDE_DIRS
# - LLVM_LIBS
# - LLVM_DEFINITIONS
# - TVM_LLVM_VERSION
#
macro(find_llvm use_llvm)
  if(${use_llvm} MATCHES ${IS_FALSE_PATTERN})
    return()
  endif()
  set(LLVM_CONFIG ${use_llvm})
  if(${ARGC} EQUAL 2)
    set(llvm_version_required ${ARGV1})
  endif()

  if(${LLVM_CONFIG} MATCHES ${IS_TRUE_PATTERN})
    # This is a workaround for an upstream LLVM issue [0], in which
    # the `CMAKE_INSTALL_LIBDIR` variable is used before definition.
    # While there is an LLVM PR to resolve this fix [1], as of
    # 2024-08-19 it has not yet been merged to LLVM.
    #
    # [0] https://github.com/llvm/llvm-project/issues/83802
    # [1] https://github.com/llvm/llvm-project/pull/83807
    include(GNUInstallDirs)

    find_package(LLVM ${llvm_version_required} REQUIRED CONFIG)
    llvm_map_components_to_libnames(LLVM_LIBS "all")
    if (NOT LLVM_LIBS)
      message(STATUS "Not found - LLVM_LIBS")
      message(STATUS "Fall back to using llvm-config")
      set(LLVM_CONFIG "${LLVM_TOOLS_BINARY_DIR}/llvm-config")
    endif()
  endif()

  if(LLVM_LIBS) # check if defined, not if it is true
    list (FIND LLVM_LIBS "LLVM" _llvm_dynlib_index)
    if (${_llvm_dynlib_index} GREATER -1)
      set(LLVM_LIBS LLVM)
      message(STATUS "Link with dynamic LLVM library")
    else()
      list(REMOVE_ITEM LLVM_LIBS LTO)
      message(STATUS "Link with static LLVM libraries")
    endif()
    set(TVM_LLVM_VERSION ${LLVM_VERSION_MAJOR}${LLVM_VERSION_MINOR})
    set(TVM_LLVM_HAS_AARCH64_TARGET 0)
    if(DEFINED LLVM_TARGETS_TO_BUILD AND "AArch64" IN_LIST LLVM_TARGETS_TO_BUILD)
      set(TVM_LLVM_HAS_AARCH64_TARGET 1)
    endif()
  else()
    # use llvm config
    message(STATUS "Use llvm-config=" ${LLVM_CONFIG})
    separate_arguments(LLVM_CONFIG)
    execute_process(COMMAND ${LLVM_CONFIG} --libfiles
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_libfiles_space
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --libfiles")
    endif()
    execute_process(COMMAND ${LLVM_CONFIG} --system-libs
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_system_libs
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --system-libs")
    endif()
    execute_process(COMMAND ${LLVM_CONFIG} --cxxflags
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_cxxflags_space
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --cxxflags")
    endif()
    execute_process(COMMAND ${LLVM_CONFIG} --version
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_version
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --version")
    endif()
    execute_process(COMMAND ${LLVM_CONFIG} --prefix
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_prefix
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --prefix")
    endif()
    execute_process(COMMAND ${LLVM_CONFIG} --libdir
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_libdir
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --libdir")
    endif()
    message(STATUS "LLVM libdir: ${__llvm_libdir}")
    execute_process(COMMAND ${LLVM_CONFIG} --cmakedir
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_cmakedir
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --cmakedir")
    endif()
    execute_process(COMMAND ${LLVM_CONFIG} --targets-built
      RESULT_VARIABLE __llvm_exit_code
      OUTPUT_VARIABLE __llvm_targets_built
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT "${__llvm_exit_code}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error executing: ${LLVM_CONFIG} --targets-built")
    endif()
    cmake_path(SET "__llvm_cmakedir" "${__llvm_cmakedir}")
    message(STATUS "LLVM cmakedir: ${__llvm_cmakedir}")
    # map prefix => $
    # to handle the case when the prefix contains space.
    string(REPLACE ${__llvm_prefix} "$" __llvm_cxxflags ${__llvm_cxxflags_space})
    string(REPLACE ${__llvm_prefix} "$" __llvm_libfiles ${__llvm_libfiles_space})
    # llvm version
    string(REGEX REPLACE "^([^.]+)\.([^.])+\.[^.]+.*$" "\\1\\2" TVM_LLVM_VERSION ${__llvm_version})
    string(STRIP ${TVM_LLVM_VERSION} TVM_LLVM_VERSION)
    # definitions
    string(REGEX MATCHALL "(^| )-D[A-Za-z0-9_]*" __llvm_defs ${__llvm_cxxflags})
    set(LLVM_DEFINITIONS "")
    foreach(__flag IN ITEMS ${__llvm_defs})
      string(STRIP "${__flag}" __llvm_def)
      list(APPEND LLVM_DEFINITIONS "${__llvm_def}")
    endforeach()
    # include dir
    string(REGEX MATCHALL "(^| )-I[^ ]*" __llvm_include_flags ${__llvm_cxxflags})
    set(LLVM_INCLUDE_DIRS "")
    foreach(__flag IN ITEMS ${__llvm_include_flags})
      string(REGEX REPLACE "(^| )-I" "" __dir "${__flag}")
      # map $ => prefix
      string(REPLACE "$" ${__llvm_prefix} __dir_with_prefix "${__dir}")
      list(APPEND LLVM_INCLUDE_DIRS "${__dir_with_prefix}")
    endforeach()
    # libfiles
    set(LLVM_LIBS "")
    separate_arguments(__llvm_libfiles)
    foreach(__flag IN ITEMS ${__llvm_libfiles})
      # map $ => prefix
      string(REPLACE "$" ${__llvm_prefix} __lib_with_prefix "${__flag}")
      list(APPEND LLVM_LIBS "${__lib_with_prefix}")
    endforeach()
    # targets built
    set(TVM_LLVM_HAS_AARCH64_TARGET 0)
    separate_arguments(BUILT_TARGET_LIST NATIVE_COMMAND ${__llvm_targets_built})
    if("AArch64" IN_LIST BUILT_TARGET_LIST)
      set(TVM_LLVM_HAS_AARCH64_TARGET 1)
    endif()
    if (${USE_MLIR})
      if (EXISTS "${__llvm_libdir}/libMLIRPresburger.a")
        if (EXISTS "${__llvm_libdir}/libMLIRSupport.a")
          message(STATUS "Found MLIR")
          list(APPEND LLVM_LIBS "${__llvm_libdir}/libMLIRPresburger.a")
          list(APPEND LLVM_LIBS "${__llvm_libdir}/libMLIRSupport.a")
          set(TVM_MLIR_VERSION ${TVM_LLVM_VERSION})
          message(STATUS "Build with MLIR")
          message(STATUS "Set TVM_MLIR_VERSION=" ${TVM_MLIR_VERSION})
        endif()
      endif()
    endif()
    separate_arguments(__llvm_system_libs)
    foreach(__flag IN ITEMS ${__llvm_system_libs})
      if("${__flag}" STREQUAL "-lm")
        message(STATUS "LLVM links against math")
        list(APPEND LLVM_LIBS "m")
      elseif(("${__flag}" STREQUAL "-lz") OR ("${__flag}" STREQUAL "z.lib"))
        message(STATUS "LLVM links against zlib")
        find_package(ZLIB REQUIRED)
        list(APPEND LLVM_LIBS "ZLIB::ZLIB")
      elseif("${__flag}" STREQUAL "-lzstd")
        list(APPEND CMAKE_MODULE_PATH "${__llvm_cmakedir}")
        find_package(zstd REQUIRED)
        if (TARGET "zstd::libzstd_static")
          message(STATUS "LLVM links against static zstd")
          list(APPEND LLVM_LIBS "zstd::libzstd_static")
        else()
          message(STATUS "LLVM links against shared zstd")
          list(APPEND LLVM_LIBS "zstd::libzstd_shared")
        endif()
      elseif("${__flag}" STREQUAL "-lxml2")
        message(STATUS "LLVM links against xml2")
        list(APPEND LLVM_LIBS "-lxml2")
      elseif("${__flag}" STREQUAL "zstd.dll.lib")
        if (EXISTS "${__llvm_libdir}/zstd_static.lib")
          message(STATUS "LLVM links against static zstd")
          list(APPEND LLVM_LIBS "${__llvm_libdir}/zstd_static.lib")
        else()
          message(STATUS "LLVM linker flag under LLVM libdir: ${__llvm_libdir}/zstd.lib")
          list(APPEND LLVM_LIBS "${__llvm_libdir}/zstd.lib")
        endif()
      elseif((__flag MATCHES ".lib$") AND (EXISTS "${__llvm_libdir}/${__flag}"))
        # If the library file ends in .lib try to also search the llvm_libdir
        message(STATUS "LLVM linker flag under LLVM libdir: ${__llvm_libdir}/${__flag}")
        list(APPEND LLVM_LIBS "${__llvm_libdir}/${__flag}")
      elseif((__flag MATCHES ".lib$") AND (EXISTS "${__llvm_libdir}/lib${__flag}"))
        # If the library file ends in .lib try to also search the llvm_libdir with lib prefix
        message(STATUS "LLVM linker flag under LLVM libdir: ${__llvm_libdir}/lib${__flag}")
        list(APPEND LLVM_LIBS "${__llvm_libdir}/lib${__flag}")
      else()
        message(STATUS "LLVM linker flag: ${__flag}")
        list(APPEND LLVM_LIBS "${__flag}")
      endif()
    endforeach()
  endif()
  message(STATUS "Found LLVM_INCLUDE_DIRS=" "${LLVM_INCLUDE_DIRS}")
  message(STATUS "Found LLVM_DEFINITIONS=" "${LLVM_DEFINITIONS}")
  message(STATUS "Found LLVM_LIBS=" "${LLVM_LIBS}")
  message(STATUS "Found TVM_LLVM_VERSION=" ${TVM_LLVM_VERSION})
  if (${TVM_LLVM_VERSION} LESS 150)
    message(FATAL_ERROR "TVM requires LLVM 15.0 or higher.")
  endif()
  message(STATUS "Found TVM_LLVM_HAS_AARCH64_TARGET=" ${TVM_LLVM_HAS_AARCH64_TARGET})

  # Detect whether DIBuilder insertion APIs (insertDeclare,
  # insertDbgValueIntrinsic) accept BasicBlock::iterator as the insertion point
  # (upstream LLVM 20+) vs Instruction* (pre-LLVM 20 and ROCm-bundled LLVM 20,
  # which reports version 200 but retains the legacy Instruction* overload).
  if (${TVM_LLVM_VERSION} GREATER_EQUAL 200)
    include(CheckCXXSourceCompiles)
    include(CMakePushCheckState)
    cmake_push_check_state(RESET)
    set(CMAKE_REQUIRED_INCLUDES ${LLVM_INCLUDE_DIRS})
    # Only force -std= when the outer project hasn't already selected a
    # standard; signature-only probe does not need -fno-rtti. Use the
    # compiler-appropriate form so the probe works under MSVC as well.
    if(NOT CMAKE_CXX_STANDARD)
      if(MSVC)
        set(CMAKE_REQUIRED_FLAGS "/std:c++20")
      else()
        set(CMAKE_REQUIRED_FLAGS "-std=c++20")
      endif()
    endif()
    check_cxx_source_compiles("
      #include <llvm/IR/DIBuilder.h>
      #include <llvm/IR/Instructions.h>
      void f(llvm::DIBuilder* b, llvm::Value* v, llvm::DILocalVariable* var,
             llvm::DIExpression* e, const llvm::DILocation* dl,
             llvm::Instruction* i) {
        b->insertDeclare(v, var, e, dl, llvm::BasicBlock::iterator(i));
        b->insertDbgValueIntrinsic(v, var, e, dl, llvm::BasicBlock::iterator(i));
      }
    " TVM_LLVM_DIBUILDER_USES_ITERATOR)
    cmake_pop_check_state()
    if(TVM_LLVM_DIBUILDER_USES_ITERATOR)
      add_definitions(-DTVM_LLVM_DIBUILDER_USES_ITERATOR=1)
      message(STATUS "LLVM DIBuilder insertion APIs use BasicBlock::iterator")
    else()
      message(STATUS "LLVM DIBuilder insertion APIs use Instruction* (ROCm or older LLVM 20)")
    endif()
  endif()
endmacro(find_llvm)
