From 46defdb437ee362df68a72ae3af8c95d6c0c7712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Thu, 6 Aug 2026 19:35:25 +0000 Subject: [PATCH 01/13] build(c++): report machine flags used --- .github/workflows/test.yml | 2 +- benches/conftest.py | 2 + cmake/compiler_flags/CXXFlags.cmake | 74 ++++++++++++++++++-- cpp/include/monoprop/CMakeLists.txt | 5 +- cpp/include/monoprop/Info.h.in | 4 +- cpp/include/monoprop/Variants.h.in | 63 +++++++++++++++++ src/monoprop/__init__.py | 2 + src/monoprop/bindings/bindings.cpp.in | 2 + tests/test_gcc_target_help_clean.py | 98 +++++++++++++++++++++++++++ tools/gcc-target-help-clean.py | 80 ++++++++++++++++++++++ 10 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 cpp/include/monoprop/Variants.h.in create mode 100644 tests/test_gcc_target_help_clean.py create mode 100644 tools/gcc-target-help-clean.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18f4cde2..ddb17a80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,7 +101,7 @@ jobs: - name: Get monoprop version run: | - uv run python -c "import monoprop as mp; print(mp.__version__)" + uv run python -c "import monoprop as mp; print(mp.__version__); print(mp.__variant__); print(mp.__compiler_flags__)" - name: Verify that find_package(monoprop) works run: | diff --git a/benches/conftest.py b/benches/conftest.py index 194ab191..a6eaf9ef 100644 --- a/benches/conftest.py +++ b/benches/conftest.py @@ -166,6 +166,8 @@ def _meta() -> dict[str, Any]: "cpu_count_physical": psutil.cpu_count(logical=False), "hostname": socket.gethostname(), "monoprop_version": monoprop.__version__, + "monoprop_variant": monoprop.__variant__, + "monoprop_compiler_flags": monoprop.__compiler_flags__, } diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index d5ebdc10..65174cf0 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -48,10 +48,7 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) # do not use compiler extensions to the C++ standard set(CMAKE_CXX_EXTENSIONS FALSE) -# CMP0155 has CMake scan every C++20-or-later source for `import`s. There are no modules here, so -# the scan is pure build overhead, and it is not portable: Clang needs the separate clang-scan-deps -# binary (packaged apart from the compiler), whose absence surfaces as a build failure rather than a -# configure error. Must be set before any target is created. +# disable scanning for C++20 modules (unused) set(CMAKE_CXX_SCAN_FOR_MODULES OFF) # generate a JSON database of compiler commands (useful for LSP IDEs) set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) @@ -74,6 +71,75 @@ if(monoprop_ENABLE_ARCH_FLAGS AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") endif() endif() +# Query the machine-dependent flags for a given -march value and store the +# cleaned, space-separated string in the variable named by OUTPUT_VARIABLE. A +# MARCH of "default" queries the default target (no -march flag). +# +# Usage: +# _monoprop_query_machine_flags(MARCH OUTPUT_VARIABLE ) +function(_monoprop_query_machine_flags) + set( + _one_value_args + MARCH + OUTPUT_VARIABLE + ) + cmake_parse_arguments(PARSE_ARGV 0 _arg "" "${_one_value_args}" "") + + if(NOT _arg_OUTPUT_VARIABLE) + message( + FATAL_ERROR + "_monoprop_query_machine_flags: OUTPUT_VARIABLE is required" + ) + endif() + if(NOT _arg_MARCH) + message(FATAL_ERROR "_monoprop_query_machine_flags: MARCH is required") + endif() + + if(_arg_MARCH STREQUAL "default") + set(_march_args "") + else() + set(_march_args "-march=${_arg_MARCH}") + endif() + + # Report the machine-dependent flags GCC uses for each target variant by + # querying `gcc -march= -Q --help=target` and cleaning the output with + # tools/gcc-target-help-clean.py. + execute_process( + COMMAND + ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target + COMMAND + ${Python_EXECUTABLE} + "${PROJECT_SOURCE_DIR}/tools/gcc-target-help-clean.py" + OUTPUT_VARIABLE _flags + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _result + ) + if(NOT _result EQUAL 0) + message( + FATAL_ERROR + "Failed to query machine-dependent flags for '${_arg_MARCH}' (exit code ${_result})" + ) + endif() + set(${_arg_OUTPUT_VARIABLE} "${_flags}" PARENT_SCOPE) +endfunction() + +set(monoprop_DEFAULT_VARIANT_FLAGS "") +if(monoprop_ENABLE_ARCH_FLAGS) + _monoprop_query_machine_flags(MARCH native OUTPUT_VARIABLE monoprop_DEFAULT_VARIANT_FLAGS) +else() + _monoprop_query_machine_flags(MARCH default OUTPUT_VARIABLE monoprop_DEFAULT_VARIANT_FLAGS) +endif() + +set(monoprop_VARIANTS "") +set(monoprop_VARIANT_FLAGS "") + +# generate a header file with the macros needed to describe the variant +configure_file( + ${PROJECT_SOURCE_DIR}/cpp/include/monoprop/Variants.h.in + ${PROJECT_BINARY_DIR}/include/monoprop/Variants.h + @ONLY +) + set(monoprop_CXX_FLAGS "") include(${CMAKE_CURRENT_LIST_DIR}/GNU.CXX.cmake) include(${CMAKE_CURRENT_LIST_DIR}/Intel.CXX.cmake) diff --git a/cpp/include/monoprop/CMakeLists.txt b/cpp/include/monoprop/CMakeLists.txt index 99c964fa..4e9d7afa 100644 --- a/cpp/include/monoprop/CMakeLists.txt +++ b/cpp/include/monoprop/CMakeLists.txt @@ -12,10 +12,11 @@ target_sources( ${PROJECT_SOURCE_DIR}/cpp/include ${PROJECT_BINARY_DIR}/include FILES + "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/${PROJECT_NAME}Export.h" + "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Info.h" + "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/VariantMacros.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/Evolution.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MPFunctions.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MPGraph.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MonomialPropagator.h" - "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/${PROJECT_NAME}Export.h" - "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Info.h" ) diff --git a/cpp/include/monoprop/Info.h.in b/cpp/include/monoprop/Info.h.in index 8c50e5c6..f3027536 100644 --- a/cpp/include/monoprop/Info.h.in +++ b/cpp/include/monoprop/Info.h.in @@ -18,6 +18,8 @@ #include #include +#include "monoprop/Variants.h" + namespace monoprop { static constexpr auto build_type() noexcept -> std::string_view { return "@CMAKE_BUILD_TYPE@"; @@ -27,7 +29,7 @@ static auto compiler_flags() noexcept -> std::map { return { {"from-environment", "@CMAKE_CXX_FLAGS@"}, {"build-type-flags", "@_cmake_build_type_specific_flags@"}, - {"vectorization", "@ARCH_FLAG@"}, + {"machine-flags", std::string(variant_flags())}, {"project-defaults", "@CMAKE_CXX23_STANDARD_COMPILE_OPTION@ @monoprop_CXX_FLAGS@"}, {"user-appended", "@EXTRA_CXXFLAGS@"}, }; diff --git a/cpp/include/monoprop/Variants.h.in b/cpp/include/monoprop/Variants.h.in new file mode 100644 index 00000000..54e13fb0 --- /dev/null +++ b/cpp/include/monoprop/Variants.h.in @@ -0,0 +1,63 @@ +// Copyright 2026 Algorithmiq +// +// Licensed 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. + +#pragma once + +#include + +/** + * @brief Declares a compile-time function that reports the active FMV variant. + * + * Expands to a `consteval` function named `variant()` with a GNU + * `target("arch=...")` attribute bound to the provided architecture string. + * + * @param archstr Architecture suffix used in `arch=`. + */ +#define monoprop_VARIANT(archstr) \ + [[using gnu: target("arch=" archstr)]] consteval auto variant() noexcept -> std::string_view { \ + return "arch=" archstr; \ + } + +/** + * @brief Declares a compile-time function that reports the machine-dependent + * flags GCC applies for the given FMV variant. + * + * Expands to a `consteval` function named `machine_flags()` with a GNU + * `target("arch=...")` attribute bound to the provided architecture string. The + * returned value is the cleaned, space-separated list of machine flags GCC uses + * for that architecture, as reported by `gcc -march= -Q --help=target`. + * + * @param archstr Architecture suffix used in `arch=`. + * @param flagsstr Machine-dependent flags reported for the architecture. + */ +#define monoprop_VARIANT_FLAGS(archstr, flagsstr) \ + [[using gnu: target("arch=" archstr)]] consteval auto variant_flags() noexcept -> std::string_view { \ + return flagsstr; \ + } + +namespace monoprop { +[[using gnu: target("default")]] consteval auto variant() noexcept -> std::string_view { + return "default"; +} + +[[using gnu: target("default")]] consteval auto variant_flags() noexcept -> std::string_view { + return "@monoprop_DEFAULT_VARIANT_FLAGS@"; +} + +// clang-format off +@monoprop_VARIANTS@ + +@monoprop_VARIANT_FLAGS@ +// clang-format on +} // namespace monoprop diff --git a/src/monoprop/__init__.py b/src/monoprop/__init__.py index 33640365..323a4478 100644 --- a/src/monoprop/__init__.py +++ b/src/monoprop/__init__.py @@ -22,6 +22,7 @@ MAX_NUM_MODES, __build_type__, __compiler_flags__, + __variant__, antihermitian_generator_correction, has_mpi, is_antihermitian, @@ -57,6 +58,7 @@ "PauliPropagator", "__build_type__", "__compiler_flags__", + "__variant__", "__version__", "antihermitian_generator_correction", "expand_monomials", diff --git a/src/monoprop/bindings/bindings.cpp.in b/src/monoprop/bindings/bindings.cpp.in index 44770819..0fdc30ac 100644 --- a/src/monoprop/bindings/bindings.cpp.in +++ b/src/monoprop/bindings/bindings.cpp.in @@ -112,6 +112,8 @@ NB_MODULE(_core, m) { // clang-format on m.attr("__build_type__") = std::string(build_type()); m.attr("__compiler_flags__") = compiler_flags(); + m.attr("__variant__") = std::string(variant()); + #ifdef monoprop_ENABLE_MPI m.attr("has_mpi") = true; #else diff --git a/tests/test_gcc_target_help_clean.py b/tests/test_gcc_target_help_clean.py new file mode 100644 index 00000000..20a34020 --- /dev/null +++ b/tests/test_gcc_target_help_clean.py @@ -0,0 +1,98 @@ +# Copyright 2026 Algorithmiq +# +# Licensed 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. + +"""Tests for the ``gcc -Q --help=target`` output cleaner.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +_MODULE_PATH = Path(__file__).parents[1] / "tools" / "gcc-target-help-clean.py" +_spec = importlib.util.spec_from_file_location("gcc_target_help_clean", _MODULE_PATH) +assert _spec is not None +assert _spec.loader is not None +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +clean_target_help = _module.clean_target_help + + +def _wrap(body: str) -> str: + return ( + "The following options are target specific:\n" + f"{body}\n" + "\n" + " Known assembler dialects (for use with the -masm= option):\n" + " att intel\n" + ) + + +def test_enabled_keeps_only_name() -> None: + text = _wrap(" -m64 \t\t[enabled]") + assert clean_target_help(text) == "-m64" + + +def test_disabled_is_dropped() -> None: + text = _wrap(" -m16 \t\t[disabled]") + assert clean_target_help(text) == "" + + +def test_equals_joins_with_value() -> None: + text = _wrap(" -mabi= \t\tsysv") + assert clean_target_help(text) == "-mabi=sysv" + + +def test_equals_empty_value_is_dropped() -> None: + text = _wrap(" -mcpu= \t\t") + assert clean_target_help(text) == "" + + +def test_equals_default_value_is_dropped() -> None: + text = _wrap(" -mcmodel= \t\t[default]") + assert clean_target_help(text) == "" + + +def test_alias_line_keeps_both_fields() -> None: + text = _wrap(" -msse5 \t\t-mavx") + assert clean_target_help(text) == "-msse5 -mavx" + + +def test_range_hint_is_stripped() -> None: + text = _wrap(" -mbranch-cost=<0,5> \t\t3") + assert clean_target_help(text) == "-mbranch-cost=3" + + +def test_only_section_between_markers_is_used() -> None: + text = ( + "-mignored-before \t\t[enabled]\n" + "The following options are target specific:\n" + " -m64 \t\t[enabled]\n" + " Known assembler dialects (for use with the -masm= option):\n" + " -mignored-after \t\t[enabled]\n" + ) + assert clean_target_help(text) == "-m64" + + +def test_missing_start_marker_returns_empty() -> None: + assert clean_target_help("nothing relevant here") == "" + + +def test_multiple_entries_joined_by_space() -> None: + text = _wrap( + " -m64 \t\t[enabled]\n" + " -m16 \t\t[disabled]\n" + " -mabi= \t\tsysv\n" + " -msse5 \t\t-mavx" + ) + assert clean_target_help(text) == "-m64 -mabi=sysv -msse5 -mavx" diff --git a/tools/gcc-target-help-clean.py b/tools/gcc-target-help-clean.py new file mode 100644 index 00000000..71d2d843 --- /dev/null +++ b/tools/gcc-target-help-clean.py @@ -0,0 +1,80 @@ +# Copyright 2026 Algorithmiq +# +# Licensed 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. + +# ruff: noqa: INP001 + +"""Clean up the output of ``gcc -Q --help=target``. + +Reads the command's output from stdin, extracts the target-specific options +section, normalizes each line, and prints a single space-separated string. +""" + +from __future__ import annotations + +import re +import sys + +_START_MARKER = "The following options are target specific:" +_END_MARKER = "Known assembler dialects (for use with the -masm= option):" +_MULTISPACE = re.compile(r"\s{2,}") +_HINT = re.compile(r"<[^>]*>") + + +def clean_target_help(text: str) -> str: + """Normalize ``gcc -Q --help=target`` output into a single string. + + Args: + text: The full stdout of ``gcc -Q --help=target``. + + Returns: + A single space-separated string of the cleaned options. Rules: + lines containing ``[disabled]`` are dropped; lines whose value is + ``[enabled]`` keep only the option name; options ending in ``=`` are + joined to their value unless the value is empty or ``[default]`` (in + which case the line is dropped); any remaining line keeps both fields + joined by a single space. + """ + start = text.find(_START_MARKER) + if start == -1: + return "" + end = text.find(_END_MARKER, start) + section = text[start + len(_START_MARKER) : end if end != -1 else None] + + entries: list[str] = [] + for raw in section.splitlines(): + line = raw.strip() + if not line or "[disabled]" in line: + continue + fields = _MULTISPACE.split(line, maxsplit=1) + name = _HINT.sub("", fields[0]) + value = fields[1].strip() if len(fields) > 1 else "" + + if "[enabled]" in value: + entries.append(name) + elif "=" in name: + if value and value != "[default]": + entries.append(name + value) + else: + entries.append(f"{name} {value}".strip()) + + return " ".join(entries) + + +def main() -> None: + """Read stdin and print the cleaned target options.""" + sys.stdout.write(clean_target_help(sys.stdin.read()) + "\n") + + +if __name__ == "__main__": + main() From 0ee7b100e8ab201afc1babe66acad22d875bc321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 10:20:55 +0200 Subject: [PATCH 02/13] fix: name of generated header file --- cpp/include/monoprop/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/monoprop/CMakeLists.txt b/cpp/include/monoprop/CMakeLists.txt index 4e9d7afa..f658be51 100644 --- a/cpp/include/monoprop/CMakeLists.txt +++ b/cpp/include/monoprop/CMakeLists.txt @@ -14,7 +14,7 @@ target_sources( FILES "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/${PROJECT_NAME}Export.h" "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Info.h" - "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/VariantMacros.h" + "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Variants.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/Evolution.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MPFunctions.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MPGraph.h" From 8b16e5ba8be3500a6d3075fbb1f51634c6186a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 10:26:33 +0000 Subject: [PATCH 03/13] fix: extend variant features extraction to AppleClang Assisted-by: GitHub Copilot, gpt-5.3-codex --- cmake/compiler_flags/CXXFlags.cmake | 73 ++++++++++++++++------ tests/test_clang_target_help_clean.py | 85 ++++++++++++++++++++++++++ tools/clang-target-help-clean.py | 87 +++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 tests/test_clang_target_help_clean.py create mode 100644 tools/clang-target-help-clean.py diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index 65174cf0..2c5a3750 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -101,24 +101,63 @@ function(_monoprop_query_machine_flags) set(_march_args "-march=${_arg_MARCH}") endif() - # Report the machine-dependent flags GCC uses for each target variant by - # querying `gcc -march= -Q --help=target` and cleaning the output with - # tools/gcc-target-help-clean.py. - execute_process( - COMMAND - ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target - COMMAND - ${Python_EXECUTABLE} - "${PROJECT_SOURCE_DIR}/tools/gcc-target-help-clean.py" - OUTPUT_VARIABLE _flags - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE _result - ) - if(NOT _result EQUAL 0) - message( - FATAL_ERROR - "Failed to query machine-dependent flags for '${_arg_MARCH}' (exit code ${_result})" + if(CMAKE_CXX_COMPILER_ID STREQUAL AppleClang) + # AppleClang does not support `-Q --help=target`. Query the driver with + # `-###` and normalize CPU/march flags from the reported invocation. + execute_process( + COMMAND + # gersemi: off + ${CMAKE_CXX_COMPILER} ${_march_args} -### -x c++ -c /dev/null + # gersemi: on + ERROR_VARIABLE _query_output + ERROR_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _query_result + ) + if(NOT _query_result EQUAL 0) + message( + WARNING + "Failed to query machine-dependent flags for '${_arg_MARCH}' with AppleClang (exit code ${_query_result}). Continuing with empty machine flags." + ) + set(_flags "") + else() + execute_process( + COMMAND + ${CMAKE_COMMAND} -E echo "${_query_output}" + COMMAND + ${Python_EXECUTABLE} + "${PROJECT_SOURCE_DIR}/tools/clang-target-help-clean.py" + OUTPUT_VARIABLE _flags + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _parse_result + ) + if(NOT _parse_result EQUAL 0) + message( + WARNING + "Failed to parse AppleClang machine-dependent flags for '${_arg_MARCH}' (exit code ${_parse_result}). Continuing with empty machine flags." + ) + set(_flags "") + endif() + endif() + else() + # Report the machine-dependent flags GCC uses for each target variant by + # querying `gcc -march= -Q --help=target` and cleaning the output + # with tools/gcc-target-help-clean.py. + execute_process( + COMMAND + ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target + COMMAND + ${Python_EXECUTABLE} + "${PROJECT_SOURCE_DIR}/tools/gcc-target-help-clean.py" + OUTPUT_VARIABLE _flags + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _result ) + if(NOT _result EQUAL 0) + message( + FATAL_ERROR + "Failed to query machine-dependent flags for '${_arg_MARCH}' (exit code ${_result})" + ) + endif() endif() set(${_arg_OUTPUT_VARIABLE} "${_flags}" PARENT_SCOPE) endfunction() diff --git a/tests/test_clang_target_help_clean.py b/tests/test_clang_target_help_clean.py new file mode 100644 index 00000000..6425ddeb --- /dev/null +++ b/tests/test_clang_target_help_clean.py @@ -0,0 +1,85 @@ +# Copyright 2026 Algorithmiq +# +# Licensed 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. + +"""Tests for the ``clang -###`` output cleaner.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +_MODULE_PATH = Path(__file__).parents[1] / "tools" / "clang-target-help-clean.py" +_spec = importlib.util.spec_from_file_location("clang_target_help_clean", _MODULE_PATH) +assert _spec is not None +assert _spec.loader is not None +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +clean_target_help = _module.clean_target_help + + +def test_target_cpu_and_feature_are_emitted() -> None: + text = ( + "Apple clang version 16.0.0\n" + "Target: arm64-apple-darwin\n" + ' "/usr/bin/clang++" "-cc1" "-triple" "arm64-apple-macosx14.0.0" ' + '"-target-cpu" "apple-m4" "-target-feature" "+neon"\n' + ) + assert clean_target_help(text) == "-target-cpu=apple-m4 -target-feature=+neon" + + +def test_only_selected_m_flags_are_kept() -> None: + text = ( + ' "/usr/bin/clang++" "-cc1" "-mframe-pointer=non-leaf" ' + '"-march=armv8.6-a" "-mtune=apple-m4" "-mllvm" "-something"\n' + ) + assert clean_target_help(text) == "-march=armv8.6-a -mtune=apple-m4" + + +def test_spaced_flag_forms_are_normalized() -> None: + text = "-mcpu apple-m3 -mtune generic -march native -target-feature +crc" + assert ( + clean_target_help(text) + == "-mcpu=apple-m3 -mtune=generic -march=native -target-feature=+crc" + ) + + +def test_duplicates_are_removed_preserving_order() -> None: + text = ( + "-march=native -target-cpu apple-m3 -target-feature +neon " + "-target-feature +neon -march=native" + ) + assert ( + clean_target_help(text) + == "-march=native -target-cpu=apple-m3 -target-feature=+neon" + ) + + +def test_realistic_appleclang_output_shape() -> None: + text = ( + "Apple clang version 21.0.0 (clang-2100.0.123.102)\n" + "Target: arm64-apple-darwin25.3.0\n" + '"/Library/Developer/CommandLineTools/usr/bin/clang" "-cc1" ' + '"-target-cpu" "apple-m1" ' + '"-target-feature" "+v8.5a" ' + '"-target-feature" "+dotprod" ' + '"-target-feature" "+neon"\n' + ) + assert ( + clean_target_help(text) == "-target-cpu=apple-m1 -target-feature=+v8.5a " + "-target-feature=+dotprod -target-feature=+neon" + ) + + +def test_empty_input_returns_empty() -> None: + assert clean_target_help("") == "" diff --git a/tools/clang-target-help-clean.py b/tools/clang-target-help-clean.py new file mode 100644 index 00000000..46594418 --- /dev/null +++ b/tools/clang-target-help-clean.py @@ -0,0 +1,87 @@ +# Copyright 2026 Algorithmiq +# +# Licensed 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. + +# ruff: noqa: INP001 + +"""Normalize machine flags from ``clang -###`` output. + +Reads the command output from stdin and extracts a stable, space-separated list +of machine-relevant flags. For AppleClang we keep only ``-march=``, ``-mcpu=``, +``-mtune=``, ``-target-cpu=``, and ``-target-feature=`` forms. +""" + +from __future__ import annotations + +import shlex +import sys + +_ALLOWED_PREFIXES = ( + "-march=", + "-mcpu=", + "-mtune=", + "-target-cpu=", + "-target-feature=", +) + + +def _append_unique(entries: list[str], seen: set[str], value: str) -> None: + """Append a flag only once, preserving first-seen order.""" + if value not in seen: + seen.add(value) + entries.append(value) + + +def _normalize_pair_flag(flag: str, value: str) -> str: + """Normalize pair-style machine flags into ``-key=value`` form.""" + return f"{flag}={value}" + + +def clean_target_help(text: str) -> str: + """Normalize ``clang -###`` output into a single machine-flag string. + + Args: + text: The full output generated by ``clang -###``. + + Returns: + A space-separated string containing unique normalized machine flags in + first-seen order. + """ + tokens = shlex.split(text.replace("\n", " ")) + entries: list[str] = [] + seen: set[str] = set() + pair_flags = {"-march", "-mcpu", "-mtune", "-target-cpu", "-target-feature"} + + idx = 0 + while idx < len(tokens): + tok = tokens[idx] + + if tok.startswith(_ALLOWED_PREFIXES): + _append_unique(entries, seen, tok) + elif tok in pair_flags and idx + 1 < len(tokens): + normalized = _normalize_pair_flag(tok, tokens[idx + 1]) + _append_unique(entries, seen, normalized) + idx += 1 + + idx += 1 + + return " ".join(entries) + + +def main() -> None: + """Read stdin and print normalized machine flags.""" + sys.stdout.write(clean_target_help(sys.stdin.read()) + "\n") + + +if __name__ == "__main__": + main() From 2bc47da6108e28c9da4c03258b17a4a5199b3402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 10:38:43 +0000 Subject: [PATCH 04/13] fix: remove the attribute --- cpp/include/monoprop/Variants.h.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/include/monoprop/Variants.h.in b/cpp/include/monoprop/Variants.h.in index 54e13fb0..7cfe51a9 100644 --- a/cpp/include/monoprop/Variants.h.in +++ b/cpp/include/monoprop/Variants.h.in @@ -47,11 +47,11 @@ } namespace monoprop { -[[using gnu: target("default")]] consteval auto variant() noexcept -> std::string_view { +consteval auto variant() noexcept -> std::string_view { return "default"; } -[[using gnu: target("default")]] consteval auto variant_flags() noexcept -> std::string_view { +consteval auto variant_flags() noexcept -> std::string_view { return "@monoprop_DEFAULT_VARIANT_FLAGS@"; } From 38a3ada9c8db62a5a98ea96e4ce92c8c0cb159fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 11:28:34 +0000 Subject: [PATCH 05/13] ci: print messages --- cmake/compiler_flags/CXXFlags.cmake | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index 2c5a3750..2f39bb70 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -102,8 +102,6 @@ function(_monoprop_query_machine_flags) endif() if(CMAKE_CXX_COMPILER_ID STREQUAL AppleClang) - # AppleClang does not support `-Q --help=target`. Query the driver with - # `-###` and normalize CPU/march flags from the reported invocation. execute_process( COMMAND # gersemi: off @@ -113,6 +111,10 @@ function(_monoprop_query_machine_flags) ERROR_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _query_result ) + message( + STATUS + "_query_output : ${_query_output}\n_query_result : ${_query_result}" + ) if(NOT _query_result EQUAL 0) message( WARNING @@ -139,9 +141,14 @@ function(_monoprop_query_machine_flags) endif() endif() else() - # Report the machine-dependent flags GCC uses for each target variant by - # querying `gcc -march= -Q --help=target` and cleaning the output - # with tools/gcc-target-help-clean.py. + execute_process( + COMMAND + ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target + OUTPUT_VARIABLE _foo + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _result + ) + message(STATUS "_foo : ${_foo}\n_result : ${_result}") execute_process( COMMAND ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target From ccc299739e37975e1128d560e1b85d790fcc1c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 12:09:40 +0000 Subject: [PATCH 06/13] chore: figure out clang behavior --- cmake/compiler_flags/CXXFlags.cmake | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index 2f39bb70..1ae51015 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -105,11 +105,12 @@ function(_monoprop_query_machine_flags) execute_process( COMMAND # gersemi: off - ${CMAKE_CXX_COMPILER} ${_march_args} -### -x c++ -c /dev/null + ${CMAKE_CXX_COMPILER} ${_march_args} -\#\#\# -x c++ -c /dev/null # gersemi: on ERROR_VARIABLE _query_output ERROR_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _query_result + COMMAND_ECHO STDERR ) message( STATUS @@ -127,10 +128,11 @@ function(_monoprop_query_machine_flags) ${CMAKE_COMMAND} -E echo "${_query_output}" COMMAND ${Python_EXECUTABLE} - "${PROJECT_SOURCE_DIR}/tools/clang-target-help-clean.py" + "${PROJECT_SOURCE_DIR}/tools/target-help-clean.py" --mode clang OUTPUT_VARIABLE _flags OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _parse_result + COMMAND_ECHO STDERR ) if(NOT _parse_result EQUAL 0) message( @@ -147,17 +149,19 @@ function(_monoprop_query_machine_flags) OUTPUT_VARIABLE _foo OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _result + COMMAND_ECHO STDERR ) message(STATUS "_foo : ${_foo}\n_result : ${_result}") execute_process( COMMAND ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target COMMAND - ${Python_EXECUTABLE} - "${PROJECT_SOURCE_DIR}/tools/gcc-target-help-clean.py" + ${Python_EXECUTABLE} "${PROJECT_SOURCE_DIR}/tools/target-help-clean.py" + --mode gcc OUTPUT_VARIABLE _flags OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _result + COMMAND_ECHO STDERR ) if(NOT _result EQUAL 0) message( From c0662f6b19af315e38bb2cda78729c81e9cc9dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 12:14:03 +0000 Subject: [PATCH 07/13] refactor: cleaner scripts for flags --- tests/test_clang_target_help_clean.py | 85 --------- tests/test_gcc_target_help_clean.py | 98 ----------- tests/test_target_help_clean.py | 241 ++++++++++++++++++++++++++ tools/clang-target-help-clean.py | 87 ---------- tools/gcc-target-help-clean.py | 80 --------- tools/target-help-clean.py | 142 +++++++++++++++ 6 files changed, 383 insertions(+), 350 deletions(-) delete mode 100644 tests/test_clang_target_help_clean.py delete mode 100644 tests/test_gcc_target_help_clean.py create mode 100644 tests/test_target_help_clean.py delete mode 100644 tools/clang-target-help-clean.py delete mode 100644 tools/gcc-target-help-clean.py create mode 100644 tools/target-help-clean.py diff --git a/tests/test_clang_target_help_clean.py b/tests/test_clang_target_help_clean.py deleted file mode 100644 index 6425ddeb..00000000 --- a/tests/test_clang_target_help_clean.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2026 Algorithmiq -# -# Licensed 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. - -"""Tests for the ``clang -###`` output cleaner.""" - -from __future__ import annotations - -import importlib.util -from pathlib import Path - -_MODULE_PATH = Path(__file__).parents[1] / "tools" / "clang-target-help-clean.py" -_spec = importlib.util.spec_from_file_location("clang_target_help_clean", _MODULE_PATH) -assert _spec is not None -assert _spec.loader is not None -_module = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(_module) -clean_target_help = _module.clean_target_help - - -def test_target_cpu_and_feature_are_emitted() -> None: - text = ( - "Apple clang version 16.0.0\n" - "Target: arm64-apple-darwin\n" - ' "/usr/bin/clang++" "-cc1" "-triple" "arm64-apple-macosx14.0.0" ' - '"-target-cpu" "apple-m4" "-target-feature" "+neon"\n' - ) - assert clean_target_help(text) == "-target-cpu=apple-m4 -target-feature=+neon" - - -def test_only_selected_m_flags_are_kept() -> None: - text = ( - ' "/usr/bin/clang++" "-cc1" "-mframe-pointer=non-leaf" ' - '"-march=armv8.6-a" "-mtune=apple-m4" "-mllvm" "-something"\n' - ) - assert clean_target_help(text) == "-march=armv8.6-a -mtune=apple-m4" - - -def test_spaced_flag_forms_are_normalized() -> None: - text = "-mcpu apple-m3 -mtune generic -march native -target-feature +crc" - assert ( - clean_target_help(text) - == "-mcpu=apple-m3 -mtune=generic -march=native -target-feature=+crc" - ) - - -def test_duplicates_are_removed_preserving_order() -> None: - text = ( - "-march=native -target-cpu apple-m3 -target-feature +neon " - "-target-feature +neon -march=native" - ) - assert ( - clean_target_help(text) - == "-march=native -target-cpu=apple-m3 -target-feature=+neon" - ) - - -def test_realistic_appleclang_output_shape() -> None: - text = ( - "Apple clang version 21.0.0 (clang-2100.0.123.102)\n" - "Target: arm64-apple-darwin25.3.0\n" - '"/Library/Developer/CommandLineTools/usr/bin/clang" "-cc1" ' - '"-target-cpu" "apple-m1" ' - '"-target-feature" "+v8.5a" ' - '"-target-feature" "+dotprod" ' - '"-target-feature" "+neon"\n' - ) - assert ( - clean_target_help(text) == "-target-cpu=apple-m1 -target-feature=+v8.5a " - "-target-feature=+dotprod -target-feature=+neon" - ) - - -def test_empty_input_returns_empty() -> None: - assert clean_target_help("") == "" diff --git a/tests/test_gcc_target_help_clean.py b/tests/test_gcc_target_help_clean.py deleted file mode 100644 index 20a34020..00000000 --- a/tests/test_gcc_target_help_clean.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2026 Algorithmiq -# -# Licensed 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. - -"""Tests for the ``gcc -Q --help=target`` output cleaner.""" - -from __future__ import annotations - -import importlib.util -from pathlib import Path - -_MODULE_PATH = Path(__file__).parents[1] / "tools" / "gcc-target-help-clean.py" -_spec = importlib.util.spec_from_file_location("gcc_target_help_clean", _MODULE_PATH) -assert _spec is not None -assert _spec.loader is not None -_module = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(_module) -clean_target_help = _module.clean_target_help - - -def _wrap(body: str) -> str: - return ( - "The following options are target specific:\n" - f"{body}\n" - "\n" - " Known assembler dialects (for use with the -masm= option):\n" - " att intel\n" - ) - - -def test_enabled_keeps_only_name() -> None: - text = _wrap(" -m64 \t\t[enabled]") - assert clean_target_help(text) == "-m64" - - -def test_disabled_is_dropped() -> None: - text = _wrap(" -m16 \t\t[disabled]") - assert clean_target_help(text) == "" - - -def test_equals_joins_with_value() -> None: - text = _wrap(" -mabi= \t\tsysv") - assert clean_target_help(text) == "-mabi=sysv" - - -def test_equals_empty_value_is_dropped() -> None: - text = _wrap(" -mcpu= \t\t") - assert clean_target_help(text) == "" - - -def test_equals_default_value_is_dropped() -> None: - text = _wrap(" -mcmodel= \t\t[default]") - assert clean_target_help(text) == "" - - -def test_alias_line_keeps_both_fields() -> None: - text = _wrap(" -msse5 \t\t-mavx") - assert clean_target_help(text) == "-msse5 -mavx" - - -def test_range_hint_is_stripped() -> None: - text = _wrap(" -mbranch-cost=<0,5> \t\t3") - assert clean_target_help(text) == "-mbranch-cost=3" - - -def test_only_section_between_markers_is_used() -> None: - text = ( - "-mignored-before \t\t[enabled]\n" - "The following options are target specific:\n" - " -m64 \t\t[enabled]\n" - " Known assembler dialects (for use with the -masm= option):\n" - " -mignored-after \t\t[enabled]\n" - ) - assert clean_target_help(text) == "-m64" - - -def test_missing_start_marker_returns_empty() -> None: - assert clean_target_help("nothing relevant here") == "" - - -def test_multiple_entries_joined_by_space() -> None: - text = _wrap( - " -m64 \t\t[enabled]\n" - " -m16 \t\t[disabled]\n" - " -mabi= \t\tsysv\n" - " -msse5 \t\t-mavx" - ) - assert clean_target_help(text) == "-m64 -mabi=sysv -msse5 -mavx" diff --git a/tests/test_target_help_clean.py b/tests/test_target_help_clean.py new file mode 100644 index 00000000..553a293e --- /dev/null +++ b/tests/test_target_help_clean.py @@ -0,0 +1,241 @@ +# Copyright 2026 Algorithmiq +# +# Licensed 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. + +"""Tests for the unified compiler target-help cleaner.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +_MODULE_PATH = Path(__file__).parents[1] / "tools" / "target-help-clean.py" +_spec = importlib.util.spec_from_file_location("target_help_clean", _MODULE_PATH) +assert _spec is not None +assert _spec.loader is not None +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) + +clean_clang_target_help = _module.clean_clang_target_help +clean_gcc_target_help = _module.clean_gcc_target_help +clean_target_help = _module.clean_target_help + + +def _wrap_gcc( + body: str, + trailer: str = "Known assembler dialects (for use with the -masm= option):", +) -> str: + return ( + "The following options are target specific:\n" + f"{body}\n" + "\n" + f" {trailer}\n" + " att intel\n" + ) + + +def test_dispatches_gcc_mode() -> None: + text = _wrap_gcc(" -m64 \t\t[enabled]") + assert clean_target_help(text, mode="gcc") == "-m64" + + +def test_dispatches_clang_mode() -> None: + text = ( + "Apple clang version 16.0.0\n" + "Target: arm64-apple-darwin\n" + ' "/usr/bin/clang++" "-cc1" "-target-cpu" "apple-m4" ' + '"-target-feature" "+neon"\n' + ) + assert ( + clean_target_help(text, mode="clang") + == "-target-cpu=apple-m4 -target-feature=+neon" + ) + + +def test_gcc_enabled_keeps_only_name() -> None: + text = _wrap_gcc(" -m64 \t\t[enabled]") + assert clean_gcc_target_help(text) == "-m64" + + +def test_gcc_disabled_is_dropped() -> None: + text = _wrap_gcc(" -m16 \t\t[disabled]") + assert clean_gcc_target_help(text) == "" + + +def test_gcc_equals_joins_with_value() -> None: + text = _wrap_gcc(" -mabi= \t\tsysv") + assert clean_gcc_target_help(text) == "-mabi=sysv" + + +def test_gcc_equals_empty_value_is_dropped() -> None: + text = _wrap_gcc(" -mcpu= \t\t") + assert clean_gcc_target_help(text) == "" + + +def test_gcc_equals_default_value_is_dropped() -> None: + text = _wrap_gcc(" -mcmodel= \t\t[default]") + assert clean_gcc_target_help(text) == "" + + +def test_gcc_alias_line_keeps_both_fields() -> None: + text = _wrap_gcc(" -msse5 \t\t-mavx") + assert clean_gcc_target_help(text) == "-msse5 -mavx" + + +def test_gcc_range_hint_is_stripped() -> None: + text = _wrap_gcc(" -mbranch-cost=<0,5> \t\t3") + assert clean_gcc_target_help(text) == "-mbranch-cost=3" + + +def test_gcc_only_target_section_is_used() -> None: + text = ( + "-mignored-before \t\t[enabled]\n" + "The following options are target specific:\n" + " -m64 \t\t[enabled]\n" + "\n" + " Known assembler dialects (for use with the -masm= option):\n" + " -mignored-after \t\t[enabled]\n" + ) + assert clean_gcc_target_help(text) == "-m64" + + +def test_gcc_blank_line_terminates_section_for_aarch64_trailer() -> None: + text = _wrap_gcc( + " -mabi= \t\tilp32\n" + " -mstrict-align \t\t[enabled]", + trailer="Known AArch64 ABIs (for use with the -mabi= option):", + ) + assert clean_gcc_target_help(text) == "-mabi=ilp32 -mstrict-align" + + +def test_gcc_trailer_content_after_blank_line_is_ignored() -> None: + text = ( + "The following options are target specific:\n" + " -m64 \t\t[enabled]\n" + "\n" + " Known AArch64 ABIs (for use with the -mabi= option):\n" + " -mabi= \t\tilp32\n" + ) + assert clean_gcc_target_help(text) == "-m64" + + +def test_gcc_missing_start_marker_returns_empty() -> None: + assert clean_gcc_target_help("nothing relevant here") == "" + + +def test_gcc_multiple_entries_joined_by_space() -> None: + text = _wrap_gcc( + " -m64 \t\t[enabled]\n" + " -m16 \t\t[disabled]\n" + " -mabi= \t\tsysv\n" + " -msse5 \t\t-mavx" + ) + assert clean_gcc_target_help(text) == "-m64 -mabi=sysv -msse5 -mavx" + + +def test_gcc_x86_64_reference_output_shapes() -> None: + text = _wrap_gcc( + " -m128bit-long-double \t\t[enabled]\n" + " -m16 \t\t[disabled]\n" + " -mabi= \t\tsysv\n" + " -mavx10.1-512 \t\t-mavx10.1\n" + " -mbranch-cost=<0,5> \t\t3\n" + " -mcmodel= \t\t[default]\n" + " -mcpu=\n" + " -mlarge-data-threshold= \t65536" + ) + assert clean_gcc_target_help(text) == ( + "-m128bit-long-double -mabi=sysv -mavx10.1-512 -mavx10.1 " + "-mbranch-cost=3 -mlarge-data-threshold=65536" + ) + + +def test_gcc_aarch64_reference_output_shapes() -> None: + text = _wrap_gcc( + " -mabi= lp64\n" + " -mbranch-protection=\n" + " -mearly-ldp-fusion [enabled]\n" + " -moverride=\n" + " -mstrict-align [disabled]\n" + " -msve-vector-bits= scalable", + trailer="Known AArch64 ABIs (for use with the -mabi= option):", + ) + assert clean_gcc_target_help(text) == ( + "-mabi=lp64 -mearly-ldp-fusion -msve-vector-bits=scalable" + ) + + +def test_clang_target_cpu_and_feature_are_emitted() -> None: + text = ( + "Apple clang version 16.0.0\n" + "Target: arm64-apple-darwin\n" + ' "/usr/bin/clang++" "-cc1" "-triple" "arm64-apple-macosx14.0.0" ' + '"-target-cpu" "apple-m4" "-target-feature" "+neon"\n' + ) + assert clean_clang_target_help(text) == "-target-cpu=apple-m4 -target-feature=+neon" + + +def test_clang_only_selected_m_flags_are_kept() -> None: + text = ( + ' "/usr/bin/clang++" "-cc1" "-mframe-pointer=non-leaf" ' + '"-march=armv8.6-a" "-mtune=apple-m4" "-mllvm" "-something"\n' + ) + assert clean_clang_target_help(text) == "-march=armv8.6-a -mtune=apple-m4" + + +def test_clang_spaced_flag_forms_are_normalized() -> None: + text = "-mcpu apple-m3 -mtune generic -march native -target-feature +crc" + assert ( + clean_clang_target_help(text) + == "-mcpu=apple-m3 -mtune=generic -march=native -target-feature=+crc" + ) + + +def test_clang_duplicates_are_removed_preserving_order() -> None: + text = ( + "-march=native -target-cpu apple-m3 -target-feature +neon " + "-target-feature +neon -march=native" + ) + assert ( + clean_clang_target_help(text) + == "-march=native -target-cpu=apple-m3 -target-feature=+neon" + ) + + +def test_clang_realistic_appleclang_output_shape() -> None: + text = ( + "Apple clang version 21.0.0 (clang-2100.0.123.102)\n" + "Target: arm64-apple-darwin25.3.0\n" + "Thread model: posix\n" + '\N{NO-BREAK SPACE}"/Library/Developer/CommandLineTools/usr/bin/clang" ' + '"-cc1" "-mframe-pointer=non-leaf" ' + '"-target-cpu" "apple-m1" ' + '"-target-feature" "+v8.5a" ' + '"-target-feature" "+dotprod" ' + '"-target-feature" "+neon"\n' + '"-target-feature" "+sb" "-target-abi" "darwinpcs"\n' + ) + assert ( + clean_clang_target_help(text) == "-target-cpu=apple-m1 -target-feature=+v8.5a " + "-target-feature=+dotprod -target-feature=+neon -target-feature=+sb" + ) + + +def test_clang_trailing_pair_flag_without_value_is_ignored() -> None: + assert clean_clang_target_help("-target-cpu apple-m1 -target-feature") == ( + "-target-cpu=apple-m1" + ) + + +def test_clang_empty_input_returns_empty() -> None: + assert clean_clang_target_help("") == "" diff --git a/tools/clang-target-help-clean.py b/tools/clang-target-help-clean.py deleted file mode 100644 index 46594418..00000000 --- a/tools/clang-target-help-clean.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2026 Algorithmiq -# -# Licensed 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. - -# ruff: noqa: INP001 - -"""Normalize machine flags from ``clang -###`` output. - -Reads the command output from stdin and extracts a stable, space-separated list -of machine-relevant flags. For AppleClang we keep only ``-march=``, ``-mcpu=``, -``-mtune=``, ``-target-cpu=``, and ``-target-feature=`` forms. -""" - -from __future__ import annotations - -import shlex -import sys - -_ALLOWED_PREFIXES = ( - "-march=", - "-mcpu=", - "-mtune=", - "-target-cpu=", - "-target-feature=", -) - - -def _append_unique(entries: list[str], seen: set[str], value: str) -> None: - """Append a flag only once, preserving first-seen order.""" - if value not in seen: - seen.add(value) - entries.append(value) - - -def _normalize_pair_flag(flag: str, value: str) -> str: - """Normalize pair-style machine flags into ``-key=value`` form.""" - return f"{flag}={value}" - - -def clean_target_help(text: str) -> str: - """Normalize ``clang -###`` output into a single machine-flag string. - - Args: - text: The full output generated by ``clang -###``. - - Returns: - A space-separated string containing unique normalized machine flags in - first-seen order. - """ - tokens = shlex.split(text.replace("\n", " ")) - entries: list[str] = [] - seen: set[str] = set() - pair_flags = {"-march", "-mcpu", "-mtune", "-target-cpu", "-target-feature"} - - idx = 0 - while idx < len(tokens): - tok = tokens[idx] - - if tok.startswith(_ALLOWED_PREFIXES): - _append_unique(entries, seen, tok) - elif tok in pair_flags and idx + 1 < len(tokens): - normalized = _normalize_pair_flag(tok, tokens[idx + 1]) - _append_unique(entries, seen, normalized) - idx += 1 - - idx += 1 - - return " ".join(entries) - - -def main() -> None: - """Read stdin and print normalized machine flags.""" - sys.stdout.write(clean_target_help(sys.stdin.read()) + "\n") - - -if __name__ == "__main__": - main() diff --git a/tools/gcc-target-help-clean.py b/tools/gcc-target-help-clean.py deleted file mode 100644 index 71d2d843..00000000 --- a/tools/gcc-target-help-clean.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2026 Algorithmiq -# -# Licensed 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. - -# ruff: noqa: INP001 - -"""Clean up the output of ``gcc -Q --help=target``. - -Reads the command's output from stdin, extracts the target-specific options -section, normalizes each line, and prints a single space-separated string. -""" - -from __future__ import annotations - -import re -import sys - -_START_MARKER = "The following options are target specific:" -_END_MARKER = "Known assembler dialects (for use with the -masm= option):" -_MULTISPACE = re.compile(r"\s{2,}") -_HINT = re.compile(r"<[^>]*>") - - -def clean_target_help(text: str) -> str: - """Normalize ``gcc -Q --help=target`` output into a single string. - - Args: - text: The full stdout of ``gcc -Q --help=target``. - - Returns: - A single space-separated string of the cleaned options. Rules: - lines containing ``[disabled]`` are dropped; lines whose value is - ``[enabled]`` keep only the option name; options ending in ``=`` are - joined to their value unless the value is empty or ``[default]`` (in - which case the line is dropped); any remaining line keeps both fields - joined by a single space. - """ - start = text.find(_START_MARKER) - if start == -1: - return "" - end = text.find(_END_MARKER, start) - section = text[start + len(_START_MARKER) : end if end != -1 else None] - - entries: list[str] = [] - for raw in section.splitlines(): - line = raw.strip() - if not line or "[disabled]" in line: - continue - fields = _MULTISPACE.split(line, maxsplit=1) - name = _HINT.sub("", fields[0]) - value = fields[1].strip() if len(fields) > 1 else "" - - if "[enabled]" in value: - entries.append(name) - elif "=" in name: - if value and value != "[default]": - entries.append(name + value) - else: - entries.append(f"{name} {value}".strip()) - - return " ".join(entries) - - -def main() -> None: - """Read stdin and print the cleaned target options.""" - sys.stdout.write(clean_target_help(sys.stdin.read()) + "\n") - - -if __name__ == "__main__": - main() diff --git a/tools/target-help-clean.py b/tools/target-help-clean.py new file mode 100644 index 00000000..577b9bed --- /dev/null +++ b/tools/target-help-clean.py @@ -0,0 +1,142 @@ +# Copyright 2026 Algorithmiq +# +# Licensed 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. + +# ruff: noqa: INP001 + +"""Normalize compiler target-help output into stable machine-flag strings. + +The script reads compiler output from stdin and cleans it according to the +selected mode: + +- ``gcc`` for ``gcc -Q --help=target`` output +- ``clang`` for ``clang -###`` output +""" + +from __future__ import annotations + +import argparse +import re +import shlex +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterator + +_PAIR_FLAGS = ( + "-march", + "-mcpu", + "-mtune", + "-target-cpu", + "-target-feature", +) +_ALLOWED_PREFIXES = tuple(f"{flag}=" for flag in _PAIR_FLAGS) + +_START_MARKER = "The following options are target specific:" +_MULTISPACE = re.compile(r"\s{2,}") +_HINT = re.compile(r"<[^>]*>") + + +def _gcc_option_lines(text: str) -> Iterator[str]: + """Yield option lines from GCC's target-specific section.""" + _, marker, section = text.partition(_START_MARKER) + if not marker: + return + + options_started = False + for raw_line in section.splitlines(): + line = raw_line.strip() + if line.startswith("-"): + options_started = True + yield line + elif options_started: + return + + +def _normalize_gcc_option(line: str) -> str | None: + """Normalize one GCC target option, omitting inactive values.""" + if "[disabled]" in line: + return None + + fields = _MULTISPACE.split(line, maxsplit=1) + name = _HINT.sub("", fields[0]) + value = fields[1].strip() if len(fields) > 1 else "" + + if "[enabled]" in value: + return name + if "=" not in name: + return f"{name} {value}".strip() + if not value or value == "[default]": + return None + return name + value + + +def _clang_machine_flags(text: str) -> Iterator[str]: + """Yield normalized machine flags from Clang's command-line trace.""" + tokens = iter(shlex.split(text.replace("\n", " "))) + for token in tokens: + if token.startswith(_ALLOWED_PREFIXES): + yield token + elif token in _PAIR_FLAGS and (value := next(tokens, None)) is not None: + yield f"{token}={value}" + + +def clean_clang_target_help(text: str) -> str: + """Normalize ``clang -###`` output into a machine-flag string.""" + return " ".join(dict.fromkeys(_clang_machine_flags(text))) + + +def clean_gcc_target_help(text: str) -> str: + """Normalize ``gcc -Q --help=target`` output into a machine-flag string. + + Parsing starts after the target-specific marker and stops at the first + blank separator line. This keeps section detection architecture-agnostic. + """ + entries = ( + normalized + for line in _gcc_option_lines(text) + if (normalized := _normalize_gcc_option(line)) is not None + ) + return " ".join(entries) + + +def clean_target_help(text: str, mode: str) -> str: + """Dispatch target-help normalization according to compiler mode.""" + if mode == "gcc": + return clean_gcc_target_help(text) + if mode == "clang": + return clean_clang_target_help(text) + + raise ValueError(f"Unsupported mode: {mode}") + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--mode", + required=True, + choices=("gcc", "clang"), + help="Parser mode matching the compiler output format.", + ) + return parser + + +def main() -> None: + """Read stdin and print normalized target flags for the selected mode.""" + args = _build_parser().parse_args() + sys.stdout.write(clean_target_help(sys.stdin.read(), mode=args.mode) + "\n") + + +if __name__ == "__main__": + main() From 606a86191ab5f829f1ef6426ee2ec71eb5bc3e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 12:20:00 +0000 Subject: [PATCH 08/13] chore: remove now useless prints --- cmake/compiler_flags/CXXFlags.cmake | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index 1ae51015..fedf2f4b 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -112,10 +112,6 @@ function(_monoprop_query_machine_flags) RESULT_VARIABLE _query_result COMMAND_ECHO STDERR ) - message( - STATUS - "_query_output : ${_query_output}\n_query_result : ${_query_result}" - ) if(NOT _query_result EQUAL 0) message( WARNING @@ -143,15 +139,6 @@ function(_monoprop_query_machine_flags) endif() endif() else() - execute_process( - COMMAND - ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target - OUTPUT_VARIABLE _foo - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE _result - COMMAND_ECHO STDERR - ) - message(STATUS "_foo : ${_foo}\n_result : ${_result}") execute_process( COMMAND ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target From 85fdac9b40f7e0b6cc11aff961c2b644b26c112a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 12:22:14 +0000 Subject: [PATCH 09/13] ci: pretty-print version, variant, compiler flags --- .github/workflows/test.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddb17a80..bd64a22b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,9 +99,21 @@ jobs: run: | uv tree - - name: Get monoprop version + - name: Get monoprop information run: | - uv run python -c "import monoprop as mp; print(mp.__version__); print(mp.__variant__); print(mp.__compiler_flags__)" + uv run python <<'EOF' + import pprint + + import monoprop as mp + + pprint.pprint( + { + "version": mp.__version__, + "variant": mp.__variant__, + "compiler_flags": mp.__compiler_flags__, + } + ) + EOF - name: Verify that find_package(monoprop) works run: | From bb529af4c813cbb658213baed8e0170f33a6bdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Fri, 7 Aug 2026 12:25:20 +0000 Subject: [PATCH 10/13] chore: remove one more debug print in cmake --- cmake/compiler_flags/CXXFlags.cmake | 3 --- cpp/include/monoprop/CMakeLists.txt | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index fedf2f4b..f56236bd 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -110,7 +110,6 @@ function(_monoprop_query_machine_flags) ERROR_VARIABLE _query_output ERROR_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _query_result - COMMAND_ECHO STDERR ) if(NOT _query_result EQUAL 0) message( @@ -128,7 +127,6 @@ function(_monoprop_query_machine_flags) OUTPUT_VARIABLE _flags OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _parse_result - COMMAND_ECHO STDERR ) if(NOT _parse_result EQUAL 0) message( @@ -148,7 +146,6 @@ function(_monoprop_query_machine_flags) OUTPUT_VARIABLE _flags OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE _result - COMMAND_ECHO STDERR ) if(NOT _result EQUAL 0) message( diff --git a/cpp/include/monoprop/CMakeLists.txt b/cpp/include/monoprop/CMakeLists.txt index f658be51..98159824 100644 --- a/cpp/include/monoprop/CMakeLists.txt +++ b/cpp/include/monoprop/CMakeLists.txt @@ -14,7 +14,7 @@ target_sources( FILES "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/${PROJECT_NAME}Export.h" "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Info.h" - "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Variants.h" + "${PROJECT_BINARY_DIR}/include/${PROJECT_NAME}/Variants.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/Evolution.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MPFunctions.h" "${PROJECT_SOURCE_DIR}/cpp/include/${PROJECT_NAME}/MPGraph.h" From e534e8199151ab0495b50b03aaf94c0aed21add8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Mon, 10 Aug 2026 08:36:26 +0000 Subject: [PATCH 11/13] chore(toml): :art: add .h.in files to cache keys --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0ae666a0..966d2208 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ keyring-provider = "subprocess" cache-keys = [ { file = "pyproject.toml" }, { file = "cpp/include/**/*.h" }, + { file = "cpp/include/**/*.h.in" }, { file = "cpp/monoprop/**/*.{h,cpp}" }, { file = "**/CMakeLists.txt" }, { file = "cmake/**/*" }, From 9898858b1f42237583e618f1f77fac4db1c6fed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Mon, 10 Aug 2026 08:37:05 +0000 Subject: [PATCH 12/13] chore(c++): :lipstick: clean up doxygen docstrings, add ifdef for x86 --- cpp/include/monoprop/Variants.h.in | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cpp/include/monoprop/Variants.h.in b/cpp/include/monoprop/Variants.h.in index 7cfe51a9..45ea84e2 100644 --- a/cpp/include/monoprop/Variants.h.in +++ b/cpp/include/monoprop/Variants.h.in @@ -17,7 +17,7 @@ #include /** - * @brief Declares a compile-time function that reports the active FMV variant. + * @brief Declares a compile-time function that reports the active variant. * * Expands to a `consteval` function named `variant()` with a GNU * `target("arch=...")` attribute bound to the provided architecture string. @@ -31,7 +31,7 @@ /** * @brief Declares a compile-time function that reports the machine-dependent - * flags GCC applies for the given FMV variant. + * flags GCC applies for the given variant. * * Expands to a `consteval` function named `machine_flags()` with a GNU * `target("arch=...")` attribute bound to the provided architecture string. The @@ -47,10 +47,16 @@ } namespace monoprop { +#if defined(__x86_64__) || defined(_M_X64) +[[using gnu: target("default")]] +#endif consteval auto variant() noexcept -> std::string_view { return "default"; } +#if defined(__x86_64__) || defined(_M_X64) +[[using gnu: target("default")]] +#endif consteval auto variant_flags() noexcept -> std::string_view { return "@monoprop_DEFAULT_VARIANT_FLAGS@"; } From 6a207bbbda6201dcaa0f0a37c6a917dac4acaafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Mon, 10 Aug 2026 09:05:44 +0000 Subject: [PATCH 13/13] build(c++): :ambulance: ensure the clang flag extraction also works on linux --- cmake/compiler_flags/CXXFlags.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/compiler_flags/CXXFlags.cmake b/cmake/compiler_flags/CXXFlags.cmake index f56236bd..2fa94331 100644 --- a/cmake/compiler_flags/CXXFlags.cmake +++ b/cmake/compiler_flags/CXXFlags.cmake @@ -101,7 +101,7 @@ function(_monoprop_query_machine_flags) set(_march_args "-march=${_arg_MARCH}") endif() - if(CMAKE_CXX_COMPILER_ID STREQUAL AppleClang) + if(CMAKE_CXX_COMPILER_ID MATCHES Clang) execute_process( COMMAND # gersemi: off