Skip to content

Commit 718ac93

Browse files
[V4] Tuple and captured transform (#55)
* copy * nameing * use hof * apply * import/export * in structure * c++23 -> c++26 * fixups * start * double empty failinh * simplify * reorder * constructors * renames * apply test * structured binding in test * order * Revert "order" This reverts commit c369056. * more-tests * lock/key * no warn missing braces * tmp * correct type signature * mimic tuple! * move to shim * try zero sized * noexcept * test gcc workaround * compiler dep code paths * try dropping zero sized (gcc) * final tuple tweaks * wip * tmp working * faster! * restore test * split package and call * both co_await * async concepts * non-void promise/call * renames * return benchmark * add no except etc * wip use lock/key * Revert "wip use lock/key" This reverts commit bdce13c. * try single await * Revert "try single await" This reverts commit cc580ab. * immovable swap * mark pkg as nodiscard * drop lock/key * Update test/src/tuple.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/src/tuple.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/core/concepts.cxx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/core/tuple.cxx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * compare with std::tuple * correct noexcept * clarify std * spell * abbreviate --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 6b7abcf commit 718ac93

10 files changed

Lines changed: 451 additions & 20 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
strict fork-join parallelism using C++20 coroutines.
77

88
- **Type**: C++ library with module/`import std` support
9-
- **Languages**: C++23
9+
- **Languages**: C++26
1010

1111
## Critical Build Requirements
1212

1313
### Compiler & Module Support
1414

15-
This project **requires C++23 `import std`** and **MUST** use the appropriate
15+
This project **requires C++23's `import std`** and **MUST** use the appropriate
1616
toolchain file:
1717

1818
- **MacOS**: Use `-DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake`
@@ -89,7 +89,7 @@ cmake --build --preset <preset-name>
8989
**Build warnings** (expected and safe):
9090

9191
- "It is recommended to build benchmarks in Release mode" - only relevant for `ci-hardened`
92-
- CMake experimental `import std;` warning - expected for C++23 modules
92+
- CMake experimental `import std;` warning - expected for C++23's `import std`
9393

9494
### 3. Test
9595

@@ -194,7 +194,6 @@ Strive to add tests for new features/bug fixes.
194194
- Add `.cpp` files to `test/src/`
195195
- Tests auto-discovered by CMake (GLOB_RECURSE)
196196
- Links against `libfork::libfork` and `Catch2::Catch2WithMain`
197-
- Uses `cxx_std_23` feature requirement
198197

199198
### Modifying Build Configuration
200199

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ add_library(libfork::libfork ALIAS libfork_libfork)
3535

3636
set_property(TARGET libfork_libfork PROPERTY EXPORT_NAME libfork)
3737

38-
target_compile_features(libfork_libfork PUBLIC cxx_std_23)
38+
target_compile_features(libfork_libfork PUBLIC cxx_std_26)
3939

4040
# Public headers
4141
target_sources(libfork_libfork
@@ -61,6 +61,7 @@ target_sources(libfork_libfork
6161
src/core/utility.cxx
6262
src/core/frame.cxx
6363
src/core/constants.cxx
64+
src/core/tuple.cxx
6465
PRIVATE
6566
src/exception.cpp
6667
)

CMakePresets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"displayName": "Debug with warnings and hardening",
3333
"cacheVariables": {
3434
"CMAKE_BUILD_TYPE": "Debug",
35-
"CMAKE_CXX_FLAGS": "-O2 -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast -Werror=format-security -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -fstrict-flex-arrays=3 -fstack-protector-strong"
35+
"CMAKE_CXX_FLAGS": "-O2 -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast -Werror=format-security -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -fstrict-flex-arrays=3 -fstack-protector-strong -Wno-missing-braces"
3636
}
3737
},
3838
{

benchmark/src/libfork_benchmark/fib/lf_parts.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ constexpr auto await = [](this auto fib, std::int64_t *ret, std::int64_t n) -> l
4646
std::int64_t lhs = 0;
4747
std::int64_t rhs = 0;
4848

49-
co_await fib(&lhs, n - 1);
50-
co_await fib(&rhs, n - 2);
49+
co_await lf::call(fib, &lhs, n - 1);
50+
co_await lf::call(fib, &rhs, n - 2);
5151

5252
*ret = lhs + rhs;
5353
};
@@ -68,7 +68,13 @@ void fib(benchmark::State &state) {
6868
benchmark::DoNotOptimize(n);
6969
std::int64_t result = 0;
7070

71-
Fn(&result, n).promise->handle().resume();
71+
if constexpr (requires { Fn(&result, n); }) {
72+
Fn(&result, n).promise->handle().resume();
73+
} else {
74+
auto task = Fn(n);
75+
task.promise->return_address = &result;
76+
task.promise->handle().resume();
77+
}
7278

7379
CHECK_RESULT(result, expect);
7480
benchmark::DoNotOptimize(result);
@@ -79,6 +85,21 @@ void fib(benchmark::State &state) {
7985
}
8086
}
8187

88+
template <lf::alloc_mixin StackPolicy>
89+
constexpr auto ret = [](this auto fib, std::int64_t n) -> lf::task<std::int64_t, StackPolicy> {
90+
if (n < 2) {
91+
co_return n;
92+
}
93+
94+
std::int64_t lhs = 0;
95+
std::int64_t rhs = 0;
96+
97+
co_await lf::call(&lhs, fib, n - 1);
98+
co_await lf::call(&rhs, fib, n - 2);
99+
100+
co_return lhs + rhs;
101+
};
102+
82103
} // namespace
83104

