diff --git a/CMakeLists.txt b/CMakeLists.txt index d10c279d..a183920a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,33 +2,59 @@ cmake_minimum_required(VERSION 3.15) project(reflex VERSION 6.3.0 LANGUAGES CXX) +# Make the project-specific modules under cmake/ available to include(). set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") +# Ask CMake to compile all targets as C++11. set(CMAKE_CXX_STANDARD 11) -# The following setups the simd_* variables -include(SIMDTestAndSetup) +# GNUInstallDirs supplies portable install paths, while CTest defines the +# BUILD_TESTING option and enables test registration when requested. +include(GNUInstallDirs) +include(CTest) -# -# Defining source variables -# +# Clang-cl uses MSVC command-line and binary interfaces despite having a +# different compiler ID. Treat both toolchains alike when selecting targets. +set(REFLEX_MSVC_TOOLCHAIN FALSE) +if(MSVC OR CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC") + set(REFLEX_MSVC_TOOLCHAIN TRUE) +endif() -set(lib_sources - lib/convert.cpp +# The following sets up the simd_* variables. +include(SIMDTestAndSetup) + +# Keep the matcher implementation shared by the full and minimal libraries in +# one list so the two variants do not drift apart. +set(libreflex_common_sources lib/debug.cpp lib/error.cpp lib/input.cpp lib/matcher.cpp - lib/matcher_avx2.cpp - lib/matcher_avx512bw.cpp lib/pattern.cpp - lib/posix.cpp lib/simd.cpp - lib/simd_avx2.cpp - lib/simd_avx512bw.cpp - lib/unicode.cpp lib/utf8.cpp +) +# Use AVX2 flags for AVX2 sources. +if(REFLEX_COMPILER_HAS_AVX2) + set(reflex_avx2_sources lib/matcher_avx2.cpp lib/simd_avx2.cpp) + list(APPEND libreflex_common_sources ${reflex_avx2_sources}) + set_source_files_properties(${reflex_avx2_sources} PROPERTIES COMPILE_OPTIONS "${simd_avx2_flags}") +endif() + +# Use AVX512BW flags for AVX512BW wources. +if(REFLEX_COMPILER_HAS_AVX512BW) + set(reflex_avx512bw_sources lib/matcher_avx512bw.cpp lib/simd_avx512bw.cpp) + list(APPEND libreflex_common_sources ${reflex_avx512bw_sources}) + set_source_files_properties(${reflex_avx512bw_sources} PROPERTIES COMPILE_OPTIONS "${simd_avx512bw_flags}") +endif() + +# The full library adds pattern conversion, POSIX support, and Unicode tables. +set(libreflex_sources + ${libreflex_common_sources} + lib/convert.cpp + lib/posix.cpp + lib/unicode.cpp unicode/block_scripts.cpp unicode/language_scripts.cpp unicode/letter_scripts.cpp @@ -36,74 +62,111 @@ set(lib_sources unicode/composer.cpp ) -set(bin_sources - src/reflex.cpp +# The minimal library contains only the runtime needed by generated scanners. +set(libreflexmin_sources + ${libreflex_common_sources} ) -list(TRANSFORM lib_sources PREPEND ${PROJECT_SOURCE_DIR}/) -list(TRANSFORM bin_sources PREPEND ${PROJECT_SOURCE_DIR}/) +# Only build static libs for MSVC because the public headers do not declare symbols with dllexport/dllimport. +if(REFLEX_MSVC_TOOLCHAIN) + add_library(ReflexLib STATIC) + add_library(ReflexMinLib STATIC) + set(reflex_library_targets ReflexLib) + set(reflexmin_library_targets ReflexMinLib) +else() + add_library(ReflexLib SHARED) + add_library(ReflexLibStatic STATIC) + add_library(ReflexMinLib SHARED) + add_library(ReflexMinLibStatic STATIC) + set(reflex_library_targets ReflexLib ReflexLibStatic) + set(reflexmin_library_targets ReflexMinLib ReflexMinLibStatic) +endif() -# -# Defining targets section -# +# Set the source files contained in each full-library target. +foreach(target IN LISTS reflex_library_targets) + target_sources(${target} PRIVATE ${libreflex_sources}) +endforeach() -add_library(ReflexLib SHARED "") -target_sources(ReflexLib PRIVATE ${lib_sources}) -target_include_directories(ReflexLib PUBLIC - $ - $ -) -target_compile_definitions(ReflexLib PRIVATE ${simd_definitions}) -target_compile_options(ReflexLib PRIVATE ${simd_flags}) - -add_library(ReflexLibStatic STATIC "") -target_sources(ReflexLibStatic PRIVATE ${lib_sources}) -target_include_directories(ReflexLibStatic PUBLIC - $ - $ +# Set the smaller source set contained in each minimal-library target. +foreach(target IN LISTS reflexmin_library_targets) + target_sources(${target} PRIVATE ${libreflexmin_sources}) +endforeach() + +# Gather all library targets so their common build settings remain identical. +set(reflex_all_library_targets + ${reflex_library_targets} + ${reflexmin_library_targets} ) -target_compile_definitions(ReflexLibStatic PRIVATE ${simd_definitions}) -target_compile_options(ReflexLibStatic PRIVATE ${simd_flags}) - -add_executable(Reflex "") -target_sources(Reflex PRIVATE ${bin_sources}) -target_link_libraries(Reflex PRIVATE ReflexLibStatic) -target_compile_definitions(Reflex PRIVATE ${simd_definitions}) -target_compile_options(Reflex PRIVATE ${simd_flags}) - -# Don't user target name as filename instead use lowercase name for backwards compatibility -set_target_properties(ReflexLibStatic PROPERTIES OUTPUT_NAME reflex_static_lib) -set_target_properties(ReflexLib PROPERTIES OUTPUT_NAME reflex_shared_lib) -set_target_properties(Reflex PROPERTIES OUTPUT_NAME reflex) -# -# Exporting targets section -# +# Use source-tree headers while building and installed headers for consumers; +# SIMD definitions and options remain private implementation details. +foreach(target IN LISTS reflex_all_library_targets) + target_include_directories(${target} PUBLIC + $ + $ + ) + target_compile_definitions(${target} PRIVATE ${simd_definitions}) + target_compile_options(${target} PRIVATE ${simd_flags}) +endforeach() + +# Link the generator to the static full library where it has a distinct target; +# on MSVC-style toolchains ReflexLib is already the static library. +add_executable(Reflex src/reflex.cpp) +if(TARGET ReflexLibStatic) + target_link_libraries(Reflex PRIVATE ReflexLibStatic) +else() + target_link_libraries(Reflex PRIVATE ReflexLib) +endif() -include(GNUInstallDirs) +# CTest defines BUILD_TESTING; when enabled, build and register the same rtest +# regression executable used by the Autotools build. +if(BUILD_TESTING) + add_executable(rtest tests/rtest.cpp) + if(TARGET ReflexLibStatic) + target_link_libraries(rtest PRIVATE ReflexLibStatic) + else() + # MSVC-style builds expose ReflexLib itself as the static library because + # the public headers do not provide the exports required by a usable DLL. + target_link_libraries(rtest PRIVATE ReflexLib) + endif() + add_test(NAME rtest COMMAND rtest) +endif() + +# Target names are internal to CMake; give installed artifacts their traditional +# reflex and reflexmin basenames for compatibility with existing consumers. +foreach(target IN LISTS reflex_library_targets) + set_target_properties(${target} PROPERTIES OUTPUT_NAME reflex) +endforeach() + +foreach(target IN LISTS reflexmin_library_targets) + set_target_properties(${target} PROPERTIES OUTPUT_NAME reflexmin) +endforeach() + +set_target_properties(Reflex PROPERTIES OUTPUT_NAME reflex) -install(TARGETS Reflex ReflexLib ReflexLibStatic +# Install the generator and public libraries, and record the library targets for +# the find_package export generated below. +install(TARGETS Reflex ${reflex_all_library_targets} EXPORT ReflexTargets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +# Install only the public C++ headers from the source tree. install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/reflex DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h" ) +# Install imported targets such as Reflex::ReflexLib for downstream CMake users. install(EXPORT ReflexTargets NAMESPACE Reflex:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/reflex ) -# -# Packaging section (find_package support) -# - +# Generate and install the relocatable configuration loaded by +# find_package(Reflex CONFIG). include(CMakePackageConfigHelpers) configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/Config.cmake.in @@ -111,39 +174,60 @@ configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/Config.cmake.in INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/reflex ) +# Do not promise compatibility between releases until the project defines an +# explicit C++ API and ABI compatibility policy. +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/ReflexConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY ExactVersion +) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ReflexConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/ReflexConfigVersion.cmake" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/reflex ) -# -# Packaging section (pkg-config support) -# - -set(prefix "${CMAKE_INSTALL_PREFIX}") -set(exec_prefix "\${prefix}") -set(REFLEX_PKGCONFIG_LIBRARY "reflex_shared_lib") - +# Make relative installations relocatable by deriving their prefix from the +# installed .pc file. Absolute library destinations retain their fixed paths. if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(prefix "${CMAKE_INSTALL_PREFIX}") + set(exec_prefix "\${prefix}") set(libdir "${CMAKE_INSTALL_LIBDIR}") else() + # Anchor both inputs in the native build directory because RELATIVE_PATH + # requires full paths; only their relative structure enters the .pc file. + file(RELATIVE_PATH reflex_pc_prefix_relative + "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/pkgconfig" + "${CMAKE_CURRENT_BINARY_DIR}" + ) + set(prefix "\${pcfiledir}/${reflex_pc_prefix_relative}") + set(exec_prefix "\${prefix}") set(libdir "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}") endif() +# GNUInstallDirs also permits the header destination to be absolute. if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") set(includedir "${CMAKE_INSTALL_INCLUDEDIR}") else() set(includedir "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") endif() -# Compatibility note: CMake currently installs libreflex_shared_lib rather than -# libreflex, so its pkg-config file must use the CMake library name. -configure_file(${PROJECT_SOURCE_DIR}/cmake/reflex.pc.in +# Reuse the Autotools templates, which expect the project version in VERSION. +set(VERSION "${PROJECT_VERSION}") +configure_file(${PROJECT_SOURCE_DIR}/reflex.pc.in "${CMAKE_CURRENT_BINARY_DIR}/reflex.pc" @ONLY ) +configure_file(${PROJECT_SOURCE_DIR}/reflexmin.pc.in + "${CMAKE_CURRENT_BINARY_DIR}/reflexmin.pc" + @ONLY +) + +# Install both pkg-config descriptions alongside the libraries. install(FILES "${CMAKE_CURRENT_BINARY_DIR}/reflex.pc" + "${CMAKE_CURRENT_BINARY_DIR}/reflexmin.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig ) diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in index 5cf14bf5..dbf74ad5 100644 --- a/cmake/Config.cmake.in +++ b/cmake/Config.cmake.in @@ -1,5 +1,9 @@ +# CMake expands this marker into helpers that make the installed package +# relocatable and provide check_required_components(). @PACKAGE_INIT@ +# Load the imported Reflex:: targets generated by the project's install(EXPORT). include("${CMAKE_CURRENT_LIST_DIR}/ReflexTargets.cmake") -check_required_components(Reflex) \ No newline at end of file +# Honor REQUIRED semantics if consumers request package components later. +check_required_components(Reflex) diff --git a/cmake/SIMDTestAndSetup.cmake b/cmake/SIMDTestAndSetup.cmake index e82891d5..9d7e4f9c 100644 --- a/cmake/SIMDTestAndSetup.cmake +++ b/cmake/SIMDTestAndSetup.cmake @@ -1,96 +1,135 @@ -# SIMD intrinsics setup and test. -# Tests whether specifics intrinsics are available and then defines macro definitions -# Expose the simd_definitions and simd_flags when included from another cmake file. +# Test whether the compiler accepts each SIMD implementation with its required +# options. The resulting variables configure baseline and optimized sources. -include(CheckCXXSourceRuns) +# These modules provide compile probes and isolate their temporary flags from +# the rest of the project configuration. +include(CheckCXXSourceCompiles) +include(CMakePushCheckState) +# Options let packagers disable a backend; an enabled backend must still pass +# its compiler probe before it is built. option(USE_AVX512BW "Enable AVX512BW intrinsics (if available)" ON) option(USE_AVX2 "Enable AVX2 intrinsics (if available)" ON) option(USE_SSE2 "Enable SSE2 intrinsics (if available)" ON) option(USE_NEON "Enable NEON intrinsics (if available)" ON) -if (USE_AVX512BW) - check_cxx_source_runs(" - #include +# Select the SIMD option spelling understood by the active compiler interface. +if(REFLEX_MSVC_TOOLCHAIN) + set(reflex_sse2_flag "/arch:SSE2") + set(reflex_avx2_flag "/arch:AVX2") + set(reflex_avx512bw_flag "/arch:AVX512") +else() + set(reflex_sse2_flag "-msse2") + set(reflex_avx2_flag "-mavx2") + set(reflex_avx512bw_flag "-mavx512bw") +endif() + +# Compile rather than run the probe so configuration also works when the build +# machine cannot execute binaries for the target architecture. +if(USE_SSE2) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_FLAGS "${reflex_sse2_flag}") + check_cxx_source_compiles(" + #include int main() { - __m512 n = _mm512_set1_epi8(42); - (void)_mm512_cmpeq_epi8_mask(n, n); - return 0; + __m128i n = _mm_set1_epi8(42); + return _mm_cvtsi128_si32(n) != 42; } - " HAVE_AVX512BW) + " REFLEX_COMPILER_HAS_SSE2) + cmake_pop_check_state() +else() + set(REFLEX_COMPILER_HAS_SSE2 FALSE) endif() -if (USE_AVX2) - check_cxx_source_runs(" +# AVX implementations use SSE2 as their portable runtime fallback, so do not +# enable them when the baseline SSE2 implementation was explicitly disabled. +if(USE_AVX2 AND REFLEX_COMPILER_HAS_SSE2) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_FLAGS "${reflex_avx2_flag}") + check_cxx_source_compiles(" #include int main() { __m256i n = _mm256_set1_epi8(42); - (void)_mm256_movemask_epi8(_mm256_and_si256(n, n)); - return 0; + return _mm256_movemask_epi8(_mm256_and_si256(n, n)) == 0; } - " HAVE_AVX2) + " REFLEX_COMPILER_HAS_AVX2) + cmake_pop_check_state() +else() + set(REFLEX_COMPILER_HAS_AVX2 FALSE) endif() -if (USE_SSE2) - check_cxx_source_runs(" - #include +# The AVX512BW implementation dispatches to AVX2 on machines without AVX512BW, +# so only build it when the AVX2 implementation is also available. +if(USE_AVX512BW AND REFLEX_COMPILER_HAS_AVX2) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_FLAGS "${reflex_avx512bw_flag}") + check_cxx_source_compiles(" + #include int main() { - __m128i n = _mm_set1_epi8(42); - return 0; + __m512i n = _mm512_set1_epi8(42); + return _mm512_cmpeq_epi8_mask(n, n) == 0; } - " HAVE_SSE2) + " REFLEX_COMPILER_HAS_AVX512BW) + cmake_pop_check_state() +else() + set(REFLEX_COMPILER_HAS_AVX512BW FALSE) endif() -if (USE_NEON) - check_cxx_source_runs(" +# Probe NEON only when no x86 backend is available. AArch64 needs no extra +# option, while some 32-bit ARM compilers require -mfpu=neon. +if(USE_NEON AND NOT REFLEX_COMPILER_HAS_SSE2) + cmake_push_check_state(RESET) + check_cxx_source_compiles(" #include int main() { - uint64x2_t n; - uint64_t m = vgetq_lane_u64(n, 0); - return 0; + uint64x2_t n = vdupq_n_u64(42); + return vgetq_lane_u64(n, 0) != 42; } - " HAVE_NEON) + " REFLEX_COMPILER_HAS_NEON) + cmake_pop_check_state() + + if(NOT REFLEX_COMPILER_HAS_NEON AND NOT REFLEX_MSVC_TOOLCHAIN) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_FLAGS "-mfpu=neon") + check_cxx_source_compiles(" + #include + int main() { + uint64x2_t n = vdupq_n_u64(42); + return vgetq_lane_u64(n, 0) != 42; + } + " REFLEX_COMPILER_HAS_NEON_WITH_FLAG) + cmake_pop_check_state() + else() + set(REFLEX_COMPILER_HAS_NEON_WITH_FLAG FALSE) + endif() +else() + set(REFLEX_COMPILER_HAS_NEON FALSE) + set(REFLEX_COMPILER_HAS_NEON_WITH_FLAG FALSE) endif() +# These lists are consumed by the parent CMakeLists.txt when it defines targets. set(simd_definitions "") set(simd_flags "") +set(simd_avx2_flags "") +set(simd_avx512bw_flags "") -if (${HAVE_AVX512BW}) +# Define only the strongest backend macro. Baseline sources stay at SSE2 while +# optimized AVX sources receive the flags required by their specialized code. +if(REFLEX_COMPILER_HAS_AVX512BW) list(APPEND simd_definitions HAVE_AVX512BW) - if (WIN32 AND MSVC) - list(APPEND simd_flags "/arch:AVX512") - else() - list(APPEND simd_flags "-mavx512bw") - endif() -endif() - -if (${HAVE_AVX2}) + list(APPEND simd_flags "${reflex_sse2_flag}") + list(APPEND simd_avx2_flags "${reflex_avx2_flag}") + list(APPEND simd_avx512bw_flags "${reflex_avx512bw_flag}") +elseif(REFLEX_COMPILER_HAS_AVX2) list(APPEND simd_definitions HAVE_AVX2) - if (WIN32 AND MSVC) - list(APPEND simd_flags "/arch:AVX2") - else() - list(APPEND simd_flags "-mavx2") - endif() -endif() - -if (${HAVE_SSE2}) + list(APPEND simd_flags "${reflex_sse2_flag}") + list(APPEND simd_avx2_flags "${reflex_avx2_flag}") +elseif(REFLEX_COMPILER_HAS_SSE2) list(APPEND simd_definitions HAVE_SSE2) - if (WIN32 AND MSVC) - if ("${CMAKE_GENERATOR_PLATFORM}" MATCHES "Win32") - # SSE2 is a given if the system is running x64 - list(APPEND simd_flags "/arch:SSE2") - endif() - else() - list(APPEND simd_flags "-msse2") - endif() -endif() - -if (${HAVE_NEON}) + list(APPEND simd_flags "${reflex_sse2_flag}") +elseif(REFLEX_COMPILER_HAS_NEON) list(APPEND simd_definitions HAVE_NEON) - if ((${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm64|aarch64") OR (WIN32 AND MSVC)) - # Arm64 compilers and MSVC runs NEON by default according to their docs - else() - list(APPEND simd_flags "-march=native" "-mfpu=neon") - endif() +elseif(REFLEX_COMPILER_HAS_NEON_WITH_FLAG) + list(APPEND simd_definitions HAVE_NEON) + list(APPEND simd_flags "-mfpu=neon") endif() - diff --git a/cmake/reflex.pc.in b/cmake/reflex.pc.in deleted file mode 100644 index 0ef7c448..00000000 --- a/cmake/reflex.pc.in +++ /dev/null @@ -1,11 +0,0 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - -Name: RE/flex -Description: high-performance C++ regex library and lexical analyzer generator -Version: @PROJECT_VERSION@ -Requires: -Libs: -L${libdir} -l@REFLEX_PKGCONFIG_LIBRARY@ -Cflags: -I${includedir}