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
21 changes: 14 additions & 7 deletions include/exec/timed_scheduler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,17 +213,22 @@ namespace experimental::execution
operator()(_Scheduler &&__sched, const duration_of_t<_Scheduler> &__duration) const noexcept
{
using __sched_t = __decay_t<_Scheduler>;
// The let_value sender below invokes the native CPO with the captured
// scheduler as a `const __sched_t&`. Probe the native sender type the
// same way, so instead-of-inventory such as the completion-scheduler
// claim is computed from the overload the deferred call resolves to.
using __native_sender_t =
__call_result_t<__schedule_at_base_t, _Scheduler, time_point_of_t<_Scheduler> const &>;
__call_result_t<__schedule_at_base_t, __sched_t const &, time_point_of_t<_Scheduler>>;

return __sexpr{
__timed_scheduler_fallback::__tag(),
__timed_scheduler_fallback::__data<__sched_t, __native_sender_t>{__sched},
let_value(just(),
[__sched, __duration]() noexcept(
__nothrow_callable<schedule_at_t, _Scheduler &, time_point_of_t<_Scheduler>>
&&__nothrow_callable<now_t, _Scheduler &>)
{ return schedule_at(__sched, now(__sched) + __duration); })};
let_value(
just(),
[__sched, __duration]() noexcept(
__nothrow_callable<schedule_at_t, __sched_t const &, time_point_of_t<_Scheduler>>
&&__nothrow_callable<now_t, __sched_t const &>)
{ return schedule_at(__sched, now(__sched) + __duration); })};
}
};
} // namespace __schedule_after
Expand Down Expand Up @@ -279,8 +284,10 @@ namespace experimental::execution
noexcept(noexcept(schedule_after(__sched, __time_point - now(__sched))))
{
using __sched_t = __decay_t<_Scheduler>;
// Same as above: probe through the const-qualified lvalue the
// deferred lambda captures, not the call site's value category.
using __native_sender_t =
__call_result_t<__schedule_after_base_t, _Scheduler, duration_of_t<_Scheduler> const &>;
__call_result_t<__schedule_after_base_t, __sched_t const &, duration_of_t<_Scheduler>>;

return __sexpr{__timed_scheduler_fallback::__tag(),
__timed_scheduler_fallback::__data<__sched_t, __native_sender_t>{__sched},
Expand Down
1 change: 1 addition & 0 deletions test/exec/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ set(exec_test_sources
test_any_sender.cpp
test_task.cpp
test_timed_thread_scheduler.cpp
test_timed_scheduler_fallback.cpp
test_thread_pool_base.cpp
test_variant_sender.cpp
test_type_async_scope.cpp
Expand Down
179 changes: 179 additions & 0 deletions test/exec/test_timed_scheduler_fallback.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright (c) 2026 NVIDIA Corporation
*
* Licensed under the Apache License Version 2.0 with LLVM Exceptions
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://llvm.org/LICENSE.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Regression tests for #2165: the `schedule_after`/`schedule_at` fallbacks must
// probe the native sender through the same const-qualification the deferred
// invocation uses. The fallback's let_value lambda captures the scheduler by
// copy and is not mutable, so it invokes the native CPO with a `const S&`. The
// probe in the fallback deduced the native sender type through a non-const
// lvalue instead, so a scheduler whose const-lvalue and non-const-lvalue
// overloads return differing senders advertises a completion scheduler that
// its deferred invocation never resolves to.

#include <exec/timed_scheduler.hpp>

#include <test_common/catch2.hpp>

#include <chrono>
#include <utility>

namespace
{
// A scheduler with no `schedule_after` member and lvalue-ref-qualified
// `schedule_at` members. `schedule_after` on it goes through the fallback
// (into `schedule_at`), and the two lvalue ref-qualified `schedule_at`
// overloads return senders that differ in what their attributes advertise.
struct ref_qualified_scheduler
{
using scheduler_concept = STDEXEC::scheduler_tag;
using time_point = std::chrono::steady_clock::time_point;
using duration = time_point::duration;

struct sender;

struct const_lvalue_sender;

constexpr auto operator==(ref_qualified_scheduler const &) const noexcept -> bool = default;

[[nodiscard]]
auto now() const noexcept -> time_point
{
return {};
}

[[nodiscard]]
auto schedule() const noexcept -> sender;

// The overload the probe reaches: a non-const lvalue. Its sender does not
// advertise a completion scheduler.
[[nodiscard]]
auto schedule_at(time_point) & noexcept -> sender;

// The overload the fallback's deferred invocation reaches: the lambda
// holds the scheduler as a `const S&`. Its sender advertises this
// scheduler as the completion scheduler.
[[nodiscard]]
auto schedule_at(time_point) const & noexcept -> const_lvalue_sender;
};

// The native sender resolved through the non-const-lvalue overload. Its
// attributes do not advertise a completion scheduler.
struct ref_qualified_scheduler::sender
{
using sender_concept = STDEXEC::sender_tag;
using completion_signatures = STDEXEC::completion_signatures<STDEXEC::set_value_t()>;

template <class Receiver>
struct operation
{
void start() & noexcept
{
STDEXEC::set_value(static_cast<Receiver &&>(receiver_));
}

Receiver receiver_;
};

template <class Receiver>
auto connect(Receiver receiver) const -> operation<Receiver>
{
return {static_cast<Receiver &&>(receiver)};
}
};

// The native sender resolved through the const-lvalue overload. Its
// attributes advertise this scheduler as the completion scheduler.
struct ref_qualified_scheduler::const_lvalue_sender
{
using sender_concept = STDEXEC::sender_tag;
using completion_signatures = STDEXEC::completion_signatures<STDEXEC::set_value_t()>;

struct attrs
{
[[nodiscard]]
auto query(STDEXEC::get_completion_scheduler_t<STDEXEC::set_value_t>) const noexcept
-> ref_qualified_scheduler
{
return {};
}
};

template <class Receiver>
struct operation
{
void start() & noexcept
{
STDEXEC::set_value(static_cast<Receiver &&>(receiver_));
}

Receiver receiver_;
};

template <class Receiver>
auto connect(Receiver receiver) const -> operation<Receiver>
{
return {static_cast<Receiver &&>(receiver)};
}

[[nodiscard]]
constexpr auto get_env() const noexcept -> attrs
{
return {};
}
};

auto ref_qualified_scheduler::schedule() const noexcept -> sender
{
return {};
}

auto ref_qualified_scheduler::schedule_at(time_point) & noexcept -> sender
{
return {};
}

auto ref_qualified_scheduler::schedule_at(time_point) const & noexcept -> const_lvalue_sender
{
return {};
}

static_assert(exec::timed_scheduler<ref_qualified_scheduler>);
} // namespace

TEST_CASE("timed scheduler fallback probes through const-lvalue scheduler",
"[timed_scheduler][completion_scheduler]")
{
using completion_scheduler_query = STDEXEC::get_completion_scheduler_t<STDEXEC::set_value_t>;

// Passing the scheduler as a prvalue: the fallback must probe the native
// sender the same way its deferred lambda will invoke it, i.e. through a
// `const S&`.
auto after = exec::schedule_after(ref_qualified_scheduler{}, std::chrono::milliseconds(10));
using after_t = decltype(after);
static_assert(STDEXEC::__callable<completion_scheduler_query, STDEXEC::env_of_t<after_t>>);

// Same behavior regardless of the value category at the call site.
ref_qualified_scheduler sched;
auto after_from_lvalue = exec::schedule_after(sched, std::chrono::milliseconds(10));
using after_from_lvalue_t = decltype(after_from_lvalue);
static_assert(
STDEXEC::__callable<completion_scheduler_query, STDEXEC::env_of_t<after_from_lvalue_t>>);

CHECK(STDEXEC::get_completion_scheduler<STDEXEC::set_value_t>(STDEXEC::get_env(after))
== ref_qualified_scheduler{});
CHECK(STDEXEC::get_completion_scheduler<STDEXEC::set_value_t>(STDEXEC::get_env(after_from_lvalue))
== sched);
}
Loading