84105
BENCHMARK(fib<no_await<stack_on_heap>>)->Name("test/libfork/fib/heap/no_await")->Arg(fib_test);
@@ -87,8 +108,11 @@ BENCHMARK(fib<no_await<stack_on_heap>>)->Name("base/libfork/fib/heap/no_await")-
87108
BENCHMARK(fib<await<stack_on_heap>>)->Name("test/libfork/fib/heap/await")->Arg(fib_test);
88109
BENCHMARK(fib<await<stack_on_heap>>)->Name("base/libfork/fib/heap/await")->Arg(fib_base);
89110

90-
BENCHMARK(fib<no_await<fib_bump_allocator>>)->Name("test/libfork/fib/data/no_await")->Arg(fib_test);
91-
BENCHMARK(fib<no_await<fib_bump_allocator>>)->Name("base/libfork/fib/data/no_await")->Arg(fib_base);
111+
BENCHMARK(fib<no_await<fib_bump_allocator>>)->Name("test/libfork/fib/bump_alloc/no_await")->Arg(fib_test);
112+
BENCHMARK(fib<no_await<fib_bump_allocator>>)->Name("base/libfork/fib/bump_alloc/no_await")->Arg(fib_base);
113+
114+
BENCHMARK(fib<await<fib_bump_allocator>>)->Name("test/libfork/fib/bump_alloc/await")->Arg(fib_test);
115+
BENCHMARK(fib<await<fib_bump_allocator>>)->Name("base/libfork/fib/bump_alloc/await")->Arg(fib_base);
92116

93-
BENCHMARK(fib<await<fib_bump_allocator>>)->Name("test/libfork/fib/data/await")->Arg(fib_test);
94-
BENCHMARK(fib<await<fib_bump_allocator>>)->Name("base/libfork/fib/data/await")->Arg(fib_base);
117+
BENCHMARK(fib<ret<fib_bump_allocator>>)->Name("test/libfork/fib/bump_alloc/return")->Arg(fib_test);
118+
BENCHMARK(fib<ret<fib_bump_allocator>>)->Name("base/libfork/fib/bump_alloc/return")->Arg(fib_base);

src/core/concepts.cxx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,37 @@ concept alloc_mixin = mixinable<T> && requires (std::size_t n, T *ptr) {
2222
{ T::operator delete(ptr, n) } noexcept -> std::same_as<void>;
2323
};
2424

25+
template <typename T, template <typename...> typename Template>
26+
struct is_specialization_of : std::false_type {};
27+
28+
template <template <typename...> typename Template, typename... Args>
29+
struct is_specialization_of<Template<Args...>, Template> : std::true_type {};
30+
31+
/**
32+
* @brief Test if `T` is a specialization of the template `Template`.
33+
*/
34+
template <typename T, template <typename...> typename Template>
35+
concept specialization_of = is_specialization_of<std::remove_cvref_t<T>, Template>::value;
36+
37+
// Forward-decl
38+
export template <returnable T, alloc_mixin Stack>
39+
struct task;
40+
41+
/**
42+
* @brief Test if a callable `Fn` when invoked with `Args...` returns an `lf::task`.
43+
*/
44+
export template <typename Fn, typename... Args>
45+
concept async_invocable =
46+
std::invocable<Fn, Args...> && specialization_of<std::invoke_result_t<Fn, Args...>, task>;
47+
48+
/**
49+
* @brief The result type of invoking an async function `Fn` with `Args...`.
50+
*/
51+
export template <typename Fn, typename... Args>
52+
requires async_invocable<Fn, Args...>
53+
using async_result_t = std::invoke_result_t<Fn, Args...>::type;
54+
55+
template <typename Fn, typename R, typename... Args>
56+
concept async_invocable_to = async_invocable<Fn, Args...> && std::same_as<async_result_t<Fn, Args...>, R>;
57+
2558
} // namespace lf

src/core/core.cxx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export import :concepts;
66
export import :utility;
77
export import :frame;
88
export import :constants;
9+
export import :tuple;

src/core/promise.cxx

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module;
22
#include <version>
33

44
#include "libfork/__impl/assume.hpp"
5+
#include "libfork/__impl/exception.hpp"
56
#include "libfork/__impl/utils.hpp"
67
export module libfork.core:promise;
78

@@ -10,6 +11,7 @@ import std;
1011
import :concepts;
1112
import :utility;
1213
import :frame;
14+
import :tuple;
1315

