Skip to content
Merged
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
15 changes: 11 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,14 @@ ifeq (${hostSystemName},Darwin)

ifeq ($(origin CXX),default)
$(info CXX is using the built-in default: $(CXX))
export CXX=g++-15
CXXLIB=libstdc++
export CXXFLAGS=-stdlib=$(CXXLIB)
ifeq ($(shell uname -m),arm64)
export CXX=clang++
CXXLIB=libc++
else
export CXX=g++-15
CXXLIB=libstdc++
endif
export CXX_FLAGS=-stdlib=$(CXXLIB)
endif
Comment on lines +62 to 70
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

export CXX_FLAGS=-stdlib=$(CXXLIB) overwrites the existing CXX_FLAGS (e.g., drops -g) and is later overwritten by the SANITIZER-specific CXX_FLAGS = ... assignments, so the stdlib selection can be lost. Also, -stdlib=... is a clang-specific flag; if the else branch selects g++-15, passing -stdlib=libstdc++ via CMAKE_CXX_FLAGS will likely fail. Consider keeping stdlib selection in a separate variable and appending it to CXX_FLAGS, and only adding -stdlib=... when using clang/libc++.

Copilot uses AI. Check for mistakes.
export GCOV="gcov"
else ifeq (${hostSystemName},Linux)
Expand Down Expand Up @@ -120,6 +125,7 @@ doc:
# NOTE: cmake configure to only test without modules! CK
# ==========================================================
build build-interface:
@echo CXX=$(CXX) CXX_FLAGS=$(CXX_FLAGS)
cmake -G Ninja -S $(SOURCEDIR) -B $(BUILD) $(TOOLCHAIN) $(SYSROOT) \
-D CMAKE_EXPORT_COMPILE_COMMANDS=ON \
-D CMAKE_SKIP_INSTALL_RULES=ON \
Expand All @@ -130,7 +136,8 @@ build build-interface:
-D BEMAN_USE_STD_MODULE=OFF \
-D CMAKE_BUILD_TYPE=Release \
-D CMAKE_SKIP_TEST_ALL_DEPENDENCY=OFF \
-D CMAKE_CXX_COMPILER=$(CXX) --log-level=VERBOSE --fresh
-D CMAKE_CXX_COMPILER=$(CXX) --log-level=VERBOSE --fresh \
-D CMAKE_CXX_FLAGS="$(CXX_FLAGS) $(SAN_FLAGS)"
# XXX -D CMAKE_CXX_FLAGS="$(CXX_FLAGS) $(SAN_FLAGS)"
cmake --build $(BUILD) --target all_verify_interface_header_sets
cmake --build $(BUILD) --target all
Expand Down
17 changes: 12 additions & 5 deletions include/beman/execution/detail/sender_awaitable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import std;
#include <concepts>
#include <coroutine>
#include <exception>
#include <thread>
#include <tuple>
#include <type_traits>
#include <utility>
Expand Down Expand Up @@ -53,13 +54,15 @@ class sender_awaitable {
::beman::execution::detail::single_sender_value_type<Sndr, ::beman::execution::env_of_t<Promise>>;
using result_type = ::std::conditional_t<::std::is_void_v<value_type>, unit, value_type>;
using variant_type = ::std::variant<::std::monostate, result_type, ::std::exception_ptr>;
using data_type = ::std::tuple<variant_type, ::std::atomic<bool>, ::std::coroutine_handle<Promise>>;
using data_type = ::std::tuple<variant_type, ::std::atomic<::std::thread::id>, ::std::coroutine_handle<Promise>>;

Comment on lines 56 to 58
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

std::atomic<std::thread::id> is not guaranteed to be well-formed/portable: std::atomic<T> requires T to be trivially copyable, and std::thread::id is not specified to be. This can fail to compile on some standard libraries. Consider keeping the original std::atomic<bool> state (or using an integral state enum) and store any thread-id info separately if needed.

Copilot uses AI. Check for mistakes.
struct awaitable_receiver {
using receiver_concept = ::beman::execution::receiver_t;

void resume() {
if (::std::get<1>(*result_ptr_).exchange(true, std::memory_order_acq_rel)) {
std::thread::id id(::std::this_thread::get_id());
if (not ::std::get<1>(*result_ptr_)
.compare_exchange_strong(id, ::std::thread::id{}, std::memory_order_acq_rel)) {
::std::get<2>(*result_ptr_).resume();
}
Comment on lines 62 to 67
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

This compare_exchange_strong-based gate can resume the coroutine before it has actually suspended: if start(state) causes completion to happen on a different thread before await_suspend executes its CAS, this CAS will fail (thread ids differ) and resume() will call coroutine_handle::resume() while the coroutine is still running, which is undefined behavior. The original atomic<bool> handshake avoided this by only resuming after await_suspend published the suspended state; this should use a proper suspend/completion state machine (e.g., atomic enum/int) rather than comparing thread ids.

Copilot uses AI. Check for mistakes.
}
Expand All @@ -82,7 +85,9 @@ class sender_awaitable {
}

void set_stopped() && noexcept {
if (::std::get<1>(*result_ptr_).exchange(true, ::std::memory_order_acq_rel)) {
std::thread::id id(::std::this_thread::get_id());
if (not ::std::get<1>(*result_ptr_)
.compare_exchange_strong(id, ::std::thread::id{}, ::std::memory_order_acq_rel)) {
static_cast<::std::coroutine_handle<>>(::std::get<2>(*result_ptr_).promise().unhandled_stopped())
.resume();
}
Expand All @@ -102,14 +107,16 @@ class sender_awaitable {

public:
sender_awaitable(Sndr&& sndr, Promise& p)
: result{::std::monostate{}, false, ::std::coroutine_handle<Promise>::from_promise(p)},
: result{::std::monostate{}, ::std::this_thread::get_id(), ::std::coroutine_handle<Promise>::from_promise(p)},
state{::beman::execution::connect(::std::forward<Sndr>(sndr),
sender_awaitable::awaitable_receiver{::std::addressof(result)})} {}

static constexpr bool await_ready() noexcept { return false; }
::std::coroutine_handle<> await_suspend(::std::coroutine_handle<Promise> handle) noexcept {
::beman::execution::start(state);
if (::std::get<1>(this->result).exchange(true, std::memory_order_acq_rel)) {
::std::thread::id id(::std::this_thread::get_id());
if (not ::std::get<1>(this->result)
.compare_exchange_strong(id, ::std::thread::id{}, ::std::memory_order_acq_rel)) {
if (::std::holds_alternative<::std::monostate>(::std::get<0>(this->result))) {
return ::std::get<2>(this->result).promise().unhandled_stopped();
}
Expand Down