diff options
Diffstat (limited to 'nihil.core')
| -rw-r--r-- | nihil.core/CMakeLists.txt | 29 | ||||
| -rw-r--r-- | nihil.core/error.test.cc | 1 | ||||
| -rw-r--r-- | nihil.core/generator.ccm | 240 | ||||
| -rw-r--r-- | nihil.core/generator.test.cc | 109 | ||||
| -rw-r--r-- | nihil.core/nihil.core.ccm | 1 |
5 files changed, 379 insertions, 1 deletions
diff --git a/nihil.core/CMakeLists.txt b/nihil.core/CMakeLists.txt index e9998a3..d0713ce 100644 --- a/nihil.core/CMakeLists.txt +++ b/nihil.core/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(nihil.core error.ccm features.ccm flagset.ccm + generator.ccm guard.ccm match.ccm monad.ccm @@ -25,3 +26,31 @@ target_sources(nihil.core tabulate.ccm uuid.ccm ) + +if (NIHIL_TESTS) + add_executable(nihil.core.test + capture_stream.test.cc + ctype.test.cc + error.test.cc + flagset.test.cc + generator.test.cc + guard.test.cc + match.test.cc + monad.test.cc + next_word.test.cc + parse_size.test.cc + skipws.test.cc + tabulate.test.cc + ) + + target_link_libraries(nihil.core.test PRIVATE + nihil.std + nihil.core + Catch2::Catch2WithMain) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.core.test) + + enable_testing() +endif () diff --git a/nihil.core/error.test.cc b/nihil.core/error.test.cc index f4ec1ee..7a923e4 100644 --- a/nihil.core/error.test.cc +++ b/nihil.core/error.test.cc @@ -4,7 +4,6 @@ import nihil.std; import nihil.core; -import nihil.util; namespace { inline constexpr auto *test_tags = "[nihil][nihil.error]"; diff --git a/nihil.core/generator.ccm b/nihil.core/generator.ccm new file mode 100644 index 0000000..4d2bfe3 --- /dev/null +++ b/nihil.core/generator.ccm @@ -0,0 +1,240 @@ +// generator - Single-header, ranges-compatible generator type built +// on C++20 coroutines +// Written in 2021 by Sy Brand (tartanllama@gmail.com, @TartanLlama) +// +// Documentation available at https://tl.tartanllama.xyz/ +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to the +// public domain worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. If not, see +// <http://creativecommons.org/publicdomain/zero/1.0/>. +export module nihil.core:generator; + +import nihil.std; + +namespace nihil { + +export template <typename T> +struct generator +{ +private: + struct promise + { + using value_type = std::remove_reference_t<T>; + using reference_type = + std::conditional_t<std::is_pointer_v<value_type>, value_type, value_type &>; + using pointer_type = + std::conditional_t<std::is_pointer_v<value_type>, value_type, value_type *>; + + promise() = default; + + [[nodiscard]] auto get_return_object() -> generator + { + return generator(std::coroutine_handle<promise>::from_promise(*this)); + } + + [[nodiscard]] auto initial_suspend() const -> std::suspend_always + { + return {}; + } + + [[nodiscard]] auto final_suspend() const noexcept -> std::suspend_always + { + return {}; + } + + void return_void() const noexcept + { + } + + void unhandled_exception() noexcept + { + exception_ = std::current_exception(); + } + + void rethrow_if_exception() + { + if (exception_) { + std::rethrow_exception(exception_); + } + } + + std::suspend_always yield_value(value_type &&v) noexcept + { + value_ = std::move(v); + return {}; + } + + std::suspend_always yield_value(value_type const &v) noexcept + requires(!std::is_reference_v<T>) + { + value_ = v; + return {}; + } + + std::suspend_always yield_value(value_type &v) noexcept + requires(std::is_reference_v<T>) + { + value_ = &v; + return {}; + } + + std::exception_ptr exception_; + std::variant<std::monostate, value_type, value_type *> value_; + }; + +public: + using promise_type = promise; + class sentinel + { + }; + + class iterator + { + using handle_type = std::coroutine_handle<promise_type>; + + public: + using value_type = typename promise_type::value_type; + using reference_type = typename promise_type::reference_type; + using pointer_type = typename promise_type::pointer_type; + using difference_type = std::ptrdiff_t; + + iterator() = default; + ~iterator() + { + if (handle_) + handle_.destroy(); + } + + // Non-copyable because coroutine handles point to a unique resource + iterator(iterator const &) = delete; + iterator(iterator &&rhs) noexcept + : handle_(std::exchange(rhs.handle_, nullptr)) + { + } + + auto operator=(iterator const &) -> iterator & = delete; + + auto operator=(iterator &&rhs) noexcept -> iterator & + { + handle_ = std::exchange(rhs.handle_, nullptr); + return *this; + } + + friend auto operator==(iterator const &it, sentinel) noexcept -> bool + { + return (!it.handle_ || it.handle_.done()); + } + + auto operator++() -> iterator & + { + handle_.resume(); + if (handle_.done()) { + handle_.promise().rethrow_if_exception(); + } + return *this; + } + + void operator++(int) + { + (void)this->operator++(); + } + + reference_type operator*() const + noexcept(noexcept(std::is_nothrow_copy_constructible_v<reference_type>)) + requires(std::is_reference_v<T>) + { + return *std::get<value_type *>(handle_.promise().value_); + } + + reference_type operator*() const + noexcept(noexcept(std::is_nothrow_copy_constructible_v<reference_type>)) + requires(!std::is_reference_v<T>) + { + return std::get<value_type>(handle_.promise().value_); + } + + value_type *operator->() const + noexcept(noexcept(std::is_nothrow_copy_constructible_v<reference_type>)) + requires(std::is_reference_v<T>) + { + return std::get<value_type *>(handle_.promise().value_); + } + + value_type *operator->() const + noexcept(noexcept(std::is_nothrow_copy_constructible_v<reference_type>)) + requires(!std::is_reference_v<T>) + { + return &std::get<value_type>(handle_.promise().value_); + } + + private: + friend struct generator; + + explicit iterator(handle_type handle) + : handle_(handle) + { + } + + handle_type handle_; + }; + + using handle_type = std::coroutine_handle<promise_type>; + + generator() noexcept = default; + ~generator() + { + if (handle_) + handle_.destroy(); + } + + generator(generator const &) = delete; + + generator(generator &&rhs) noexcept + : handle_(std::exchange(rhs.handle_, nullptr)) + { + } + + auto operator=(generator const &) -> generator & = delete; + + auto operator=(generator &&rhs) noexcept -> generator & + { + swap(rhs); + return *this; + } + + auto begin() -> iterator + { + handle_.resume(); + if (handle_.done()) { + handle_.promise().rethrow_if_exception(); + } + return iterator(std::exchange(handle_, nullptr)); + } + + auto end() const noexcept -> sentinel + { + return {}; + } + + void swap(generator &other) noexcept + { + std::swap(handle_, other.handle_); + } + +private: + friend class iterator; + explicit generator(handle_type handle) noexcept + : handle_(handle) + { + } + + handle_type handle_ = nullptr; +}; +} // namespace nihil + +export template <typename T> +inline constexpr bool std::ranges::enable_view<nihil::generator<T>> = true; diff --git a/nihil.core/generator.test.cc b/nihil.core/generator.test.cc new file mode 100644 index 0000000..1fc0f22 --- /dev/null +++ b/nihil.core/generator.test.cc @@ -0,0 +1,109 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.std; +import nihil.core; + +namespace { +inline auto constexpr test_tags = "[nihil][nihil.generator]"; + +SCENARIO("A generator that yields values", test_tags) +{ + GIVEN ("A generator that yields values") { + auto fn = [&]() -> nihil::generator<int> { + co_yield 1; + co_yield 2; + }; + + THEN ("The generator yields the original values") { + REQUIRE(std::ranges::equal(fn(), std::vector{1, 2})); + } + } +} + +SCENARIO("A generator that yields references", test_tags) +{ + GIVEN ("A generator that yields references") { + auto one = 1, two = 2; + auto fn = [&]() -> nihil::generator<int &> { + co_yield one; + co_yield two; + }; + auto range = fn(); + + THEN ("The references refer to the original values") { + auto it = std::ranges::begin(range); + REQUIRE(&*it == &one); + ++it; + REQUIRE(&*it == &two); + ++it; + REQUIRE(it == std::ranges::end(range)); + } + } +} + +SCENARIO("A generator that yields pointers", test_tags) +{ + GIVEN ("A generator that yields pointers") { + auto one = 1, two = 2; + auto fn = [&]() -> nihil::generator<int *> { + co_yield &one; + co_yield &two; + }; + + THEN ("The pointers point to the original values") { + REQUIRE(std::ranges::equal(fn(), std::vector{&one, &two})); + } + } +} + +SCENARIO("A generator that yields lvalues", test_tags) +{ + GIVEN ("A generator that yields pointers") { + auto one = 1, two = 2; + auto fn = [&]() -> nihil::generator<int> { + co_yield one; + co_yield two; + }; + + THEN ("The pointers point to the original values") { + REQUIRE(std::ranges::equal(fn(), std::vector{1, 2})); + } + } +} + +TEST_CASE("generator: exceptions", "[generator]") +{ + auto fn = []() -> nihil::generator<int> { + co_yield 1; + throw std::runtime_error("test"); + }; + + auto range = fn(); + auto it = std::ranges::begin(range); + REQUIRE(*it == 1); + REQUIRE_THROWS_AS(it++, std::runtime_error); +} + +#if 0 +// TODO: Re-enable this test once we have a standard-compliant generator. +TEST_CASE("generator: elements_of", "[generator]") +{ + auto fn1 = [] -> nihil::generator<int> { + co_yield 1; + co_yield 2; + co_yield 3; + }; + + auto fn2 = [&fn1] -> nihil::generator<int> { + co_yield nihil::elements_of(fn1()); + }; + + auto values = std::vector<int>(); + std::ranges::copy(fn2(), std::back_inserter(values)); + + REQUIRE(values == std::vector{1, 2, 3}); +} +#endif +} // anonymous namespace diff --git a/nihil.core/nihil.core.ccm b/nihil.core/nihil.core.ccm index 37ad032..7634026 100644 --- a/nihil.core/nihil.core.ccm +++ b/nihil.core/nihil.core.ccm @@ -8,6 +8,7 @@ export import :errc; export import :error; export import :features; export import :flagset; +export import :generator; export import :guard; export import :match; export import :monad; |