1416
namespace lf {
1517

@@ -37,7 +39,7 @@ struct promise_type;
3739
* \endrst
3840
*/
3941
export template <returnable T, alloc_mixin Stack>
40-
struct task {
42+
struct task : immovable, std::type_identity<T> {
4143
promise_type<T, Stack> *promise;
4244
};
4345

@@ -46,6 +48,8 @@ struct task {
4648
[[nodiscard]]
4749
constexpr auto final_suspend(frame_type *frame) -> std::coroutine_handle<> {
4850

51+
// TODO: noexcept
52+
4953
LF_ASSUME(frame != nullptr);
5054

5155
frame_type *parent_frame = frame->parent;
@@ -68,13 +72,15 @@ struct final_awaitable : std::suspend_always {
6872
}
6973
};
7074

71-
struct just_awaitable : std::suspend_always {
75+
struct call_awaitable : std::suspend_always {
7276

7377
frame_type *child;
7478

7579
template <typename... Us>
7680
auto await_suspend(std::coroutine_handle<promise_type<Us...>> parent) noexcept -> std::coroutine_handle<> {
7781

82+
// TODO: destroy on child if cannot launch i.e. scheduling failure
83+
7884
LF_ASSUME(child != nullptr);
7985
LF_ASSUME(child->parent == nullptr);
8086

@@ -84,6 +90,36 @@ struct just_awaitable : std::suspend_always {
8490
}
8591
};
8692

93+
// clang-format off
94+
95+
template <typename R, typename Fn, typename... Args>
96+
struct pkg {
97+
R *return_address;
98+
[[no_unique_address]] Fn fn;
99+
[[no_unique_address]] tuple<Args...> args;
100+
};
101+
102+
template <typename Fn, typename... Args>
103+
struct pkg<void, Fn, Args...> {
104+
[[no_unique_address]] Fn fn;
105+
[[no_unique_address]] tuple<Args...> args;
106+
};
107+
108+
// clang-format on
109+
110+
template <typename R, typename Fn, typename... Args>
111+
struct [[nodiscard("You should immediately co_await this!")]] call_pkg : pkg<R, Fn, Args...>, immovable {};
112+
113+
export template <typename... Args, async_invocable_to<void, Args...> Fn>
114+
constexpr auto call(Fn &&fn, Args &&...args) noexcept -> call_pkg<void, Fn, Args &&...> {
115+
return {LF_FWD(fn), {LF_FWD(args)...}};
116+
}
117+
118+
export template <typename R, typename... Args, async_invocable_to<R, Args...> Fn>
119+
constexpr auto call(R *ret, Fn &&fn, Args &&...args) noexcept -> call_pkg<R, Fn, Args &&...> {
120+
return {ret, LF_FWD(fn), {LF_FWD(args)...}};
121+
}
122+
87123
struct mixin_frame {
88124

89125
// === For internal use === //
@@ -99,8 +135,15 @@ struct mixin_frame {
99135

100136
// === Called by the compiler === //
101137

102-
template <alloc_mixin P>
103-
constexpr static auto await_transform(task<void, P> child) noexcept -> just_awaitable {
138+
template <typename R, typename Fn, typename... Args>
139+
constexpr static auto await_transform(call_pkg<R, Fn, Args...> &&pkg) noexcept -> call_awaitable {
140+
141+
task child = std::move(pkg.args).apply(std::move(pkg.fn));
142+
143+
if constexpr (!std::is_void_v<R>) {
144+
child.promise->return_address = pkg.return_address;
145+
}
146+
104147
return {.child = &child.promise->frame};
105148
}
106149

@@ -120,9 +163,9 @@ struct promise_type<void, StackPolicy> : StackPolicy, mixin_frame {
120163

121164
frame_type frame;
122165

123-
constexpr auto get_return_object() -> task<void, StackPolicy> { return {.promise = this}; }
166+
constexpr auto get_return_object() noexcept -> task<void, StackPolicy> { return {.promise = this}; }
124167

125-
constexpr static void return_void() {}
168+
constexpr static void return_void() noexcept {}
126169
};
127170

128171
struct dummy_alloc {
@@ -142,8 +185,17 @@ static_assert(std::is_standard_layout_v<promise_type<void, dummy_alloc>>);
142185

143186
template <typename T, alloc_mixin StackPolicy>
144187
struct promise_type : StackPolicy, mixin_frame {
188+
145189
frame_type frame;
146190
T *return_address;
191+
192+
constexpr auto get_return_object() noexcept -> task<T, StackPolicy> { return {.promise = this}; }
193+
194+
template <typename U = T>
195+
requires std::assignable_from<T &, U &&>
196+
constexpr void return_value(U &&value) noexcept(std::is_nothrow_assignable_v<T &, U &&>) {
197+
*return_address = LF_FWD(value);
198+
}
147199
};
148200

149201
static_assert(alignof(promise_type<int, dummy_alloc>) == alignof(frame_type));

0 commit comments

Comments
 (0)