Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ AccessModifierOffset: -4
ColumnLimit: 100

Language: Cpp
Standard: c++17
Standard: c++20
AlignAfterOpenBracket: Align
AlignEscapedNewlines: Right
AllowAllArgumentsOnNextLine: false
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ env:
OPTREE_CXX_WERROR: "OFF"
_GLIBCXX_USE_CXX11_ABI: "1"
_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR: "1"
# Apple's libc++ gates `std::format` behind macOS 13.3 / iOS 16.3. iOS wheel tags carry the
# minor, so 16.3 is taggable exactly. macOS tags do not: PEP 425 floors the minor to zero above
# macOS 11, so a 13.3 binary would ship as `macosx_13_0_*`, install on 13.0-13.2 and fail in
# `dyld`. 14.0 is the lowest target whose tag does not lie.
MACOSX_DEPLOYMENT_TARGET: "14.0"
IPHONEOS_DEPLOYMENT_TARGET: "16.3"
PYTHONUNBUFFERED: "1"
PYTHON_TAG: "py3" # to be updated
PYTHON_VERSION: "3" # to be updated
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
run: make clang-format

- name: clang-tidy
run: make clang-tidy CMAKE_CXX_STANDARD=17
run: make clang-tidy CMAKE_CXX_STANDARD=20

- name: cpplint
run: make cpplint
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests-with-pydebug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ env:
OPTREE_CXX_WERROR: "ON"
_GLIBCXX_USE_CXX11_ABI: "1"
_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR: "1"
# Apple's libc++ gates `std::format` behind macOS 13.3; 14.0 keeps the wheel tag honest, see
# `build.yml`.
MACOSX_DEPLOYMENT_TARGET: "14.0"
PYTHONDEVMODE: "1"
PYTHONUNBUFFERED: "1"
PYTHON: "python" # to be updated
Expand Down
34 changes: 3 additions & 31 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ env:
OPTREE_CXX_WERROR: "ON"
_GLIBCXX_USE_CXX11_ABI: "1"
_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR: "1"
# Apple's libc++ gates `std::format` behind macOS 13.3; 14.0 keeps the wheel tag honest, see
# `build.yml`.
MACOSX_DEPLOYMENT_TARGET: "14.0"
FULL_TEST_PYTHON_VERSIONS: "3.12;3.13"
PYTHONDEVMODE: "1"
PYTHONUNBUFFERED: "1"
Expand Down Expand Up @@ -207,37 +210,6 @@ jobs:
cat pyproject.toml
echo "::endgroup::"

- name: Test installable with C++17
shell: bash
if: runner.os != 'Windows'
run: |
(
set -ex
${{ env.PYTHON }} -m venv venv
source venv/bin/activate
OPTREE_CXX_WERROR=OFF CMAKE_CXX_STANDARD=17 \
${{ env.PYTHON }} -m pip install -v .
pushd tests
${{ env.PYTHON }} -X dev -Walways -Werror -c 'import optree'
popd
rm -rf venv
)

if [[ "$?" -ne 0 ]]; then
echo "::error::Failed to install with C++17." >&2
exit 1
fi
CORE_DUMP_FILES="$(
find . -type d -path "./venv" -prune \
-o '(' -iname "core.*.[1-9]*" -o -iname "core_*.dmp" ')' -print
)"
if [[ -n "${CORE_DUMP_FILES}" ]]; then
echo "::error::Coredump files found, indicating a crash during tests." >&2
echo "Coredump files:" >&2
ls -alh ${CORE_DUMP_FILES} >&2
exit 1
fi

