cmake_minimum_required(VERSION 3.24)
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" sprite_version LIMIT_COUNT 1)
# CMake accepts the numeric release; Python metadata retains the PEP 440 suffix.
string(REGEX MATCH "^[0-9]+\\.[0-9]+(\\.[0-9]+)?" sprite_release "${sprite_version}")
project(Sprite VERSION ${sprite_release} LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
set(default_pakcs AUTO)
if(DEFINED ENV{PAKCS})
  set(default_pakcs "$ENV{PAKCS}")
endif()
set(default_frontend AUTO)
if(DEFINED ENV{CURRY_FRONTEND})
  set(default_frontend "$ENV{CURRY_FRONTEND}")
endif()
set(WITH_PAKCS "${default_pakcs}" CACHE STRING "Optional PAKCS oracle")
set(WITH_CURRY_FRONTEND "${default_frontend}" CACHE STRING "Curry front end (default 3.1.0; also accepts 2.0.0)")
set(WITH_ICURRY "$ENV{ICURRY}" CACHE FILEPATH "Optional icurry translator")
set(WITH_CCACHE "$ENV{CCACHE}" CACHE FILEPATH "Optional compiler launcher (ccache)")
set(default_postinstall "${CMAKE_CXX_COMPILER}")
if(DEFINED ENV{CXX_POSTINSTALL})
  set(default_postinstall "$ENV{CXX_POSTINSTALL}")
endif()
set(WITH_CXX_POSTINSTALL "${default_postinstall}" CACHE FILEPATH "Compiler for generated modules")
set(CURRY2ICURRY_TOOL frontend CACHE STRING "Default translation route")
set(GC wdgc CACHE STRING "Collector: wdgc or mps")
set_property(CACHE GC PROPERTY STRINGS wdgc mps)
option(TRACE "Enable computation tracing" OFF)
option(COUNTERS "Enable scheduler counters" OFF)
option(GC_WRITE_COUNTERS "Count writes into old nodes" OFF)
option(ENABLE_ICURRY_CACHE "Enable the user ICurry cache by default" OFF)
option(ENABLE_PARSED_JSON_CACHE "Enable the parsed JSON cache by default" OFF)
option(SPRITE_REBUILD_ICY "Rebuild pinned ICurry products" OFF)
set(JOBS auto CACHE STRING "Parallelism of the Curry library build")
if(NOT JOBS MATCHES "^([1-9][0-9]*|auto)$")
  message(FATAL_ERROR "JOBS must be a positive integer or auto")
endif()
if(NOT CURRY2ICURRY_TOOL MATCHES "^(frontend|icurry)$")
  message(FATAL_ERROR "CURRY2ICURRY_TOOL must be frontend or icurry")
endif()
if(CURRY2ICURRY_TOOL STREQUAL icurry AND NOT WITH_ICURRY)
  message(FATAL_ERROR "CURRY2ICURRY_TOOL=icurry needs WITH_ICURRY")
endif()
if(WITH_ICURRY)
  find_program(SPRITE_ICURRY NAMES "${WITH_ICURRY}" REQUIRED NO_CACHE)
  set(WITH_ICURRY "${SPRITE_ICURRY}")
endif()
if(NOT GC MATCHES "^(wdgc|mps)$")
  message(FATAL_ERROR "GC must be wdgc or mps")
endif()
find_package(Python 3.11.4 REQUIRED COMPONENTS Interpreter Development.Module)
set(PYBIND11_FINDPYTHON ON)
add_subdirectory(extern/pybind11 EXCLUDE_FROM_ALL)
find_package(Threads REQUIRED)
if(WITH_CCACHE)
  find_program(SPRITE_CCACHE NAMES "${WITH_CCACHE}" REQUIRED NO_CACHE)
  set(WITH_CCACHE "${SPRITE_CCACHE}")
  set(CMAKE_C_COMPILER_LAUNCHER "${SPRITE_CCACHE}")
  set(CMAKE_CXX_COMPILER_LAUNCHER "${SPRITE_CCACHE}")
endif()
# AUTO selects the pinned current pair, even beside a stale legacy install.
# Explicit options and environment overrides continue to accept either pair.
function(sprite_find_versioned_tool output version)
  cmake_parse_arguments(ARG "" "" "NAMES;HINTS" ${ARGN})
  cmake_path(CONVERT "$ENV{PATH}" TO_CMAKE_PATH_LIST tool_path NORMALIZE)
  foreach(directory IN LISTS ARG_HINTS CMAKE_PROGRAM_PATH tool_path CMAKE_SYSTEM_PROGRAM_PATH)
    foreach(name IN LISTS ARG_NAMES)
      unset(candidate)
      find_program(candidate NAMES "${name}" PATHS "${directory}" NO_DEFAULT_PATH NO_CACHE)
      if(candidate)
        execute_process(COMMAND "${CMAKE_COMMAND}" -E env LC_ALL=C.UTF-8
          "${candidate}" --numeric-version TIMEOUT 30
          RESULT_VARIABLE result OUTPUT_VARIABLE found_version
          OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
        if(result EQUAL 0 AND found_version STREQUAL version)
          file(REAL_PATH "${candidate}" resolved)
          set(${output} "${resolved}" PARENT_SCOPE)
          return()
        endif()
      endif()
    endforeach()
  endforeach()
  set(${output} "" PARENT_SCOPE)
endfunction()
if(WITH_PAKCS STREQUAL "AUTO")
  sprite_find_versioned_tool(WITH_PAKCS "3.10.0" NAMES pakcs
    HINTS "${CMAKE_SOURCE_DIR}/install/tools")
endif()
if(WITH_CURRY_FRONTEND STREQUAL "AUTO")
  set(WITH_CURRY_FRONTEND "")
  if(DEFINED ENV{SPRITE_FRONTEND})
    set(WITH_CURRY_FRONTEND "$ENV{SPRITE_FRONTEND}")
  else()
    set(expected_frontend "3.1.0")
    get_filename_component(pakcs_bin "${WITH_PAKCS}" DIRECTORY)
    if(WITH_PAKCS)
      execute_process(COMMAND "${WITH_PAKCS}" --numeric-version TIMEOUT 30
        OUTPUT_VARIABLE selected_release OUTPUT_STRIP_TRAILING_WHITESPACE)
      if(selected_release STREQUAL "3.4.1")
        set(expected_frontend "2.0.0")
      endif()
    endif()
    sprite_find_versioned_tool(WITH_CURRY_FRONTEND "${expected_frontend}"
      NAMES pakcs-frontend curry-frontend
      HINTS "${pakcs_bin}" "${CMAKE_SOURCE_DIR}/install/tools")
  endif()
endif()
foreach(tool WITH_PAKCS WITH_CURRY_FRONTEND WITH_ICURRY WITH_CXX_POSTINSTALL)
  if(${tool})
    unset(resolved_tool)
    find_program(resolved_tool NAMES "${${tool}}" REQUIRED NO_CACHE)
    if(tool STREQUAL "WITH_CXX_POSTINSTALL")
      # A compiler alias can be a ccache symlink; its basename matters.
      set(${tool} "${resolved_tool}")
    else()
      file(REAL_PATH "${resolved_tool}" ${tool})
    endif()
  endif()
endforeach()
execute_process(COMMAND "${Python_EXECUTABLE}" -c "import json, zlib"
  COMMAND_ERROR_IS_FATAL ANY)
if(WITH_CURRY_FRONTEND)
  execute_process(COMMAND "${WITH_CURRY_FRONTEND}" --numeric-version
    TIMEOUT 30
    OUTPUT_VARIABLE frontend_version OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
else()
  if(SPRITE_REBUILD_ICY)
    message(FATAL_ERROR "SPRITE_REBUILD_ICY needs the Curry front end")
  endif()
  execute_process(COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/library_pins.py"
    "${CMAKE_SOURCE_DIR}/curry/lib" RESULT_VARIABLE pins_result
    OUTPUT_VARIABLE pins_output ERROR_VARIABLE pins_error)
  if(NOT pins_result EQUAL 0)
    message(FATAL_ERROR "${pins_output}${pins_error}")
  endif()
  set(frontend_version "3.1.0")
  message(STATUS "Building from the shipped current-pair library products (no Curry front end)")
endif()
# The current pair owns the pinned products; explicit tools can select legacy.
if(frontend_version STREQUAL "3.1.0")
  set(frontend_release "3.10.0")
  set(frontend_pakcs_macro 310)
elseif(frontend_version STREQUAL "2.0.0")
  set(frontend_release "3.4.1")
  set(frontend_pakcs_macro 304)
else()
  message(FATAL_ERROR "Expected Curry front end 2.0.0 or 3.1.0, found ${frontend_version}")
endif()
if(WITH_PAKCS)
  execute_process(COMMAND "${WITH_PAKCS}" --compiler-name TIMEOUT 30 OUTPUT_VARIABLE pakcs_name
    OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
  if(NOT pakcs_name STREQUAL "pakcs")
    message(FATAL_ERROR "WITH_PAKCS must name the PAKCS Curry system")
  endif()
  execute_process(COMMAND "${WITH_PAKCS}" --numeric-version TIMEOUT 30 OUTPUT_VARIABLE pakcs_version
    OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
  if(NOT pakcs_version STREQUAL frontend_release)
    message(FATAL_ERROR "Expected PAKCS ${frontend_release}, found ${pakcs_version}")
  endif()
endif()

if(WITH_ICURRY)
  # These releases expose a dated compiler banner, not a numeric version.
  if(frontend_version STREQUAL "3.1.0")
    set(icurry_version "3.3.0")
    set(icurry_banner "17/10/22")
  else()
    set(icurry_version "3.1.0")
    set(icurry_banner "28/07/21")
  endif()
  execute_process(COMMAND "${WITH_ICURRY}" --help TIMEOUT 30
    RESULT_VARIABLE icurry_result OUTPUT_VARIABLE icurry_output ERROR_VARIABLE icurry_error)
  string(FIND "${icurry_output}" "ICurry Compiler (Version of ${icurry_banner})" icurry_banner_pos)
  if(NOT icurry_result EQUAL 0 OR icurry_banner_pos EQUAL -1)
    message(FATAL_ERROR "Expected icurry ${icurry_version} with Curry front end ${frontend_version}: ${icurry_output}${icurry_error}")
  endif()
  set(probe "${CMAKE_CURRENT_BINARY_DIR}/icurry-probe")
  file(MAKE_DIRECTORY "${probe}")
  file(REMOVE_RECURSE "${probe}/.curry")
  file(WRITE "${probe}/sample.curry" "main :: Int\nmain = x where x free\n")
  execute_process(COMMAND "${WITH_ICURRY}" sample TIMEOUT 120 WORKING_DIRECTORY "${probe}"
    RESULT_VARIABLE icurry_result OUTPUT_VARIABLE icurry_output ERROR_VARIABLE icurry_error)
  if(NOT icurry_result EQUAL 0 OR NOT EXISTS "${probe}/.curry/pakcs-${frontend_release}/sample.icy")
    message(FATAL_ERROR "WITH_ICURRY failed its translation probe: ${icurry_output}${icurry_error}")
  endif()
endif()

# gc/*.cpp are include fragments selected by memory.cpp, not translation units.
# Keep the historical translation-unit order: the module registry is
# initialized before the builtin modules register their tables.
file(GLOB runtime_sources CONFIGURE_DEPENDS src/curryrt/*.cpp)
foreach(dir currylib currylib/prelude graph state)
  file(GLOB sources CONFIGURE_DEPENDS "src/curryrt/${dir}/*.cpp")
  list(APPEND runtime_sources ${sources})
endforeach()
set(image "${CMAKE_CURRENT_BINARY_DIR}/stage")
# Build the pinned, private C99 arithmetic library into the runtime.
file(GLOB tommath_sources CONFIGURE_DEPENDS extern/libtommath/bn_*.c)
add_library(sprite_tommath STATIC ${tommath_sources})
set_target_properties(sprite_tommath PROPERTIES POSITION_INDEPENDENT_CODE ON
  C_STANDARD 99 C_STANDARD_REQUIRED ON)
target_include_directories(sprite_tommath PRIVATE extern/libtommath)
add_library(curryrt SHARED ${runtime_sources})
target_include_directories(curryrt PRIVATE extern/libtommath)
target_link_libraries(curryrt PRIVATE sprite_tommath)
target_include_directories(curryrt PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_link_libraries(curryrt PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
# The link flags are those of the platform's linker.  GNU ld binds the
# functions of the runtime locally and refuses an undefined symbol; ld64 of
# macOS does both by default (the two-level namespace, -undefined error),
# and the run path is spelled @loader_path there.  Windows is a later step.
if(APPLE)
  set(sprite_origin "@loader_path")
  set(elf_release_flags "")
else()
  set(sprite_origin "$ORIGIN")
  # The procedure linkage table and semantic interposition are ELF notions.
  set(elf_release_flags ";-fno-plt;-fno-semantic-interposition")
  target_link_options(curryrt PRIVATE -Wl,-Bsymbolic-functions -Wl,--no-undefined)
endif()
set_target_properties(curryrt PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${image}/lib")
if(GC STREQUAL mps)
  if(NOT EXISTS "${CMAKE_SOURCE_DIR}/extern/mps/code/mps.h")
    message(FATAL_ERROR "GC=mps needs the extern/mps submodule")
  endif()
  target_sources(curryrt PRIVATE src/curryrt/graph/mpsgc.c)
  target_include_directories(curryrt PRIVATE extern/mps/code)
  target_compile_definitions(curryrt PUBLIC SPRITE_GC_MPS PRIVATE CONFIG_VAR_HOT)
elseif(GC_WRITE_COUNTERS)
  target_compile_definitions(curryrt PUBLIC SPRITE_GC_WRITE_COUNTERS)
endif()
if(TRACE)
  target_compile_definitions(curryrt PUBLIC SPRITE_TRACE_ENABLED)
endif()
if(COUNTERS)
  target_compile_definitions(curryrt PUBLIC SPRITE_SCHEDULER_COUNTERS)
endif()
file(GLOB binding_sources CONFIGURE_DEPENDS src/python/backends/cxx/curryrtbindings/*.cpp)
pybind11_add_module(_curryrtbindings MODULE NO_EXTRAS ${binding_sources})
target_link_libraries(_curryrtbindings PRIVATE curryrt)
# The runtime reads every node through the pointer union NodeU, and a step
# overlays a rewritten node with another struct (Node::forward_to), accesses
# the aliasing rules of C++ call unrelated.  Strict aliasing is therefore
# off here, in the generated code (toolchain.py, _cxxflags) and in the C++
# tests; GCC 14 for riscv64 kept a stale info tag without it (issue #170).
foreach(target curryrt _curryrtbindings)
  target_compile_options(${target} PRIVATE -Wall -Wno-register -fno-strict-aliasing
    "$<$<CXX_COMPILER_ID:AppleClang,Clang>:-Wno-missing-braces>"
    "$<$<CONFIG:Release>:-O2;-fno-stack-protector${elf_release_flags}>")
endforeach()
# The private C objects obey the same release flavor as libcurryrt.
target_compile_options(sprite_tommath PRIVATE -Wall
  "$<$<CONFIG:Release>:-O2;-fno-stack-protector${elf_release_flags}>")
set_target_properties(_curryrtbindings PROPERTIES
  LIBRARY_OUTPUT_DIRECTORY "${image}/python/curry/backends/cxx/curryrtbindings"
  BUILD_WITH_INSTALL_RPATH TRUE
  INSTALL_RPATH "${sprite_origin}/../../../../../lib;${sprite_origin}/../../../_sprite/lib")

# Immutable runtime facts contain no installation prefix. The Python step
# prepares the traditional tree using these facts and the just-built package.
# The version of the compiler of generated code: -dumpfullversion of GCC
# and of clang proper; Apple's clang knows -dumpversion alone.
execute_process(COMMAND "${WITH_CXX_POSTINSTALL}" -dumpfullversion
  OUTPUT_VARIABLE compiler_version RESULT_VARIABLE compiler_version_result
  OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(NOT compiler_version_result EQUAL 0 OR compiler_version STREQUAL "")
  execute_process(COMMAND "${WITH_CXX_POSTINSTALL}" -dumpversion
    OUTPUT_VARIABLE compiler_version OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
endif()
execute_process(COMMAND "${WITH_CXX_POSTINSTALL}" -dumpmachine
  OUTPUT_VARIABLE compiler_target OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
set(cxx_flavor release)
if(CMAKE_BUILD_TYPE STREQUAL Debug)
  set(cxx_flavor debug)
endif()
set(write_counters 0)
if(GC STREQUAL wdgc AND GC_WRITE_COUNTERS)
  set(write_counters 1)
endif()
execute_process(COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/curry/module_names.py"
  OUTPUT_VARIABLE module_names OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
set(unsupported "Control.Search.SearchTree Curry.Compiler.Distribution Data.IORef Debug.Trace System.CPUTime System.Environment System.IO System.IO.Unsafe Test.Prop Test.Prop.Types")
file(MAKE_DIRECTORY "${image}/sysconfig")
function(sprite_var name value)
  file(CONFIGURE OUTPUT "${image}/sysconfig/${name}" CONTENT "${value}\n" @ONLY)
endfunction()
sprite_var(curry2icurry_tool "${CURRY2ICURRY_TOOL}")
sprite_var(currylib_module_names "${module_names}")
sprite_var(currylib_unsupported_modules "${unsupported}")
sprite_var(currylib_version "3.4.0")
sprite_var(cxx_compiler "${compiler_version} ${compiler_target}")
sprite_var(cxx_flavor "${cxx_flavor}")
sprite_var(cxx_gc "${GC}")
sprite_var(cxx_gc_write_counters "${write_counters}")
sprite_var(default_sprite_cache_file "cache.db")
foreach(name ENABLE_ICURRY_CACHE ENABLE_PARSED_JSON_CACHE)
  string(TOLOWER "${name}" resource)
  if(${name})
    sprite_var(${resource} "1")
  else()
    sprite_var(${resource} "0")
  endif()
endforeach()
sprite_var(frontend_version "${frontend_version}")
sprite_var(frontend_flags "--extended -D__PAKCS__=${frontend_pakcs_macro}")
sprite_var(frontend_subdir "pakcs-${frontend_release}")
sprite_var(intermediate_subdir "sprite-pakcs-${frontend_release}")
# The suffix of a shared library on this platform (.so, or .dylib on macOS):
# the name of the runtime library, libcurryrt<suffix>, is formed from it
# (config.shared_library_suffix).  The objects of compiled Curry modules
# keep the suffix .so on every platform; dlopen reads the file, not its name.
sprite_var(shared_library_suffix "${CMAKE_SHARED_LIBRARY_SUFFIX}")
sprite_var(python_package_name "curry")
set(wheel_args "")
if(SKBUILD)
  list(APPEND wheel_args --wheel)
endif()
add_custom_target(currylib ALL
  COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/build-package.py"
    --source "${CMAKE_SOURCE_DIR}" --prefix "${image}"
    --frontend "${WITH_CURRY_FRONTEND}" --pakcs "${WITH_PAKCS}"
    --icurry "${WITH_ICURRY}" --cxx "${WITH_CXX_POSTINSTALL}"
    --ccache "${WITH_CCACHE}" ${wheel_args} --jobs "${JOBS}" --rebuild-icy "${SPRITE_REBUILD_ICY}"
  DEPENDS _curryrtbindings USES_TERMINAL VERBATIM)
install(FILES extern/libtommath/LICENSE DESTINATION licenses RENAME libtommath-LICENSE)
if(SKBUILD)
  install(FILES extern/libtommath/LICENSE DESTINATION curry/_sprite/licenses RENAME libtommath-LICENSE)
  install(FILES LICENSE DESTINATION curry/_sprite/licenses)
  install(FILES extern/pybind11/LICENSE DESTINATION curry/_sprite/licenses RENAME pybind11-LICENSE)
  install(DIRECTORY "${image}/python/curry/" DESTINATION curry
    PATTERN __pycache__ EXCLUDE)
  foreach(dir lib include sysconfig curry)
    install(DIRECTORY "${image}/${dir}" DESTINATION curry/_sprite
      PATTERN "*.gch" EXCLUDE PATTERN __pycache__ EXCLUDE)
  endforeach()
else()
  install(DIRECTORY "${image}/" DESTINATION . USE_SOURCE_PERMISSIONS
    PATTERN "*.gch" EXCLUDE)
  install(CODE "
    execute_process(COMMAND \"${Python_EXECUTABLE}\" \"${CMAKE_SOURCE_DIR}/scripts/install-package.py\"
      \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}\" \"${image}\" COMMAND_ERROR_IS_FATAL ANY)
  ")
endif()
