diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e97953e..f66711b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,27 +57,31 @@ jobs: build-macos: runs-on: macos-15 - timeout-minutes: 20 + timeout-minutes: 25 steps: - name: Checkout uses: actions/checkout@v4 - - name: Install Xlings - env: - XLINGS_NON_INTERACTIVE: 1 - run: | - curl -fsSL https://raw.githubusercontent.com/d2learn/xlings/refs/heads/main/tools/other/quick_install.sh | bash -s -- $XLINGS_VERSION - echo "PATH=$HOME/.xlings/subos/current/bin:$PATH" >> "$GITHUB_ENV" + - name: Setup xmake + uses: xmake-io/github-action-setup-xmake@v1 + with: + xmake-version: latest + package-cache: true + + - name: Install LLVM 20 + run: brew install llvm@20 - - name: Install Project Dependencies via Xlings + - name: Verify toolchain run: | - xlings install + xmake --version + /opt/homebrew/opt/llvm@20/bin/clang++ --version clang --version - name: Build with xmake run: | - xmake f -m release --toolchain=llvm -vv -y - xmake -j$(nproc) + export PATH=/opt/homebrew/opt/llvm@20/bin:$PATH + xmake f -p macosx -m release --toolchain=llvm --sdk=/opt/homebrew/opt/llvm@20 -y -vv + xmake -y -vv -j$(sysctl -n hw.ncpu) build-windows: runs-on: windows-latest diff --git a/.gitignore b/.gitignore index 00504a5..9b29fcb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .xmake build/ build-*/ +out/ # xlings .xlings @@ -50,4 +51,4 @@ Makefile # IDE /.idea -/.cache \ No newline at end of file +/.cache diff --git a/examples/ex07_custom_policy.cpp b/examples/ex07_custom_policy.cpp index ef36c13..f0bd159 100644 --- a/examples/ex07_custom_policy.cpp +++ b/examples/ex07_custom_policy.cpp @@ -87,6 +87,34 @@ struct mcpplibs::primitives::policy::concurrency::handler< } }; +template +struct mcpplibs::primitives::policy::concurrency::handler< + demo::custom_concurrency, void, CommonRep, ErrorPayload> { + static constexpr bool enabled = true; + using injection_type = mcpplibs::primitives::policy::concurrency::injection; + using result_type = std::expected; + + static constexpr auto load(CommonRep const &value) noexcept -> CommonRep { + return value; + } + + static constexpr auto store(CommonRep &value, CommonRep desired) noexcept + -> void { + value = desired; + } + + static constexpr auto compare_exchange(CommonRep &value, CommonRep &expected, + CommonRep desired) noexcept -> bool { + if (value != expected) { + expected = value; + return false; + } + + value = desired; + return true; + } +}; + // Point 7 / Step 3C: Implement custom value handler. // Complex point: finalize() post-processes decision and adjusts output. template ::to_rep(lhs.value()); + underlying::traits::to_rep(lhs.load()); auto const rhs_rep_raw = - underlying::traits::to_rep(rhs.value()); + underlying::traits::to_rep(rhs.load()); if (!underlying::traits::is_valid_rep( lhs_rep_raw) || diff --git a/src/policy/impl.cppm b/src/policy/impl.cppm index 0ec7c17..cb173da 100644 --- a/src/policy/impl.cppm +++ b/src/policy/impl.cppm @@ -158,6 +158,20 @@ template constexpr void assert_atomic_ref_compatible() { "satisfy std::atomic_ref::required_alignment"); } +template +concept has_underlying_rep_not_equal = + underlying_type && + requires(T const &lhs, T const &rhs) { + { + underlying::traits>::to_rep(lhs) != + underlying::traits>::to_rep(rhs) + } -> std::convertible_to; + }; + +template +inline constexpr bool none_compare_exchange_available_v = + has_underlying_rep_not_equal; + template inline constexpr bool is_arithmetic_operation_v = operations::op_has_capability_v; @@ -239,6 +253,35 @@ struct concurrency::handler { } }; +template +struct concurrency::handler { + static constexpr bool enabled = true; + using injection_type = concurrency::injection; + using result_type = std::expected; + + static constexpr auto load(CommonRep const &value) noexcept -> CommonRep { + return value; + } + + static constexpr auto store(CommonRep &value, CommonRep desired) noexcept + -> void { + value = desired; + } + + static constexpr auto compare_exchange(CommonRep &value, CommonRep &expected, + CommonRep desired) noexcept -> bool + requires(details::none_compare_exchange_available_v) { + using traits_type = underlying::traits>; + if (traits_type::to_rep(value) != traits_type::to_rep(expected)) { + expected = value; + return false; + } + + value = desired; + return true; + } +}; + template struct concurrency::handler { }; template -using resolve_concurrency_policy_t = - typename resolve_concurrency_policy::type; +using resolve_concurrency_policy_t = resolve_concurrency_policy::type; } // namespace details @@ -58,50 +57,102 @@ public: "Multiple concurrency policies are not allowed"); constexpr explicit primitive(value_type v) noexcept : value_(v) {} + + constexpr primitive(primitive const &other) noexcept { + if consteval { + value_ = other.value_; + } else { + value_ = other.load(); + } + } + + constexpr auto operator=(primitive const &other) noexcept -> primitive & { + if (this == &other) { + return *this; + } + + if consteval { + value_ = other.value_; + } else { + store(other.load()); + } + return *this; + } + + constexpr primitive(primitive &&other) noexcept { + if consteval { + value_ = other.value_; + } else { + value_ = other.load(); + } + } + + constexpr auto operator=(primitive &&other) noexcept -> primitive & { + if (this == &other) { + return *this; + } + + if consteval { + value_ = other.value_; + } else { + store(other.load()); + } + return *this; + } + constexpr value_type &value() noexcept { return value_; } + [[nodiscard]] constexpr value_type const &value() const noexcept { return value_; } + constexpr explicit operator value_type() const noexcept { return value_; } - [[nodiscard]] auto load() const noexcept -> value_type { - using access_handler_t = - policy::concurrency::handler; - static_assert( - policy::concurrency::handler_access_available, - "Selected concurrency policy does not provide primitive " - "load/store/CAS support"); + [[nodiscard]] constexpr auto load() const noexcept -> value_type { + if consteval { + return value_; + } + require_access_handler_(); return access_handler_t::load(value_); } - auto store(value_type desired) noexcept -> void { - using access_handler_t = - policy::concurrency::handler; - static_assert( - policy::concurrency::handler_access_available, - "Selected concurrency policy does not provide primitive " - "load/store/CAS support"); - access_handler_t::store(value_, desired); + constexpr auto store(value_type desired) noexcept -> void { + if consteval { + value_ = desired; + } else { + require_access_handler_(); + access_handler_t::store(value_, desired); + } } - auto compare_exchange(value_type &expected, value_type desired) noexcept - -> bool { - using access_handler_t = - policy::concurrency::handler; + constexpr auto compare_exchange(value_type &expected, + value_type desired) noexcept -> bool { + if consteval { + if (value_ != expected) { + expected = value_; + return false; + } + + value_ = desired; + return true; + } + require_access_handler_(); + return access_handler_t::compare_exchange(value_, expected, desired); + } + +private: + using access_handler_t = + policy::concurrency::handler; + + static constexpr auto require_access_handler_() noexcept -> void { static_assert( policy::concurrency::handler_access_available, "Selected concurrency policy does not provide primitive " "load/store/CAS support"); - return access_handler_t::compare_exchange(value_, expected, desired); } -private: value_type value_; }; diff --git a/tests/basic/test_operations.cpp b/tests/basic/test_operations.cpp index ceb1fcc..b894775 100644 --- a/tests/basic/test_operations.cpp +++ b/tests/basic/test_operations.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include import mcpplibs.primitives; @@ -196,6 +197,122 @@ TEST(OperationsTest, PrimitiveFencedCasSupportsConcurrentIncrements) { EXPECT_EQ(counter.load(), kThreadCount * kIterationsPerThread); } +TEST(OperationsTest, + BinaryOperationsWithLoadStoreRemainStableUnderHighConcurrency) { + using value_t = + primitive; + + constexpr int kWriterThreads = 6; + constexpr int kReaderThreads = 8; + constexpr int kIterationsPerThread = 25000; + constexpr int kMaxOperand = 100000; + + auto lhs = value_t{0}; + auto rhs = value_t{0}; + auto sink = value_t{0}; + + std::atomic add_error_count{0}; + std::atomic sub_error_count{0}; + std::atomic range_violation_count{0}; + std::atomic start{false}; + + std::vector workers; + workers.reserve(kWriterThreads + kReaderThreads); + + for (int writer = 0; writer < kWriterThreads; ++writer) { + workers.emplace_back([&, writer]() { + while (!start.load(std::memory_order_acquire)) { + } + + for (int n = 0; n < kIterationsPerThread; ++n) { + auto const v1 = (writer + n) % (kMaxOperand + 1); + auto const v2 = (writer * 3 + n * 7) % (kMaxOperand + 1); + lhs.store(v1); + rhs.store(v2); + } + }); + } + + for (int reader = 0; reader < kReaderThreads; ++reader) { + workers.emplace_back([&, reader]() { + while (!start.load(std::memory_order_acquire)) { + } + + for (int n = 0; n < kIterationsPerThread; ++n) { + if (((reader + n) & 1) == 0) { + auto const out = operations::add(lhs, rhs); + if (!out.has_value()) { + add_error_count.fetch_add(1, std::memory_order_relaxed); + continue; + } + + auto const v = out->load(); + if (v < 0 || v > (kMaxOperand * 2)) { + range_violation_count.fetch_add(1, std::memory_order_relaxed); + } + sink.store(v); + auto const snapshot = sink.load(); + if (snapshot < -kMaxOperand || snapshot > (kMaxOperand * 2)) { + range_violation_count.fetch_add(1, std::memory_order_relaxed); + } + continue; + } + + auto const out = operations::sub(lhs, rhs); + if (!out.has_value()) { + sub_error_count.fetch_add(1, std::memory_order_relaxed); + continue; + } + + auto const v = out->load(); + if (v < -kMaxOperand || v > kMaxOperand) { + range_violation_count.fetch_add(1, std::memory_order_relaxed); + } + sink.store(v); + auto const snapshot = sink.load(); + if (snapshot < -kMaxOperand || snapshot > (kMaxOperand * 2)) { + range_violation_count.fetch_add(1, std::memory_order_relaxed); + } + } + }); + } + + start.store(true, std::memory_order_release); + + for (auto &worker : workers) { + worker.join(); + } + + EXPECT_EQ(add_error_count.load(std::memory_order_relaxed), 0); + EXPECT_EQ(sub_error_count.load(std::memory_order_relaxed), 0); + EXPECT_EQ(range_violation_count.load(std::memory_order_relaxed), 0); +} + +TEST(OperationsTest, PrimitiveSupportsCopyAndMoveSpecialMembers) { + using value_t = primitive; + + static_assert(std::is_copy_constructible_v); + static_assert(std::is_copy_assignable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_move_assignable_v); + + auto original = value_t{42}; + auto copy_constructed = value_t{original}; + EXPECT_EQ(copy_constructed.load(), 42); + + auto copy_assigned = value_t{0}; + copy_assigned = original; + EXPECT_EQ(copy_assigned.load(), 42); + + auto move_constructed = value_t{std::move(copy_assigned)}; + EXPECT_EQ(move_constructed.load(), 42); + + auto move_assigned = value_t{0}; + move_assigned = std::move(move_constructed); + EXPECT_EQ(move_assigned.load(), 42); +} + TEST(OperationsTest, StrictTypeRejectsMixedTypesAtCompileTime) { using lhs_t = primitive; diff --git a/tests/basic/test_policies.cpp b/tests/basic/test_policies.cpp index 7872382..36b0e66 100644 --- a/tests/basic/test_policies.cpp +++ b/tests/basic/test_policies.cpp @@ -122,6 +122,9 @@ TEST(PolicyConcurrencyTest, FencedVariantsUseExpectedMemoryOrders) { } TEST(PolicyConcurrencyTest, PrimitiveAccessHandlerProtocolByPolicy) { + EXPECT_TRUE( + (policy::concurrency::handler_access_available)); EXPECT_TRUE(( policy::concurrency::handler_access_available)); @@ -131,9 +134,6 @@ TEST(PolicyConcurrencyTest, PrimitiveAccessHandlerProtocolByPolicy) { policy::concurrency::fenced_acq_rel, int>)); EXPECT_TRUE((policy::concurrency::handler_access_available< policy::concurrency::fenced_seq_cst, int>)); - EXPECT_FALSE( - (policy::concurrency::handler_access_available)); } TEST(PolicyConcurrencyTest, PrimitiveAccessRejectsNonTriviallyCopyableRep) { @@ -149,6 +149,9 @@ TEST(PolicyConcurrencyTest, PrimitiveAccessRejectsNonTriviallyCopyableRep) { } TEST(PolicyConcurrencyTest, PrimitiveAccessRespectsAtomicRefAlignmentGate) { + EXPECT_FALSE((policy::concurrency::handler_access_available< + policy::concurrency::none, LowAlignmentRep>)); + constexpr bool requires_stronger_alignment = std::atomic_ref::required_alignment > alignof(LowAlignmentRep);