- name: Test buildable without Python frontend
if: runner.os != 'Windows'
run: |
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Update minimal version of `typing-extensions` to 4.10.0 for `typing_extensions.TypeIs` by [@XuehaiPan](https://github.com/XuehaiPan) in [#285](https://github.com/metaopt/optree/pull/285).
- Raise the minimum required C++ standard for building the C extension from C++17 to C++20, for `std::format`, `std::source_location`, `std::span`, `std::ranges`, and constrained (`requires`) templates by [@XuehaiPan](https://github.com/XuehaiPan) in [#235](https://github.com/metaopt/optree/pull/235). Building from source now needs GCC 13+, Clang 16+ with libstdc++ 13+, Clang 17+ with libc++ 17+, Apple Clang 16+ (Xcode 16+), or MSVC 19.32+ (Visual Studio 2022 17.2+); `std::format` sets the floor on GCC and MSVC, and `P0634R3` sets it on Clang.
- Raise the deployment targets of the macOS and iOS wheels to 14.0 and 16.3, as Apple's libc++ gates `std::format` behind macOS 13.3 / iOS 16.3; macOS wheel tags floor the minor version to zero, so 14.0 is the lowest target that does not advertise support for 13.0-13.2, while the iOS wheels keep the exact 16.3 floor and are retagged to match, since cibuildwheel's cross-build environment would otherwise tag them `ios_13_0_*` by [@XuehaiPan](https://github.com/XuehaiPan) in [#235](https://github.com/metaopt/optree/pull/235).
- Stop passing `-Wno-error=attributes` under `OPTREE_CXX_WERROR=ON`; it existed so that the C++20 `[[likely]]`/`[[unlikely]]` attributes would not fail a C++17 build, and now only masks genuine attribute errors by [@XuehaiPan](https://github.com/XuehaiPan) in [#235](https://github.com/metaopt/optree/pull/235).

### Fixed

Expand Down Expand Up @@ -68,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

-
- Remove support for building the C extension with C++17; `CMAKE_CXX_STANDARD` must now be at least `20` by [@XuehaiPan](https://github.com/XuehaiPan) in [#235](https://github.com/metaopt/optree/pull/235).

------

Expand Down
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")

setdefault_ifndef(CMAKE_CXX_STANDARD 20) # for likely/unlikely attributes
if(CMAKE_CXX_STANDARD VERSION_LESS 17)
message(FATAL_ERROR "C++17 or higher is required")
setdefault_ifndef(CMAKE_CXX_STANDARD 20)
if(CMAKE_CXX_STANDARD VERSION_LESS 20)
message(FATAL_ERROR "C++20 or higher is required")
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
message(STATUS "Use C++ standard: C++${CMAKE_CXX_STANDARD}")
Expand Down Expand Up @@ -138,7 +138,9 @@ if(OPTREE_CXX_WERROR)
if(MSVC)
string(APPEND CMAKE_CXX_FLAGS " /WX")
else()
string(APPEND CMAKE_CXX_FLAGS " -Werror -Wno-error=attributes -Wno-error=redundant-move")
# `-Wno-error=redundant-move` stays: C++20's implicit-move rules (P1825) change what
# `-Wredundant-move` fires on, and the warning differs between GCC and Clang.
string(APPEND CMAKE_CXX_FLAGS " -Werror -Wno-error=redundant-move")
endif()
endif()

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The following options are available while building the Python C extension from s
```bash
export CMAKE_COMMAND="/path/to/custom/cmake"
export CMAKE_BUILD_TYPE="Debug"
export CMAKE_CXX_STANDARD="20" # C++17 is tested on Linux/macOS (C++20 is required on Windows)
export CMAKE_CXX_STANDARD="20" # 20 is the minimum supported standard
export OPTREE_CXX_WERROR="OFF"
export _GLIBCXX_USE_CXX11_ABI="1" # set to 0 to use the old libstdc++ ABI
export _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR="1" # set to "" to disable the workaround for MSVC mutex layout change in VS 2022 v17.10+
Expand All @@ -72,7 +72,7 @@ export pybind11_DIR="/path/to/custom/pybind11"
pip3 install .
```

Compiling from source requires Python 3.9+, a C++ compiler (`g++` / `clang++` / `icpx` / `cl.exe`) that supports C++20, and a `cmake` installation.
Compiling from source requires Python 3.9+, a `cmake` installation (3.18+), and a C++ compiler (`g++` / `clang++` / `icpx` / `cl.exe`) with complete C++20 support: GCC 13+, Clang 16+ with libstdc++ 13+, Clang 17+ with libc++ 17+, Apple Clang 16+ (Xcode 16+), or MSVC 19.32+ (Visual Studio 2022 17.2+). Two features set those floors: `std::format` on GCC and MSVC, and `P0634R3` (`typename` made optional) on Clang. On Apple platforms `std::format` is additionally gated behind macOS 13.3 / iOS 16.3 availability, so a build targeting an older release will not compile.

--------------------------------------------------------------------------------

Expand Down
80 changes: 34 additions & 46 deletions include/optree/exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,61 @@ limitations under the License.

#pragma once

#include <cstddef> // std::size_t
#include <optional> // std::optional, std::nullopt
#include <sstream> // std::ostringstream
#include <stdexcept> // std::logic_error
#include <string> // std::string, std::char_traits, std::to_string
#include <type_traits> // std::void_t, std::{true,false}_type
#include <utility> // std::declval
#include <cstddef> // std::size_t
#include <format> // std::format
#include <source_location> // std::source_location
#include <stdexcept> // std::logic_error
#include <string> // std::string, std::char_traits, std::to_string
#include <string_view> // std::string_view

namespace optree {

constexpr std::size_t CURRENT_FILE_PATH_SIZE = std::char_traits<char>::length(__FILE__);
constexpr std::size_t CURRENT_FILE_PATH_SIZE =
std::char_traits<char>::length(std::source_location::current().file_name());
constexpr std::size_t CURRENT_FILE_RELPATH_FROM_PROJECT_ROOT_SIZE =
std::char_traits<char>::length("include/optree/exceptions.h");
static_assert(CURRENT_FILE_PATH_SIZE >= CURRENT_FILE_RELPATH_FROM_PROJECT_ROOT_SIZE,
"SOURCE_PATH_PREFIX_SIZE must be greater than 0.");
constexpr std::size_t SOURCE_PATH_PREFIX_SIZE =
CURRENT_FILE_PATH_SIZE - CURRENT_FILE_RELPATH_FROM_PROJECT_ROOT_SIZE;
// NOLINTNEXTLINE[bugprone-reserved-identifier]
#define __FILE_RELPATH_FROM_PROJECT_ROOT__ ((const char *)&(__FILE__[SOURCE_PATH_PREFIX_SIZE]))

// Strip the prefix `SOURCE_PATH_PREFIX_SIZE` measured off this header's own path. A translation
// unit compiled with a shorter path is returned as-is rather than letting `substr` throw.
constexpr std::string_view RelpathFromProjectRoot(const std::string_view &abspath) {
return abspath.size() >= SOURCE_PATH_PREFIX_SIZE ? abspath.substr(SOURCE_PATH_PREFIX_SIZE)
: abspath;
}
constexpr std::string_view RelpathFromProjectRoot(
const std::source_location &source_location = std::source_location::current()) {
return RelpathFromProjectRoot(source_location.file_name());
}

class InternalError : public std::logic_error {
public:
explicit InternalError(const std::string &message) noexcept(noexcept(std::logic_error{message}))
: std::logic_error{message} {}
explicit InternalError(const std::string &message,
const std::string &file,
const std::size_t &lineno,
const std::optional<std::string> function =
std::nullopt) noexcept(noexcept(std::logic_error{message}))
: InternalError([&message, &file, &lineno, &function]() -> std::string {
std::ostringstream oss{};
oss << message << " (";
if (function) [[likely]] {
oss << "function `" << *function << "` ";
}
oss << "at file " << file << ":" << lineno << ")\n\n"
<< "Please file a bug report at https://github.com/metaopt/optree/issues.";
return oss.str();
}()) {}
explicit InternalError(
const std::string_view &message,
const std::source_location &source_location = std::source_location::current())
: std::logic_error{
std::format("{} (in function `{}` at file {}:{}:{})\n\n"
"Please file a bug report at https://github.com/metaopt/optree/issues.",
message,
source_location.function_name(),
RelpathFromProjectRoot(source_location),
source_location.line(),
source_location.column())} {}
};

} // namespace optree

inline namespace { // NOLINT[build/namespaces_headers]
// SFINAE helper to detect if std::to_string is available for a type
template <typename T, typename = void>
struct has_to_string : std::false_type {};

// Detect whether `std::to_string` accepts a `const T &`, which is how `try_to_string` calls it.
template <typename T>
struct has_to_string<T, std::void_t<decltype(std::to_string(std::declval<T>()))>> : std::true_type {
};

template <typename T>
inline constexpr bool has_to_string_v = has_to_string<T>::value;
concept has_to_string = requires(const T &value) { std::to_string(value); };

// Convert value to string if possible, otherwise return a placeholder.
template <typename T>
inline std::string try_to_string([[maybe_unused]] const T &value) {
if constexpr (has_to_string_v<T>) {
if constexpr (has_to_string<T>) {
return std::to_string(value);
}
return "<?>";
Expand All @@ -85,16 +81,8 @@ inline std::string try_to_string([[maybe_unused]] const T &value) {
#define VA_FUNC2_(__0, __1, NAME, ...) NAME
#define VA_FUNC3_(__0, __1, __2, NAME, ...) NAME

#if !defined(__GNUC__)
# define __PRETTY_FUNCTION__ std::nullopt // NOLINT[bugprone-reserved-identifier]
#endif

#define INTERNAL_ERROR0_() INTERNAL_ERROR1_("Unreachable code.")
#define INTERNAL_ERROR1_(message) \
throw optree::InternalError((message), \
__FILE_RELPATH_FROM_PROJECT_ROOT__, \
__LINE__, \
__PRETTY_FUNCTION__)
#define INTERNAL_ERROR1_(message) throw optree::InternalError(message)
#define INTERNAL_ERROR(...) \
VA_FUNC2_(__0 __VA_OPT__(, ) __VA_ARGS__, INTERNAL_ERROR1_, INTERNAL_ERROR0_)(__VA_ARGS__)

Expand Down
67 changes: 63 additions & 4 deletions include/optree/hashing.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ limitations under the License.

#pragma once

#include <cstddef> // std::size_t
#include <functional> // std::hash, std::{not_,}equal_to
#include <string> // std::string
#include <utility> // std::pair
#include <cstddef> // std::size_t
#include <functional> // std::hash, std::{not_,}equal_to
#include <string> // std::string
#include <string_view> // std::string_view
#include <utility> // std::pair

#include <Python.h>

Expand Down Expand Up @@ -123,3 +124,61 @@ struct std::hash<std::pair<T, U>> {
}
};
// NOLINTEND[bugprone-std-namespace-modification]

namespace optree {

// Transparent hashers and comparators for the registry's pair keys. Their `operator()` MUST be
// templates: `is_transparent` only tells the container it may forward a foreign key type, and a
// non-template call operator then converts it back to the exact `key_type` — the very temporary
// heterogeneous lookup exists to avoid. The marker was inert for unordered containers before
// P0919R3, so this began costing a namespace-string copy per `Lookup` only at C++20.
// `std::hash<std::string_view>` is guaranteed to agree with `std::hash<std::string>`, so probing
// with a view finds entries inserted with a string.

// Key of `PyTreeTypeRegistry::m_named_registrations`: (namespace, type).
struct NamespacedTypeHash {
using is_transparent = void;
template <class S>
inline Py_ALWAYS_INLINE std::size_t operator()(
const std::pair<S, py::handle> &key) const noexcept {
std::size_t seed = 0;
HashCombine(seed, std::string_view{key.first});
HashCombine(seed, key.second);
return seed;
}
};
struct NamespacedTypeEqual {
using is_transparent = void;
template <class S1, class S2>
inline Py_ALWAYS_INLINE bool operator()(const std::pair<S1, py::handle> &lhs,
const std::pair<S2, py::handle> &rhs) const noexcept {
// Compare the type first: it is a pointer identity test, and it discriminates far more
// often than the namespace does.
return lhs.second.is(rhs.second) &&
std::string_view{lhs.first} == std::string_view{rhs.first};
}
};

// Key of `PyTreeTypeRegistry::sm_dict_insertion_ordered_namespaces`: (interpreter, namespace).
struct InterpreterNamespaceHash {
using is_transparent = void;
template <class S>
inline Py_ALWAYS_INLINE std::size_t operator()(
const std::pair<interpid_t, S> &key) const noexcept {
std::size_t seed = 0;
HashCombine(seed, key.first);
HashCombine(seed, std::string_view{key.second});
return seed;
}
};
struct InterpreterNamespaceEqual {
using is_transparent = void;
template <class S1, class S2>
inline Py_ALWAYS_INLINE bool operator()(const std::pair<interpid_t, S1> &lhs,
const std::pair<interpid_t, S2> &rhs) const noexcept {
return lhs.first == rhs.first &&
std::string_view{lhs.second} == std::string_view{rhs.second};
}
};

} // namespace optree
Loading
Loading