diff --git a/.clang-tidy b/.clang-tidy index cdfaf8b..f002604 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,30 +1,14 @@ ---- -Checks: "*, - -abseil-*, - -altera-*, - -android-*, - -fuchsia-*, - -google-*, - -llvm*, - -modernize-use-trailing-return-type, - -zircon-*, - -readability-else-after-return, - -readability-static-accessed-through-instance, - -readability-avoid-const-params-in-decls, - -cppcoreguidelines-non-private-member-variables-in-classes, - -misc-non-private-member-variables-in-classes, -" -WarningsAsErrors: '' -HeaderFilterRegex: '' -FormatStyle: none - +Checks: > + cert-*, + performance-*, + modernize-*, + misc-include-cleaner, + -readability-identifier-length, + -readability-magic-numbers, + -cppcoreguidelines-avoid-magic-numbers CheckOptions: - - key: readability-identifier-length.IgnoredVariableNames - value: 'x|y|z' - - key: readability-identifier-length.IgnoredParameterNames - value: 'x|y|z' - - - - - + - key: misc-include-cleaner.IgnoreHeaders + value: > + boost/test/.*; + __chrono/.* +HeaderFilterRegex: '^.*/include/chains/.*$' diff --git a/CMakeLists.txt b/CMakeLists.txt index 64a9c00..96eb756 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,15 @@ add_library(chains::chains_warnings ALIAS chains_warnings) #add_library(chains::chains_options INTERFACE IMPORTED) #add_library(chains::chains_warnings INTERFACE IMPORTED) + +add_library(chains INTERFACE) +add_library(chains::chains ALIAS stlab) + +target_include_directories(stlab INTERFACE + $/include + $/include + $) + # configure files based on CMake configuration options add_subdirectory(configured_files) @@ -74,14 +83,14 @@ if(BUILD_TESTING) endif() -if(chains_BUILD_FUZZ_TESTS) - message(AUTHOR_WARNING "Building Fuzz Tests, using fuzzing sanitizer https://www.llvm.org/docs/LibFuzzer.html") - if (NOT chains_ENABLE_ADDRESS_SANITIZER AND NOT chains_ENABLE_THREAD_SANITIZER) - message(WARNING "You need asan or tsan enabled for meaningful fuzz testing") - endif() - add_subdirectory(fuzz_test) +# if(chains_BUILD_FUZZ_TESTS) +# message(AUTHOR_WARNING "Building Fuzz Tests, using fuzzing sanitizer https://www.llvm.org/docs/LibFuzzer.html") +# if (NOT chains_ENABLE_ADDRESS_SANITIZER AND NOT chains_ENABLE_THREAD_SANITIZER) +# message(WARNING "You need asan or tsan enabled for meaningful fuzz testing") +# endif() +# add_subdirectory(fuzz_test) -endif() +# endif() # If MSVC is being used, and ASAN is enabled, we need to set the debugger environment # so that it behaves well with MSVC's debugger, and we can run the target from visual studio @@ -92,30 +101,30 @@ if(MSVC) endif() # set the startup project for the "play" button in MSVC -set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT intro) +# set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT intro) -if(CMAKE_SKIP_INSTALL_RULES) - return() -endif() +# if(CMAKE_SKIP_INSTALL_RULES) +# return() +# endif() -include(cmake/PackageProject.cmake) +# include(cmake/PackageProject.cmake) # Add other targets that you want installed here, by default we just package the one executable # we know we want to ship -chains_package_project( - TARGETS - intro - chains_options - chains_warnings - # FIXME: this does not work! CK - # PRIVATE_DEPENDENCIES_CONFIGURED project_options project_warnings -) +# chains_package_project( +# TARGETS +# intro +# chains_options +# chains_warnings +# # FIXME: this does not work! CK +# # PRIVATE_DEPENDENCIES_CONFIGURED project_options project_warnings +# ) # Experience shows that explicit package naming can help make it easier to sort # out potential ABI related issues before they start, while helping you # track a build to a specific GIT SHA -set(CPACK_PACKAGE_FILE_NAME - "${CMAKE_PROJECT_NAME}-${CMAKE_PROJECT_VERSION}-${GIT_SHORT_SHA}-${CMAKE_SYSTEM_NAME}-${CMAKE_BUILD_TYPE}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}" -) +# set(CPACK_PACKAGE_FILE_NAME +# "${CMAKE_PROJECT_NAME}-${CMAKE_PROJECT_VERSION}-${GIT_SHORT_SHA}-${CMAKE_SYSTEM_NAME}-${CMAKE_BUILD_TYPE}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}" +# ) -include(CPack) +# include(CPack) diff --git a/Dependencies.cmake b/Dependencies.cmake index 2978210..6d60004 100644 --- a/Dependencies.cmake +++ b/Dependencies.cmake @@ -11,38 +11,38 @@ function(chains_setup_dependencies) if(NOT TARGET stlab::stlab) cpmaddpackage( NAME stlab - VERSION 2.0.0a3 + VERSION 2.0.0a9 GITHUB_REPOSITORY "stlab/libraries" OPTIONS "BUILD_TESTING OFF") endif() - if(NOT TARGET fmtlib::fmtlib) - cpmaddpackage("gh:fmtlib/fmt#9.1.0") - endif() - - if(NOT TARGET spdlog::spdlog) - cpmaddpackage( - NAME - spdlog - VERSION - 1.11.0 - GITHUB_REPOSITORY - "gabime/spdlog" - OPTIONS - "SPDLOG_FMT_EXTERNAL ON") - endif() + # if(NOT TARGET fmtlib::fmtlib) + # cpmaddpackage("gh:fmtlib/fmt#9.1.0") + # endif() + + # if(NOT TARGET spdlog::spdlog) + # cpmaddpackage( + # NAME + # spdlog + # VERSION + # 1.11.0 + # GITHUB_REPOSITORY + # "gabime/spdlog" + # OPTIONS + # "SPDLOG_FMT_EXTERNAL ON") + # endif() if(NOT TARGET Catch2::Catch2WithMain) cpmaddpackage("gh:catchorg/Catch2@3.3.2") endif() - if(NOT TARGET CLI11::CLI11) - cpmaddpackage("gh:CLIUtils/CLI11@2.3.2") - endif() + # if(NOT TARGET CLI11::CLI11) + # cpmaddpackage("gh:CLIUtils/CLI11@2.3.2") + # endif() - if(NOT TARGET ftxui::screen) - cpmaddpackage("gh:ArthurSonzogni/FTXUI#e23dbc7473654024852ede60e2121276c5aab660") - endif() + # if(NOT TARGET ftxui::screen) + # cpmaddpackage("gh:ArthurSonzogni/FTXUI#e23dbc7473654024852ede60e2121276c5aab660") + # endif() if(NOT TARGET tools::tools) cpmaddpackage("gh:lefticus/tools#update_build_system") diff --git a/ProjectOptions.cmake b/ProjectOptions.cmake index 19237f5..409d338 100644 --- a/ProjectOptions.cmake +++ b/ProjectOptions.cmake @@ -1,5 +1,5 @@ include(cmake/SystemLink.cmake) -include(cmake/LibFuzzer.cmake) +# include(cmake/LibFuzzer.cmake) include(CMakeDependentOption) include(CheckCXXCompilerFlag) @@ -78,14 +78,14 @@ macro(chains_setup_options) chains_ENABLE_CACHE) endif() - chains_check_libfuzzer_support(LIBFUZZER_SUPPORTED) - if(LIBFUZZER_SUPPORTED AND (chains_ENABLE_SANITIZER_ADDRESS OR chains_ENABLE_SANITIZER_THREAD OR chains_ENABLE_SANITIZER_UNDEFINED)) - set(DEFAULT_FUZZER ON) - else() - set(DEFAULT_FUZZER OFF) - endif() + # chains_check_libfuzzer_support(LIBFUZZER_SUPPORTED) + # if(LIBFUZZER_SUPPORTED AND (chains_ENABLE_SANITIZER_ADDRESS OR chains_ENABLE_SANITIZER_THREAD OR chains_ENABLE_SANITIZER_UNDEFINED)) + # set(DEFAULT_FUZZER ON) + # else() + # set(DEFAULT_FUZZER OFF) + # endif() - option(chains_BUILD_FUZZ_TESTS "Enable fuzz testing executable" ${DEFAULT_FUZZER}) + # option(chains_BUILD_FUZZ_TESTS "Enable fuzz testing executable" ${DEFAULT_FUZZER}) endmacro() diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index 3786c25..b1f5a5b 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -42,7 +42,7 @@ function( set(CLANG_WARNINGS -Wall -Wextra # reasonable and standard - -Wshadow # warn the user if a variable declaration shadows one from a parent context + #-Wshadow # warn the user if a variable declaration shadows one from a parent context -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps # catch hard to track down memory errors -Wold-style-cast # warn for c-style casts diff --git a/cmake/LibFuzzer.cmake b/cmake/LibFuzzer.cmake deleted file mode 100644 index 60538be..0000000 --- a/cmake/LibFuzzer.cmake +++ /dev/null @@ -1,17 +0,0 @@ -function(chains_check_libfuzzer_support var_name) - set(LibFuzzerTestSource - " -#include - -extern \"C\" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) { - return 0; -} - ") - - include(CheckCXXSourceCompiles) - - set(CMAKE_REQUIRED_FLAGS "-fsanitize=fuzzer") - set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=fuzzer") - check_cxx_source_compiles("${LibFuzzerTestSource}" ${var_name}) - -endfunction() diff --git a/cmake/StandardProjectSettings.cmake b/cmake/StandardProjectSettings.cmake index b9f4123..3f45a08 100644 --- a/cmake/StandardProjectSettings.cmake +++ b/cmake/StandardProjectSettings.cmake @@ -39,6 +39,10 @@ else() message(STATUS "No colored compiler diagnostic set for '${CMAKE_CXX_COMPILER_ID}' compiler.") endif() +# Enable __cplusplus makro on MSVC which is disabled by default +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options($<$:/Zc:__cplusplus>) +endif() # run vcvarsall when msvc is used include("${CMAKE_CURRENT_LIST_DIR}/VCEnvironment.cmake") diff --git a/fuzz_test/CMakeLists.txt b/fuzz_test/CMakeLists.txt deleted file mode 100644 index 56bd81f..0000000 --- a/fuzz_test/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# A fuzz test runs until it finds an error. This particular one is going to rely on libFuzzer. -# - -find_package(fmt) - -add_executable(fuzz_tester fuzz_tester.cpp) -target_link_libraries( - fuzz_tester - PRIVATE chains_options - chains_warnings - fmt::fmt - -coverage - -fsanitize=fuzzer) -target_compile_options(fuzz_tester PRIVATE -fsanitize=fuzzer) - -# Allow short runs during automated testing to see if something new breaks -set(FUZZ_RUNTIME - 10 - CACHE STRING "Number of seconds to run fuzz tests during ctest run") # Default of 10 seconds - -add_test(NAME fuzz_tester_run COMMAND fuzz_tester -max_total_time=${FUZZ_RUNTIME}) diff --git a/fuzz_test/fuzz_tester.cpp b/fuzz_test/fuzz_tester.cpp deleted file mode 100644 index 29d698b..0000000 --- a/fuzz_test/fuzz_tester.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include -#include - -[[nodiscard]] auto sum_values(const uint8_t* Data, size_t Size) { - constexpr auto scale = 1000; - - int value = 0; - for (std::size_t offset = 0; offset < Size; ++offset) { - value += static_cast(*std::next(Data, static_cast(offset))) * scale; - } - return value; -} - -// Fuzzer that attempts to invoke undefined behavior for signed integer overflow -// cppcheck-suppress unusedFunction symbolName=LLVMFuzzerTestOneInput -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size) { - fmt::print("Value sum: {}, len{}\n", sum_values(Data, Size), Size); - return 0; -} diff --git a/include/chains/sample_library.hpp b/include/chains/sample_library.hpp deleted file mode 100644 index 22afe40..0000000 --- a/include/chains/sample_library.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef SAMPLE_LIBRARY_HPP -#define SAMPLE_LIBRARY_HPP - -#include - -[[nodiscard]] SAMPLE_LIBRARY_EXPORT int factorial(int) noexcept; - -[[nodiscard]] constexpr int factorial_constexpr(int input) noexcept { - if (input == 0) { - return 1; - } - - return input * factorial_constexpr(input - 1); -} - -#endif diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 2f54238..1e4ecf6 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -1,28 +1,26 @@ -#include // std::apply, std::forward_as_tuple, std::tuple -#include // std::is_same_v -#include // std::forward, std::move -#include // std::monostate - #ifndef CHAIN_TUPLE_HPP #define CHAIN_TUPLE_HPP +#include // std::size_t +#include // std::invoke +#include // std::tuple, std::get, std::apply, std::tuple_size_v +#include // std::is_same_v, std::decay_t, std::is_void_v +#include // std::forward, std::move, std::index_sequence, std::make_index_sequence +#include // std::monostate + namespace chains::inline v0 { namespace detail { -/* - Operators for fold expression for sequential execution. Simpler way? -*/ - +/* Map void return to std::monostate */ template -inline auto void_to_monostate(F& f) { - return [&_f = f](auto&&... args) mutable { - if constexpr (std::is_same_v(args)...)), - void>) { - std::move(_f)(std::forward(args)...); +auto void_to_monostate(F& f) { + return [&_f = f](Args&&... args) mutable { + if constexpr (std::is_same_v(args)...)), void>) { + std::move(_f)(std::forward(args)...); return std::monostate{}; } else { - return std::move(_f)(std::forward(args)...); + return std::move(_f)(std::forward(args)...); } }; } @@ -30,7 +28,7 @@ inline auto void_to_monostate(F& f) { template struct tuple_pipeable { T _value; - tuple_pipeable(T&& a) : _value{std::move(a)} {} + explicit tuple_pipeable(T&& a) : _value{std::move(a)} {} }; template @@ -38,14 +36,60 @@ auto operator|(tuple_pipeable&& p, F& f) { return tuple_pipeable{void_to_monostate(f)(std::move(p._value))}; } +/* Check if F is invocable with first K elements of tuple T */ +template +constexpr auto invocable_with_prefix(std::index_sequence) { + return requires(F&& f, T&& tup) { std::invoke(f, std::move(std::get(tup))...); }; +} + +/* Find largest prefix size (0..N) for which F is invocable */ +template +struct find_max_prefix { + static constexpr std::size_t value = + invocable_with_prefix(std::make_index_sequence{}) ? + N : + find_max_prefix::value; +}; + +template +struct find_max_prefix { + static constexpr std::size_t value = 0; +}; + +/* Invoke F with first K elements of tuple t (K known at compile time) */ +template +constexpr auto invoke_prefix(F&& f, Tuple&& t) { + if constexpr (K == 0) { + if constexpr (requires(F&& f2) { std::invoke(f2); }) { + if constexpr (std::is_void_v) { + std::invoke(f); + return std::monostate{}; + } else { + return std::invoke(f); + } + } else { + return std::monostate{}; + } + } else { + return [&](std::index_sequence) { + if constexpr (std::is_void_v(t))...))>) { + std::invoke(f, std::move(std::get(t))...); + return std::monostate{}; + } else { + return std::invoke(f, std::move(std::get(t))...); + } + }(std::make_index_sequence{}); + } +} + } // namespace detail //-------------------------------------------------------------------------------------------------- template auto tuple_compose(std::tuple&& sequence) { - return [_sequence = std::move(sequence)](auto&&... args) mutable { + return [_sequence = std::move(sequence)](Args&&... args) mutable { return std::move(std::apply( - [_args = std::forward_as_tuple(std::forward(args)...)]( + [_args = std::forward_as_tuple(std::forward(args)...)]( auto& first, auto&... functions) mutable { return ( detail::tuple_pipeable{std::apply(first, std::move(_args))} | @@ -58,6 +102,72 @@ auto tuple_compose(std::tuple&& sequence) { //-------------------------------------------------------------------------------------------------- +/* Construct tuple tail starting at Offset (compile time) */ +template +constexpr auto tuple_tail_at(Tuple&& t, std::index_sequence) { + return std::tuple{std::move(std::get(t))...}; +} + +//-------------------------------------------------------------------------------------------------- +/* + tuple_consume: + Invokes the given callable with the largest invocable prefix of the stored tuple. + Returns pair. + - If no prefix is invocable, result is std::monostate and remaining_tuple is the original tuple. + - If callable returns void, result is std::monostate. +*/ +template +constexpr auto tuple_consume(Tuple&& values) { + return [_values = std::forward(values)](F&& f) mutable { + using tuple_t = std::decay_t; + constexpr std::size_t N = std::tuple_size_v; + + constexpr std::size_t consumed = detail::find_max_prefix::value; + auto result = detail::invoke_prefix(std::forward(f), _values); + + if constexpr (consumed == 0) { + // Remaining is original tuple (no elements consumed) + return std::tuple_cat(std::tuple{std::move(result)}, std::move(_values)); + } else { + auto remaining = tuple_tail_at( + std::move(_values), std::make_index_sequence{}); + return std::tuple_cat(std::tuple{std::move(result)}, std::move(remaining)); + } + }; +} + +template +constexpr auto calc_step(F& f, T t) { + if constexpr (I == std::tuple_size_v) { + // Base case: we finished applying all functions. + // If there are no remaining tuple elements, return std::monostate + // (mirrors void -> monostate mapping elsewhere). + if constexpr (std::tuple_size_v == 0) { + return std::monostate{}; + } else { + return std::get<0>(std::move(t)); + } + } else { + auto&& fn = std::get(f); + auto next = chains::tuple_consume(std::move(t))(fn); + return calc_step(f, std::move(next)); + } +} + +template +constexpr auto calc(F f, Args&&... args) { + return calc_step<0>(f, std::tuple{std::forward(args)...}); +} + +template +constexpr auto tuple_compose_greedy(std::tuple&& sequence) { + return [_sequence = std::move(sequence)](Args&&... args) mutable { + return calc(std::move(_sequence), std::forward(args)...); + }; +} + +//-------------------------------------------------------------------------------------------------- + } // namespace chains::inline v0 //-------------------------------------------------------------------------------------------------- diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0f92a9d..e69de29 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +0,0 @@ -add_subdirectory(sample_library) -add_subdirectory(ftxui_sample) diff --git a/src/ftxui_sample/CMakeLists.txt b/src/ftxui_sample/CMakeLists.txt deleted file mode 100644 index 264d3d3..0000000 --- a/src/ftxui_sample/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -add_executable(intro main.cpp) - -target_link_libraries( - intro - PRIVATE chains::chains_options - chains::chains_warnings) - -target_link_system_libraries( - intro - PRIVATE - CLI11::CLI11 - fmt::fmt - spdlog::spdlog - lefticus::tools - ftxui::screen - ftxui::dom - ftxui::component) - -target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") diff --git a/src/ftxui_sample/main.cpp b/src/ftxui_sample/main.cpp deleted file mode 100644 index dc6d786..0000000 --- a/src/ftxui_sample/main.cpp +++ /dev/null @@ -1,375 +0,0 @@ -#include -#include -#include -#include - -#include - -#include -#include // for ftxui -#include // for Slider -#include // for ScreenInteractive -#include - -#include - -// This file will be generated automatically when cur_you run the CMake -// configuration step. It creates a namespace called `chains`. You can modify -// the source template at `configured_files/config.hpp.in`. -#include - -template -struct GameBoard { - static constexpr std::size_t width = Width; - static constexpr std::size_t height = Height; - - std::array, width> strings; - std::array, width> values{}; - - std::size_t move_count{0}; - - std::string& get_string(std::size_t cur_x, std::size_t cur_y) { - return strings.at(cur_x).at(cur_y); - } - - void set(std::size_t cur_x, std::size_t cur_y, bool new_value) { - get(cur_x, cur_y) = new_value; - - if (new_value) { - get_string(cur_x, cur_y) = " ON"; - } else { - get_string(cur_x, cur_y) = "OFF"; - } - } - - void visit(auto visitor) { - for (std::size_t cur_x = 0; cur_x < width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height; ++cur_y) { - visitor(cur_x, cur_y, *this); - } - } - } - - [[nodiscard]] bool get(std::size_t cur_x, std::size_t cur_y) const { - return values.at(cur_x).at(cur_y); - } - - [[nodiscard]] bool& get(std::size_t cur_x, std::size_t cur_y) { - return values.at(cur_x).at(cur_y); - } - - GameBoard() { - visit([](const auto cur_x, const auto cur_y, auto& gameboard) { - gameboard.set(cur_x, cur_y, true); - }); - } - - void update_strings() { - for (std::size_t cur_x = 0; cur_x < width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height; ++cur_y) { - set(cur_x, cur_y, get(cur_x, cur_y)); - } - } - } - - void toggle(std::size_t cur_x, std::size_t cur_y) { set(cur_x, cur_y, !get(cur_x, cur_y)); } - - void press(std::size_t cur_x, std::size_t cur_y) { - ++move_count; - toggle(cur_x, cur_y); - if (cur_x > 0) { - toggle(cur_x - 1, cur_y); - } - if (cur_y > 0) { - toggle(cur_x, cur_y - 1); - } - if (cur_x < width - 1) { - toggle(cur_x + 1, cur_y); - } - if (cur_y < height - 1) { - toggle(cur_x, cur_y + 1); - } - } - - [[nodiscard]] bool solved() const { - for (std::size_t cur_x = 0; cur_x < width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height; ++cur_y) { - if (!get(cur_x, cur_y)) { - return false; - } - } - } - - return true; - } -}; - -void consequence_game() { - auto screen = ftxui::ScreenInteractive::TerminalOutput(); - - GameBoard<3, 3> game_board; - - std::string quit_text; - - const auto update_quit_text = [&quit_text](const auto& game_board_param) { - quit_text = fmt::format("Quit ({} moves)", game_board_param.move_count); - if (game_board_param.solved()) { - quit_text += " Solved!"; - } - }; - - const auto make_buttons = [&] { - std::vector buttons; - for (std::size_t cur_x = 0; cur_x < game_board.width; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < game_board.height; ++cur_y) { - buttons.push_back( - ftxui::Button(&game_board.get_string(cur_x, cur_y), [=, &game_board] { - if (!game_board.solved()) { - game_board.press(cur_x, cur_y); - } - update_quit_text(game_board); - })); - } - } - return buttons; - }; - - auto buttons = make_buttons(); - - auto quit_button = ftxui::Button(&quit_text, screen.ExitLoopClosure()); - - auto make_layout = [&] { - std::vector rows; - - std::size_t idx = 0; - - for (std::size_t cur_x = 0; cur_x < game_board.width; ++cur_x) { - std::vector row; - for (std::size_t cur_y = 0; cur_y < game_board.height; ++cur_y) { - row.push_back(buttons[idx]->Render()); - ++idx; - } - rows.push_back(ftxui::hbox(std::move(row))); - } - - rows.push_back(ftxui::hbox({quit_button->Render()})); - - return ftxui::vbox(std::move(rows)); - }; - - static constexpr int randomization_iterations = 100; - static constexpr int random_seed = 42; - - std::mt19937 gen32{random_seed}; // NOLINT fixed seed - - // NOLINTNEXTLINE This cannot be const - std::uniform_int_distribution cur_x(static_cast(0), - game_board.width - 1); - // NOLINTNEXTLINE This cannot be const - std::uniform_int_distribution cur_y(static_cast(0), - game_board.height - 1); - - for (int i = 0; i < randomization_iterations; ++i) { - game_board.press(cur_x(gen32), cur_y(gen32)); - } - game_board.move_count = 0; - update_quit_text(game_board); - - auto all_buttons = buttons; - all_buttons.push_back(quit_button); - auto container = ftxui::Container::Horizontal(all_buttons); - - auto renderer = ftxui::Renderer(container, make_layout); - - screen.Loop(renderer); -} - -struct Color { - lefticus::tools::uint_np8_t R{static_cast(0)}; - lefticus::tools::uint_np8_t G{static_cast(0)}; - lefticus::tools::uint_np8_t B{static_cast(0)}; -}; - -// A simple way of representing a bitmap on screen using only characters -struct Bitmap : ftxui::Node { - Bitmap(std::size_t width, - std::size_t height) // NOLINT same typed parameters adjacent to each other - : width_(width), height_(height) {} - - Color& at(std::size_t cur_x, std::size_t cur_y) { return pixels.at(width_ * cur_y + cur_x); } - - void ComputeRequirement() override { - requirement_ = ftxui::Requirement{.min_x = static_cast(width_), - .min_y = static_cast(height_ / 2), - .selected_box{0, 0, 0, 0}}; - } - - void Render(ftxui::Screen& screen) override { - for (std::size_t cur_x = 0; cur_x < width_; ++cur_x) { - for (std::size_t cur_y = 0; cur_y < height_ / 2; ++cur_y) { - auto& pixel = screen.PixelAt(box_.x_min + static_cast(cur_x), - box_.y_min + static_cast(cur_y)); - pixel.character = "▄"; - const auto& top_color = at(cur_x, cur_y * 2); - const auto& bottom_color = at(cur_x, cur_y * 2 + 1); - pixel.background_color = - ftxui::Color{top_color.R.get(), top_color.G.get(), top_color.B.get()}; - pixel.foreground_color = - ftxui::Color{bottom_color.R.get(), bottom_color.G.get(), bottom_color.B.get()}; - } - } - } - - [[nodiscard]] auto width() const noexcept { return width_; } - - [[nodiscard]] auto height() const noexcept { return height_; } - - [[nodiscard]] auto& data() noexcept { return pixels; } - -private: - std::size_t width_; - std::size_t height_; - - std::vector pixels = std::vector(width_ * height_, Color{}); -}; - -void game_iteration_canvas() { - // this should probably have a `bitmap` helper function that does what cur_you expect - // similar to the other parts of FTXUI - auto bm = std::make_shared(50, 50); // NOLINT magic numbers - auto small_bm = std::make_shared(6, 6); // NOLINT magic numbers - - double fps = 0; - - std::size_t max_row = 0; - std::size_t max_col = 0; - - // to do, add total game time clock also, not just current elapsed time - auto game_iteration = [&](const std::chrono::steady_clock::duration elapsed_time) { - // in here we simulate however much game time has elapsed. Update animations, - // run character AI, whatever, update stats, etc - - // this isn't actually timing based for now, it's just updating the display however fast it - // can - fps = 1.0 / - (static_cast( - std::chrono::duration_cast(elapsed_time).count()) / - 1'000'000.0); // NOLINT magic numbers - - for (std::size_t row = 0; row < max_row; ++row) { - for (std::size_t col = 0; col < bm->width(); ++col) { - ++(bm->at(col, row).R); - } - } - - for (std::size_t row = 0; row < bm->height(); ++row) { - for (std::size_t col = 0; col < max_col; ++col) { - ++(bm->at(col, row).G); - } - } - - // for the fun of it, let's have a second window doing interesting things - auto& small_bm_pixel = small_bm->data().at(static_cast(elapsed_time.count()) % - small_bm->data().size()); - - switch (elapsed_time.count() % 3) { - case 0: - small_bm_pixel.R += 11; // NOLINT Magic Number - break; - case 1: - small_bm_pixel.G += 11; // NOLINT Magic Number - break; - case 2: - small_bm_pixel.B += 11; // NOLINT Magic Number - break; - } - - ++max_row; - if (max_row >= bm->height()) { - max_row = 0; - } - ++max_col; - if (max_col >= bm->width()) { - max_col = 0; - } - }; - - auto screen = ftxui::ScreenInteractive::TerminalOutput(); - - int counter = 0; - - auto last_time = std::chrono::steady_clock::now(); - - auto make_layout = [&] { - // This code actually processes the draw event - const auto new_time = std::chrono::steady_clock::now(); - - ++counter; - // we will dispatch to the game_iteration function, where the work happens - game_iteration(new_time - last_time); - last_time = new_time; - - // now actually draw the game elements - return ftxui::hbox( - {bm | ftxui::border, - ftxui::vbox({ftxui::text("Frame: " + std::to_string(counter)), - ftxui::text("FPS: " + std::to_string(fps)), small_bm | ftxui::border})}); - }; - - auto renderer = ftxui::Renderer(make_layout); - - std::atomic refresh_ui_continue = true; - - // This thread exists to make sure that the event queue has an event to - // process at approximately a rate of 30 FPS - std::thread refresh_ui([&] { - while (refresh_ui_continue) { - using namespace std::chrono_literals; - std::this_thread::sleep_for(1.0s / 30.0); // NOLINT magic numbers - screen.PostEvent(ftxui::Event::Custom); - } - }); - - screen.Loop(renderer); - - refresh_ui_continue = false; - refresh_ui.join(); -} - -// NOLINTNEXTLINE(bugprone-exception-escape) -int main(int argc, const char** argv) { - try { - CLI::App app{fmt::format("{} version {}", chains::cmake::project_name, - chains::cmake::project_version)}; - - std::optional message; - app.add_option("-m,--message", message, "A message to print back out"); - bool show_version = false; - app.add_flag("--version", show_version, "Show version information"); - - bool is_turn_based = false; - auto* turn_based = app.add_flag("--turn_based", is_turn_based); - - bool is_loop_based = false; - auto* loop_based = app.add_flag("--loop_based", is_loop_based); - - turn_based->excludes(loop_based); - loop_based->excludes(turn_based); - - CLI11_PARSE(app, argc, argv); - - if (show_version) { - fmt::print("{}\n", chains::cmake::project_version); - return EXIT_SUCCESS; - } - - if (is_turn_based) { - consequence_game(); - } else { - game_iteration_canvas(); - } - - } catch (const std::exception& e) { - spdlog::error("Unhandled exception in main: {}", e.what()); - } -} diff --git a/src/sample_library/CMakeLists.txt b/src/sample_library/CMakeLists.txt deleted file mode 100644 index c8fafce..0000000 --- a/src/sample_library/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -include(GenerateExportHeader) - - -add_library(sample_library sample_library.cpp) - - - -add_library(chains::sample_library ALIAS sample_library) - -target_link_libraries(sample_library PRIVATE chains_options chains_warnings) - -target_include_directories(sample_library ${WARNING_GUARD} PUBLIC $ - $) - -target_compile_features(sample_library PUBLIC cxx_std_20) - -set_target_properties( - sample_library - PROPERTIES VERSION ${PROJECT_VERSION} - CXX_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN YES) - -generate_export_header(sample_library EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/include/chains/sample_library_export.hpp) - -if(NOT BUILD_SHARED_LIBS) - target_compile_definitions(sample_library PUBLIC SAMPLE_LIBRARY_STATIC_DEFINE) -endif() diff --git a/src/sample_library/sample_library.cpp b/src/sample_library/sample_library.cpp deleted file mode 100644 index aaeb999..0000000 --- a/src/sample_library/sample_library.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include - -int factorial(int input) noexcept { - int result = 1; - - while (input > 0) { - result *= input; - --input; - } - - return result; -} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9394e30..a5e428e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,12 +29,12 @@ add_test(NAME cli.has_help COMMAND intro --help) add_test(NAME cli.version_matches COMMAND intro --version) set_tests_properties(cli.version_matches PROPERTIES PASS_REGULAR_EXPRESSION "${PROJECT_VERSION}") -add_executable(tests tuple_tests.cpp initial_draft.cpp) +add_executable(tests main.cpp tuple_tests.cpp initial_draft.cpp) target_link_libraries( tests PRIVATE chains::chains_warnings chains::chains_options - chains::sample_library + chains::chains stlab::stlab Catch2::Catch2WithMain) diff --git a/test/initial_draft.cpp b/test/initial_draft.cpp index 339ea8a..0d94ed6 100644 --- a/test/initial_draft.cpp +++ b/test/initial_draft.cpp @@ -3,21 +3,18 @@ #include -#include -#include - -#include - -// temporary -#include #include #include +#include +#include +#include + #define STLAB_FWD(x) std::forward(x) /* -If exception inside of a segment _apply_ function throws an exception then the exception must be +If exception inside a segment _apply_ function throws an exception then the exception must be set on the receiver. */ @@ -26,25 +23,54 @@ namespace chains::inline v1 { /* segment is invoked with a receiver - +*/ +template +struct receiver_ref { + Receiver* _receiver; + void operator()(auto&&... args) { + _receiver->operator()(std::forward(args)...); + } + void set_exception(std::exception_ptr p) { _receiver->set_exception(p); } + bool canceled() const { return _receiver->canceled(); } +}; +template +struct type {}; -*/ +template +class segment; + +#if 0 +template +inline auto make_segment(Applicator&& apply, Fs&&... fs) { + return segment, std::decay_t...>{ + std::forward(apply), std::forward(fs)...}; +} +#endif -template +template class segment { std::tuple _functions; Applicator _apply; public: + /* + An apply operation may inject additional arguments into the segment. The plan is that the + receiver will get sent to apply and this is how cancellation tokens can be injected into an + operation. Something like `with_cancellation`. + + This feature is also used for the `then` operation where the resolve future is injected into + the segment. + */ template auto result_type_helper(Args&&... args) && { - return tuple_compose(std::move(_functions))(std::forward(args)...); + return tuple_compose_greedy(std::move(_functions))(std::forward(args)...); } - explicit segment(Applicator&& apply, std::tuple&& functions) + explicit segment(type, Applicator&& apply, std::tuple&& functions) : _functions{std::move(functions)}, _apply{std::move(apply)} {} - explicit segment(Applicator&& apply, Fs&&... functions) + explicit segment(type, Applicator&& apply, Fs&&... functions) : _functions{std::move(functions)...}, _apply{std::move(apply)} {} /* @@ -58,8 +84,9 @@ class segment { template auto append(F&& f) && { - return chains::segment{std::move(_apply), std::tuple_cat(std::move(_functions), - std::tuple{std::forward(f)})}; + return chains::segment{ + type{}, std::move(_apply), + std::tuple_cat(std::move(_functions), std::tuple{std::forward(f)})}; } #if 0 @@ -71,23 +98,23 @@ class segment { /* The apply function for a segment always returns void. - Invoke will check the receiver for cancelation - - If not conceled, apply(segement), cancelation is checked before execution of the segment - and any exception during the segment is propogated to the receiever. + Invoke will check the receiver for cancellation - + If not canceled, apply(segment), cancellation is checked before execution of the segment + and any exception during the segment is propagated to the receiver. */ template - void invoke(const R& receiver, Args&&... args) && { - if (receiver.canceled()) return; - - std::move(_apply)( - [_f = tuple_compose(std::move(_functions)), - _receiver = receiver](auto&&... args) mutable noexcept { - if (_receiver.canceled()) return; + auto invoke(R&& receiver, Args&&... args) && { + // TODO: must handle this cancel prior to invoking the segment. + // if (receiver.canceled()) return; + return std::move(_apply)( + [_f = tuple_compose_greedy(std::move(_functions)), + _receiver = std::forward(receiver)](T&&... args) mutable noexcept { + if (_receiver->canceled()) return; try { - std::move(_f)(std::forward(args)...); + std::move(_f)(std::forward(args)...); } catch (...) { - _receiver.set_exception(std::current_exception()); + _receiver->set_exception(std::current_exception()); } }, std::forward(args)...); @@ -99,39 +126,42 @@ namespace detail { /// Apply a recursive lambda to each element in the tuple-like Segments. template constexpr auto fold_over(Fold fold, Segments&& segments) { - return std::apply([fold](auto&&... links) { return fold(fold, STLAB_FWD(links)...); }, - STLAB_FWD(segments)); + return std::apply( + [fold](Links&&... links) mutable { + return fold(fold, std::forward(links)...); + }, + STLAB_FWD(segments)); } } // namespace detail -template -using segment_result_type = - decltype(std::declval().result_type_helper(std::declval()...)); - /* simplify this code by handing the multi-argument case earlier (somehow). */ -template +template class chain { Tail _tail; - segment _head; + segment _head; /// Return a lambda with the signature of /// head( tail( tail<1>( tail<0>( auto&& args... ) ) ) ) /// for computing the result type of this chain. - static consteval auto result_type_helper(Tail&& tail, segment&& head) { + static consteval auto result_type_helper(Tail&& tail, + segment&& head) { return detail::fold_over( - [](auto fold, auto&& first, auto&&... rest) { + []([[maybe_unused]] Fold fold, + First&& first, Rest&&... rest) { if constexpr (sizeof...(rest) == 0) { - return [_segment = STLAB_FWD(first)](auto&&... args) mutable { - return std::move(_segment).result_type_helper(STLAB_FWD(args)...); + return [_segment = std::forward(first)]( + Args&&... args) mutable { + return std::move(_segment).result_type_helper(std::forward(args)...); }; } else { - return [_segment = STLAB_FWD(first).append(fold(fold, STLAB_FWD(rest)...))]( - auto&&... args) mutable { - return std::move(_segment).result_type_helper(STLAB_FWD(args)...); + return [_segment = std::forward(first).append( + fold(fold, std::forward(rest)...))]( + Args&&... args) mutable { + return std::move(_segment).result_type_helper(std::forward(args)...); }; } }, @@ -139,30 +169,49 @@ class chain { } template - auto expand(const R& receiver) && { - return detail::fold_over( - [receiver](auto fold, auto&& first, auto&&... rest) { + auto expand(R&& receiver) && { + return detail::fold_over([_receiver = std::forward(receiver)] + ([[maybe_unused]] Fold fold, First&& first, + Rest&&... rest) mutable { if constexpr (sizeof...(rest) == 0) { - return [receiver, - _segment = STLAB_FWD(first).append(receiver)](auto&&... args) mutable { - return std::move(_segment).invoke(receiver, STLAB_FWD(args)...); + return [_receiver, + _segment = std::forward(first).append([_receiver](V&& val) { + _receiver->operator()(std::forward(val)); + })](Args&&... args) mutable { + return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } else { - return [receiver, _segment = STLAB_FWD(first).append( - fold(fold, STLAB_FWD(rest)...))](auto&&... args) mutable { - return std::move(_segment).invoke(receiver, STLAB_FWD(args)...); + return [_receiver, _segment = std::forward(first).append( + fold(fold, std::forward(rest)...))]( + Args&&... args) mutable { + return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } }, std::tuple_cat(std::move(_tail), std::tuple{std::move(_head)})); } + template + struct result_type_void_injects { + using type = decltype(result_type_helper( + std::declval(), + std::declval>())(std::declval()...)); + }; + + template + struct result_type_injects { + using type = decltype(result_type_helper( + std::declval(), std::declval>())( + std::declval(), std::declval()...)); + }; + public: template - using result_type = decltype(result_type_helper( - std::declval(), std::declval>())(std::declval()...)); + using result_type = std::conditional_t, + result_type_void_injects, + result_type_injects>::type; - explicit chain(Tail&& tail, segment&& head) + explicit chain(Tail&& tail, segment&& head) : _tail{std::move(tail)}, _head{std::move(head)} {} /* @@ -170,7 +219,7 @@ class chain { and see if the compiler gets it correct. */ - explicit chain(const chain&) = default; + chain(const chain&) = default; chain(chain&&) noexcept = default; chain& operator=(const chain&) = default; chain& operator=(chain&&) noexcept = default; @@ -181,37 +230,46 @@ class chain { return chains::chain{std::move(_tail), std::move(_head).append(std::forward(f))}; } - template - auto append(segment&& head) && { - return chains::chain{std::tuple_cat(std::move(_tail), std::make_tuple(std::move(_head))), + template + auto append(segment&& head) && { + return chains::chain{std::tuple_cat(std::move(_tail), std::tuple{std::move(_head)}), std::move(head)}; } + template + auto invoke(Receiver&& receiver, Args&&... args) && { + return std::move(*this).expand(std::forward(receiver))( + std::forward(args)...); + } + +#if 0 template - auto operator()(Args&&... args) && { + [[deprecated]] auto operator()(Args&&... args) && { using result_t = result_type; auto [receiver, future] = stlab::package(stlab::immediate_executor, std::identity{}); - (void)std::move(*this).expand(receiver)(std::forward(args)...); + invoke(std::move(receiver), std::forward(args)...); return std::move(future); } +#endif template friend auto operator|(chain&& c, F&& f) { return std::move(c).append(std::forward(f)); } - template - friend auto operator|(chain&& c, segment&& head) { + template + friend auto operator|(chain&& c, segment&& head) { return std::move(c).append(std::move(head)); } }; -template -chain(Tail&& tail, segment&& head) -> chain; +template +chain(Tail&& tail, segment&& head) + -> chain; -template -inline auto operator|(segment&& head, F&& f) { +template +auto operator|(segment&& head, F&& f) { return chain{std::tuple<>{}, std::move(head).append(std::forward(f))}; } @@ -224,16 +282,6 @@ inline auto operator|(segment&& head, F&& f) { namespace chains::inline v1 { -#if 0 -template -inline auto on(E&& executor) { - return segment{[_executor = std::forward(executor)](auto&& f, auto&&... args) mutable { - return stlab::async(std::move(_executor), std::forward(f), - std::forward(args)...); - }}; -} -#endif - /* Each segment invokes the next segment with result and returns void. Promise is bound to the @@ -241,114 +289,525 @@ last item in the chain as a segment. */ template -inline auto on(E&& executor) { - return segment{[_executor = std::forward(executor)](auto&& f, auto&&... args) mutable { - std::move(_executor)( - [_f = std::forward(f), - _args = std::tuple{std::forward(args)...}]() mutable noexcept { - std::apply(std::move(_f), std::move(_args)); - }); - return std::monostate{}; - }}; +auto on(E&& executor) { + return segment{type{}, + [_executor = std::forward(executor)]( + F&& f, Args&&... args) mutable { + std::move(_executor)( + [_f = std::forward(f), + _args = std::tuple{std::forward(args)...}]() mutable noexcept { + std::apply(std::move(_f), std::move(_args)); + }); + // return std::monostate{}; + }}; } -#if 0 +/* + The `then` algorithm takes a future and returns a segment (chain) that will schedule the + segment as a continuation of the future. + The segment returns void so the future is (kind of) detached - but this should be done + without the overhead of a future::detach. -/* - TODO: The ergonimics of chains are painful with three arguements. We could reduce to a single - argument or move to a concept? Here I really want the forward reference to be an rvalue ref. + How is cancellation handled here? Let's say we have this: + + `auto f = start(then(future));` - The implementation of sync_wait is complicated by the fact that the promise is currently hard/ - wired into the chain. sync_wait needs to be able to invoke the promise/receiver - _then_ flag - the condition that it is ready. + And we destruct f. We need to _delete_ the (detached) future. Where is this held? f is only + holding the promise. */ +template +auto then(F&& future) { + return chain{std::tuple<>{}, + segment{type::result_type>{}, + [_future = std::forward(future)](C&& continuation) mutable { + return std::move(_future).then(std::forward(continuation)); + }}}; +} + +// TODO: (sean-parent) - should we make this pipeable? +// TODO: (sean-parent) - fix case where invoke_t is void. + +template +inline auto start(Chain&& chain, Args&&... args) { + using result_t = Chain::template result_type; + using package_task_t = stlab::packaged_task; + + using invoke_t = decltype(std::forward(chain).invoke( + std::declval>>(), + std::forward(args)...)); + + auto shared_receiver = std::shared_ptr(); + + if constexpr (std::is_same_v) { + auto [receiver, future] = stlab::package( + stlab::immediate_executor, [](T&& val) { return std::forward(val); }); + + // Promote receiver to shared_ptr to extend lifetime beyond this scope and circumvent the + // move only capabilities of package_task. + shared_receiver = std::make_shared(std::move(receiver)); + + std::forward(chain).invoke(std::move(shared_receiver), std::forward(args)...); + + return std::move(future); + } else { + auto p = std::make_shared>(); + auto [receiver, future] = stlab::package( + stlab::immediate_executor, + [p](V&& value) { return std::forward(value); }); + + shared_receiver = std::make_shared(std::move(receiver)); + + *p = std::forward(chain).invoke(std::move(shared_receiver), + std::forward(args)...); + return std::move(future); + } +} + + +template +auto sync_wait(Chain&& chain, Args&&... args) { + using result_t = typename Chain::template result_type; + + struct receiver_t { + std::optional result; + std::exception_ptr error{nullptr}; + std::mutex m; + std::condition_variable cv; + + void operator()(result_t&& value) { + { + std::lock_guard lock(m); + result = std::move(value); + } + cv.notify_one(); + } + + void set_exception(std::exception_ptr p) { + { + std::lock_guard lock(m); + error = p; + } + cv.notify_one(); + } + + bool canceled() const { return false; } + }; -template -inline auto sync_wait(Chain&& chain) { /* - TODO: (sean-parent) - we should have an invoke awaiting parameterized on what we are waiting - The implementation of which would be used in stlab::await() and used here. With this - construct we don't spin up more than one thread (hmm, maybe we shouldn't?). + REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should + fix that but for now create a receiver-ref. */ - auto appended = std::forward(chain) | [&] - invoke_awaiting( - ); -} -#endif -#if 0 -inline auto apply() { - return segment{[](auto&& f, auto&&... args) { - return std::forward(f)(std::forward(args)...); - }}; -} + auto receiver = std::make_shared(); -template -inline auto then(F&& future) { - return segment{[_future = std::forward(future)](auto&& f) { - return std::move(_future).then(std::forward(f)); - }}; + std::forward(chain).invoke(receiver, std::forward(args)...); + + std::unique_lock lock(receiver->m); + receiver->cv.wait(lock, [&] { return receiver->result.has_value() || receiver->error; }); + + if (receiver->error) { + std::rethrow_exception(receiver->error); + } + return *receiver->result; } -#endif +/* + TODO: The ergonomics of chains are painful with three arguments. We could reduce to a + single argument or move to a concept? Here I really want the forward reference to be an + rvalue ref. + + The implementation of sync_wait is complicated by the fact that the promise is currently + hard/ wired into the chain. sync_wait needs to be able to invoke the promise/receiver - + _then_ flag the condition that it is ready. +*/ } // namespace chains::inline v1 //-------------------------------------------------------------------------------------------------- #include -#include #include +#include #include using namespace std; using namespace chains; using namespace stlab; -TEST_CASE("Initial draft", "[initial_draft]") { - auto a0 = on(default_executor) | [] { - cout << "Hello from thread: " << std::this_thread::get_id() << "\n"; - return 42; - }; +// Cancellation example - auto a1 = std::move(a0) | on(default_executor) | [](int x) { - cout << "received: " << x << " on thread: " << std::this_thread::get_id() << "\n"; - // throw std::runtime_error("test-exception"); - return "forwarding: " + std::to_string(x + 1); +struct cancellation_source { + struct state { + std::atomic_bool canceled{false}; }; + std::shared_ptr _state = std::make_shared(); + void cancel() const { _state->canceled.store(true, std::memory_order_relaxed); } +}; - cout << "Main thread: " << std::this_thread::get_id() << "\n"; - cout << "Ready to go async!\n"; +struct cancellation_token { + std::shared_ptr _state; + auto canceled() const { return _state->canceled.load(std::memory_order_relaxed); } +}; -#if 0 - auto a2 = then(std::move(a1)()) | [](std::string s){ - cout << s << "<-- \n"; - return 0; - }; -#endif +// Segment that injects a cancellation_token (Injects != void) +inline auto with_cancellation(cancellation_source src) { + return chains::segment{ + chains::type{}, + [_src = std::move(src)](F&& f, Args&&... args) mutable { + // Create token and forward it as first argument + cancellation_token token{_src._state}; + std::forward(f)(token, std::forward(args)...); + }}; +} -#if 0 +// executor variant that also injects the token and schedules asynchronously +template +auto on_with_cancellation(E&& executor, cancellation_source source) { + return chains::segment{ + chains::type{}, + [_executor = std::forward(executor), + _source = std::move(source)](F&& f, Args&&... args) mutable { + cancellation_token token{_source._state}; + std::move(_executor)( + [_f = std::forward(f), _token = token, + _args = std::tuple{std::forward(args)...}]() mutable noexcept { + std::apply( + [&_f, &_token](As&&... as) { + std::forward(_f)(_token, std::forward(as)...); + }, + std::move(_args)); + }); + }}; +} + +template + requires std::is_copy_constructible_v +struct split_state { + std::mutex _m; + std::condition_variable _cv; + std::optional _value; + std::exception_ptr _error{nullptr}; + std::vector> _continuations; + std::atomic_bool _started{false}; + std::atomic_bool _completed{false}; + + void set_value(T v) { + if (_completed.load(std::memory_order_acquire)) return; + { + std::lock_guard lk(_m); + _value.emplace(std::move(v)); + _completed.store(true, std::memory_order_release); + } + auto continuations = extract_continuations(); + for (auto& c : continuations) + c(*_value); + _cv.notify_all(); + } + + void set_exception(std::exception_ptr p) { + { + std::lock_guard lk(_m); + _error = p; + _completed.store(true, std::memory_order_release); + } + // auto continuations = extract_continuations(); + // for (auto& c : continuations) { + // // We skip invoking branch continuations on exception; if needed they can be + // // generalized to receive exception too. + // } + _cv.notify_all(); + } + + void add_continuation(std::function fn) { + std::unique_lock lk(_m); + if (_completed.load(std::memory_order_acquire) && _value) { + auto v = *_value; // copy out + lk.unlock(); + fn(v); + return; + } + _continuations.push_back(std::move(fn)); + } + +private: + std::vector> extract_continuations() { + std::vector> tmp; + std::swap(tmp, _continuations); + return tmp; + } +}; + +template +struct split_holder { + std::shared_ptr _upstream; + std::shared_ptr _state; + std::once_flag _init_once; + + explicit split_holder(Upstream&& u) : _upstream(std::make_shared(std::move(u))) {} + +private: + template + auto make_branch(F&& f) { + using upstream_t = Upstream; + + auto branch_segment = segment{ + type{}, + [this](Composed&& composed, + StartArgs&&... start_args) mutable { + using result_t = typename upstream_t::template result_type; + + // Allocate shared state only once (first branch start) + std::call_once(_init_once, + [this] { _state = std::make_shared>(); }); + + auto state = std::static_pointer_cast>(_state); + + // Register this branch's continuation + state->add_continuation( + [comp = std::forward(composed)](const result_t& v) mutable noexcept { + try { + comp(v); + } catch (...) { /* optional: log */ + } + }); + + // Start upstream only once + if (!state->_started.exchange(true, std::memory_order_acq_rel)) { + struct upstream_receiver { + std::shared_ptr> _s; + void operator()(result_t&& val) { _s->set_value(std::move(val)); } + void set_exception(std::exception_ptr p) { _s->set_exception(p); } + bool canceled() const { return false; } + }; + auto receiver = std::make_shared(); + receiver->_s = state; + // Move upstream here, because chain wants an rvalue and we only start once. + std::move(*_upstream).invoke(receiver, std::forward(start_args)...); + } + }, + std::forward(f)}; + + return chain{std::tuple<>{}, std::move(branch_segment)}; + } + +public: + template + auto fan(F&& f) & { + return make_branch(std::forward(f)); + } + template + auto fan(F&& f) && { + return make_branch(std::forward(f)); + } +}; + +template +auto split(Chain&& c) { + return split_holder{std::forward(c)}; +} + +template +struct split_holder_bound { + using upstream_result_t = typename Upstream::template result_type; + + std::shared_ptr _upstream; + std::tuple...> _bound_args; + std::shared_ptr> _state; + std::once_flag _start_once; + + explicit split_holder_bound(Upstream&& u, BoundArgs&&... args) + : _upstream(std::make_shared(std::move(u))), + _bound_args(std::forward(args)...), + _state(std::make_shared>()) {} + +private: + template + auto make_branch(F&& f) { + // Segment injects upstream_result_t so result_type<> with () sees correct type. + auto branch_segment = segment{ + type{}, + [this](Composed&& composed) mutable { + // Register branch continuation (called after upstream completes) + _state->add_continuation( + [comp = std::forward(composed)](const upstream_result_t& v) mutable { + try { + comp(v); + } catch (...) { /* optional branch error handling */ + } + }); + + // Start upstream only once + if (!_state->_started.exchange(true, std::memory_order_acq_rel)) { + struct upstream_receiver { + std::shared_ptr> _s; + void operator()(upstream_result_t&& val) { _s->set_value(std::move(val)); } + void set_exception(std::exception_ptr p) { _s->set_exception(p); } + bool canceled() const { return false; } + }; + auto receiver = std::make_shared(); + receiver->_s = _state; + + // Invoke upstream with bound arguments (no external start args) + std::apply( + [this, &receiver](auto&... args) { + std::move(*_upstream).invoke(receiver, args...); + }, + _bound_args); + } + }, + std::forward(f) // branch function (receives upstream_result_t injected as first arg) + }; + + return chain{std::tuple<>{}, std::move(branch_segment)}; + } + +public: + template + auto fan(F&& f) & { + return make_branch(std::forward(f)); + } + template + auto fan(F&& f) && { + return make_branch(std::forward(f)); + } +}; + +// Helper to build a bound split holder +template +auto split_bind(Chain&& c, Args&&... args) { + return split_holder_bound, std::decay_t...>{ + std::forward(c), std::forward(args)...}; +} + +TEST_CASE("Cancellation injection", "[initial_draft]") { { - auto f = std::move(a1)(); // start and cancel. - std::this_thread::sleep_for(1ns); + cancellation_source src; + + // Build a chain where the first function expects the token as first argument. + auto c = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 2; + } | [](int y) { return y + 10; }; // token only needed by first step + + auto f = start(std::move(c), 5); + REQUIRE(f.get_ready() == 20); // (5*2)+10 + + // Demonstrate cancel before start + src.cancel(); + auto c2 = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 3; + }; + auto f2 = start(std::move(c2), 7); + REQUIRE(f2.get_ready() == 0); } -#endif -#if 0 - // TODO: (sean-parent) await on a chain can be optimized. + //{ + // cancellation_source src; + + // // Build a chain where each function expects the token as first argument. + // // First function uses the token, returns an int. + // auto c = with_cancellation(src) | [](cancellation_token token, int x) { + // if (token.canceled()) return 0; + // return x * 2; + // } | [](int y) { return y + 10; }; // token only needed by first step + + // auto f = start(std::move(c), 5); + // REQUIRE(f.get_ready() == 20); // (5*2)+10 + //} +} + +// --- Example test demonstrating split --------------------------------------------------------- +TEST_CASE("Split fan-out", "[initial_draft]") { + auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; + auto splitter = split(std::move(base)); + auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; + auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); + + auto f_right = start(std::move(right), 10); + auto f_left = start(std::move(left), 5); + REQUIRE(f_right.get_ready() == std::string("v=15")); + REQUIRE(f_left.get_ready() == 31); +} + +TEST_CASE("Split fan-out bound", "[initial_draft]") { + auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; - try { - std::cout << any_cast(await(std::move(a1)())) << "\n"; - } catch(const std::exception& error) { - std::cout << "exception: " << error.what() << "\n"; + // Bind upstream start argument 10 once: + auto splitter = split_bind(std::move(base), 10); + + // Branches now start with no args; upstream result (15) is injected. + auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; + auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); + + auto f_right = start(std::move(right)); // no argument + auto f_left = start(std::move(left)); // no argument + + REQUIRE(f_right.get_ready() == std::string("v=15")); + REQUIRE(f_left.get_ready() == 31); +} + +TEST_CASE("Initial draft", "[initial_draft]") { + GIVEN("a sequence of callables with different arguments") { + auto oneInt2Int = [](int a) { return a * 2; }; + auto twoInt2Int = [](int a, int b) { return a + b; }; + auto void2Int = []() { return 42; }; + + auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; + + auto f = start(std::move(a0), 2); + REQUIRE(f.is_ready()); + auto val = f.get_ready(); + REQUIRE(46 == val); } -#endif - // std::this_thread::sleep_for(3s); + GIVEN("a sequence of callables that just work with move only value") { + auto oneInt2Int = [](move_only a) { return move_only(a.member() * 2); }; + auto twoInt2Int = [](move_only a, move_only b) { + return move_only(a.member() + b.member()); + }; + auto void2Int = []() { return move_only(42); }; + + auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; - std::cout << await(std::move(a1)()) << "\n"; + auto f = start(std::move(a0), move_only(2)); + REQUIRE(f.is_ready()); + auto val = std::move(f).get_ready(); + REQUIRE(46 == val.member()); + } + + GIVEN("a sequence of callables in a chain of chains synchronous") { + auto a0 = on(immediate_executor) | [](int x) { return x * 2; } | on(immediate_executor) | [](int x) { return to_string(x); } | + on(immediate_executor) | [](const string& s) { return s + "!"; }; + + auto f = start(std::move(a0), 42); + auto val = f.get_ready(); + REQUIRE(val == string("84!")); + } + + GIVEN("a sequence of callables in a chain of chains asynchronous") { + auto a0 = on(default_executor) | [](int x) { return x * 2; } | on(immediate_executor) | [](int x) { return to_string(x); } | + on(default_executor) | [](const string& s) { return s + "!"; }; - pre_exit(); + auto val = sync_wait(std::move(a0), 42); + REQUIRE(val == string("84!")); + } } + + +TEST_CASE("Cancellation of then()", "[initial_draft]") { + annotate_counters cnt; + GIVEN("that a ") { + auto fut = async(default_executor, [] { + std::this_thread::sleep_for(std::chrono::seconds{3}); + std::cout << "Future did run" << std::endl; + return std::string("42"); + }).then([_counter = annotate{cnt}](const auto& s) { std::cout << s << std::endl; }); + + auto result_f = start(then(fut)); + } + std::this_thread::sleep_for(std::chrono::seconds{5}); + std::cout << cnt << std::endl; +} \ No newline at end of file diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..5e1cf68 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,13 @@ +#define CATCH_CONFIG_RUNNER + +#include + +#include + +int main(int argc, char** argv) { + int result = Catch::Session().run(argc, argv); + + stlab::pre_exit(); + + return result; +} \ No newline at end of file diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index db7751c..58015e0 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -6,10 +6,58 @@ #include #include -#include - TEST_CASE("Test tuple compose", "[tuple_compose]") { std::tuple t{[](int x) { return x + 1.0; }, [](double x) { return x * 2.0; }, [](double x) { return std::to_string(x / 2.0); }}; - REQUIRE(chains::tuple_compose(std::move(t))(1) == "2.000000"); + auto f = chains::tuple_compose(std::move(t)); + REQUIRE(f(1) == "2.000000"); +} + +struct multi_callable { + int operator()(int a, float b) const { return a + static_cast(b); } + int operator()(int a, float b, int c) const { return a + static_cast(b) + c; } +}; + +struct void_t {}; + +TEST_CASE("Test tuple consume", "[tuple_consume]") { + + GIVEN("A tuple of mixed types") + { + std::tuple t{1, 2.0f, 3, 4.0f}; + + THEN("it returns the correct result for a lambda with two parameters") + { + auto func = [](int a, float b) { return a + static_cast(b); }; + auto result = chains::tuple_consume(t)(func); + REQUIRE((result == std::make_tuple(3, 3, 4.0f))); + } + THEN("it returns the correct result for a lambda with no parameters") { + auto func = []() { return 42; }; + auto result = chains::tuple_consume(t)(func); + REQUIRE((result == std::make_tuple(42, 1, 2.0f, 3, 4.0f))); + } + THEN("it returns the correct result for a callable with various call operators") { + auto result = chains::tuple_consume(t)(multi_callable{}); + REQUIRE((result == std::make_tuple(6, 4.0f))); + } + } +} + + +TEST_CASE("Test concat functions", "[tuple_consume]") { + + GIVEN("A tuple of mixed types") + { + auto oneInt2Int = [](int a) { return a * 2; }; + auto twoInt2Int = [](int a, int b) { return a + b; }; + auto void2Int = []() { return 42; }; + auto functions = std::make_tuple(oneInt2Int, void2Int, twoInt2Int); + + GIVEN("the calculation is done") + { + auto result = chains::calc(functions, 2); + REQUIRE(46 == result); + } + } }