diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dddbc4fe..563db649 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,7 +158,7 @@ Match the assertion to the tested property: * **Conditional Specifications**: Pair negative assertions with their positive converses (e.g., pair `static_assert(not noexcept(expr))` with a witness for which it is `noexcept`). This proves that the specification is conditional rather than unconditionally false. Apply the same positive-control discipline to negative constraint or viability probes. * **Substitution Contexts**: Ensure expressions in negative viability probes are dependent on a template parameter. A `requires`-expression over concrete types is not a substitution context, meaning an invalid requirement triggers a hard error rather than a SFINAE failure. Enclose the dependent probe in a generic lambda or a type-keyed concept to yield `false` on invalid substitution. * **Exception Fixtures**: Select exception-proving fixtures carefully. The library's `helper_t` implements a separate constructor for each value category, and its non-const-lvalue copy constructor is always `noexcept`. A `helper_t` configured to throw is therefore not throwing for every value category, which can hide incorrect `noexcept` specifications. Use a simple, local type if the test requires every relevant constructor to be potentially throwing. -* **Expected Failures**: Compile-must-fail tests build excluded object targets through CTest with `WILL_FAIL`. Pair each rejection with a normal-build positive control using the same fixture and compiler configuration. Check rejection-fixture assumptions in a build that must succeed, with the rejected call omitted. The suite cannot test runtime crashes; record these untested cases beside their positive controls. +* **Expected Failures**: Put a statement that must not compile in a `tests/pfn/reject/` source, alone in an `#ifdef LIBFN_REJECT_` block, after its positive control: the same expression with a fixture differing only in what makes it fail. The build compiles each source as is, which must succeed; CTest compiles it once per guard with that guard defined, which must fail. The suite cannot test runtime crashes; record these untested cases beside their positive controls. ## Client code diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e1f6c31a..73aad5ea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,9 +57,63 @@ set(TESTS_PFN_SOURCES pfn/utility.cpp ) +set(TESTS_PFN_REJECT_SOURCES + pfn/reject/expected_monadic.cpp + pfn/reject/optional_monadic.cpp +) + include(TargetGenerator) include(CompilationOptions) +# Generate an object target for SOURCE, which must compile, and for each +# `#ifdef LIBFN_REJECT_` guard in it a test building SOURCE with only that guard defined, +# which must fail +function(add_reject_tests) + set(oneValueArgs SOURCE MODE) + set(multiValueArgs DEPENDENCIES LABELS) + cmake_parse_arguments(Reject "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(path "${CMAKE_CURRENT_SOURCE_DIR}/${Reject_SOURCE}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${path}") + file(STRINGS "${path}" guards REGEX "^#ifdef LIBFN_REJECT_[A-Za-z0-9_]+$") + file(STRINGS "${path}" directives REGEX "^[ \t]*#.*LIBFN_REJECT_") + if(NOT guards OR NOT guards STREQUAL directives) + message(FATAL_ERROR "${Reject_SOURCE}: needs at least one guard, each spelled `#ifdef LIBFN_REJECT_`") + endif() + list(TRANSFORM guards REPLACE "^#ifdef LIBFN_REJECT_" "" OUTPUT_VARIABLE scenarios) + + string(REGEX REPLACE "\\.cpp$" "" root_name "${Reject_SOURCE}") + string(REPLACE "/" "_" root_name "${root_name}") + set(control "tests_${root_name}_cxx${Reject_MODE}") + add_library("${control}" OBJECT "${Reject_SOURCE}") + add_dependencies("cxx${Reject_MODE}" "${control}") + add_dependencies("tests" "${control}") + set(targets "${control}") + + foreach(scenario IN LISTS scenarios) + set(target "tests_${root_name}_${scenario}_cxx${Reject_MODE}") + add_library("${target}" OBJECT EXCLUDE_FROM_ALL "${Reject_SOURCE}") + target_compile_definitions("${target}" PRIVATE "LIBFN_REJECT_${scenario}") + + add_test( + NAME "${target}" + COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" --config "$" --target "${target}" + ) + # Concurrent builds in one tree would race on its build-system state + set_tests_properties("${target}" PROPERTIES + WILL_FAIL TRUE + RESOURCE_LOCK build_tree + LABELS "${Reject_LABELS};cxx${Reject_MODE};${root_name}") + list(APPEND targets "${target}") + endforeach() + + foreach(target IN LISTS targets) + target_link_libraries("${target}" PRIVATE ${Reject_DEPENDENCIES}) + append_compilation_options("${target}" WARNINGS) + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${Reject_MODE}") + endforeach() +endfunction() + # Generate separate target for each individual test source foreach(mode 20 23 26) if (NOT VALIDATE_CXX23 AND mode EQUAL 23) @@ -75,73 +129,6 @@ foreach(mode 20 23 26) continue() endif() - foreach(scenario RANGE 1 9) - set(rejection_target "tests_optional_monadic_reject_${scenario}_cxx${mode}") - set(control_target "tests_optional_monadic_control_${scenario}_cxx${mode}") - set(fixture_target "tests_optional_monadic_fixture_${scenario}_cxx${mode}") - add_library("${rejection_target}" OBJECT EXCLUDE_FROM_ALL compile_fail/optional_monadic.cpp) - add_library("${control_target}" OBJECT compile_fail/optional_monadic.cpp) - add_library("${fixture_target}" OBJECT compile_fail/optional_monadic.cpp) - foreach(target IN ITEMS "${rejection_target}" "${control_target}" "${fixture_target}") - target_link_libraries("${target}" PRIVATE include_pfn) - append_compilation_options("${target}" WARNINGS) - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") - target_compile_definitions("${target}" PRIVATE OPTIONAL_MONADIC_CASE=${scenario}) - endforeach() - target_compile_definitions("${rejection_target}" PRIVATE OPTIONAL_MONADIC_POSITIVE=0) - target_compile_definitions("${control_target}" PRIVATE OPTIONAL_MONADIC_POSITIVE=1) - target_compile_definitions("${fixture_target}" PRIVATE - OPTIONAL_MONADIC_POSITIVE=0 OPTIONAL_MONADIC_FIXTURE_ONLY=1) - add_dependencies("tests" "${control_target}" "${fixture_target}") - add_dependencies("cxx${mode}" "${control_target}" "${fixture_target}") - add_test(NAME "${rejection_target}" - COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" - --config "$" --target "${rejection_target}") - set_tests_properties("${rejection_target}" PROPERTIES - WILL_FAIL TRUE RUN_SERIAL TRUE - LABELS "tests_pfn;cxx${mode};pfn_optional;compile_fail") - unset(rejection_target) - unset(control_target) - unset(fixture_target) - endforeach() - - foreach(kind IN ITEMS object void) - foreach(operation RANGE 0 3) - foreach(direction RANGE 0 4) - if(kind STREQUAL "void" AND operation LESS 2 AND direction GREATER 0) - continue() - endif() - set(test_stem "tests_expected_monadic_${kind}_${operation}_${direction}_cxx${mode}") - foreach(variant IN ITEMS reject control fixture) - set(target "${test_stem}_${variant}") - if(variant STREQUAL "reject") - add_library("${target}" OBJECT EXCLUDE_FROM_ALL compile_fail/expected_monadic.cpp) - else() - add_library("${target}" OBJECT compile_fail/expected_monadic.cpp) - add_dependencies("tests" "${target}") - add_dependencies("cxx${mode}" "${target}") - endif() - target_link_libraries("${target}" PRIVATE include_pfn) - append_compilation_options("${target}" WARNINGS) - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") - target_compile_definitions("${target}" PRIVATE - EXPECTED_MONADIC_VOID=$ EXPECTED_MONADIC_OP=${operation} - EXPECTED_MONADIC_DIRECTION=${direction} - EXPECTED_MONADIC_POSITIVE=$ - EXPECTED_MONADIC_FIXTURE_ONLY=$) - endforeach() - add_test(NAME "${test_stem}_reject" - COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" - --config "$" --target "${test_stem}_reject") - set_tests_properties("${test_stem}_reject" PROPERTIES - WILL_FAIL TRUE RUN_SERIAL TRUE - LABELS "tests_pfn;cxx${mode};pfn_expected;compile_fail") - unset(target) - unset(test_stem) - endforeach() - endforeach() - endforeach() - foreach(source IN ITEMS ${TESTS_PFN_SOURCES}) string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) set(target "tests_${root_name}_cxx${mode}") @@ -167,6 +154,10 @@ foreach(mode 20 23 26) unset(target) unset(root_name) endforeach() + + foreach(source IN ITEMS ${TESTS_PFN_REJECT_SOURCES}) + add_reject_tests(SOURCE "${source}" MODE "${mode}" DEPENDENCIES include_pfn LABELS tests_pfn) + endforeach() endforeach() ### tests/fn diff --git a/tests/compile_fail/expected_monadic.cpp b/tests/compile_fail/expected_monadic.cpp deleted file mode 100644 index 1c8e383b..00000000 --- a/tests/compile_fail/expected_monadic.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2026 Bronek Kozicki -// -// Distributed under the ISC License. See accompanying file LICENSE.md -// or copy at https://opensource.org/licenses/ISC - -#include - -#include -#include - -#if EXPECTED_MONADIC_VOID -using value_type = void; -#else -using value_type = int; -#endif -using subject_type = pfn::expected; - -#if EXPECTED_MONADIC_OP == 0 -using result_type = pfn::expected; -#define EXPECTED_MONADIC_CALL and_then -#elif EXPECTED_MONADIC_OP == 1 -using result_type = int; -#define EXPECTED_MONADIC_CALL transform -#elif EXPECTED_MONADIC_OP == 2 -using result_type = pfn::expected; -#define EXPECTED_MONADIC_CALL or_else -#else -using result_type = int; -#define EXPECTED_MONADIC_CALL transform_error -#endif - -#if EXPECTED_MONADIC_DIRECTION == 0 - -struct callback { -#if EXPECTED_MONADIC_POSITIVE -#if EXPECTED_MONADIC_VOID && EXPECTED_MONADIC_OP <= 1 - result_type operator()() const { return {}; } -#else - result_type operator()(int &) const { return {}; } -#endif -#endif -}; - -#if EXPECTED_MONADIC_VOID && EXPECTED_MONADIC_OP <= 1 -static_assert(std::is_invocable_v == bool(EXPECTED_MONADIC_POSITIVE)); -#else -static_assert(std::is_invocable_v == bool(EXPECTED_MONADIC_POSITIVE)); -static_assert(!std::is_invocable_v); -static_assert(!std::is_invocable_v); -static_assert(!std::is_invocable_v); -#endif - -#if !EXPECTED_MONADIC_FIXTURE_ONLY -template -concept can_call = requires(subject_type value, F f) { value.EXPECTED_MONADIC_CALL(f); }; - -// Invalid invocation is a hard error even inside a dependent requires-expression. -static_assert(can_call == bool(EXPECTED_MONADIC_POSITIVE)); -#endif - -#else - -struct callback { -#if EXPECTED_MONADIC_DIRECTION == 1 && !EXPECTED_MONADIC_POSITIVE - result_type operator()(int &) const = delete; -#else - result_type operator()(int &) const { return {}; } -#endif - result_type operator()(int const &) const { return {}; } -#if (EXPECTED_MONADIC_DIRECTION == 2 || EXPECTED_MONADIC_DIRECTION == 3) && !EXPECTED_MONADIC_POSITIVE - result_type operator()(int &&) const = delete; -#else - result_type operator()(int &&) const { return {}; } -#endif -#if (EXPECTED_MONADIC_DIRECTION == 3 || EXPECTED_MONADIC_DIRECTION == 4) && !EXPECTED_MONADIC_POSITIVE - result_type operator()(int const &&) const = delete; -#else - result_type operator()(int const &&) const { return {}; } -#endif -}; - -#if EXPECTED_MONADIC_DIRECTION == 1 -using selected_arg = int &; -using fallback_arg = int const &; -#elif EXPECTED_MONADIC_DIRECTION == 2 -using selected_arg = int &&; -using fallback_arg = int const &&; -#elif EXPECTED_MONADIC_DIRECTION == 3 -using selected_arg = int &&; -using fallback_arg = int const &; -#else -using selected_arg = int const &&; -using fallback_arg = int const &; -#endif - -static_assert(std::is_invocable_v); -static_assert(std::is_invocable_v == bool(EXPECTED_MONADIC_POSITIVE)); -#if EXPECTED_MONADIC_DIRECTION == 3 -static_assert(std::is_invocable_v == bool(EXPECTED_MONADIC_POSITIVE)); -#endif - -#if !EXPECTED_MONADIC_FIXTURE_ONLY -// The selected overload must reject the call instead of falling back to another cv/ref category. -void probe_expected_monadic() -{ -#if EXPECTED_MONADIC_DIRECTION == 1 - subject_type value; - (void)value.EXPECTED_MONADIC_CALL(callback{}); -#elif EXPECTED_MONADIC_DIRECTION == 4 - subject_type const value; - (void)std::move(value).EXPECTED_MONADIC_CALL(callback{}); -#else - subject_type value; - (void)std::move(value).EXPECTED_MONADIC_CALL(callback{}); -#endif -} -#endif - -#endif diff --git a/tests/compile_fail/optional_monadic.cpp b/tests/compile_fail/optional_monadic.cpp deleted file mode 100644 index 9e29b73d..00000000 --- a/tests/compile_fail/optional_monadic.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2026 Bronek Kozicki -// -// Distributed under the ISC License. See accompanying file LICENSE.md -// or copy at https://opensource.org/licenses/ISC - -#include - -#include -#include - -#if OPTIONAL_MONADIC_CASE == 9 - -struct callback { -#if OPTIONAL_MONADIC_POSITIVE - pfn::optional operator()(int &) const { return {}; } -#endif -}; - -static_assert(std::is_invocable_v == bool(OPTIONAL_MONADIC_POSITIVE)); - -#if !OPTIONAL_MONADIC_FIXTURE_ONLY -template -concept can_bind = requires(pfn::optional value, F f) { value.and_then(f); }; - -#if OPTIONAL_MONADIC_POSITIVE -static_assert(can_bind); -#else -// Invalid invocation is a hard error even inside a dependent requires-expression. -static_assert(!can_bind); -#endif -#endif - -#else - -#if OPTIONAL_MONADIC_CASE <= 4 -using result_type = int; -#define OPTIONAL_MONADIC_CALL transform -#else -using result_type = pfn::optional; -#define OPTIONAL_MONADIC_CALL and_then -#endif - -#define OPTIONAL_MONADIC_DIRECTION ((OPTIONAL_MONADIC_CASE - 1) % 4 + 1) - -struct callback { -#if OPTIONAL_MONADIC_DIRECTION == 1 && !OPTIONAL_MONADIC_POSITIVE - result_type operator()(int &) const = delete; -#else - result_type operator()(int &) const { return {}; } -#endif - result_type operator()(int const &) const { return {}; } -#if (OPTIONAL_MONADIC_DIRECTION == 2 || OPTIONAL_MONADIC_DIRECTION == 3) && !OPTIONAL_MONADIC_POSITIVE - result_type operator()(int &&) const = delete; -#else - result_type operator()(int &&) const { return {}; } -#endif -#if (OPTIONAL_MONADIC_DIRECTION == 3 || OPTIONAL_MONADIC_DIRECTION == 4) && !OPTIONAL_MONADIC_POSITIVE - result_type operator()(int const &&) const = delete; -#else - result_type operator()(int const &&) const { return {}; } -#endif -}; - -#if OPTIONAL_MONADIC_DIRECTION == 1 -using selected_arg = int &; -using fallback_arg = int const &; -#elif OPTIONAL_MONADIC_DIRECTION == 2 -using selected_arg = int &&; -using fallback_arg = int const &&; -#elif OPTIONAL_MONADIC_DIRECTION == 3 -using selected_arg = int &&; -using fallback_arg = int const &; -#else -using selected_arg = int const &&; -using fallback_arg = int const &; -#endif - -static_assert(std::is_invocable_v); -static_assert(std::is_invocable_v == bool(OPTIONAL_MONADIC_POSITIVE)); -#if OPTIONAL_MONADIC_DIRECTION == 3 -static_assert(std::is_invocable_v == bool(OPTIONAL_MONADIC_POSITIVE)); -#endif - -#if !OPTIONAL_MONADIC_FIXTURE_ONLY -// The selected overload must reject the call instead of falling back to another cv/ref category. -void probe_optional_monadic() -{ -#if OPTIONAL_MONADIC_DIRECTION == 1 - pfn::optional value; - (void)value.OPTIONAL_MONADIC_CALL(callback{}); -#elif OPTIONAL_MONADIC_DIRECTION == 4 - pfn::optional const value; - (void)std::move(value).OPTIONAL_MONADIC_CALL(callback{}); -#else - pfn::optional value; - (void)std::move(value).OPTIONAL_MONADIC_CALL(callback{}); -#endif -} -#endif - -#endif diff --git a/tests/pfn/optional.cpp b/tests/pfn/optional.cpp index 6651722b..fb00c9f5 100644 --- a/tests/pfn/optional.cpp +++ b/tests/pfn/optional.cpp @@ -1655,7 +1655,7 @@ TEST_CASE("optional", "[optional][polyfill]") static_assert(std::is_invocable_v); static_assert(not std::is_invocable_v); static_assert(not std::is_invocable_v); - // pfn/std owning rvalue calls with F are hard errors, covered in compile_fail/optional_monadic.cpp. + // pfn/std owning rvalue calls with F are hard errors, covered in reject/optional_monadic.cpp. constexpr auto check = [] { optional a(7); return a.and_then(F{}) == a && std::as_const(a).and_then(F{}) == a; @@ -1925,7 +1925,7 @@ TEST_CASE("optional", "[optional][polyfill]") static_assert(std::is_invocable_v); static_assert(not std::is_invocable_v); static_assert(not std::is_invocable_v); - // pfn/std owning rvalue calls with F are hard errors, covered in compile_fail/optional_monadic.cpp. + // pfn/std owning rvalue calls with F are hard errors, covered in reject/optional_monadic.cpp. constexpr auto check = [] { optional a(7); return a.transform(F{}) == a && std::as_const(a).transform(F{}) == a; diff --git a/tests/pfn/reject/expected_monadic.cpp b/tests/pfn/reject/expected_monadic.cpp new file mode 100644 index 00000000..f8595c28 --- /dev/null +++ b/tests/pfn/reject/expected_monadic.cpp @@ -0,0 +1,231 @@ +// Copyright (c) 2026 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#include "refusing.hpp" + +#include + +#include + +// A guard named _to_ holds a call on a expected whose callback refuses +// the value or error in that category: the call must not compile, rather than run the +// overload. + +template +concept can_and_then = requires(O o, F f) { o.and_then(f); }; +template +concept can_transform = requires(O o, F f) { o.transform(f); }; +template +concept can_or_else = requires(O o, F f) { o.or_else(f); }; +template +concept can_transform_error = requires(O o, F f) { o.transform_error(f); }; + +// As in the draft, a non-invocable callback is a hard error even in a dependent requires-expression. + +void and_then() +{ + using O = pfn::expected; + using R = pfn::expected; + O o; + O const c; + + (void)o.and_then(refusing{}); +#ifdef LIBFN_REJECT_and_then_lvalue_to_clvalue + (void)o.and_then(refusing{}); +#endif + + (void)std::move(o).and_then(refusing{}); +#ifdef LIBFN_REJECT_and_then_rvalue_to_crvalue + (void)std::move(o).and_then(refusing{}); +#endif +#ifdef LIBFN_REJECT_and_then_rvalue_to_clvalue + (void)std::move(o).and_then(refusing{}); +#endif + + (void)std::move(c).and_then(refusing{}); +#ifdef LIBFN_REJECT_and_then_crvalue_to_clvalue + (void)std::move(c).and_then(refusing{}); +#endif + + static_assert(can_and_then>); +#ifdef LIBFN_REJECT_and_then_not_invocable + (void)can_and_then; +#endif +} + +void transform() +{ + using O = pfn::expected; + O o; + O const c; + + (void)o.transform(refusing{}); +#ifdef LIBFN_REJECT_transform_lvalue_to_clvalue + (void)o.transform(refusing{}); +#endif + + (void)std::move(o).transform(refusing{}); +#ifdef LIBFN_REJECT_transform_rvalue_to_crvalue + (void)std::move(o).transform(refusing{}); +#endif +#ifdef LIBFN_REJECT_transform_rvalue_to_clvalue + (void)std::move(o).transform(refusing{}); +#endif + + (void)std::move(c).transform(refusing{}); +#ifdef LIBFN_REJECT_transform_crvalue_to_clvalue + (void)std::move(c).transform(refusing{}); +#endif + + static_assert(can_transform>); +#ifdef LIBFN_REJECT_transform_not_invocable + (void)can_transform; +#endif +} + +void or_else() +{ + using O = pfn::expected; + using R = pfn::expected; + O o; + O const c; + + (void)o.or_else(refusing{}); +#ifdef LIBFN_REJECT_or_else_lvalue_to_clvalue + (void)o.or_else(refusing{}); +#endif + + (void)std::move(o).or_else(refusing{}); +#ifdef LIBFN_REJECT_or_else_rvalue_to_crvalue + (void)std::move(o).or_else(refusing{}); +#endif +#ifdef LIBFN_REJECT_or_else_rvalue_to_clvalue + (void)std::move(o).or_else(refusing{}); +#endif + + (void)std::move(c).or_else(refusing{}); +#ifdef LIBFN_REJECT_or_else_crvalue_to_clvalue + (void)std::move(c).or_else(refusing{}); +#endif + + static_assert(can_or_else>); +#ifdef LIBFN_REJECT_or_else_not_invocable + (void)can_or_else; +#endif +} + +void transform_error() +{ + using O = pfn::expected; + O o; + O const c; + + (void)o.transform_error(refusing{}); +#ifdef LIBFN_REJECT_transform_error_lvalue_to_clvalue + (void)o.transform_error(refusing{}); +#endif + + (void)std::move(o).transform_error(refusing{}); +#ifdef LIBFN_REJECT_transform_error_rvalue_to_crvalue + (void)std::move(o).transform_error(refusing{}); +#endif +#ifdef LIBFN_REJECT_transform_error_rvalue_to_clvalue + (void)std::move(o).transform_error(refusing{}); +#endif + + (void)std::move(c).transform_error(refusing{}); +#ifdef LIBFN_REJECT_transform_error_crvalue_to_clvalue + (void)std::move(c).transform_error(refusing{}); +#endif + + static_assert(can_transform_error>); +#ifdef LIBFN_REJECT_transform_error_not_invocable + (void)can_transform_error; +#endif +} + +void void_and_then() +{ + using O = pfn::expected; + using R = pfn::expected; + using F = decltype([] { return R{}; }); + + static_assert(can_and_then); +#ifdef LIBFN_REJECT_void_and_then_not_invocable + (void)can_and_then; +#endif +} + +void void_transform() +{ + using O = pfn::expected; + using F = decltype([] { return 0; }); + + static_assert(can_transform); +#ifdef LIBFN_REJECT_void_transform_not_invocable + (void)can_transform; +#endif +} + +void void_or_else() +{ + using O = pfn::expected; + using R = pfn::expected; + O o; + O const c; + + (void)o.or_else(refusing{}); +#ifdef LIBFN_REJECT_void_or_else_lvalue_to_clvalue + (void)o.or_else(refusing{}); +#endif + + (void)std::move(o).or_else(refusing{}); +#ifdef LIBFN_REJECT_void_or_else_rvalue_to_crvalue + (void)std::move(o).or_else(refusing{}); +#endif +#ifdef LIBFN_REJECT_void_or_else_rvalue_to_clvalue + (void)std::move(o).or_else(refusing{}); +#endif + + (void)std::move(c).or_else(refusing{}); +#ifdef LIBFN_REJECT_void_or_else_crvalue_to_clvalue + (void)std::move(c).or_else(refusing{}); +#endif + + static_assert(can_or_else>); +#ifdef LIBFN_REJECT_void_or_else_not_invocable + (void)can_or_else; +#endif +} + +void void_transform_error() +{ + using O = pfn::expected; + O o; + O const c; + + (void)o.transform_error(refusing{}); +#ifdef LIBFN_REJECT_void_transform_error_lvalue_to_clvalue + (void)o.transform_error(refusing{}); +#endif + + (void)std::move(o).transform_error(refusing{}); +#ifdef LIBFN_REJECT_void_transform_error_rvalue_to_crvalue + (void)std::move(o).transform_error(refusing{}); +#endif +#ifdef LIBFN_REJECT_void_transform_error_rvalue_to_clvalue + (void)std::move(o).transform_error(refusing{}); +#endif + + (void)std::move(c).transform_error(refusing{}); +#ifdef LIBFN_REJECT_void_transform_error_crvalue_to_clvalue + (void)std::move(c).transform_error(refusing{}); +#endif + + static_assert(can_transform_error>); +#ifdef LIBFN_REJECT_void_transform_error_not_invocable + (void)can_transform_error; +#endif +} diff --git a/tests/pfn/reject/optional_monadic.cpp b/tests/pfn/reject/optional_monadic.cpp new file mode 100644 index 00000000..8a6ed58d --- /dev/null +++ b/tests/pfn/reject/optional_monadic.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#include "refusing.hpp" + +#include + +#include + +// A guard named _to_ holds a call on a optional whose callback refuses +// the payload in that category: the call must not compile, rather than run the overload. + +void transform() +{ + pfn::optional o; + pfn::optional const c; + + (void)o.transform(refusing{}); +#ifdef LIBFN_REJECT_transform_lvalue_to_clvalue + (void)o.transform(refusing{}); +#endif + + (void)std::move(o).transform(refusing{}); +#ifdef LIBFN_REJECT_transform_rvalue_to_crvalue + (void)std::move(o).transform(refusing{}); +#endif +#ifdef LIBFN_REJECT_transform_rvalue_to_clvalue + (void)std::move(o).transform(refusing{}); +#endif + + (void)std::move(c).transform(refusing{}); +#ifdef LIBFN_REJECT_transform_crvalue_to_clvalue + (void)std::move(c).transform(refusing{}); +#endif +} + +void and_then() +{ + using R = pfn::optional; + pfn::optional o; + pfn::optional const c; + + (void)o.and_then(refusing{}); +#ifdef LIBFN_REJECT_and_then_lvalue_to_clvalue + (void)o.and_then(refusing{}); +#endif + + (void)std::move(o).and_then(refusing{}); +#ifdef LIBFN_REJECT_and_then_rvalue_to_crvalue + (void)std::move(o).and_then(refusing{}); +#endif +#ifdef LIBFN_REJECT_and_then_rvalue_to_clvalue + (void)std::move(o).and_then(refusing{}); +#endif + + (void)std::move(c).and_then(refusing{}); +#ifdef LIBFN_REJECT_and_then_crvalue_to_clvalue + (void)std::move(c).and_then(refusing{}); +#endif +} + +template +concept can_and_then = requires(O o, F f) { o.and_then(f); }; + +// As in the draft, a non-invocable callback is a hard error even in a dependent requires-expression. +void reference_and_then() +{ + using O = pfn::optional; + static_assert(can_and_then>>); +#ifdef LIBFN_REJECT_reference_and_then_not_invocable + (void)can_and_then; +#endif +} diff --git a/tests/pfn/reject/refusing.hpp b/tests/pfn/reject/refusing.hpp new file mode 100644 index 00000000..a84314dd --- /dev/null +++ b/tests/pfn/reject/refusing.hpp @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#ifndef TESTS_PFN_REJECT_REFUSING +#define TESTS_PFN_REJECT_REFUSING + +#include + +// Callback accepting its argument in every cv/ref category except the Refused ones, which select a +// deleted overload instead of falling back to another category. +template struct refusing { + template R operator()(T &&) const { return R{}; } + template + requires(std::is_same_v || ...) + R operator()(T &&) const = delete; +}; + +static_assert(std::is_invocable_v, int &>); +static_assert(std::is_invocable_v, int const &>); +static_assert(std::is_invocable_v, int &&>); +static_assert(std::is_invocable_v, int const &&>); + +static_assert(not std::is_invocable_v, int &>); +static_assert(std::is_invocable_v, int const &>); +static_assert(not std::is_invocable_v, int &&>); +static_assert(std::is_invocable_v, int const &&>); +static_assert(not std::is_invocable_v, int &&>); +static_assert(not std::is_invocable_v, int const &&>); +static_assert(std::is_invocable_v, int const &>); +static_assert(not std::is_invocable_v, int const &&>); +static_assert(std::is_invocable_v, int const &>); + +#endif // TESTS_PFN_REJECT_REFUSING