diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18f4cde2..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__)" + 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: | 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..2fa94331 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,109 @@ 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() + + if(CMAKE_CXX_COMPILER_ID MATCHES Clang) + 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/target-help-clean.py" --mode clang + 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() + execute_process( + COMMAND + ${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target + COMMAND + ${Python_EXECUTABLE} "${PROJECT_SOURCE_DIR}/tools/target-help-clean.py" + --mode gcc + 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() + +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..98159824 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}/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" "${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..45ea84e2 --- /dev/null +++ b/cpp/include/monoprop/Variants.h.in @@ -0,0 +1,69 @@ +// 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 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 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 { +#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@"; +} + +// clang-format off +@monoprop_VARIANTS@ + +@monoprop_VARIANT_FLAGS@ +// clang-format on +} // namespace monoprop diff --git a/pyproject.toml b/pyproject.toml index 925a95bb..327a48c1 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/**/*" }, 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_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/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()