libutil: basic generator type with mapping
Change-Id: I2cebcefa0148b631fb30df4c8cfa92167a407e34
This commit is contained in:
parent
a81ec42ecf
commit
f19e7f6974
2 changed files with 322 additions and 0 deletions
181
src/libutil/generator.hh
Normal file
181
src/libutil/generator.hh
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "overloaded.hh"
|
||||||
|
|
||||||
|
#include <coroutine>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
template<typename T, typename Transform = std::identity>
|
||||||
|
struct Generator : private Generator<T, void>
|
||||||
|
{
|
||||||
|
struct promise_type;
|
||||||
|
using handle_type = std::coroutine_handle<promise_type>;
|
||||||
|
|
||||||
|
explicit Generator(handle_type h) : Generator<T, void>{h, h.promise().state} {}
|
||||||
|
|
||||||
|
using Generator<T, void>::operator bool;
|
||||||
|
using Generator<T, void>::operator();
|
||||||
|
|
||||||
|
operator Generator<T, void> &() &
|
||||||
|
{
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
operator Generator<T, void>() &&
|
||||||
|
{
|
||||||
|
return std::move(*this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct Generator<T, void>
|
||||||
|
{
|
||||||
|
template<typename T2, typename Transform>
|
||||||
|
friend struct Generator<T2, Transform>::promise_type;
|
||||||
|
|
||||||
|
struct promise_state;
|
||||||
|
|
||||||
|
struct _link
|
||||||
|
{
|
||||||
|
std::coroutine_handle<> handle{};
|
||||||
|
promise_state * state{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct promise_state
|
||||||
|
{
|
||||||
|
std::variant<_link, T> value{};
|
||||||
|
std::exception_ptr exception{};
|
||||||
|
_link parent{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE coroutine handles are LiteralType, own a memory resource (that may
|
||||||
|
// itself own unique resources), and are "typically TriviallyCopyable". we
|
||||||
|
// need to take special care to wrap this into a less footgunny interface,
|
||||||
|
// which mostly means move-only.
|
||||||
|
Generator(Generator && other)
|
||||||
|
{
|
||||||
|
swap(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
Generator & operator=(Generator && other)
|
||||||
|
{
|
||||||
|
Generator(std::move(other)).swap(*this);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
~Generator()
|
||||||
|
{
|
||||||
|
if (h) {
|
||||||
|
h.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit operator bool()
|
||||||
|
{
|
||||||
|
return ensure();
|
||||||
|
}
|
||||||
|
|
||||||
|
T operator()()
|
||||||
|
{
|
||||||
|
ensure();
|
||||||
|
auto result = std::move(*current);
|
||||||
|
current = nullptr;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::coroutine_handle<> h{};
|
||||||
|
_link active{};
|
||||||
|
T * current{};
|
||||||
|
|
||||||
|
Generator(std::coroutine_handle<> h, promise_state & state) : h(h), active(h, &state) {}
|
||||||
|
|
||||||
|
void swap(Generator & other)
|
||||||
|
{
|
||||||
|
std::swap(h, other.h);
|
||||||
|
std::swap(active, other.active);
|
||||||
|
std::swap(current, other.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ensure()
|
||||||
|
{
|
||||||
|
while (!current && active.handle) {
|
||||||
|
active.handle.resume();
|
||||||
|
auto & p = *active.state;
|
||||||
|
if (p.exception) {
|
||||||
|
std::rethrow_exception(p.exception);
|
||||||
|
} else if (active.handle.done()) {
|
||||||
|
active = p.parent;
|
||||||
|
} else {
|
||||||
|
std::visit(
|
||||||
|
overloaded{
|
||||||
|
[&](_link & inner) {
|
||||||
|
auto base = inner.state;
|
||||||
|
while (base->parent.handle) {
|
||||||
|
base = base->parent.state;
|
||||||
|
}
|
||||||
|
base->parent = active;
|
||||||
|
active = inner;
|
||||||
|
},
|
||||||
|
[&](T & value) { current = &value; },
|
||||||
|
},
|
||||||
|
p.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T, typename Transform>
|
||||||
|
struct Generator<T, Transform>::promise_type
|
||||||
|
{
|
||||||
|
Generator<T, void>::promise_state state;
|
||||||
|
Transform convert;
|
||||||
|
std::optional<Generator<T, void>> inner;
|
||||||
|
|
||||||
|
Generator get_return_object()
|
||||||
|
{
|
||||||
|
return Generator(handle_type::from_promise(*this));
|
||||||
|
}
|
||||||
|
std::suspend_always initial_suspend()
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::suspend_always final_suspend() noexcept
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
void unhandled_exception()
|
||||||
|
{
|
||||||
|
state.exception = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename From>
|
||||||
|
requires requires(Transform t, From && f) {
|
||||||
|
{
|
||||||
|
t(std::forward<From>(f))
|
||||||
|
} -> std::convertible_to<T>;
|
||||||
|
}
|
||||||
|
std::suspend_always yield_value(From && from)
|
||||||
|
{
|
||||||
|
state.value = convert(std::forward<From>(from));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename From>
|
||||||
|
requires requires(Transform t, From f) { static_cast<Generator<T, void>>(t(std::move(f))); }
|
||||||
|
std::suspend_always yield_value(From from)
|
||||||
|
{
|
||||||
|
inner = static_cast<Generator<T, void>>(convert(std::move(from)));
|
||||||
|
state.value = inner->active;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void return_void() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
141
tests/unit/libutil/generator.cc
Normal file
141
tests/unit/libutil/generator.cc
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
#include "generator.hh"
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
TEST(Generator, yields)
|
||||||
|
{
|
||||||
|
auto g = []() -> Generator<int> {
|
||||||
|
co_yield 1;
|
||||||
|
co_yield 2;
|
||||||
|
}();
|
||||||
|
|
||||||
|
ASSERT_TRUE(bool(g));
|
||||||
|
ASSERT_EQ(g(), 1);
|
||||||
|
ASSERT_EQ(g(), 2);
|
||||||
|
ASSERT_FALSE(bool(g));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Generator, nests)
|
||||||
|
{
|
||||||
|
auto g = []() -> Generator<int> {
|
||||||
|
co_yield 1;
|
||||||
|
co_yield []() -> Generator<int> {
|
||||||
|
co_yield 9;
|
||||||
|
co_yield []() -> Generator<int> {
|
||||||
|
co_yield 99;
|
||||||
|
co_yield 100;
|
||||||
|
}();
|
||||||
|
}();
|
||||||
|
|
||||||
|
auto g2 = []() -> Generator<int> {
|
||||||
|
co_yield []() -> Generator<int> {
|
||||||
|
co_yield 2000;
|
||||||
|
co_yield 2001;
|
||||||
|
}();
|
||||||
|
co_yield 1001;
|
||||||
|
}();
|
||||||
|
|
||||||
|
co_yield g2();
|
||||||
|
co_yield std::move(g2);
|
||||||
|
co_yield 2;
|
||||||
|
}();
|
||||||
|
|
||||||
|
ASSERT_TRUE(bool(g));
|
||||||
|
ASSERT_EQ(g(), 1);
|
||||||
|
ASSERT_EQ(g(), 9);
|
||||||
|
ASSERT_EQ(g(), 99);
|
||||||
|
ASSERT_EQ(g(), 100);
|
||||||
|
ASSERT_EQ(g(), 2000);
|
||||||
|
ASSERT_EQ(g(), 2001);
|
||||||
|
ASSERT_EQ(g(), 1001);
|
||||||
|
ASSERT_EQ(g(), 2);
|
||||||
|
ASSERT_FALSE(bool(g));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Generator, nestsExceptions)
|
||||||
|
{
|
||||||
|
auto g = []() -> Generator<int> {
|
||||||
|
co_yield 1;
|
||||||
|
co_yield []() -> Generator<int> {
|
||||||
|
co_yield 9;
|
||||||
|
throw 1;
|
||||||
|
co_yield 10;
|
||||||
|
}();
|
||||||
|
co_yield 2;
|
||||||
|
}();
|
||||||
|
|
||||||
|
ASSERT_TRUE(bool(g));
|
||||||
|
ASSERT_EQ(g(), 1);
|
||||||
|
ASSERT_EQ(g(), 9);
|
||||||
|
ASSERT_THROW(g(), int);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Generator, exception)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto g = []() -> Generator<int> {
|
||||||
|
throw 1;
|
||||||
|
co_return;
|
||||||
|
}();
|
||||||
|
|
||||||
|
ASSERT_THROW(void(bool(g)), int);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto g = []() -> Generator<int> {
|
||||||
|
throw 1;
|
||||||
|
co_return;
|
||||||
|
}();
|
||||||
|
|
||||||
|
ASSERT_THROW(g(), int);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct Transform
|
||||||
|
{
|
||||||
|
int state = 0;
|
||||||
|
|
||||||
|
std::pair<uint32_t, int> operator()(std::integral auto x)
|
||||||
|
{
|
||||||
|
return {x, state++};
|
||||||
|
}
|
||||||
|
|
||||||
|
Generator<std::pair<uint32_t, int>, Transform> operator()(const char *)
|
||||||
|
{
|
||||||
|
co_yield 9;
|
||||||
|
co_yield 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
Generator<std::pair<uint32_t, int>, Transform> operator()(Generator<int> && inner)
|
||||||
|
{
|
||||||
|
return [](auto g) mutable -> Generator<std::pair<uint32_t, int>, Transform> {
|
||||||
|
while (g) {
|
||||||
|
co_yield g();
|
||||||
|
}
|
||||||
|
}(std::move(inner));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Generator, transform)
|
||||||
|
{
|
||||||
|
auto g = []() -> Generator<std::pair<uint32_t, int>, Transform> {
|
||||||
|
co_yield int32_t(-1);
|
||||||
|
co_yield "";
|
||||||
|
std::cerr << "1\n";
|
||||||
|
co_yield []() -> Generator<int> { co_yield 7; }();
|
||||||
|
co_yield 20;
|
||||||
|
}();
|
||||||
|
|
||||||
|
ASSERT_EQ(g(), (std::pair<unsigned, int>{4294967295, 0}));
|
||||||
|
ASSERT_EQ(g(), (std::pair<unsigned, int>{9, 0}));
|
||||||
|
ASSERT_EQ(g(), (std::pair<unsigned, int>{19, 1}));
|
||||||
|
ASSERT_EQ(g(), (std::pair<unsigned, int>{7, 0}));
|
||||||
|
ASSERT_EQ(g(), (std::pair<unsigned, int>{20, 1}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue