Skip to content

build: require std::format (C++20 <format>) for Python builds; GCC>=13 fast-path#961

Merged
pcchen merged 4 commits into
masterfrom
require-gcc-13
Jul 7, 2026
Merged

build: require std::format (C++20 <format>) for Python builds; GCC>=13 fast-path#961
pcchen merged 4 commits into
masterfrom
require-gcc-13

Conversation

@pcchen

@pcchen pcchen commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Enforce, at configure time, that the toolchain can compile std::format — the
only thing that pins Cytnx's compiler floor — and fail early with an actionable
message instead of a confusing mid-build error. This is split out of #950 (the
host-compiler slice only); the CUDA/cuTENSOR gates, finder fixes, diagnostics, and
docs from #950 will follow as separate PRs.

Why std::format is the constraint (explicitly)

Cytnx compiles as C++20 (CMAKE_CXX_STANDARD 20) and uses std::format — but
only in the Python binding layer:

  • pybind/symmetry_py.cpp (#include <format>, std::format(...))
  • pybind/unitensor_py.cpp (#include <format>, std::format(...))

There is no std::format in include/ or src/. std::format (the C++20 <format>
library, P0645) is missing from early C++20-era standard libraries, and the minimum
version differs per implementation — and is not implied by the compiler front-end
version
, since Clang can pair with either standard library:

  • libstdc++ (GCC): since GCC 13
  • libc++ (Clang/AppleClang): since LLVM 17 (earlier needed -fexperimental-library; AppleClang additionally has macOS deployment-target availability markers)

What this PR does (CMakeLists.txt only)

  1. Authoritative feature check. A check_cxx_source_compiles probe that actually
    calls std::format (not just #include <format>), so it catches every toolchain
    that lacks a usable std::format regardless of compiler/stdlib pairing — including
    libc++'s experimental-gated and AppleClang availability-gated cases. The probe is
    compiled at C++20 via CMAKE_CXX_STANDARD (cmake_minimum_required >= 3.25
    policy CMP0067 is NEW), so it doesn't false-negative at the compiler's default C++17.
  2. Friendly GCC fast-path. A GCC < 13 branch emits a targeted message (the most
    common miss) before the generic probe.
  3. Gated on BUILD_PYTHON. Because std::format is used only by the pybind layer,
    the whole requirement lives inside if(BUILD_PYTHON). A C++-only build
    (-DBUILD_PYTHON=OFF) skips the check and builds on any C++20 compiler (e.g. GCC
    11/12). The error messages point at -DBUILD_PYTHON=OFF as an escape hatch.

If include//src/ ever adopt std::format, move the guard out of the BUILD_PYTHON
block (noted in a code comment).

Testing

  • Standalone cmake validation:
    • probe passes at C++20, fails at the compiler default (C++17) — confirming the
      standard must be (and is) honored;
    • BUILD_PYTHON=ON runs the probe, BUILD_PYTHON=OFF skips it;
    • GCC fast-path matrix: 11/12 → rejected, 13/14 → allowed, AppleClang → not gated.
  • pre-commit (EOF/whitespace) clean; clang-format N/A (CMake).

Review points addressed

  • "Gating only GCC by version is fragile / use check_cxx_source_compiles" → feature-check probe (6fb2a268).
  • "Gate the GCC floor / std::format probe only for Python builds"if(BUILD_PYTHON) guard (6d43f974).

Related

🤖 Generated with Claude Code

Cytnx targets C++20 and calls std::format in pybind/symmetry_py.cpp and
pybind/unitensor_py.cpp. libstdc++ implements the C++20 <format> library
only from GCC 13, so GCC 12 and earlier fail to compile those files with
"'format' is not a member of 'std'".

Add an early CMake FATAL_ERROR when the GNU host compiler is older than
13, with an actionable message (how to point CMake at g++-13). Only GNU
is gated; Clang/AppleClang provide <format> via their own standard
library and are validated separately.

Split out of #950 (the GCC-floor portion only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a compiler check in CMakeLists.txt to enforce GCC >= 13, ensuring support for C++20's std::format. The reviewer rightly pointed out that gating only GCC is fragile since other compilers like Clang on Linux also rely on libstdc++ and would bypass this check while still failing to compile. They suggested a more robust, compiler-agnostic approach using check_cxx_source_compiles to directly verify support.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread CMakeLists.txt Outdated
Comment on lines +208 to +216
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)
message(FATAL_ERROR
"GCC >= 13 is required: Cytnx uses std::format (C++20 <format>), which "
"libstdc++ implements only from GCC 13. Found GCC "
"${CMAKE_CXX_COMPILER_VERSION} at ${CMAKE_CXX_COMPILER}.\n"
"Install GCC 13+ and point CMake at it, e.g. "
"-DCMAKE_CXX_COMPILER=g++-13 (and -DCMAKE_C_COMPILER=gcc-13), "
"or set the CXX/CC environment variables.")
endif()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Gating only GCC by version is fragile because other compilers (such as Clang on Linux) use libstdc++ by default. If the system's libstdc++ is < 13, Clang will still fail to compile std::format but will bypass this check, leading to the exact "confusing mid-build compile error" this PR aims to prevent.

Using check_cxx_source_compiles to directly check for <format> support is the standard, robust, and compiler-agnostic way to verify C++20 std::format support.

include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
  #include <format>
  int main() {
    auto s = std::format(\"{}\", 42);
    return 0;
  }
" HAS_STD_FORMAT)

if(NOT HAS_STD_FORMAT)
  message(FATAL_ERROR
    "C++20 <format> support (std::format) is required but not found. "
    "If you are using GCC, GCC >= 13 is required. If you are using Clang, "
    "ensure your standard library (libstdc++ or libc++) supports <format>.")
endif()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6fb2a26. Replaced the version-only gate with an authoritative check_cxx_source_compiles probe of std::format (compiler-agnostic), keeping the GCC>=13 branch only as a friendlier fast-path message. The probe actually calls std::format (not just #include <format>) so it also catches libc++'s experimental-gated / AppleClang availability-gated cases, and it compiles at C++20 via CMAKE_CXX_STANDARD (CMP0067 NEW) so it doesn't false-negative at the compiler's default C++17.


Posted by Claude Code on behalf of @pcchen

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f74ecc2e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CMakeLists.txt Outdated
# Enforce a GCC >= 13 floor here with an early, actionable message instead of a
# confusing mid-build compile error. Only GNU is gated: Clang/AppleClang provide
# <format> via their own standard library and are validated separately.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate the GCC floor only for Python builds

When configuring a C++-only build with -DBUILD_PYTHON=OFF, this unconditional check now rejects GCC 12 even though the cited std::format users are only pybind/symmetry_py.cpp and pybind/unitensor_py.cpp, and those sources are added exclusively inside the later IF(BUILD_PYTHON) block. This turns a previously valid C++ package/library build into a configure-time failure; the GCC >= 13 requirement should be tied to BUILD_PYTHON unless another non-pybind source also requires <format>.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6d43f97. The GCC>=13 fast-path is now wrapped in if(BUILD_PYTHON)std::format is used only by the pybind layer (pybind/symmetry_py.cpp, pybind/unitensor_py.cpp), so a C++-only build (-DBUILD_PYTHON=OFF) skips the floor. The error message also points at -DBUILD_PYTHON=OFF as an escape hatch.


Posted by Claude Code on behalf of @pcchen

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may use std::format project-wide, like error message handling, in the future.

Add an authoritative, compiler-agnostic check_cxx_source_compiles probe
for std::format at C++20, so any toolchain lacking <format> fails early
regardless of compiler/standard-library pairing. The GCC>=13 check is
kept only as a friendlier fast-path message.

Rationale: std::format's availability is a standard-library property, not
a Clang front-end version (Clang may use libc++ or libstdc++), so a Clang
version gate would be unreliable. libstdc++ ships it from GCC 13; libc++
from LLVM 17. The probe honors CMAKE_CXX_STANDARD (cmake_minimum_required
>= 3.25 => CMP0067 NEW), so it compiles at C++20 rather than the default
C++17 (which would false-negative). Verified locally that the probe
detects std::format only when compiled at C++20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fb2a26816

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CMakeLists.txt Outdated
Comment on lines +232 to +235
check_cxx_source_compiles(
"#include <format>
int main() { return std::format(\"{}\", 42).size() == 2 ? 0 : 1; }"
CYTNX_HAS_STD_FORMAT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate the std::format probe on Python builds

With -DBUILD_PYTHON=OFF on a toolchain that lacks <format> (for example Clang using libstdc++ 12), this unconditional probe now fails configure even though the std::format call sites are only in pybind/symmetry_py.cpp and pybind/unitensor_py.cpp, which are added only inside the later IF(BUILD_PYTHON) block. Fresh evidence beyond the earlier GCC-only comment is that this new compiler-agnostic probe preserves the same C++-only build regression even if the GCC fast-path is guarded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6d43f97. The check_cxx_source_compiles std::format probe is likewise inside the if(BUILD_PYTHON) guard. Verified locally: BUILD_PYTHON=ON runs the probe (HAS=1); BUILD_PYTHON=OFF skips it entirely.


Posted by Claude Code on behalf of @pcchen

@pcchen

pcchen commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Good point — and it checks out. std::format is used in exactly two files, both under pybind/:

  • pybind/symmetry_py.cpp (#include <format>, line 29)
  • pybind/unitensor_py.cpp (#include <format>, line 91)

There is no std::format (or <format>) usage anywhere in include/ or src/ — the core C++ library doesn't need it. And those pybind translation units are compiled only inside IF(BUILD_PYTHON) (CMakeLists.txt:375; BUILD_PYTHON defaults ON). So a C++-only build (-DBUILD_PYTHON=OFF) never touches std::format, and would build fine on any C++20 compiler — e.g. GCC 11/12 — that doesn't meet the GCC-13 floor.

So I agree: the std::format requirement (both the GCC-13 fast-path and the check_cxx_source_compiles probe) should be gated behind BUILD_PYTHON. BUILD_PYTHON is declared at CMakeLists.txt:167, before the project() call where the check sits, so wrapping the whole block in if(BUILD_PYTHON) ... endif() is a one-line change and keeps the early-failure behavior for the common (Python) build.

Two caveats worth a maintainer decision:

  1. This ties the floor to current usage. If the core library (include//src/) ever adopts std::format, the guard must widen again. I'd add a short comment at the two pybind call sites (and/or the CMake guard) noting that std::format is what pins GCC 13, so the coupling is discoverable. Alternatively, if you'd rather keep a single uniform toolchain requirement for all builds, leave it unconditional — that's a policy call, not a correctness one.
  2. Purely local std::format uses in pybind/ could also be sidestepped entirely (e.g. fmt/manual string building) to drop the GCC-13 requirement even for Python builds — but that's a larger change and out of scope for this PR.

If we go with gating: I'll wrap the block in if(BUILD_PYTHON) and update the PR/comment accordingly. Want me to push that?


Posted by Claude Code on behalf of @pcchen

std::format is used only by the Python bindings (pybind/symmetry_py.cpp,
pybind/unitensor_py.cpp), which compile only when BUILD_PYTHON=ON. The
core C++ library (include/, src/) does not use it, so a C++-only build
(-DBUILD_PYTHON=OFF) needs just a C++20 compiler, not GCC 13. Wrap the
GCC-13 fast-path and the std::format probe in if(BUILD_PYTHON) so the
floor applies only to Python builds, and mention -DBUILD_PYTHON=OFF in
the error messages as an escape hatch.

Addresses review feedback (gate the GCC floor only for Python builds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pcchen pcchen changed the title build: require GCC >= 13 (std::format / C++20 <format>) build: require std::format (C++20 <format>) for Python builds; GCC>=13 fast-path Jul 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d43f974db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CMakeLists.txt
Comment on lines +236 to +239
check_cxx_source_compiles(
"#include <format>
int main() { return std::format(\"{}\", 42).size() == 2 ? 0 : 1; }"
CYTNX_HAS_STD_FORMAT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Probe std::format with the same availability define

On macOS builds that set a deployment target below libc++'s std::format availability, this probe now fails before targets are created, but the real Python sources inherit _LIBCPP_DISABLE_AVAILABILITY from cytnx via target_link_libraries(pycytnx PUBLIC cytnx). Because the check does not use the same compile definition, cibuildwheel-style macOS builds can be rejected at configure time even though the actual pybind translation units would compile with the project's existing target flags.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed and fixed in 2341283.

Verified the mechanism: cytnx sets target_compile_definitions(cytnx PUBLIC _LIBCPP_DISABLE_AVAILABILITY) (CMakeLists.txt:245) and pycytnx links it PUBLIC (line 415), so the real pybind TUs get the define but the probe did not.

Reproduced locally with a low deployment target:

CMAKE_OSX_DEPLOYMENT_TARGET=12.0 probe result
without _LIBCPP_DISABLE_AVAILABILITY fails (would wrongly FATAL_ERROR)
with _LIBCPP_DISABLE_AVAILABILITY passes (matches real build)

Fix sets CMAKE_REQUIRED_DEFINITIONS=-D_LIBCPP_DISABLE_AVAILABILITY around the check_cxx_source_compiles call (and unsets it after), so the probe compiles the test TU the same way the pybind sources are compiled. It's a no-op off libc++.


Posted by Claude Code on behalf of @pcchen

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 30.87%. Comparing base (4177e4f) to head (2341283).
⚠️ Report is 8 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #961   +/-   ##
=======================================
  Coverage   30.87%   30.87%           
=======================================
  Files         229      229           
  Lines       34757    34757           
  Branches    14409    14409           
=======================================
  Hits        10730    10730           
  Misses      16720    16720           
  Partials     7307     7307           
Flag Coverage Δ
cpp 30.38% <ø> (ø)
python 59.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 32.19% <ø> (ø)
Python bindings 17.28% <ø> (ø)
Python package 59.41% <ø> (ø)
see 1 file with indirect coverage changes

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4177e4f...2341283. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The real pybind sources compile with _LIBCPP_DISABLE_AVAILABILITY,
inherited from the cytnx target (target_compile_definitions(cytnx PUBLIC
_LIBCPP_DISABLE_AVAILABILITY) -> pycytnx via PUBLIC link). On macOS/libc++
that disables std::format's deployment-target availability guards. The
configure-time probe did not set it, so a low-deployment-target macOS
build (e.g. cibuildwheel) could be rejected at configure time even though
the actual translation units compile fine.

Set CMAKE_REQUIRED_DEFINITIONS=-D_LIBCPP_DISABLE_AVAILABILITY around the
check so the probe matches the real build. No-op off libc++.

Reproduced locally: with CMAKE_OSX_DEPLOYMENT_TARGET=12.0 the probe fails
without the define and passes with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread CMakeLists.txt
Comment on lines +221 to +231
# Friendlier fast-path message for the most common miss (old GCC), before the
# generic probe.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)
message(FATAL_ERROR
"GCC >= 13 is required for the Python bindings (BUILD_PYTHON=ON): Cytnx "
"uses std::format (C++20 <format>), which libstdc++ implements only from "
"GCC 13. Found GCC ${CMAKE_CXX_COMPILER_VERSION} at ${CMAKE_CXX_COMPILER}.\n"
"Install GCC 13+ and point CMake at it, e.g. -DCMAKE_CXX_COMPILER=g++-13 "
"(and -DCMAKE_C_COMPILER=gcc-13), set the CXX/CC environment variables, or "
"configure a C++-only build with -DBUILD_PYTHON=OFF.")
endif()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like what the bot suggested, we don't need version guard if we have feature guard.

The current change is fine, however, changing the whole project to C++20 means we allow developers use C++20 features in the whole project. We can move this guard out of BUILD_PYTHON, so that the usage of std::format outside pybind can also be guarded at the configure time.

@pcchen
pcchen merged commit df23e7c into master Jul 7, 2026
19 checks passed
@pcchen
pcchen deleted the require-gcc-13 branch July 7, 2026 03:50
Comment thread CMakeLists.txt
project(CYTNX VERSION ${CYTNX_VERSION} LANGUAGES CXX C)

# #####################################################################
# ## HOST COMPILER REQUIREMENT: C++20 std::format (Python bindings only)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not python binding only. PairWiseSum uses std::span which is a C++20 feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants