diff --git a/include/stdexec/__detail/__intrusive_queue.hpp b/include/stdexec/__detail/__intrusive_queue.hpp index 12f5ad95c..5d94856b8 100644 --- a/include/stdexec/__detail/__intrusive_queue.hpp +++ b/include/stdexec/__detail/__intrusive_queue.hpp @@ -249,6 +249,8 @@ namespace STDEXEC return iterator(__tail_, nullptr); } + // Moves the items in [first, last) from other into *this, inserting + // them before pos. other must not be *this. constexpr void splice(iterator pos, __intrusive_queue& other, iterator first, iterator last) noexcept { @@ -256,34 +258,43 @@ namespace STDEXEC { return; } + STDEXEC_ASSERT(&other != this); STDEXEC_ASSERT(first.__item_ != nullptr); STDEXEC_ASSERT(last.__predecessor_ != nullptr); - if (other.__head_ == first.__item_) + + _Item* const __range_head = first.__item_; + _Item* const __range_tail = last.__predecessor_; + + // Unlink [first, last) from other. + if (other.__head_ == __range_head) { other.__head_ = last.__item_; - if (other.__head_ == nullptr) - { - other.__tail_ = nullptr; - } } else { STDEXEC_ASSERT(first.__predecessor_ != nullptr); first.__predecessor_->*_Next = last.__item_; - last.__predecessor_->*_Next = pos.__item_; } - if (empty()) + if (last.__item_ == nullptr) + { + // The range ran to the end of other, so its new tail is the item + // before the range (or nullptr if other is now empty). + other.__tail_ = first.__predecessor_; + } + + // Link [first, last) into *this before pos. + __range_tail->*_Next = pos.__item_; + if (pos.__predecessor_ == nullptr) { - __head_ = first.__item_; - __tail_ = last.__predecessor_; + __head_ = __range_head; } else { - pos.__predecessor_->*_Next = first.__item_; - if (pos.__item_ == nullptr) - { - __tail_ = last.__predecessor_; - } + pos.__predecessor_->*_Next = __range_head; + } + if (pos.__item_ == nullptr) + { + __tail_ = __range_tail; } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e3cb13040..72b25d885 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -69,6 +69,7 @@ set(stdexec_test_sources stdexec/detail/test_completion_signatures.cpp stdexec/detail/test_demangle.cpp stdexec/detail/test_intrusive_mpsc_queue.cpp + stdexec/detail/test_intrusive_queue.cpp stdexec/detail/test_utility.cpp stdexec/queries/test_env.cpp stdexec/queries/test_forwarding_queries.cpp diff --git a/test/stdexec/detail/test_intrusive_queue.cpp b/test/stdexec/detail/test_intrusive_queue.cpp new file mode 100644 index 000000000..b365be6ac --- /dev/null +++ b/test/stdexec/detail/test_intrusive_queue.cpp @@ -0,0 +1,222 @@ +/* + * 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. + */ +#include + +#include + +#include + +#if STDEXEC_USE_MODULES() +import std; +#else +# include +# include +# include +# include +# include +# include +#endif + +namespace +{ + struct test_node + { + int value_{0}; + test_node* next_{nullptr}; + }; + + using test_queue = STDEXEC::__intrusive_queue<&test_node::next_>; + + // Walk the raw links to detect lists that still contain nodes from another queue. + auto links_of(test_queue const & queue) -> std::vector + { + std::vector result; + for (test_node* node = queue.front(); node != nullptr; node = node->next_) + { + result.push_back(node->value_); + if (result.size() > 64) + { + FAIL("cycle detected in intrusive queue"); + } + } + return result; + } + + auto items_of(test_queue const & queue) -> std::vector + { + std::vector result; + for (test_node* node: queue) + { + result.push_back(node->value_); + if (result.size() > 64) + { + FAIL("cycle detected while iterating intrusive queue"); + } + } + return result; + } + + void check_queue(test_queue const & queue, std::list const & expected) + { + std::vector const expected_vec(expected.begin(), expected.end()); + CHECK(links_of(queue) == expected_vec); + CHECK(items_of(queue) == expected_vec); + if (expected.empty()) + { + CHECK(queue.empty()); + CHECK(queue.front() == nullptr); + CHECK(queue.back() == nullptr); + } + else + { + REQUIRE_FALSE(queue.empty()); + REQUIRE(queue.front() != nullptr); + REQUIRE(queue.back() != nullptr); + CHECK(queue.front()->value_ == expected.front()); + CHECK(queue.back()->value_ == expected.back()); + CHECK(queue.back()->next_ == nullptr); + } + } + + auto nth(test_queue const & queue, std::size_t n) -> test_queue::iterator + { + auto it = queue.begin(); + for (std::size_t i = 0; i < n; ++i) + { + ++it; + } + return it; + } + + TEST_CASE("intrusive_queue::splice matches std::list::splice", "[detail][intrusive_queue]") + { + constexpr std::size_t max_dst = 3; + constexpr std::size_t max_src = 4; + + for (std::size_t dst_size = 0; dst_size <= max_dst; ++dst_size) + { + for (std::size_t src_size = 0; src_size <= max_src; ++src_size) + { + for (std::size_t pos = 0; pos <= dst_size; ++pos) + { + for (std::size_t first = 0; first <= src_size; ++first) + { + for (std::size_t last = first; last <= src_size; ++last) + { + CAPTURE(dst_size, src_size, pos, first, last); + + std::vector dst_nodes(dst_size); + std::vector src_nodes(src_size); + test_queue dst; + test_queue src; + std::list dst_expected; + std::list src_expected; + + for (std::size_t i = 0; i < dst_size; ++i) + { + dst_nodes[i].value_ = static_cast(100 + i); + dst.push_back(&dst_nodes[i]); + dst_expected.push_back(dst_nodes[i].value_); + } + for (std::size_t i = 0; i < src_size; ++i) + { + src_nodes[i].value_ = static_cast(i); + src.push_back(&src_nodes[i]); + src_expected.push_back(src_nodes[i].value_); + } + + dst.splice(nth(dst, pos), src, nth(src, first), nth(src, last)); + dst_expected.splice(std::next(dst_expected.begin(), static_cast(pos)), + src_expected, + std::next(src_expected.begin(), + static_cast(first)), + std::next(src_expected.begin(), + static_cast(last))); + + check_queue(dst, dst_expected); + check_queue(src, src_expected); + + // Both queues must remain independently usable afterwards. + test_node extra_dst{.value_ = 1000}; + test_node extra_src{.value_ = 2000}; + dst.push_back(&extra_dst); + src.push_back(&extra_src); + dst_expected.push_back(extra_dst.value_); + src_expected.push_back(extra_src.value_); + check_queue(dst, dst_expected); + check_queue(src, src_expected); + + dst.clear(); + src.clear(); + } + } + } + } + } + } + TEST_CASE("intrusive_queue::splice remains valid after chained transfers", + "[detail][intrusive_queue]") + { + std::array nodes{}; + std::array queues{}; + std::array, 3> expected{}; + + for (std::size_t i = 0; i < nodes.size(); ++i) + { + nodes[i].value_ = static_cast(i); + queues[i % queues.size()].push_back(&nodes[i]); + expected[i % expected.size()].push_back(nodes[i].value_); + } + + std::mt19937 rng{0x531CEu}; + for (std::size_t step = 0; step < 1500; ++step) + { + std::size_t const src = rng() % queues.size(); + std::size_t const dst = (src + 1 + rng() % (queues.size() - 1)) % queues.size(); + std::size_t const first = rng() % (expected[src].size() + 1); + std::size_t const last = first + rng() % (expected[src].size() - first + 1); + std::size_t const pos = rng() % (expected[dst].size() + 1); + + CAPTURE(step, src, dst, first, last, pos); + queues[dst].splice(nth(queues[dst], pos), + queues[src], + nth(queues[src], first), + nth(queues[src], last)); + expected[dst].splice(std::next(expected[dst].begin(), static_cast(pos)), + expected[src], + std::next(expected[src].begin(), static_cast(first)), + std::next(expected[src].begin(), static_cast(last))); + + for (std::size_t i = 0; i < queues.size(); ++i) + { + check_queue(queues[i], expected[i]); + } + } + + // Verify that transferred nodes can also be removed in the expected order. + for (std::size_t i = 0; i < queues.size(); ++i) + { + while (!expected[i].empty()) + { + REQUIRE_FALSE(queues[i].empty()); + CHECK(queues[i].pop_front()->value_ == expected[i].front()); + expected[i].pop_front(); + } + check_queue(queues[i], expected[i]); + } + } + +} // namespace