diff --git a/libs/core/async_mpi/include/hpx/async_mpi/transform_mpi.hpp b/libs/core/async_mpi/include/hpx/async_mpi/transform_mpi.hpp index 1fdf6c31c17a..e98e94c4ae1f 100644 --- a/libs/core/async_mpi/include/hpx/async_mpi/transform_mpi.hpp +++ b/libs/core/async_mpi/include/hpx/async_mpi/transform_mpi.hpp @@ -43,10 +43,38 @@ namespace hpx::mpi::experimental { } } + // Register a completion callback for an MPI request whose backing + // function returns void. The receiver `r` and the upstream + // arguments `ts...` are captured into a lambda whose lifetime is + // owned by the polling driver until `MPI_Test*` reports completion. + // + // Contract: + // * `Ts...` must be passed by the caller in the same shape they + // were forwarded into the user-supplied MPI function. The + // lambda's pack-capture moves them once into `keep_alive`; if + // the caller has already moved them, we'd capture moved-from + // state. Callers in this file (`transform_mpi_receiver`) pass + // them unforwarded into `f` and then forward into this helper + // to satisfy the single-move invariant. + // * The lambda fires on the polling thread (inside `poll()` in + // `mpi_future.cpp`), not on the receiver's preferred completion + // scheduler. Receivers must be safe to invoke on that thread. + // + // The `static_assert` below makes the receiver-shape requirement + // explicit at the helper boundary so misuse fails here with a clear + // message rather than deep inside the captured lambda's body. template void set_value_request_callback_void( MPI_Request request, R&& r, Ts&&... ts) { + static_assert( + std::is_invocable_v&&>, + "set_value_request_callback_void: the receiver R must be " + "invocable as `set_value(receiver)` (no value args) for the " + "void MPI-return path. Did you mean to use the " + "_non_void variant?"); + detail::add_request_callback( [r = HPX_FORWARD(R, r), ... keep_alive = HPX_FORWARD(Ts, ts)]( int status) mutable { @@ -56,10 +84,26 @@ namespace hpx::mpi::experimental { request); } + // Register a completion callback for an MPI request whose backing + // function returns a value `res` to forward to the receiver. + // Same contract as the void overload; additionally, `res` is + // captured separately so it can be forwarded to the receiver's + // `set_value` on completion. template void set_value_request_callback_non_void( MPI_Request request, R&& r, InvokeResult&& res, Ts&&... ts) { + static_assert(!std::is_void_v>, + "set_value_request_callback_non_void: InvokeResult must be " + "non-void; for void-returning MPI functions use the _void " + "variant."); + static_assert( + std::is_invocable_v&&, std::decay_t&&>, + "set_value_request_callback_non_void: the receiver R must " + "be invocable as `set_value(receiver, result)` for the " + "non-void MPI-return path."); + detail::add_request_callback( [r = HPX_FORWARD(R, r), res = HPX_FORWARD(InvokeResult, res), ... keep_alive = HPX_FORWARD(Ts, ts)](int status) mutable { diff --git a/libs/core/async_mpi/tests/unit/CMakeLists.txt b/libs/core/async_mpi/tests/unit/CMakeLists.txt index e5587bea6f4a..bdd66ea72d4e 100644 --- a/libs/core/async_mpi/tests/unit/CMakeLists.txt +++ b/libs/core/async_mpi/tests/unit/CMakeLists.txt @@ -31,3 +31,37 @@ foreach(test ${tests}) add_hpx_unit_test("modules.async_mpi" ${test} ${${test}_PARAMETERS}) endforeach() + +if(HPX_WITH_COMPILE_ONLY_TESTS) + # Negative compile-only tests that exercise the static_asserts at the top of + # detail::set_value_request_callback_{void,non_void}. Each test passes a + # deliberately-wrong receiver shape and is expected to fail compilation with + # the assert's diagnostic message. + set(compile_tests) + + if(HPX_WITH_FAIL_COMPILE_TESTS) + set(fail_compile_tests fail_compile_set_value_request_callback_void + fail_compile_set_value_request_callback_non_void + ) + foreach(fail_compile_test ${fail_compile_tests}) + set(${fail_compile_test}_FLAGS FAILURE_EXPECTED) + endforeach() + + set(compile_tests ${compile_tests} ${fail_compile_tests}) + endif() + + foreach(compile_test ${compile_tests}) + set(sources ${compile_test}.cpp) + + source_group("Source Files" FILES ${sources}) + + add_hpx_unit_compile_test( + "modules.async_mpi" ${compile_test} + SOURCES ${sources} ${${compile_test}_FLAGS} + DEPENDENCIES Mpi::mpi + FOLDER "Tests/Unit/Modules/Core/AsyncMPI/CompileOnly" + ) + + endforeach() + +endif() diff --git a/libs/core/async_mpi/tests/unit/fail_compile_set_value_request_callback_non_void.cpp b/libs/core/async_mpi/tests/unit/fail_compile_set_value_request_callback_non_void.cpp new file mode 100644 index 000000000000..4484d3be8b36 --- /dev/null +++ b/libs/core/async_mpi/tests/unit/fail_compile_set_value_request_callback_non_void.cpp @@ -0,0 +1,44 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// This must fail compiling. +// +// Exercises the receiver-shape static_assert at the top of +// `detail::set_value_request_callback_non_void` which requires that the +// receiver be invocable as `set_value(receiver, result)` -- this is the +// non-void MPI-return path. Below we deliberately pass a receiver that +// takes no value argument, so the receiver-shape check should fire at +// compile time with the assert's diagnostic message. + +#include +#include + +#include +#include + +namespace ex = hpx::execution::experimental; + +// A receiver shape that takes no value -- incompatible with the +// non-void-return callback path which needs set_value(rcv, res). +struct receiver_taking_no_value +{ + using receiver_concept = ex::receiver_t; + void set_value() && noexcept {} + void set_error(std::exception_ptr) && noexcept {} + void set_stopped() && noexcept {} +}; + +int main() +{ + MPI_Request req{}; + receiver_taking_no_value r; + int res = 42; + // Instantiating this template should fire the receiver-shape + // static_assert in set_value_request_callback_non_void. + hpx::mpi::experimental::detail::set_value_request_callback_non_void( + req, std::move(r), std::move(res)); + return 0; +} diff --git a/libs/core/async_mpi/tests/unit/fail_compile_set_value_request_callback_void.cpp b/libs/core/async_mpi/tests/unit/fail_compile_set_value_request_callback_void.cpp new file mode 100644 index 000000000000..22e00cf09792 --- /dev/null +++ b/libs/core/async_mpi/tests/unit/fail_compile_set_value_request_callback_void.cpp @@ -0,0 +1,43 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +// This must fail compiling. +// +// Exercises the static_assert at the top of +// `detail::set_value_request_callback_void` which requires that the +// receiver be invocable as `set_value(receiver)` (no value args) -- this +// is the void MPI-return path. Below we deliberately pass a receiver that +// requires an int value argument, so the receiver-shape check should +// fire at compile time with the assert's diagnostic message. + +#include +#include + +#include +#include + +namespace ex = hpx::execution::experimental; + +// A receiver shape that requires an int value -- incompatible with the +// void-return callback path. +struct receiver_requiring_int_value +{ + using receiver_concept = ex::receiver_t; + void set_value(int) && noexcept {} + void set_error(std::exception_ptr) && noexcept {} + void set_stopped() && noexcept {} +}; + +int main() +{ + MPI_Request req{}; + receiver_requiring_int_value r; + // Instantiating this template should fire the static_assert in + // set_value_request_callback_void. + hpx::mpi::experimental::detail::set_value_request_callback_void( + req, std::move(r)); + return 0; +}