From 25f3834ce776200195a001aa4944f47180bbd1e4 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 9 Aug 2026 20:39:04 -0400 Subject: [PATCH 1/6] test: add constant_eval and type_name test-support components Two header-only components that let a compile-time contract be reported by the test run instead of breaking the build. constant_eval(probe) is consteval, so calling it is an immediate invocation: the probe is evaluated during translation and its result is required to be a constant expression. That answers "is this constant-evaluable?" by construction, with no static_assert, while handing the result back as an ordinary prvalue that CHECK can compare and report. The probe is a plain lambda, so the same body can also be run at runtime -- constant evaluation and ordinary evaluation take different paths through a union-based type. type_name() returns the identity of T, compared with std::is_same_v, so a check is exactly as strict as the static_assert it replaces. The compiler's spelling is consulted only to explain a failure, turning "false" into "const int& == int&". It has two implementations of that spelling: P2996 reflection via std::meta::display_string_of when the compiler offers it, and otherwise recovery from std::source_location::function_name() by calibrating on the signatures of signature and signature, which differ only where the template argument is named. Nothing about any compiler's format is hard-coded, and self-checks hold whichever implementation is selected to the same contract. Note that __cpp_lib_reflection is a library macro: must be included before testing it, or the reflection path silently never activates. --- .../beman/expected/testing/constant_eval.hpp | 72 ++++++ tests/beman/expected/testing/type_name.hpp | 221 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 tests/beman/expected/testing/constant_eval.hpp create mode 100644 tests/beman/expected/testing/type_name.hpp diff --git a/tests/beman/expected/testing/constant_eval.hpp b/tests/beman/expected/testing/constant_eval.hpp new file mode 100644 index 0000000..50a1c77 --- /dev/null +++ b/tests/beman/expected/testing/constant_eval.hpp @@ -0,0 +1,72 @@ +// tests/beman/expected/testing/constant_eval.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Reporting compile-time facts through the runtime test framework. +// +// The usual way to test a constexpr contract is `static_assert`. That has one +// bad property as a *test*: a wrong answer is a translation failure, so the +// only thing anyone ever sees is a compiler diagnostic. The test run reports +// nothing, the xUnit output is empty, and because the build stops at the +// first failing assertion no other test in the file is exercised at all. +// +// `constant_eval` separates the two questions a constexpr test actually asks: +// +// 1. "can this be constant-evaluated at all?" — still a hard translation +// failure, because that is a property of the code, not of a value. +// 2. "does it produce the right answer?" — an ordinary runtime comparison +// inside a TEST_CASE, so a wrong answer is *reported*, with the actual +// and expected values, alongside every other test in the run. +// +// `constant_eval` is `consteval`, so a call to it is an immediate invocation: +// the probe is evaluated during translation and its result is required to be +// a constant expression. Question 1 is therefore answered by the call itself, +// with no `static_assert` needed — if the probe body is not usable in a +// constant expression, the program is ill-formed. The result then behaves as +// an ordinary prvalue, free to be compared by CHECK. +// +// TEST_CASE("expected: constexpr error construction") { +// constexpr auto probe = [] { +// expt::expected e(expt::unexpect, 7); +// return int_state{e.has_value(), e.error()}; +// }; +// CHECK(constant_eval(probe) == int_state{false, 7}); +// } +// +// A probe is a plain lambda, not a `consteval` one, so the same probe body +// can be run in both evaluation modes — constant evaluation and ordinary +// evaluation can take different paths through a union-based type: +// +// CHECK(constant_eval(probe) == expect); // constant evaluation +// CHECK(probe() == expect); // ordinary evaluation +// +// Two constraints on a probe follow from the above, and neither is arbitrary: +// +// - It takes no arguments and captures nothing. A closure over a runtime +// value is not a constant expression. Construct whatever the probe +// observes inside the probe. +// - It returns a literal type by value — a scalar, or a small aggregate of +// scalars — never a reference to something it created, and never a type +// whose value cannot escape constant evaluation (`std::string`, +// `std::vector`). Reduce such state to scalars inside the probe. +// +// Give the returned aggregate an `operator<<` so that a mismatch prints both +// states. Without one, Catch2 reports `{?} == {?}` and the reporting benefit +// this component exists for is lost. + +#ifndef BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP +#define BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP + +namespace beman::expected::testing { + +// Constant-evaluate `probe` and return its result as an ordinary value. +template +consteval auto constant_eval(Probe probe); + +} // namespace beman::expected::testing + +template +consteval auto beman::expected::testing::constant_eval(Probe probe) { + return probe(); +} + +#endif // BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP diff --git a/tests/beman/expected/testing/type_name.hpp b/tests/beman/expected/testing/type_name.hpp new file mode 100644 index 0000000..904c0bb --- /dev/null +++ b/tests/beman/expected/testing/type_name.hpp @@ -0,0 +1,221 @@ +// tests/beman/expected/testing/type_name.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Type identity as a value, reported by name. +// +// A type-identity check written as `static_assert(std::is_same_v)` is a +// translation failure that names neither type usefully; written as +// `CHECK(std::is_same_v)` it is reported, but the expansion is the bare +// word `false`. `type_name` gives both halves: +// +// FAILED: CHECK( type_name() == type_name() ) +// with expansion: const int& == int& +// +// `type_name()` returns the *identity* of T, not its spelling. Comparison +// is `std::is_same_v`, so the verdict is exact and a false pass is not +// possible. The spelling is consulted only when a comparison has already +// failed and the test framework needs to explain it. Two distinct types that +// the compiler happened to print identically would therefore be reported as +// `foo == foo` — a confusing explanation of a correct verdict, rather than +// the silent false pass that comparing spellings would have given. +// +// That collision is close to unreachable in practice — gcc distinguishes even +// unnamed-namespace and function-local classes by their enclosing scope — so +// this is exactness by construction, not a fix for an observed defect. The +// point is that the guarantee no longer depends on how good any particular +// compiler's names happen to be. +// +// `display_name()` is the spelling on its own, for when a test wants the +// string rather than the comparison. +// +// The spelling is implementation-defined under either implementation below — +// `int&` on one compiler, `int &` on another — so never compare a +// `display_name` against a string literal. +// +// There are two implementations of the spelling, selected automatically. Both +// satisfy the same contract — a non-empty spelling that distinguishes +// distinct types — and the self-checks at the bottom of this header hold each +// of them to it. Which one is in use never changes what a test looks like. +// +// Reflection (P2996), when the compiler offers it: +// +// std::meta::display_string_of(^^T) +// +// This is what reflection is for. It asks the compiler for the name directly +// instead of recovering it from a diagnostic string, so there is no parsing +// and nothing to calibrate. Note that `display_string_of` is itself specified +// as implementation-defined, so this buys robustness, not a canonical +// spelling. No `dealias` is needed: substituting a template argument already +// resolves an alias, so `display_name()` names the aliased type. +// +// Without reflection, the spelling is recovered from +// `std::source_location::function_name()` by calibration: the signatures of +// `signature` and `signature` differ only where the template +// argument is named, so the common prefix and common suffix of the two give +// the offsets at which any type's name sits. Nothing about any compiler's +// format is hard-coded. +// +// Define BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION to 0 or 1 to force +// one implementation, which is how the unselected one gets exercised. + +#ifndef BEMAN_EXPECTED_TESTING_TYPE_NAME_HPP +#define BEMAN_EXPECTED_TESTING_TYPE_NAME_HPP + +// `__cpp_impl_reflection` is predefined by the compiler, but +// `__cpp_lib_reflection` is a library macro: without first it is +// never defined here and the reflection path silently never activates. +#include + +#ifndef BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION + #if defined(__cpp_lib_reflection) && defined(__cpp_impl_reflection) + #define BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION 1 + #else + #define BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION 0 + #endif +#endif + +#include +#include +#include + +#if BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION + + #include + +#else + + #include + #include + +namespace beman::expected::testing::detail { + +// The compiler's spelling of this function, template argument included. +template +consteval std::string_view signature(); + +// Length of the longest common prefix of `a` and `b`. +consteval std::size_t common_prefix(std::string_view a, std::string_view b); + +// Length of the longest common suffix of `a` and `b`. +consteval std::size_t common_suffix(std::string_view a, std::string_view b); + +} // namespace beman::expected::testing::detail + +#endif + +namespace beman::expected::testing { + +// The compiler's spelling of `T`. +template +consteval std::string_view display_name(); + +// The identity of `T`, carrying its spelling for diagnostics. Tests do not +// name this type; they compare the results of `type_name`. +template +struct type_name_t { + // HIDDEN FRIENDS + // + // Reached only once a comparison has failed and the test framework is + // explaining it. Nothing about the verdict depends on this text. + friend std::ostream& operator<<(std::ostream& os, type_name_t) { + constexpr std::string_view name = display_name(); + return os << name; + } +}; + +// Exact type identity. The spelling is never consulted. +template +constexpr bool operator==(type_name_t, type_name_t); + +// The identity of `T`, for comparison in a test. +template +consteval type_name_t type_name(); + +} // namespace beman::expected::testing + +template +constexpr bool beman::expected::testing::operator==(type_name_t, type_name_t) { + return std::is_same_v; +} + +template +consteval beman::expected::testing::type_name_t beman::expected::testing::type_name() { + return {}; +} + +#if BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION + +template +consteval std::string_view beman::expected::testing::display_name() { + return std::meta::display_string_of(^^T); +} + +#else + +template +consteval std::string_view beman::expected::testing::detail::signature() { + return std::source_location::current().function_name(); +} + +consteval std::size_t beman::expected::testing::detail::common_prefix(std::string_view a, std::string_view b) { + std::size_t n = 0; + while (n < a.size() && n < b.size() && a[n] == b[n]) + ++n; + return n; +} + +consteval std::size_t beman::expected::testing::detail::common_suffix(std::string_view a, std::string_view b) { + std::size_t n = 0; + while (n < a.size() && n < b.size() && a[a.size() - 1 - n] == b[b.size() - 1 - n]) + ++n; + return n; +} + +template +consteval std::string_view beman::expected::testing::display_name() { + // `bool` and `char` share no leading and no trailing character, so + // neither contributes to the common prefix or the common suffix. Pairs + // that look equally good can fail: gcc spells `long` as `long int`, whose + // trailing `int` is also the whole of `int`, so `int`/`long` would fold + // part of the name itself into the common suffix. + constexpr std::string_view as_bool = detail::signature(); + constexpr std::string_view as_char = detail::signature(); + constexpr std::size_t prefix = detail::common_prefix(as_bool, as_char); + constexpr std::size_t suffix = detail::common_suffix(as_bool, as_char); + static_assert(prefix + suffix < as_bool.size(), + "display_name: this compiler's function_name() does not name the template argument"); + + const std::string_view signature = detail::signature(); + return signature.substr(prefix, signature.size() - prefix - suffix); +} + +#endif + +namespace beman::expected::testing { + +// Self-check on whichever spelling implementation was selected: a recovered +// name must be non-empty, and must not have had a cv-qualifier, a reference, +// or a pointer trimmed off either end. +static_assert(!display_name().empty()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); + +// Self-check on comparison. These cannot distinguish identity from spelling, +// because no pair of types can be written here where the two disagree: gcc +// qualifies even unnamed-namespace and function-local classes distinctly, so +// a spelling collision is not constructible within one translation unit. +// Exactness is a property of how the comparison is defined, not something +// these assertions can demonstrate. +static_assert(type_name() == type_name()); +static_assert(type_name() == type_name()); +static_assert(type_name() != type_name()); +static_assert(type_name() != type_name()); +static_assert(type_name() != type_name()); +static_assert(type_name() != type_name()); + +} // namespace beman::expected::testing + +#endif // BEMAN_EXPECTED_TESTING_TYPE_NAME_HPP From 8a0d655a0b83e70003a90e54b560b462310bd353 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 9 Aug 2026 20:39:18 -0400 Subject: [PATCH 2/6] build: enable -freflection in the gcc-16 toolchain Selects the P2996 reflection implementation of testing/type_name.hpp, which is otherwise dormant. This belongs in the per-version toolchain rather than cmake/gcc-flags.cmake, which is shared with gcc-12 through gcc-15: -freflection is a gcc-16 feature, and gcc rejects it outright below -std=c++26 rather than warning. gcc-flags.cmake already pins -std=gnu++26, so the requirement is met. It deliberately does not go in CMakePresets.json. The preset toolchains under infra/cmake set CMAKE_CXX_COMPILER to the unversioned g++, so a preset carrying this flag would break the build for anyone whose default compiler is not gcc-16 -- which is nearly everyone today. That leaves the reflection path without CI coverage; see bemanproject/expected#88. --- cmake/gcc-16-toolchain.cmake | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmake/gcc-16-toolchain.cmake b/cmake/gcc-16-toolchain.cmake index ae1bb01..b7310d3 100644 --- a/cmake/gcc-16-toolchain.cmake +++ b/cmake/gcc-16-toolchain.cmake @@ -15,3 +15,17 @@ set(CMAKE_CXX_FLAGS_ASAN "C++ ASAN Flags" FORCE ) + +# Reflection (P2996). This belongs here rather than in gcc-flags.cmake, which +# is shared with gcc-12 through gcc-15: -freflection is a gcc-16 feature, and +# gcc rejects it outright below -std=c++26 rather than warning. gcc-flags.cmake +# already pins -std=gnu++26, so the requirement is met. +# +# Enabling it selects the reflection implementation of +# tests/beman/expected/testing/type_name.hpp, which is otherwise dormant. +set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -freflection" + CACHE STRING + "CXX_FLAGS" + FORCE +) From 5f6a212b44822dfa7ca80230459fd134f033fd33 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 9 Aug 2026 20:39:18 -0400 Subject: [PATCH 3/6] test: convert expected.test.cpp off static_assert Establishes the convention the rest of the suite follows. A static_assert is a poor test: a wrong answer is a translation failure, so the only thing anyone sees is a compiler diagnostic. The test run reports nothing, the xUnit output is empty, and because the build stops at the first failing assertion no other test in the file is exercised at all. Type-level properties become ordinary CHECKs, and type identities become type_name comparisons, so a mismatch prints "const int& == int&" rather than "false". The four constexpr cases build their expected inside a self-contained probe, run it through constant_eval, and compare the resulting state through a streamable aggregate -- a wrong value now reports "{ error 7 } == { error 3 }" while the other 274 assertions in the file still run. Compile-time enforcement is not lost. Whether something can be constant- evaluated is still answered by the compiler, because constant_eval's call is an immediate invocation; only the value comparison moved to the test run. Also fixes the include block to use canonical spellings. --- tests/beman/expected/expected.test.cpp | 159 +++++++++++++++++-------- 1 file changed, 110 insertions(+), 49 deletions(-) diff --git a/tests/beman/expected/expected.test.cpp b/tests/beman/expected/expected.test.cpp index 8b4a0e0..7dedc6a 100644 --- a/tests/beman/expected/expected.test.cpp +++ b/tests/beman/expected/expected.test.cpp @@ -1,12 +1,15 @@ // beman/expected/expected.test.cpp -*-C++-*- // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -#include "test_expected.hpp" +#include #include -#include "testing/types.hpp" +#include +#include +#include +#include #include #include #include @@ -15,8 +18,11 @@ namespace expt = test_ns; +using beman::expected::testing::constant_eval; +using beman::expected::testing::type_name; + // ============================================================================= -// Helper types at namespace scope (needed for static_assert outside functions) +// Helper types, at namespace scope because more than one test case uses them // ============================================================================= struct NoDefault { @@ -42,51 +48,64 @@ struct MightThrow { }; // ============================================================================= -// [expected.object.general] para 2-3 — type-level static assertions +// [expected.object.general] para 2-3 — type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is checked by comparing the compiler's spelling of +// the two types, so a mismatch prints what was deduced next to what was +// wanted rather than the bare word `false`. // ============================================================================= // Ill-formed T: reference type — tested via negative compile file expected_t_ref_fail.cpp // Ill-formed E: reference type — tested via negative compile file expected_e_ref_fail.cpp // Ill-formed T: array type — tested via negative compile file expected_t_array_fail.cpp -// Default constructor: requires is_default_constructible_v -static_assert(!std::is_default_constructible_v>); +TEST_CASE("expected: special member availability and noexcept", "[ExpectedTest]") { + // Default constructor: requires is_default_constructible_v + CHECK_FALSE(std::is_default_constructible_v>); -// Copy constructor: not present when T is not copy-constructible -static_assert(!std::is_copy_constructible_v>); + // Copy constructor: not present when T is not copy-constructible + CHECK_FALSE(std::is_copy_constructible_v>); -// Destructor: trivially destructible when T and E are -static_assert(std::is_trivially_destructible_v>); + // Destructor: trivially destructible when T and E are + CHECK(std::is_trivially_destructible_v>); -// Move constructor: noexcept when T and E are nothrow-move-constructible -static_assert(std::is_nothrow_move_constructible_v>); -static_assert(!std::is_nothrow_move_constructible_v>); + // Move constructor: noexcept when T and E are nothrow-move-constructible + CHECK(std::is_nothrow_move_constructible_v>); + CHECK_FALSE(std::is_nothrow_move_constructible_v>); -// Move assignment: noexcept when all four noexcept conditions hold -static_assert(std::is_nothrow_move_assignable_v>); + // Move assignment: noexcept when all four noexcept conditions hold + CHECK(std::is_nothrow_move_assignable_v>); +} -// operator* ref-qualification return types -static_assert(std::is_same_v&>()), int&>); -static_assert(std::is_same_v&>()), const int&>); -static_assert(std::is_same_v&&>()), int&&>); -static_assert(std::is_same_v&&>()), const int&&>); +TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]") { + using expected_t = expt::expected; + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); +} -// error() ref-qualification return types -static_assert(std::is_same_v&>().error()), int&>); -static_assert(std::is_same_v&>().error()), const int&>); -static_assert(std::is_same_v&&>().error()), int&&>); -static_assert(std::is_same_v&&>().error()), const int&&>); +TEST_CASE("expected: error() ref-qualification return types", "[ExpectedTest]") { + using expected_t = expt::expected; + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} // ============================================================================= // Type aliases // ============================================================================= TEST_CASE("expected: type aliases", "[ExpectedTest]") { - static_assert(std::is_same_v::value_type, int>); - static_assert(std::is_same_v::error_type, std::string>); - static_assert(std::is_same_v::unexpected_type, expt::unexpected>); - static_assert( - std::is_same_v::rebind, expt::expected>); + using expected_t = expt::expected; + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name>()); + CHECK(type_name>() == type_name>()); } // ============================================================================= @@ -697,30 +716,71 @@ TEST_CASE("expected: cross-type equality error", "[ExpectedTest]") { // ============================================================================= // Constexpr usage +// +// Each of these builds an expected inside a self-contained probe lambda and +// reduces its state to a literal aggregate. `constant_eval` runs the probe +// during translation — so "is this usable in a constant expression?" is still +// answered by the compiler — and hands back the result as an ordinary value, +// so "did it produce the right state?" is answered by a reported CHECK. +// Calling the same probe directly runs the identical body at runtime, which +// is worth doing separately: constant evaluation and ordinary evaluation take +// different paths through a union-based type. // ============================================================================= +namespace { +// The observable state of an expected, reduced to literal types. +// Streamable so that a mismatch prints both states; without an inserter +// Catch2 reports `{?} == {?}` and the reporting benefit is lost. +struct int_state { + bool has_value; + int observed; // *e when has_value is true, e.error() otherwise + + // HIDDEN FRIENDS + friend constexpr bool operator==(const int_state&, const int_state&) = default; + friend std::ostream& operator<<(std::ostream& os, const int_state& s) { + return os << (s.has_value ? "{ value " : "{ error ") << s.observed << " }"; + } +}; +} // namespace + TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { - constexpr expt::expected e; - static_assert(e.has_value()); - static_assert(*e == 0); + constexpr auto probe = [] { + expt::expected e; + return int_state{e.has_value(), *e}; + }; + CHECK(constant_eval(probe) == int_state{true, 0}); + CHECK(probe() == int_state{true, 0}); } TEST_CASE("expected: constexpr value construction", "[ExpectedTest]") { - constexpr expt::expected e(42); - static_assert(e.has_value()); - static_assert(*e == 42); + constexpr auto probe = [] { + expt::expected e(42); + return int_state{e.has_value(), *e}; + }; + CHECK(constant_eval(probe) == int_state{true, 42}); + CHECK(probe() == int_state{true, 42}); } TEST_CASE("expected: constexpr error construction", "[ExpectedTest]") { - constexpr expt::expected e(expt::unexpect, 7); - static_assert(!e.has_value()); - static_assert(e.error() == 7); + constexpr auto probe = [] { + expt::expected e(expt::unexpect, 7); + return int_state{e.has_value(), e.error()}; + }; + CHECK(constant_eval(probe) == int_state{false, 7}); + CHECK(probe() == int_state{false, 7}); } TEST_CASE("expected: constexpr equality", "[ExpectedTest]") { - constexpr expt::expected a(42); - constexpr expt::expected b(42); - static_assert(a == b); + // The answer here is a bare bool, so there is no richer value to report; + // the win is that a wrong answer is still a reported failure rather than + // a build break, and the rest of the file still runs. + constexpr auto probe = [] { + expt::expected a(42); + expt::expected b(42); + return a == b; + }; + CHECK(constant_eval(probe)); + CHECK(probe()); } // ============================================================================= @@ -796,7 +856,7 @@ TEST_CASE("expected: operator-> returns address of value", "[ExpectedTest]") { TEST_CASE("expected: emplace with nothrow-constructible type", "[ExpectedTest]") { // int is nothrow constructible — emplace must be available - static_assert(std::is_nothrow_constructible_v); + CHECK(std::is_nothrow_constructible_v); expt::expected e(expt::unexpect, "err"); int& r = e.emplace(99); CHECK(r == 99); @@ -809,10 +869,11 @@ TEST_CASE("expected: emplace with nothrow-constructible type", "[ExpectedTest]") // ============================================================================= TEST_CASE("expected: value() ref-qualification return types", "[ExpectedTest]") { - static_assert(std::is_same_v&>().value()), int&>); - static_assert(std::is_same_v&>().value()), const int&>); - static_assert(std::is_same_v&&>().value()), int&&>); - static_assert(std::is_same_v&&>().value()), const int&&>); + using expected_t = expt::expected; + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); } // ============================================================================= @@ -1120,14 +1181,14 @@ TEST_CASE("cross-eq: expected == expected", "[ExpectedTest][ // --------------------------------------------------------------------------- TEST_CASE("constraint: from_expected is derived from expected", "[ExpectedTest][constraint]") { - static_assert(std::is_base_of_v, from_expected>); + CHECK(std::is_base_of_v, from_expected>); from_expected fe(42); CHECK(fe.has_value()); CHECK(*fe == 42); } TEST_CASE("constraint: from_unexpected is derived from unexpected", "[ExpectedTest][constraint]") { - static_assert(std::is_base_of_v, from_unexpected>); + CHECK(std::is_base_of_v, from_unexpected>); from_unexpected fu(7); CHECK(fu.error() == 7); } From 456b84862dc53cd95bb469bb1b7e405c198ed2b7 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 9 Aug 2026 20:39:31 -0400 Subject: [PATCH 4/6] test: convert the remaining test files off static_assert Applies the convention established in expected.test.cpp to the other 17 files, removing the last 283 static_assert declarations from the .test.cpp sources. No live static_assert remains in any test file; the word survives only in comments explaining why a check is now a CHECK. These files are almost entirely type-level, so the bulk of the work is traits and SFINAE concepts becoming CHECK / CHECK_FALSE and is_same_v becoming a type_name comparison. Polarity and operands are preserved throughout; the count of passing tests rises from 1095 to 1172 because assertions that used to be invisible are now reported cases. Two files needed more than the mechanical treatment: expected_std_equivalence.test.cpp generates its assertions from BEMAN_PARITY and BEMAN_RUN macros over a battery of error types. The macros now expand to INFO + CHECK inside a TEST_CASE, so a trait divergence names the exact (T, E) pair it was found on instead of failing to compile. This does mean the trait equivalence target is no longer a translation-time gate. It also means the four pre-existing is_trivially_copyable divergences on clang with libstdc++ -- which are four hard static assertion failures at HEAD -- now report as named test failures rather than breaking the build. expected_ref_constraints.test.cpp brace-scopes each INFO/CHECK pair, because Catch2 INFO messages accumulate for the enclosing scope and a failing check would otherwise print the messages of every sibling that ran before it. --- .../expected/bad_expected_access.test.cpp | 35 ++- .../expected/expected_constraints.test.cpp | 77 ++++-- .../beman/expected/expected_hardened.test.cpp | 14 +- .../beman/expected/expected_monadic.test.cpp | 14 +- .../expected_monadic_constraints.test.cpp | 113 +++++---- tests/beman/expected/expected_ref.test.cpp | 85 ++++--- .../beman/expected/expected_ref_both.test.cpp | 110 ++++---- .../expected_ref_constraints.test.cpp | 239 ++++++++++++------ tests/beman/expected/expected_ref_e.test.cpp | 140 ++++++---- .../expected_review_corrections.test.cpp | 73 +++--- .../expected_smf_regressions.test.cpp | 33 ++- .../expected_std_equivalence.test.cpp | 81 ++++-- .../beman/expected/expected_trivial.test.cpp | 47 ++-- tests/beman/expected/expected_void.test.cpp | 49 +++- .../expected/expected_void_monadic.test.cpp | 18 +- .../expected/expected_void_ref_e.test.cpp | 89 ++++--- tests/beman/expected/unexpected.test.cpp | 79 ++++-- 17 files changed, 850 insertions(+), 446 deletions(-) diff --git a/tests/beman/expected/bad_expected_access.test.cpp b/tests/beman/expected/bad_expected_access.test.cpp index 3b9c010..c8eef7a 100644 --- a/tests/beman/expected/bad_expected_access.test.cpp +++ b/tests/beman/expected/bad_expected_access.test.cpp @@ -5,26 +5,41 @@ #include +#include + #include #include +#include #include namespace expt = test_ns; +using beman::expected::testing::type_name; + // ============================================================================= -// [expected.bad.void] and [expected.bad] — type-level assertions +// [expected.bad.void] and [expected.bad] — type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is checked by comparing the compiler's spelling of +// the two types, so a mismatch prints what was deduced next to what was +// wanted rather than the bare word `false`. // ============================================================================= -// Inheritance chain -static_assert(std::is_base_of_v>); -static_assert(std::is_base_of_v, expt::bad_expected_access>); -static_assert(std::is_base_of_v>); +TEST_CASE("bad_expected_access: inheritance chain", "[BadExpectedAccessTest]") { + CHECK(std::is_base_of_v>); + CHECK(std::is_base_of_v, expt::bad_expected_access>); + CHECK(std::is_base_of_v>); +} -// error() ref-qualification return types -static_assert(std::is_same_v&>().error()), int&>); -static_assert(std::is_same_v&>().error()), const int&>); -static_assert(std::is_same_v&&>().error()), int&&>); -static_assert(std::is_same_v&&>().error()), const int&&>); +TEST_CASE("bad_expected_access: error() ref-qualification return types", "[BadExpectedAccessTest]") { + using bad_access_t = expt::bad_expected_access; + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} TEST_CASE("bad_expected_access: breathing", "[BadExpectedAccessTest]") {} diff --git a/tests/beman/expected/expected_constraints.test.cpp b/tests/beman/expected/expected_constraints.test.cpp index ca53c5f..ccdea59 100644 --- a/tests/beman/expected/expected_constraints.test.cpp +++ b/tests/beman/expected/expected_constraints.test.cpp @@ -11,6 +11,18 @@ using namespace beman::expected; +// ============================================================================= +// How these constraints are checked +// +// Each constraint below reduces to a trait, so it is a plain bool. They are +// checked at runtime rather than at translation time so that a constraint that +// has drifted is reported by the test run, naming the type and the polarity +// responsible, instead of stopping the build at the first failure and +// reporting nothing about the rest. A constraint that must exclude an overload +// is checked with CHECK_FALSE; the explanatory text each check used to carry is +// kept as an INFO, so it is printed when that check is the one that fails. +// ============================================================================= + // --------------------------------------------------------------------------- // Converting constructor: bool exemption (constraint 18.3) // @@ -33,9 +45,10 @@ TEST_CASE("converting ctor: expected from expected error path CHECK(dst.error() == 7); } -// expected IS constructible from expected (converting ctor selected) -static_assert(std::is_constructible_v, const expected&>, - "expected must be constructible from expected via converting ctor"); +TEST_CASE("converting ctor: expected is constructible from expected", "[constraints]") { + INFO("expected must be constructible from expected via converting ctor"); + CHECK(std::is_constructible_v, const expected&>); +} // --------------------------------------------------------------------------- // Value constructor: unexpected guard (constraint 23.4) @@ -69,10 +82,12 @@ TEST_CASE("value ctor: unexpected blocked as value even when T is construct // Value ctor is blocked for U = unexpected: is_constructible via value ctor // path requires U not be an unexpected specialization. Verify by checking that -// the overall construction resolves correctly (above test covers behavior; -// the static_assert below checks the trait): -static_assert(std::is_constructible_v, unexpected>, - "construction must still work — via unexpected ctor, not value ctor"); +// the overall construction resolves correctly (the test above covers the +// behavior; the check below covers the trait): +TEST_CASE("value ctor: expected is still constructible from unexpected", "[constraints]") { + INFO("construction must still work — via unexpected ctor, not value ctor"); + CHECK(std::is_constructible_v, unexpected>); +} // --------------------------------------------------------------------------- // Value assignment: unexpected goes to unexpected overload (constraint 11.2) @@ -96,17 +111,30 @@ struct ThrowingMove { ThrowingMove& operator=(ThrowingMove&&) = default; }; -// Both T and E are throwing-move: move assignment must be deleted -static_assert(!std::is_move_assignable_v>, - "move assignment must be deleted when neither T nor E is nothrow move constructible"); - -// At least one nothrow-move: move assignment must exist -static_assert(std::is_move_assignable_v>, - "move assignment must be available when T is nothrow move constructible"); -static_assert(std::is_move_assignable_v>, - "move assignment must be available when E is nothrow move constructible"); -static_assert(std::is_move_assignable_v>, - "move assignment must be available when both are nothrow move constructible"); +TEST_CASE("move assignment: availability follows the nothrow-move-constructible condition", "[constraints]") { + // Each check is braced so that its INFO is scoped to it alone, and only the + // explanation belonging to a failing check is printed. + + // Both T and E are throwing-move: move assignment must be deleted + { + INFO("move assignment must be deleted when neither T nor E is nothrow move constructible"); + CHECK_FALSE(std::is_move_assignable_v>); + } + + // At least one nothrow-move: move assignment must exist + { + INFO("move assignment must be available when T is nothrow move constructible"); + CHECK(std::is_move_assignable_v>); + } + { + INFO("move assignment must be available when E is nothrow move constructible"); + CHECK(std::is_move_assignable_v>); + } + { + INFO("move assignment must be available when both are nothrow move constructible"); + CHECK(std::is_move_assignable_v>); + } +} // --------------------------------------------------------------------------- // operator==(expected, T2): T2 must not be an expected specialization @@ -131,8 +159,11 @@ TEST_CASE("operator==: expected compared to int uses value overload", "[constrai // The T2 value overload must NOT fire when T2 is itself an expected specialization. // is_constructible check: operator==(expected, expected) must // resolve via the expected friend, not the T2 value friend. -// (Behavioral coverage above; static check: ensure T2=expected doesn't pick value overload.) -static_assert( - !std:: - is_invocable_r_v, expected>, - "sanity: plain int equality lambda cannot be called with expected args"); +// (Behavioral coverage above; the check below ensures T2=expected doesn't pick +// the value overload.) +using int_equality_t = decltype([](int x, int y) { return x == y; }); + +TEST_CASE("operator==: a plain int equality callable is not invocable with expected arguments", "[constraints]") { + INFO("sanity: plain int equality lambda cannot be called with expected args"); + CHECK_FALSE((std::is_invocable_r_v, expected>)); +} diff --git a/tests/beman/expected/expected_hardened.test.cpp b/tests/beman/expected/expected_hardened.test.cpp index 1560d89..34f30e7 100644 --- a/tests/beman/expected/expected_hardened.test.cpp +++ b/tests/beman/expected/expected_hardened.test.cpp @@ -9,6 +9,7 @@ #include #include +#include using namespace beman::expected; @@ -91,6 +92,12 @@ TEST_CASE("hardened: error() on error-state expected", "[hardened]") { // --------------------------------------------------------------------------- // unexpected friend swap: constraint check (beman-only) +// +// The constraint is checked at runtime rather than with static_assert so that a +// violation is reported by the test run, with the responsible type named, +// instead of stopping the build and reporting nothing. Querying the trait at +// all is still the point: an unconstrained hidden-friend swap would make +// is_swappable_v a hard error rather than a well-formed `false`. // --------------------------------------------------------------------------- struct NonSwappable { @@ -100,5 +107,8 @@ struct NonSwappable { NonSwappable& operator=(const NonSwappable&) = delete; NonSwappable& operator=(NonSwappable&&) = delete; }; -static_assert(!std::is_swappable_v); -static_assert(!std::is_swappable_v>); + +TEST_CASE("hardened: unexpected is not swappable when E is not", "[hardened]") { + CHECK_FALSE(std::is_swappable_v); + CHECK_FALSE(std::is_swappable_v>); +} diff --git a/tests/beman/expected/expected_monadic.test.cpp b/tests/beman/expected/expected_monadic.test.cpp index a81565e..dd05df7 100644 --- a/tests/beman/expected/expected_monadic.test.cpp +++ b/tests/beman/expected/expected_monadic.test.cpp @@ -5,6 +5,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,6 +15,8 @@ using namespace test_ns; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- // and_then // --------------------------------------------------------------------------- @@ -144,7 +148,7 @@ TEST_CASE("or_else: value passes through chain", "[expected_monadic]") { TEST_CASE("transform: has value - transforms", "[expected_monadic]") { expected e(6); auto r = e.transform([](int v) { return v * 7; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == 42); } @@ -165,7 +169,7 @@ TEST_CASE("transform: void return type", "[expected_monadic]") { expected e(1); int count = 0; auto r = e.transform([&](int) { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(count == 1); } @@ -174,7 +178,7 @@ TEST_CASE("transform: void return - error state does not call F", "[expected_mon expected e(unexpect, "no"); int count = 0; auto r = e.transform([&](int) { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(count == 0); CHECK(r.error() == "no"); @@ -183,7 +187,7 @@ TEST_CASE("transform: void return - error state does not call F", "[expected_mon TEST_CASE("transform: type change", "[expected_monadic]") { expected e(42); auto r = e.transform([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == "42"); } @@ -216,7 +220,7 @@ TEST_CASE("transform: const rvalue overload", "[expected_monadic]") { TEST_CASE("transform_error: has error - transforms", "[expected_monadic]") { expected e(unexpect, 3); auto r = e.transform_error([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(r.error() == "3"); } diff --git a/tests/beman/expected/expected_monadic_constraints.test.cpp b/tests/beman/expected/expected_monadic_constraints.test.cpp index f9aa5bb..359d3a7 100644 --- a/tests/beman/expected/expected_monadic_constraints.test.cpp +++ b/tests/beman/expected/expected_monadic_constraints.test.cpp @@ -13,6 +13,19 @@ using namespace beman::expected; +// ============================================================================= +// How these constraints are checked +// +// Every check below is the satisfaction of a detector concept, so it is a +// plain bool. They are checked at runtime rather than with static_assert so +// that a constraint that has drifted is reported by the test run, naming the +// operation and the value category responsible, instead of stopping the build +// at the first failure and reporting nothing about the rest. +// +// The polarity matters as much as the value: an operation that must be +// constrained *out* is checked with CHECK_FALSE. +// ============================================================================= + // A type that is not constructible from lvalue ref (only move-constructible) struct MoveOnly { MoveOnly() = default; @@ -41,65 +54,77 @@ concept has_transform_error = requires(F f) { std::declval().transform_error( // MoveOnly as E: lvalue overloads (&, const&) are constrained out because // E is not copy-constructible, so is_constructible_v is false. -using MoveOnlyErr = expected; -[[maybe_unused]] auto dummy_and_then = [](int) { return expected(); }; - -static_assert(!has_and_then); -static_assert(has_and_then); -static_assert(!has_and_then); - +// +// The aliases and callables stay at namespace scope, next to the section they +// belong to: naming them there keeps each detector-concept argument short +// enough to read, and each case is then a list of the constraints alone. +using MoveOnlyErr = expected; +[[maybe_unused]] auto dummy_and_then = [](int) { return expected(); }; [[maybe_unused]] auto dummy_transform = [](int) { return 0; }; -static_assert(!has_transform); -static_assert(has_transform); -static_assert(!has_transform); +TEST_CASE("monadic constraints: and_then / transform on expected", "[monadic][constraints]") { + // Lvalue overloads require is_constructible_v, which fails for a + // move-only E, so only the rvalue overload survives. + CHECK_FALSE(has_and_then); + CHECK(has_and_then); + CHECK_FALSE(has_and_then); + + CHECK_FALSE(has_transform); + CHECK(has_transform); + CHECK_FALSE(has_transform); +} // --------------------------------------------------------------------------- // Primary template: or_else / transform_error need T constructible from *this // --------------------------------------------------------------------------- -using MoveOnlyVal = expected; -[[maybe_unused]] auto dummy_or_else = [](int) { return expected(); }; - -static_assert(!has_or_else); -static_assert(has_or_else); -static_assert(!has_or_else); - +using MoveOnlyVal = expected; +[[maybe_unused]] auto dummy_or_else = [](int) { return expected(); }; [[maybe_unused]] auto dummy_transform_error = [](int) { return 0; }; -static_assert(!has_transform_error); -static_assert(has_transform_error); -static_assert(!has_transform_error); +TEST_CASE("monadic constraints: or_else / transform_error on expected", "[monadic][constraints]") { + // Lvalue overloads require is_constructible_v, which fails for a + // move-only T, so only the rvalue overload survives. + CHECK_FALSE(has_or_else); + CHECK(has_or_else); + CHECK_FALSE(has_or_else); + + CHECK_FALSE(has_transform_error); + CHECK(has_transform_error); + CHECK_FALSE(has_transform_error); +} // --------------------------------------------------------------------------- // Void specialization: and_then / transform need E constructible from error() // --------------------------------------------------------------------------- -using VoidMoveOnlyErr = expected; -[[maybe_unused]] auto void_dummy_and_then = []() { return expected(); }; - -static_assert(!has_and_then); -static_assert(has_and_then); -static_assert(!has_and_then); - +using VoidMoveOnlyErr = expected; +[[maybe_unused]] auto void_dummy_and_then = []() { return expected(); }; [[maybe_unused]] auto void_dummy_transform = []() { return 0; }; -static_assert(!has_transform); -static_assert(has_transform); -static_assert(!has_transform); +TEST_CASE("monadic constraints: and_then / transform on expected", "[monadic][constraints]") { + CHECK_FALSE(has_and_then); + CHECK(has_and_then); + CHECK_FALSE(has_and_then); + + CHECK_FALSE(has_transform); + CHECK(has_transform); + CHECK_FALSE(has_transform); +} // --------------------------------------------------------------------------- // Void specialization: or_else / transform_error have NO constraints // --------------------------------------------------------------------------- -// or_else and transform_error on void specialization should always be available -[[maybe_unused]] auto void_dummy_or_else = [](MoveOnly&&) { return expected(); }; - -static_assert(has_or_else); - +[[maybe_unused]] auto void_dummy_or_else = [](MoveOnly&&) { return expected(); }; [[maybe_unused]] auto void_dummy_transform_error = [](MoveOnly&&) { return 0; }; -static_assert(has_transform_error); +TEST_CASE("monadic constraints: void or_else / transform_error are unconstrained", "[monadic][constraints]") { + // Nothing is required of T, because there is no T to reconstruct, so these + // are available even for an error type that is only move-constructible. + CHECK(has_or_else); + CHECK(has_transform_error); +} // --------------------------------------------------------------------------- // Normal types: all operations remain available @@ -111,14 +136,16 @@ using NormalExpected = expected; [[maybe_unused]] auto normal_transform = [](int) { return 42; }; [[maybe_unused]] auto normal_transform_err = [](int) { return 42; }; -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); +TEST_CASE("monadic constraints: every operation available for expected", "[monadic][constraints]") { + CHECK(has_and_then); + CHECK(has_and_then); + CHECK(has_and_then); + CHECK(has_and_then); -static_assert(has_or_else); -static_assert(has_transform); -static_assert(has_transform_error); + CHECK(has_or_else); + CHECK(has_transform); + CHECK(has_transform_error); +} TEST_CASE("monadic constraints: rvalue and_then works with move-only error", "[monadic][constraints]") { expected e(42); diff --git a/tests/beman/expected/expected_ref.test.cpp b/tests/beman/expected/expected_ref.test.cpp index e014aaf..088a38f 100644 --- a/tests/beman/expected/expected_ref.test.cpp +++ b/tests/beman/expected/expected_ref.test.cpp @@ -6,6 +6,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,36 +15,59 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // ============================================================================= -// Type-level static assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is compared through `type_name`, whose comparison is +// `std::is_same_v` — exactly as strict as the static_assert it replaces — so +// a mismatch prints what was deduced next to what was wanted rather than the +// bare word `false`. // ============================================================================= -static_assert(std::is_constructible_v, int&>); -static_assert(!std::is_default_constructible_v>); -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); - -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>()), int&>); -static_assert(std::is_same_v>().value()), int&>); - -// const expected still returns T* / T& (shallow const) -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>()), int&>); - -// Triviality: when E is trivial, copy/move/assign/destroy should be trivial -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); - -// Non-trivial E: still constructible/assignable but not trivially -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_move_constructible_v>); -static_assert(!std::is_trivially_destructible_v>); +TEST_CASE("expected: special member availability", "[expected_ref]") { + using expected_t = expected; + CHECK((std::is_constructible_v)); + CHECK_FALSE(std::is_default_constructible_v); + CHECK(std::is_copy_constructible_v); + CHECK(std::is_move_constructible_v); +} + +TEST_CASE("expected: observer return types", "[expected_ref]") { + using expected_t = expected; + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name().value())>() == type_name()); +} + +TEST_CASE("expected: const expected still returns T* / T& (shallow const)", "[expected_ref]") { + using expected_t = expected; + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name())>() == type_name()); +} + +TEST_CASE("expected: triviality when E is trivial", "[expected_ref]") { + // When E is trivial, copy/move/assign/destroy should be trivial + using expected_t = expected; + CHECK(std::is_trivially_copy_constructible_v); + CHECK(std::is_trivially_move_constructible_v); + CHECK(std::is_trivially_copy_assignable_v); + CHECK(std::is_trivially_move_assignable_v); + CHECK(std::is_trivially_destructible_v); +} + +TEST_CASE("expected: non-trivial E is still constructible, but not trivially", "[expected_ref]") { + using expected_t = expected; + CHECK(std::is_copy_constructible_v); + CHECK(std::is_move_constructible_v); + CHECK_FALSE(std::is_trivially_copy_constructible_v); + CHECK_FALSE(std::is_trivially_move_constructible_v); + CHECK_FALSE(std::is_trivially_destructible_v); +} // ============================================================================= // Construction @@ -234,7 +259,7 @@ TEST_CASE("expected: shallow const allows mutation of referent", "[expected_ TEST_CASE("expected: operator-> on const returns T*", "[expected_ref]") { int x = 5; const expected e(x); - static_assert(std::is_same_v()), int*>); + CHECK(type_name())>() == type_name()); *e.operator->() = 99; CHECK(x == 99); } @@ -246,7 +271,7 @@ TEST_CASE("expected: operator-> on const returns T*", "[expected_ref]") { TEST_CASE("expected: operator* returns T&", "[expected_ref]") { int x = 42; expected e(x); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); *e = 99; CHECK(x == 99); } @@ -265,7 +290,7 @@ TEST_CASE("expected: operator-> returns T*", "[expected_ref]") { TEST_CASE("expected: value() returns T& or throws", "[expected_ref]") { int x = 1; expected e(x); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(e.value() == 1); e.value() = 2; CHECK(x == 2); diff --git a/tests/beman/expected/expected_ref_both.test.cpp b/tests/beman/expected/expected_ref_both.test.cpp index d228edf..ea1f35b 100644 --- a/tests/beman/expected/expected_ref_both.test.cpp +++ b/tests/beman/expected/expected_ref_both.test.cpp @@ -6,6 +6,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,61 +15,83 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // ============================================================================= -// Type-level static assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is checked by comparing type_name, which compares by +// std::is_same_v — exactly as strict as the original — with the compiler's +// spellings used only to explain a failure. // ============================================================================= -// No default constructor — T& cannot be default-initialized -static_assert(!std::is_default_constructible_v>); +TEST_CASE("expected: special member availability", "[expected_ref_both]") { + // No default constructor — T& cannot be default-initialized + CHECK_FALSE(std::is_default_constructible_v>); -// Constructible from lvalue (value side) -static_assert(std::is_constructible_v, int&>); + // Constructible from lvalue (value side) + CHECK(std::is_constructible_v, int&>); -// Copy/move constructible -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); + // Copy/move constructible + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); +} + +TEST_CASE("expected: fully trivial — both sides are pointers", "[expected_ref_both]") { + // Trivially copyable/movable/destructible + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); +} -// Fully trivial: both sides are pointers — trivially copyable/movable/destructible -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("expected: assignable even though const E& is not", "[expected_ref_both]") { + // Finding 1: copy/move assignment must be available for const-reference E, where E + // itself is not assignable (is_copy_assignable_v is false) but the + // stored unexpected rebinds via pointer assignment. + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); +} -// Finding 1: copy/move assignment must be available for const-reference E, where E -// itself is not assignable (is_copy_assignable_v is false) but the -// stored unexpected rebinds via pointer assignment. -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); +TEST_CASE("expected: observer return types are shallow-const", "[expected_ref_both]") { + using expected_t = expected; -// operator-> returns T* (shallow const) -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>().operator->()), int*>); + // operator-> returns T* (shallow const) + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name().operator->())>() == type_name()); -// operator* returns T& (shallow const) -static_assert(std::is_same_v>()), int&>); -static_assert(std::is_same_v>()), int&>); + // operator* returns T& (shallow const) + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); -// value() returns T& (shallow const) -static_assert(std::is_same_v>().value()), int&>); -static_assert(std::is_same_v>().value()), int&>); + // value() returns T& (shallow const) + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); -// error() returns E& (shallow const) -static_assert(std::is_same_v>().error()), int&>); -static_assert(std::is_same_v>().error()), int&>); + // error() returns E& (shallow const) + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} -// Cannot construct from temporary value (T& rvalue deleted) -static_assert(!std::is_constructible_v, int&&>); +TEST_CASE("expected: temporaries cannot be bound", "[expected_ref_both]") { + // Cannot construct from temporary value (T& rvalue deleted) + CHECK_FALSE(std::is_constructible_v, int&&>); -// Cannot construct from temporary error (rvalue or any type creating a temp E) -static_assert(!std::is_constructible_v, unexpect_t, int&&>); -// Cross-type temporary: float would create a temp double when binding const double& -static_assert(!std::is_constructible_v, unexpect_t, float>); -// Lvalue of same type is fine -static_assert(std::is_constructible_v, unexpect_t, const double&>); + // Cannot construct from temporary error (rvalue or any type creating a temp E) + CHECK_FALSE(std::is_constructible_v, unexpect_t, int&&>); + // Cross-type temporary: float would create a temp double when binding const double& + CHECK_FALSE(std::is_constructible_v, unexpect_t, float>); + // Lvalue of same type is fine + CHECK(std::is_constructible_v, unexpect_t, const double&>); +} -// Converting construction from expected -static_assert(std::is_constructible_v, const expected&>); +TEST_CASE("expected: converting construction from expected is available", "[expected_ref_both]") { + CHECK(std::is_constructible_v, const expected&>); +} // ============================================================================= // Construction — value side @@ -287,7 +311,7 @@ TEST_CASE("expected: operator*() returns T& (mutation visible)", "[expect TEST_CASE("expected: value() returns T& (throws on error)", "[expected_ref_both]") { int x = 42; expected e(x); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(&e.value() == &x); } @@ -300,7 +324,7 @@ TEST_CASE("expected: value() throws bad_expected_access on error", "[expe TEST_CASE("expected: error() returns E& (mutation visible)", "[expected_ref_both]") { int err = 7; expected e(unexpect, err); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); e.error() = 99; CHECK(err == 99); } diff --git a/tests/beman/expected/expected_ref_constraints.test.cpp b/tests/beman/expected/expected_ref_constraints.test.cpp index 9805c73..c667c24 100644 --- a/tests/beman/expected/expected_ref_constraints.test.cpp +++ b/tests/beman/expected/expected_ref_constraints.test.cpp @@ -2,6 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // Beman-only: tests SFINAE behavior of constraints on expected. +// +// Every constraint below is checked at runtime rather than with static_assert +// so that a violated constraint is reported by the test run, naming the +// responsible type, instead of stopping the build at the first failure and +// reporting nothing at all. Each check is a boolean trait or concept, so a +// plain CHECK / CHECK_FALSE is exactly as strict as the static_assert it +// replaces; the polarity of the original is preserved. #include #include @@ -63,24 +70,47 @@ concept has_value_or = requires(X x, U u) { x.value_or(u); }; // C2/C3: Copy/move constructors require E to be copy/move constructible // =========================================================================== -static_assert(!std::is_copy_constructible_v>, - "expected must not be copy-constructible"); -static_assert(std::is_move_constructible_v>, - "expected must be move-constructible"); - -static_assert(std::is_copy_constructible_v>, "expected must be copy-constructible"); -static_assert(std::is_move_constructible_v>, "expected must be move-constructible"); +TEST_CASE("ref constraint: copy/move ctor track E copy/move constructibility", "[ref_constraints]") { + { + INFO("expected must not be copy-constructible"); + CHECK_FALSE((std::is_copy_constructible_v>)); + } + { + INFO("expected must be move-constructible"); + CHECK((std::is_move_constructible_v>)); + } + { + INFO("expected must be copy-constructible"); + CHECK((std::is_copy_constructible_v>)); + } + { + INFO("expected must be move-constructible"); + CHECK((std::is_move_constructible_v>)); + } +} // =========================================================================== // A1/A2: Copy/move assignment require E to be copy/move constructible+assignable // =========================================================================== -static_assert(!std::is_copy_assignable_v>, - "expected must not be copy-assignable"); -static_assert(std::is_move_assignable_v>, "expected must be move-assignable"); - -static_assert(std::is_copy_assignable_v>, "expected must be copy-assignable"); -static_assert(std::is_move_assignable_v>, "expected must be move-assignable"); +TEST_CASE("ref constraint: copy/move assignment track E copy/move assignability", "[ref_constraints]") { + { + INFO("expected must not be copy-assignable"); + CHECK_FALSE((std::is_copy_assignable_v>)); + } + { + INFO("expected must be move-assignable"); + CHECK((std::is_move_assignable_v>)); + } + { + INFO("expected must be copy-assignable"); + CHECK((std::is_copy_assignable_v>)); + } + { + INFO("expected must be move-assignable"); + CHECK((std::is_move_assignable_v>)); + } +} // =========================================================================== // C5/C6: Value ctor excludes expected self-type and unexpected specializations @@ -99,81 +129,118 @@ TEST_CASE("ref constraint: unexpected routes to unexpected ctor", "[ref_constrai // C7: Value ctor requires is_constructible_v // =========================================================================== -// Cannot construct expected from a string — int& is not bindable to string -static_assert(!std::is_constructible_v, std::string&>, - "expected must not be constructible from string&"); - -// Can construct expected from int& -static_assert(std::is_constructible_v, int&>, - "expected must be constructible from int&"); +TEST_CASE("ref constraint: value ctor requires is_constructible_v", "[ref_constraints]") { + // Cannot construct expected from a string — int& is not + // bindable to string + { + INFO("expected must not be constructible from string&"); + CHECK_FALSE((std::is_constructible_v, std::string&>)); + } + // Can construct expected from int& + { + INFO("expected must be constructible from int&"); + CHECK((std::is_constructible_v, int&>)); + } +} // =========================================================================== // C9/C10: Converting ctor from expected // =========================================================================== -// Cannot convert expected to expected (string& not bindable to int&) -static_assert(!std::is_constructible_v, const expected&>, - "expected must not be constructible from expected"); - -// Can convert expected to expected (same types) -static_assert(std::is_constructible_v, const expected&>, - "expected must be constructible from expected"); - // Cannot convert when E is not constructible from G struct Unconstructible { Unconstructible() = default; Unconstructible(int) = delete; }; -static_assert(!std::is_constructible_v, const expected&>, - "expected not constructible from expected — E(const G&) fails"); +TEST_CASE("ref constraint: converting ctor from expected", "[ref_constraints]") { + // Cannot convert expected to expected (string& + // not bindable to int&) + { + INFO("expected must not be constructible from expected"); + CHECK_FALSE((std::is_constructible_v, const expected&>)); + } + // Can convert expected to expected (same types) + { + INFO("expected must be constructible from expected"); + CHECK((std::is_constructible_v, const expected&>)); + } + { + INFO("expected not constructible from expected — E(const G&) fails"); + CHECK_FALSE((std::is_constructible_v, const expected&>)); + } +} // =========================================================================== // A3/A4/A5: Value assignment constraints // =========================================================================== -// Value assignment from int& works -static_assert(std::is_assignable_v&, int&>, "expected must be assignable from int&"); - -// Value assignment from unrelated type that can't bind to int& is blocked -static_assert(!std::is_assignable_v&, std::string&>, - "expected must not be assignable from string&"); +TEST_CASE("ref constraint: value assignment constraints", "[ref_constraints]") { + // Value assignment from int& works + { + INFO("expected must be assignable from int&"); + CHECK((std::is_assignable_v&, int&>)); + } + // Value assignment from unrelated type that can't bind to int& is blocked + { + INFO("expected must not be assignable from string&"); + CHECK_FALSE((std::is_assignable_v&, std::string&>)); + } +} // =========================================================================== // A6: unexpected assignment requires constructible+assignable E // =========================================================================== -static_assert(std::is_assignable_v&, unexpected>, - "expected must be assignable from unexpected"); +TEST_CASE("ref constraint: unexpected assignment requires constructible+assignable E", "[ref_constraints]") { + INFO("expected must be assignable from unexpected"); + CHECK((std::is_assignable_v&, unexpected>)); +} // =========================================================================== // S1: swap requires E swappable and move-constructible // =========================================================================== -static_assert(has_swap>, "expected must be swappable"); - -static_assert(!has_swap>, "expected must not be swappable"); +TEST_CASE("ref constraint: swap requires E swappable and move-constructible", "[ref_constraints]") { + { + INFO("expected must be swappable"); + CHECK((has_swap>)); + } + { + INFO("expected must not be swappable"); + CHECK_FALSE((has_swap>)); + } +} // =========================================================================== // E1: emplace requires constructible and no dangling // =========================================================================== -static_assert(has_emplace, int&>, "expected must support emplace from int&"); - -// emplace from an rvalue int — should be blocked (can't bind int& to rvalue) -static_assert(!has_emplace_from, int>, - "expected must not support emplace from rvalue int"); +TEST_CASE("ref constraint: emplace requires constructible and no dangling", "[ref_constraints]") { + { + INFO("expected must support emplace from int&"); + CHECK((has_emplace, int&>)); + } + // emplace from an rvalue int — should be blocked (can't bind int& to rvalue) + { + INFO("expected must not support emplace from rvalue int"); + CHECK_FALSE((has_emplace_from, int>)); + } +} // =========================================================================== // V1: value_or requires is_object_v && !is_array_v (LWG4304) // =========================================================================== -static_assert(has_value_or, int>, "expected must support value_or"); +TEST_CASE("ref constraint: value_or requires object type (LWG4304)", "[ref_constraints]") { + INFO("expected must support value_or"); + CHECK((has_value_or, int>)); -// Function type: int(int) is not an object type -// Note: expected would require T = int(int), which is a function type. -// We can't easily form expected due to other constraints, so we verify -// the positive case works and trust the requires clause. + // Function type: int(int) is not an object type + // Note: expected would require T = int(int), which is a function type. + // We can't easily form expected due to other constraints, so we verify + // the positive case works and trust the requires clause. +} // =========================================================================== // MC1/MC2: and_then/transform constrained by E constructibility @@ -182,21 +249,34 @@ static_assert(has_value_or, int>, "expected using RefMoveOnlyErr = expected; [[maybe_unused]] auto ref_dummy_and_then = [](int&) { return expected(); }; -static_assert(!has_and_then, - "and_then lvalue must be constrained out when E is move-only"); -static_assert(has_and_then, - "and_then rvalue must be available when E is move-constructible"); -static_assert(!has_and_then, - "and_then const lvalue must be constrained out when E is move-only"); - [[maybe_unused]] auto ref_dummy_transform = [](int&) { return 0; }; -static_assert(!has_transform, - "transform lvalue must be constrained out when E is move-only"); -static_assert(has_transform, - "transform rvalue must be available when E is move-constructible"); -static_assert(!has_transform, - "transform const lvalue must be constrained out when E is move-only"); +TEST_CASE("ref constraint: and_then/transform constrained by E constructibility", "[ref_constraints]") { + { + INFO("and_then lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_and_then)); + } + { + INFO("and_then rvalue must be available when E is move-constructible"); + CHECK((has_and_then)); + } + { + INFO("and_then const lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_and_then)); + } + { + INFO("transform lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_transform)); + } + { + INFO("transform rvalue must be available when E is move-constructible"); + CHECK((has_transform)); + } + { + INFO("transform const lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_transform)); + } +} // =========================================================================== // MC3/MC4: or_else/transform_error have no constraints — always available @@ -207,13 +287,18 @@ static_assert(!has_transform(x); }; -static_assert(has_or_else, - "or_else must be available — no constraints on value constructibility for T&"); - [[maybe_unused]] auto ref_dummy_transform_error = [](MoveOnly&&) { return 0; }; -static_assert(has_transform_error, - "transform_error must be available — no constraints for T& specialization"); +TEST_CASE("ref constraint: or_else/transform_error are unconstrained", "[ref_constraints]") { + { + INFO("or_else must be available — no constraints on value constructibility for T&"); + CHECK((has_or_else)); + } + { + INFO("transform_error must be available — no constraints for T& specialization"); + CHECK((has_transform_error)); + } +} // =========================================================================== // Positive: all operations available for normal types @@ -229,14 +314,16 @@ using NormalRef = expected; [[maybe_unused]] auto ref_normal_transform = [](int&) { return 42; }; [[maybe_unused]] auto ref_normal_transform_err = [](int) { return 42; }; -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); +TEST_CASE("ref constraint: all monadic operations available for normal types", "[ref_constraints]") { + CHECK((has_and_then)); + CHECK((has_and_then)); + CHECK((has_and_then)); + CHECK((has_and_then)); -static_assert(has_or_else); -static_assert(has_transform); -static_assert(has_transform_error); + CHECK((has_or_else)); + CHECK((has_transform)); + CHECK((has_transform_error)); +} // =========================================================================== // Runtime tests for constraint-gated paths diff --git a/tests/beman/expected/expected_ref_e.test.cpp b/tests/beman/expected/expected_ref_e.test.cpp index 0af4751..77be6a6 100644 --- a/tests/beman/expected/expected_ref_e.test.cpp +++ b/tests/beman/expected/expected_ref_e.test.cpp @@ -6,6 +6,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,82 +15,116 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // ============================================================================= // Finding 5: feature-test macro for the reference-E / reference-T extensions // ============================================================================= +// Presence of the macro is still a hard translation failure: without it there +// is nothing for a test to inspect, and `#if` is the only tool that can ask. #ifndef __cpp_lib_expected_ref #error "__cpp_lib_expected_ref must be defined by " #endif -static_assert(__cpp_lib_expected_ref > 0); + +TEST_CASE("expected: feature-test macro has a positive value", "[expected_ref_e]") { + CHECK(__cpp_lib_expected_ref > 0); +} // ============================================================================= // Finding 4: guarded delete-with-message macro (falls back to plain `delete` // pre-C++26; either way, the deleted overload stays deleted). // ============================================================================= -static_assert(!std::is_constructible_v, int&&>, - "unexpected dangling-temporary ctor must stay deleted regardless of " - "BEMAN_EXPECTED_DELETE_MSG's expansion"); -static_assert(!std::is_default_constructible_v>, - "expected must stay non-default-constructible regardless of " - "BEMAN_EXPECTED_DELETE_MSG's expansion"); +TEST_CASE("expected: BEMAN_EXPECTED_DELETE_MSG leaves deleted overloads deleted", "[expected_ref_e]") { + { + INFO("unexpected dangling-temporary ctor must stay deleted regardless of " + "BEMAN_EXPECTED_DELETE_MSG's expansion"); + CHECK_FALSE(std::is_constructible_v, int&&>); + } + { + INFO("expected must stay non-default-constructible regardless of " + "BEMAN_EXPECTED_DELETE_MSG's expansion"); + CHECK_FALSE(std::is_default_constructible_v>); + } +} // ============================================================================= -// Type-level static assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is compared by `type_name`, which decides by +// std::is_same_v and carries the compiler's spelling only so that a mismatch +// prints what was deduced next to what was wanted rather than the bare word +// `false`. // ============================================================================= -// expected is a valid specialization — default constructible (value side) -static_assert(std::is_default_constructible_v>); -static_assert(std::is_constructible_v, std::in_place_t, int>); +TEST_CASE("expected: special member availability", "[expected_ref_e]") { + // expected is a valid specialization — default constructible (value side) + CHECK(std::is_default_constructible_v>); + CHECK(std::is_constructible_v, std::in_place_t, int>); + + // Copy/move constructible + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); +} -// error() returns E& (shallow const — const expected still returns E&, not const E&) -static_assert(std::is_same_v>().error()), int&>); -static_assert(std::is_same_v>().error()), int&>); +TEST_CASE("expected: observer return types", "[expected_ref_e]") { + using expected_t = expected; -// value() returns T& (non-const) / const T& (const) -static_assert(std::is_same_v&>().value()), int&>); -static_assert(std::is_same_v&>().value()), const int&>); + // error() returns E& (shallow const — const expected still returns E&, not const E&) + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); -// operator-> returns T* / const T* -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>().operator->()), const int*>); + // value() returns T& (non-const) / const T& (const) + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); -// Copy/move constructible -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); + // operator-> returns T* / const T* + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name().operator->())>() == type_name()); +} // Finding 1: copy/move assignment must be available for reference E, including // const-reference E, where E itself is not assignable (is_copy_assignable_v is false) but the stored unexpected rebinds via pointer assignment. -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); - -// Triviality: when T is trivial, copy/move/assign/destroy should be trivial -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); - -// Non-trivial T: still constructible/assignable but not trivially -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_move_constructible_v>); -static_assert(!std::is_trivially_destructible_v>); - -// Cannot construct from temporary error (rvalue deleted, or any type creating a temp E) -static_assert(!std::is_constructible_v, unexpect_t, int&&>); -// Cross-type temporary: float would create a temp double when binding const double& -static_assert(!std::is_constructible_v, unexpect_t, float>); -// Lvalue of same type is fine -static_assert(std::is_constructible_v, unexpect_t, const double&>); +TEST_CASE("expected: copy/move assignment available for reference E", "[expected_ref_e]") { + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); +} -// Converting construction from expected -static_assert(std::is_constructible_v, const expected&>); +TEST_CASE("expected: triviality follows T", "[expected_ref_e]") { + // Triviality: when T is trivial, copy/move/assign/destroy should be trivial + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); + + // Non-trivial T: still constructible/assignable but not trivially + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); + CHECK_FALSE(std::is_trivially_copy_constructible_v>); + CHECK_FALSE(std::is_trivially_move_constructible_v>); + CHECK_FALSE(std::is_trivially_destructible_v>); +} + +TEST_CASE("expected: error construction rejects temporaries", "[expected_ref_e]") { + // Cannot construct from temporary error (rvalue deleted, or any type creating a temp E) + CHECK_FALSE(std::is_constructible_v, unexpect_t, int&&>); + // Cross-type temporary: float would create a temp double when binding const double& + CHECK_FALSE(std::is_constructible_v, unexpect_t, float>); + // Lvalue of same type is fine + CHECK(std::is_constructible_v, unexpect_t, const double&>); +} + +TEST_CASE("expected: converting construction from expected is available", "[expected_ref_e]") { + CHECK(std::is_constructible_v, const expected&>); +} // ============================================================================= // Construction — value side (same as primary template) @@ -236,7 +272,7 @@ TEST_CASE("expected: operator*() and operator->() work normally", "[expect TEST_CASE("expected: value() returns T& (owned)", "[expected_ref_e]") { expected e(42); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); e.value() = 99; CHECK(*e == 99); } @@ -250,7 +286,7 @@ TEST_CASE("expected: value() throws on error", "[expected_ref_e]") { TEST_CASE("expected: error() returns E&", "[expected_ref_e]") { int err = 7; expected e(unexpect, err); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(&e.error() == &err); } diff --git a/tests/beman/expected/expected_review_corrections.test.cpp b/tests/beman/expected/expected_review_corrections.test.cpp index 39acb99..b875551 100644 --- a/tests/beman/expected/expected_review_corrections.test.cpp +++ b/tests/beman/expected/expected_review_corrections.test.cpp @@ -22,26 +22,31 @@ using namespace beman::expected; // type (referent lives inside the wrapper) or would drop const. // ============================================================================= -// Allowed: reference G, across all three specializations. -static_assert(std::is_constructible_v, unexpected>); -static_assert(std::is_constructible_v, unexpected>); -static_assert(std::is_constructible_v, unexpected>); - -// Allowed: binding a more-const reference (const int& <- int&). -static_assert(std::is_constructible_v, unexpected>); -static_assert(std::is_constructible_v, unexpected>); - -// Deleted: value G would dangle once a temporary unexpected is destroyed. -static_assert(!std::is_constructible_v, unexpected>); -static_assert(!std::is_constructible_v, unexpected>); -static_assert(!std::is_constructible_v, unexpected>); - -// Deleted: dropping const (int& <- const int&) is not constructible. -static_assert(!std::is_constructible_v, unexpected>); -static_assert(!std::is_constructible_v, unexpected>); - -// Value-E construction is unaffected. -static_assert(std::is_constructible_v, unexpected>); +// Checked at runtime rather than with static_assert so that a violated rule is +// reported by the test run, with the offending specialization named, instead of +// stopping the build at the first failure and reporting nothing. +TEST_CASE("F6: construction from unexpected is allowed only for reference G", "[ref][unexpected]") { + // Allowed: reference G, across all three specializations. + CHECK(std::is_constructible_v, unexpected>); + CHECK(std::is_constructible_v, unexpected>); + CHECK(std::is_constructible_v, unexpected>); + + // Allowed: binding a more-const reference (const int& <- int&). + CHECK(std::is_constructible_v, unexpected>); + CHECK(std::is_constructible_v, unexpected>); + + // Deleted: value G would dangle once a temporary unexpected is destroyed. + CHECK_FALSE(std::is_constructible_v, unexpected>); + CHECK_FALSE(std::is_constructible_v, unexpected>); + CHECK_FALSE(std::is_constructible_v, unexpected>); + + // Deleted: dropping const (int& <- const int&) is not constructible. + CHECK_FALSE(std::is_constructible_v, unexpected>); + CHECK_FALSE(std::is_constructible_v, unexpected>); + + // Value-E construction is unaffected. + CHECK(std::is_constructible_v, unexpected>); +} TEST_CASE("reference-error construction from unexpected binds an external object", "[ref][unexpected]") { int err = 41; @@ -63,13 +68,15 @@ TEST_CASE("reference-error construction from unexpected binds an external ob // Rebinding assignment from unexpected — allowed for reference E only when G is a reference // (rebinds the error pointer to an external object; never dangles). Value G and const-drop are -// statically rejected, mirroring construction. -static_assert(std::is_assignable_v&, unexpected>); -static_assert(std::is_assignable_v&, unexpected>); -static_assert(std::is_assignable_v&, unexpected>); -static_assert(std::is_assignable_v&, unexpected>); // more-const OK -static_assert(!std::is_assignable_v&, unexpected>); // value G: deleted -static_assert(!std::is_assignable_v&, unexpected>); // const drop: no overload +// rejected at compile time, mirroring construction. +TEST_CASE("F6: rebinding assignment from unexpected is allowed only for reference G", "[ref][unexpected][assign]") { + CHECK(std::is_assignable_v&, unexpected>); + CHECK(std::is_assignable_v&, unexpected>); + CHECK(std::is_assignable_v&, unexpected>); + CHECK(std::is_assignable_v&, unexpected>); // more-const OK + CHECK_FALSE(std::is_assignable_v&, unexpected>); // value G: deleted + CHECK_FALSE(std::is_assignable_v&, unexpected>); // const drop: no overload +} TEST_CASE("rebinding assignment from unexpected repoints the error reference", "[ref][unexpected][assign]") { int g1 = 1, g2 = 2; @@ -114,8 +121,10 @@ struct ThrowingRef { }; } // namespace -static_assert(std::is_nothrow_constructible_v, int&>); -static_assert(!std::is_nothrow_constructible_v, ThrowingRef>); +TEST_CASE("F4/F5: value construction is noexcept only when the reference bind cannot throw", "[ref][noexcept]") { + CHECK(std::is_nothrow_constructible_v, int&>); + CHECK_FALSE(std::is_nothrow_constructible_v, ThrowingRef>); +} TEST_CASE("value constructor propagates a throwing reference conversion", "[ref][noexcept]") { int caught = 0; @@ -187,8 +196,10 @@ concept has_error_or = requires(Ex e, Arg a) { e.error_or(a); }; struct NotConvertible {}; } // namespace -static_assert(has_error_or&, const char*>); -static_assert(!has_error_or&, NotConvertible>); +TEST_CASE("F7: error_or is SFINAE-friendly", "[ref][error_or]") { + CHECK(has_error_or&, const char*>); + CHECK_FALSE(has_error_or&, NotConvertible>); +} // ============================================================================= // Shallow conversion — converting from an rvalue reference-holding expected to a diff --git a/tests/beman/expected/expected_smf_regressions.test.cpp b/tests/beman/expected/expected_smf_regressions.test.cpp index b1875f3..856ed30 100644 --- a/tests/beman/expected/expected_smf_regressions.test.cpp +++ b/tests/beman/expected/expected_smf_regressions.test.cpp @@ -60,18 +60,31 @@ struct NoexceptNonTrivial { template inline constexpr bool swap_query_is_wellformed = (static_cast(std::is_swappable_v), true); -static_assert(swap_query_is_wellformed>); -static_assert(swap_query_is_wellformed>); +} // namespace -// Bug 2: non-trivial-but-noexcept copy => nothrow copy construct/assign. -static_assert(std::is_nothrow_copy_constructible_v>); -static_assert(std::is_nothrow_copy_constructible_v>); -static_assert(std::is_nothrow_copy_constructible_v>); -static_assert(std::is_nothrow_copy_assignable_v>); -static_assert(std::is_nothrow_copy_assignable_v>); -static_assert(std::is_nothrow_copy_assignable_v>); +// The trait properties below are checked at runtime rather than with +// static_assert so that a regression is reported by the test run, with the +// responsible type named, instead of stopping the build at the first failure +// and reporting nothing. Well-formedness is still a translation-time question: +// naming `swap_query_is_wellformed` instantiates it, so a hard error in +// `is_swappable_v` remains a build failure — what the CHECK adds is that a +// *wrong answer* is reported instead. -} // namespace +TEST_CASE("querying is_swappable on a move-restricted error type is well-formed") { + // Bug 1: previously a hard error via the unconstrained hidden-friend swap. + CHECK(swap_query_is_wellformed>); + CHECK(swap_query_is_wellformed>); +} + +TEST_CASE("non-trivial but noexcept copy gives a nothrow copy constructor and assignment") { + // Bug 2: non-trivial-but-noexcept copy => nothrow copy construct/assign. + CHECK(std::is_nothrow_copy_constructible_v>); + CHECK(std::is_nothrow_copy_constructible_v>); + CHECK(std::is_nothrow_copy_constructible_v>); + CHECK(std::is_nothrow_copy_assignable_v>); + CHECK(std::is_nothrow_copy_assignable_v>); + CHECK(std::is_nothrow_copy_assignable_v>); +} TEST_CASE("swap is well-formed and works for a move-restricted error type") { // Previously a hard error via the unconstrained hidden-friend swap. diff --git a/tests/beman/expected/expected_std_equivalence.test.cpp b/tests/beman/expected/expected_std_equivalence.test.cpp index 5e87a42..9711163 100644 --- a/tests/beman/expected/expected_std_equivalence.test.cpp +++ b/tests/beman/expected/expected_std_equivalence.test.cpp @@ -3,7 +3,7 @@ // Observable-equivalence gate: beman::expected holds the exposition-only member // unexpected, but every observable special-member property is keyed on E. -// This asserts, across a battery of adversarial error types, that the resulting +// This checks, across a battery of adversarial error types, that the resulting // type traits match std::expected exactly -- i.e. that the unexpected member // is behaviourally inert against a T/E rendering. See D4280: ABI stability for // existing implementations is a design goal, and any divergence found here is a @@ -14,6 +14,12 @@ // non-trivial assignment is a specification accident). Those traits are checked // as "beman is at least as trivial as std". // +// The comparisons are made at runtime rather than with static_assert so that a +// divergence is reported by the test run -- naming the trait and the pair of +// types it disagreed on, and reporting *every* divergence -- instead of +// stopping the build at the first one and reporting nothing. The traits +// themselves are still compile-time facts; only the verdict is reported. +// // Compiled against std::expected requires C++23. Skipped on libc++, whose // std::expected has its own quirks for pathological types -- see the std-parity // gate in CMakeLists.txt and docs/std-parity.md. @@ -22,12 +28,16 @@ #include +#include + #include #include #include namespace bx = beman::expected; +using beman::expected::testing::display_name; + namespace { struct Plain { @@ -99,11 +109,25 @@ inline constexpr bool skip_trivial_parity = false; template <> inline constexpr bool skip_trivial_parity = true; +// Every trait parity for one (E, T) pair, each one reported. `Bm` and `St` are +// named in the failure output so a divergence says which pair of types it was +// found on; the trait's own name is carried by the CHECK expression and by the +// scoped message, which is what the static_assert message used to say. template -constexpr bool parity() { +void check_parity() { using Bm = bx::expected; using St = std::expected; -#define BEMAN_PARITY(TRAIT) static_assert(std::TRAIT##_v == std::TRAIT##_v, #TRAIT " parity") + + constexpr auto bm_name = display_name(); + constexpr auto st_name = display_name(); + INFO("beman: " << bm_name); + INFO("std: " << st_name); + +#define BEMAN_PARITY(TRAIT) \ + do { \ + INFO(#TRAIT " parity"); \ + CHECK(std::TRAIT##_v == std::TRAIT##_v); \ + } while (false) BEMAN_PARITY(is_copy_constructible); BEMAN_PARITY(is_move_constructible); BEMAN_PARITY(is_nothrow_copy_constructible); @@ -121,34 +145,41 @@ constexpr bool parity() { BEMAN_PARITY(is_swappable); BEMAN_PARITY(is_nothrow_swappable); #undef BEMAN_PARITY + // Drive-by fix: beman is at least as trivially copyable/assignable as std. + // Each is an implication, so it is one bool rather than a comparison; the + // extra parentheses keep Catch2 from trying to decompose the `||`. if constexpr (!skip_trivial_parity) { - static_assert(!std::is_trivially_copy_assignable_v || std::is_trivially_copy_assignable_v); - static_assert(!std::is_trivially_move_assignable_v || std::is_trivially_move_assignable_v); - static_assert(!std::is_trivially_copyable_v || std::is_trivially_copyable_v); + INFO("beman is at least as trivial as std"); + CHECK((!std::is_trivially_copy_assignable_v || std::is_trivially_copy_assignable_v)); + CHECK((!std::is_trivially_move_assignable_v || std::is_trivially_move_assignable_v)); + CHECK((!std::is_trivially_copyable_v || std::is_trivially_copyable_v)); } - return true; } -#define BEMAN_RUN(E) static_assert(parity() && parity()) -BEMAN_RUN(Plain); -BEMAN_RUN(DelMoveOkCopy); -BEMAN_RUN(MoveOnly); -BEMAN_RUN(ThrowMoveNoexceptCopy); -BEMAN_RUN(NonTrivialDtor); -BEMAN_RUN(NoexceptMoveOnly); -BEMAN_RUN(Immovable); -BEMAN_RUN(NoexceptNonTrivial); -BEMAN_RUN(int); -BEMAN_RUN(std::string); -#undef BEMAN_RUN - -// Lock in the drive-by fix itself: beman is trivially copyable for trivial T,E -// (std::expected is not, by specification accident). -static_assert(std::is_trivially_copyable_v>); - } // namespace TEST_CASE("beman::expected type traits match std::expected") { - SUCCEED("all parity checks are static_asserts evaluated at compile time"); +#define BEMAN_RUN(E) \ + do { \ + check_parity(); \ + check_parity(); \ + } while (false) + BEMAN_RUN(Plain); + BEMAN_RUN(DelMoveOkCopy); + BEMAN_RUN(MoveOnly); + BEMAN_RUN(ThrowMoveNoexceptCopy); + BEMAN_RUN(NonTrivialDtor); + BEMAN_RUN(NoexceptMoveOnly); + BEMAN_RUN(Immovable); + BEMAN_RUN(NoexceptNonTrivial); + BEMAN_RUN(int); + BEMAN_RUN(std::string); +#undef BEMAN_RUN +} + +TEST_CASE("beman::expected is trivially copyable") { + // Lock in the drive-by fix itself: beman is trivially copyable for trivial + // T,E (std::expected is not, by specification accident). + CHECK(std::is_trivially_copyable_v>); } diff --git a/tests/beman/expected/expected_trivial.test.cpp b/tests/beman/expected/expected_trivial.test.cpp index a454889..ce85ec3 100644 --- a/tests/beman/expected/expected_trivial.test.cpp +++ b/tests/beman/expected/expected_trivial.test.cpp @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // Beman-only: triviality of special member functions is implementation quality. -// libstdc++ expected may or may not match these static_asserts. +// libstdc++ expected may or may not match these checks. +// +// The triviality properties are checked at runtime rather than with +// static_assert so that a violated property is reported by the test run, with +// the responsible type named, instead of stopping the build at the first +// failure and reporting nothing. #include @@ -17,33 +22,41 @@ using namespace beman::expected; // Primary template: trivial when T and E are trivial // --------------------------------------------------------------------------- -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("trivial SMFs: expected special members are trivial", "[trivial]") { + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); +} // --------------------------------------------------------------------------- // Void specialization: trivial when E is trivial // --------------------------------------------------------------------------- -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("trivial SMFs: expected special members are trivial", "[trivial]") { + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); +} // --------------------------------------------------------------------------- // Non-trivial when T or E is non-trivial // --------------------------------------------------------------------------- -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_move_constructible_v>); -static_assert(!std::is_trivially_copy_assignable_v>); -static_assert(!std::is_trivially_move_assignable_v>); +TEST_CASE("trivial SMFs: a non-trivial T makes the special members non-trivial", "[trivial]") { + CHECK_FALSE(std::is_trivially_copy_constructible_v>); + CHECK_FALSE(std::is_trivially_move_constructible_v>); + CHECK_FALSE(std::is_trivially_copy_assignable_v>); + CHECK_FALSE(std::is_trivially_move_assignable_v>); +} -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_copy_constructible_v>); +TEST_CASE("trivial SMFs: a non-trivial E makes the special members non-trivial", "[trivial]") { + CHECK_FALSE(std::is_trivially_copy_constructible_v>); + CHECK_FALSE(std::is_trivially_copy_constructible_v>); +} TEST_CASE("trivial SMFs: expected is trivially copyable", "[trivial]") { expected a(42); diff --git a/tests/beman/expected/expected_void.test.cpp b/tests/beman/expected/expected_void.test.cpp index 576a026..00899df 100644 --- a/tests/beman/expected/expected_void.test.cpp +++ b/tests/beman/expected/expected_void.test.cpp @@ -5,6 +5,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -17,6 +19,8 @@ using test_ns::expected; using test_ns::unexpect; using test_ns::unexpected; +using beman::expected::testing::type_name; + // ============================================================================= // [expected.void.general] Ill-formed instantiation constraints // ============================================================================= @@ -24,6 +28,13 @@ using test_ns::unexpected; // not a reference, not an array, not cv-qualified, not unexpected. // These are enforced by static_asserts inside the class body; verified by // negative compile tests (expected_void_ref_fail.cpp, expected_void_array_fail.cpp). +// +// The type-level properties below are checked at runtime rather than with +// static_assert so that a violated property is reported by the test run, with +// the responsible type named, instead of stopping the build at the first +// failure and reporting nothing. Type identity is checked by comparing the +// compiler's spelling of the two types, so a mismatch prints what was deduced +// next to what was wanted rather than the bare word `false`. // ============================================================================= // [expected.void.cons] Constructors @@ -34,7 +45,7 @@ using test_ns::unexpected; TEST_CASE("expected: default construct", "[expected_void]") { expected e; CHECK(e.has_value()); - static_assert(std::is_nothrow_default_constructible_v>); + CHECK(std::is_nothrow_default_constructible_v>); } // --- Copy constructor --- @@ -44,8 +55,10 @@ struct NoCopyE { NoCopyE() = default; }; -static_assert(!std::is_copy_constructible_v>); -static_assert(std::is_trivially_copy_constructible_v>); +TEST_CASE("expected: copy constructor availability and triviality", "[expected_void]") { + CHECK_FALSE(std::is_copy_constructible_v>); + CHECK(std::is_trivially_copy_constructible_v>); +} TEST_CASE("expected: copy construct with value", "[expected_void]") { expected a; @@ -62,7 +75,9 @@ TEST_CASE("expected: copy construct with error", "[expected_void]") { // --- Move constructor --- -static_assert(std::is_nothrow_move_constructible_v>); +TEST_CASE("expected: move constructor is noexcept", "[expected_void]") { + CHECK(std::is_nothrow_move_constructible_v>); +} TEST_CASE("expected: move construct with error", "[expected_void]") { expected a(unexpect, "err"); @@ -73,8 +88,10 @@ TEST_CASE("expected: move construct with error", "[expected_void]") { // --- Converting constructor from expected where is_void_v --- -// Constraint: U must be void — cannot convert from expected -static_assert(!std::is_constructible_v, expected>); +TEST_CASE("expected: converting constructor requires void U", "[expected_void]") { + // Constraint: U must be void — cannot convert from expected + CHECK_FALSE(std::is_constructible_v, expected>); +} TEST_CASE("expected: convert from expected with value", "[expected_void]") { expected src; @@ -108,7 +125,7 @@ TEST_CASE("expected: construct from unexpected&&", "[expected_void]") { TEST_CASE("expected: in_place_t constructor", "[expected_void]") { expected e(std::in_place); CHECK(e.has_value()); - static_assert(noexcept(expected(std::in_place))); + CHECK(noexcept(expected(std::in_place))); } // --- unexpect_t constructors --- @@ -129,7 +146,9 @@ TEST_CASE("expected: unexpect_t ilist constructor", "[expected_void]") { // [expected.void.dtor] Destructor // ============================================================================= -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("expected: trivially destructible when E is", "[expected_void]") { + CHECK(std::is_trivially_destructible_v>); +} TEST_CASE("expected: destructor destroys error", "[expected_void]") { int destroyed = 0; @@ -178,7 +197,9 @@ TEST_CASE("expected: copy assign error-to-error", "[expected_void]") { // --- Move assignment --- -static_assert(std::is_nothrow_move_assignable_v>); +TEST_CASE("expected: move assignment is noexcept", "[expected_void]") { + CHECK(std::is_nothrow_move_assignable_v>); +} TEST_CASE("expected: move assign value-to-error", "[expected_void]") { expected a, b(unexpect, "err"); @@ -208,7 +229,7 @@ TEST_CASE("expected: emplace from error state", "[expected_void]") { expected e(unexpect, 5); e.emplace(); CHECK(e.has_value()); - static_assert(noexcept(e.emplace())); + CHECK(noexcept(e.emplace())); } TEST_CASE("expected: emplace from value state (no-op)", "[expected_void]") { @@ -269,7 +290,7 @@ TEST_CASE("expected: has_value and bool", "[expected_void]") { TEST_CASE("expected: operator* is void", "[expected_void]") { expected e; - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); *e; // compiles and does nothing } @@ -296,9 +317,9 @@ TEST_CASE("expected: rvalue value() throws on error", "[expected_void]") { TEST_CASE("expected: error() all ref qualifications", "[expected_void]") { expected e(unexpect, 99); - static_assert(std::is_same_v); - static_assert(std::is_same_v); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); CHECK(e.error() == 99); } diff --git a/tests/beman/expected/expected_void_monadic.test.cpp b/tests/beman/expected/expected_void_monadic.test.cpp index fa2a414..3007e6e 100644 --- a/tests/beman/expected/expected_void_monadic.test.cpp +++ b/tests/beman/expected/expected_void_monadic.test.cpp @@ -5,6 +5,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -12,6 +14,8 @@ using namespace test_ns; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- // and_then - F called with no args when void // --------------------------------------------------------------------------- @@ -50,7 +54,7 @@ TEST_CASE("and_then void: rvalue overload propagates error by move", "[expected_ TEST_CASE("and_then void: return void expected", "[expected_void_monadic]") { expected e; auto r = e.and_then([]() -> expected { return {}; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); } @@ -101,7 +105,7 @@ TEST_CASE("or_else void: error propagated through lambda", "[expected_void_monad TEST_CASE("transform void: has value - calls F, returns expected", "[expected_void_monadic]") { expected e; auto r = e.transform([]() { return 42; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == 42); } @@ -122,7 +126,7 @@ TEST_CASE("transform void: F returns void - expected()", "[expected_voi expected e; int count = 0; auto r = e.transform([&]() { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); CHECK(count == 1); } @@ -131,7 +135,7 @@ TEST_CASE("transform void: has error, F returns void - propagates", "[expected_v expected e(unexpect, 5); int count = 0; auto r = e.transform([&]() { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(count == 0); REQUIRE(!r.has_value()); CHECK(r.error() == 5); @@ -151,7 +155,7 @@ TEST_CASE("transform void: rvalue overload", "[expected_void_monadic]") { TEST_CASE("transform_error void: has error - transforms error", "[expected_void_monadic]") { expected e(unexpect, 3); auto r = e.transform_error([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(r.error() == "3"); } @@ -164,7 +168,7 @@ TEST_CASE("transform_error void: has value - returns expected()", "[exp return ""; }); CHECK(!called); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); } @@ -177,7 +181,7 @@ TEST_CASE("void monadic chaining: and_then -> transform_error", "[expected_void_ auto r = e.and_then([]() -> expected { return {}; }).transform_error([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); } diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index a9c6dac..19cca7c 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -4,6 +4,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -11,49 +13,72 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- -// Type-level assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is compared by `type_name`, which decides by +// std::is_same_v and carries the compiler's spelling only so that a mismatch +// prints what was deduced next to what was wanted rather than the bare word +// `false`. // --------------------------------------------------------------------------- -static_assert(std::is_default_constructible_v>); -static_assert(std::is_nothrow_default_constructible_v>); +TEST_CASE("expected: special member availability and triviality", "[expected_void_ref_e]") { + CHECK(std::is_default_constructible_v>); + CHECK(std::is_nothrow_default_constructible_v>); + + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_destructible_v>); + CHECK(std::is_trivially_copyable_v>); +} -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_destructible_v>); -static_assert(std::is_trivially_copyable_v>); +TEST_CASE("expected: observer return types", "[expected_void_ref_e]") { + using expected_t = expected; -static_assert(std::is_same_v>().error()), int&>); -static_assert(std::is_same_v>().error()), int&>); + // error() returns E& (shallow const — const expected still returns E&, not const E&) + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); -static_assert(std::is_void_v>())>); + CHECK(std::is_void_v())>); +} // absence of operator-> and value_or tested by _fail.cpp negative compile tests // Finding 1: copy/move assignment must be available for reference E, including // const-reference E, where E itself is not assignable (is_copy_assignable_v is false) but the stored unexpected rebinds via pointer assignment. -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); +TEST_CASE("expected: copy/move assignment available for reference E", "[expected_void_ref_e]") { + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); +} // Finding 2: the general (value-G) void converting constructors must be gated on // !is_reference_v — for reference E they are unsound: the lvalue form would bind into // the source's owned error (dangling once the source is gone), and the rvalue form hard-errors // by selecting a deleted unexpected constructor deep in the body instead of being excluded // from overload resolution. is_constructible_v must report false for both, matching reality. -static_assert(!std::is_constructible_v, const expected&>); -static_assert(!std::is_constructible_v, expected&&>); -static_assert(!std::is_constructible_v, const expected&>); -static_assert(!std::is_constructible_v, expected&&>); +TEST_CASE("expected: value-G converting constructors excluded for reference E", "[expected_void_ref_e]") { + CHECK_FALSE(std::is_constructible_v, const expected&>); + CHECK_FALSE(std::is_constructible_v, expected&&>); + CHECK_FALSE(std::is_constructible_v, const expected&>); + CHECK_FALSE(std::is_constructible_v, expected&&>); +} // The dedicated reference-E path (source error type is itself a reference) remains available, // for both lvalue and rvalue sources. -static_assert(std::is_constructible_v, const expected&>); -static_assert(std::is_constructible_v, expected&&>); -static_assert(std::is_constructible_v, const expected&>); -static_assert(std::is_constructible_v, expected&&>); +TEST_CASE("expected: reference-G converting constructors remain available", "[expected_void_ref_e]") { + CHECK(std::is_constructible_v, const expected&>); + CHECK(std::is_constructible_v, expected&&>); + CHECK(std::is_constructible_v, const expected&>); + CHECK(std::is_constructible_v, expected&&>); +} // --------------------------------------------------------------------------- // Construction @@ -62,7 +87,7 @@ static_assert(std::is_constructible_v, expected: default construct has value", "[expected_void_ref_e]") { expected e; REQUIRE(e.has_value()); - static_assert(std::is_nothrow_default_constructible_v>); + CHECK(std::is_nothrow_default_constructible_v>); } TEST_CASE("expected: construct from unexpect+ref binds E&", "[expected_void_ref_e]") { @@ -76,7 +101,7 @@ TEST_CASE("expected: construct from unexpect+ref binds E&", "[expected_ TEST_CASE("expected: in_place_t constructor", "[expected_void_ref_e]") { expected e(std::in_place); CHECK(e.has_value()); - static_assert(noexcept(expected(std::in_place))); + CHECK(noexcept(expected(std::in_place))); } TEST_CASE("expected: copy construct from value state", "[expected_void_ref_e]") { @@ -211,7 +236,7 @@ TEST_CASE("expected: emplace from error state sets has_value", "[expect expected e(unexpect, err); e.emplace(); CHECK(e.has_value()); - static_assert(noexcept(e.emplace())); + CHECK(noexcept(e.emplace())); } TEST_CASE("expected: emplace from value state is no-op", "[expected_void_ref_e]") { @@ -236,7 +261,7 @@ TEST_CASE("expected: operator bool and has_value", "[expected_void_ref_ TEST_CASE("expected: operator*() is void no-op", "[expected_void_ref_e]") { expected e; - static_assert(std::is_void_v); + CHECK(std::is_void_v); *e; } @@ -260,7 +285,7 @@ TEST_CASE("expected: rvalue value() throws on error", "[expected_void_r TEST_CASE("expected: error() returns E& with correct address", "[expected_void_ref_e]") { int err = 99; expected e(unexpect, err); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(&e.error() == &err); } @@ -407,7 +432,7 @@ TEST_CASE("expected: or_else short-circuits on success", "[expected_voi TEST_CASE("expected: transform calls F with no args", "[expected_void_ref_e]") { expected e; auto r = e.transform([]() { return 42; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == 42); } @@ -416,7 +441,7 @@ TEST_CASE("expected: transform with void-returning F", "[expected_void_ expected e; int count = 0; auto r = e.transform([&]() { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); CHECK(count == 1); } @@ -438,7 +463,7 @@ TEST_CASE("expected: transform_error transforms E& to new type", "[expe int err = 3; expected e(unexpect, err); auto r = e.transform_error([](int& v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(r.error() == "3"); } @@ -479,8 +504,8 @@ TEST_CASE("expected: monadic chaining error path", "[expected_void_ref_ // --------------------------------------------------------------------------- TEST_CASE("expected: trivial operations", "[expected_void_ref_e]") { - static_assert(std::is_trivially_copyable_v>); - static_assert(std::is_trivially_destructible_v>); + CHECK(std::is_trivially_copyable_v>); + CHECK(std::is_trivially_destructible_v>); } // ============================================================================= diff --git a/tests/beman/expected/unexpected.test.cpp b/tests/beman/expected/unexpected.test.cpp index d315bc2..aa96af1 100644 --- a/tests/beman/expected/unexpected.test.cpp +++ b/tests/beman/expected/unexpected.test.cpp @@ -5,6 +5,9 @@ #include +#include +#include + #include #include #include @@ -12,32 +15,49 @@ namespace expt = test_ns; +using beman::expected::testing::constant_eval; +using beman::expected::testing::type_name; + // ============================================================================= // [expected.un.general] para 2 — ill-formed instantiation constraints // (actual ill-formed cases tested via negative compile files) +// +// These type-level properties are checked at runtime rather than with +// static_assert so that a violated property is reported by the test run, with +// the responsible type named, instead of stopping the build at the first +// failure and reporting nothing. Type identity is checked by comparing the +// compiler's spelling of the two types, so a mismatch prints what was deduced +// next to what was wanted rather than the bare word `false`. // ============================================================================= -// [expected.un.cons] Constraint 1.3: is_constructible_v must be true -static_assert(std::is_constructible_v, int>); -static_assert(std::is_constructible_v, const char*>); -static_assert(!std::is_constructible_v, std::string>); +TEST_CASE("unexpected: construction constraints", "[UnexpectedTest]") { + // [expected.un.cons] Constraint 1.3: is_constructible_v must be true + CHECK(std::is_constructible_v, int>); + CHECK(std::is_constructible_v, const char*>); + CHECK_FALSE(std::is_constructible_v, std::string>); -// [expected.un.cons] Constraint 1.2: the *converting* ctor excludes in_place_t as Err, -// routing it to the in-place constructor instead. Both work: -static_assert(std::is_constructible_v, std::in_place_t>); // in-place ctor + // [expected.un.cons] Constraint 1.2: the *converting* ctor excludes in_place_t as Err, + // routing it to the in-place constructor instead. Both work: + CHECK(std::is_constructible_v, std::in_place_t>); // in-place ctor +} -// Copy and move constructible -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); +TEST_CASE("unexpected: copy, move and swap availability", "[UnexpectedTest]") { + // Copy and move constructible + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); -// [expected.un.swap] Constraint: is_swappable_v -static_assert(std::is_swappable_v>); + // [expected.un.swap] Constraint: is_swappable_v + CHECK(std::is_swappable_v>); +} -// [expected.un.obs] error() ref-qualification return types -static_assert(std::is_same_v&>().error()), int&>); -static_assert(std::is_same_v&>().error()), const int&>); -static_assert(std::is_same_v&&>().error()), int&&>); -static_assert(std::is_same_v&&>().error()), const int&&>); +TEST_CASE("unexpected: error() ref-qualification return types", "[UnexpectedTest]") { + // [expected.un.obs] error() ref-qualification return types + using unexpected_t = expt::unexpected; + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} TEST_CASE("unexpected: construct from int", "[UnexpectedTest]") { expt::unexpected u(42); @@ -147,29 +167,37 @@ TEST_CASE("unexpected: equality different types", "[UnexpectedTest]") { TEST_CASE("unexpected: CTAD from int", "[UnexpectedTest]") { expt::unexpected u(42); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(u.error() == 42); } TEST_CASE("unexpected: CTAD from string", "[UnexpectedTest]") { std::string s("deduced"); expt::unexpected u(s); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(u.error() == "deduced"); } TEST_CASE("unexpected: copy and move constructible", "[UnexpectedTest]") { - static_assert(std::is_copy_constructible_v>); - static_assert(std::is_move_constructible_v>); + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); } TEST_CASE("unexpected: unexpect_t tag type", "[UnexpectedTest]") { - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); } TEST_CASE("unexpected: constexpr basic usage", "[UnexpectedTest]") { - constexpr expt::unexpected u(123); - static_assert(u.error() == 123); + // The probe declares the constexpr variable itself, so "is unexpected + // usable as a constexpr variable?" is still answered by the compiler; + // `constant_eval` then hands the observed error back as an ordinary value + // so that a wrong answer is reported rather than breaking the build. + constexpr auto probe = [] { + constexpr expt::unexpected u(123); + return u.error(); + }; + CHECK(constant_eval(probe) == 123); + CHECK(probe() == 123); } TEST_CASE("unexpected: inequality operator (synthesized)", "[UnexpectedTest]") { @@ -180,6 +208,5 @@ TEST_CASE("unexpected: inequality operator (synthesized)", "[UnexpectedTest]") { TEST_CASE("unexpected: in-place ilist constraint: is_constructible from ilist", "[UnexpectedTest]") { // is_constructible_v&, Args...> must hold - static_assert( - std::is_constructible_v>, std::in_place_t, std::initializer_list>); + CHECK(std::is_constructible_v>, std::in_place_t, std::initializer_list>); } From 5d03afb17cf030bdc65ba93054d9896cc06a096b Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Mon, 10 Aug 2026 15:23:56 -0400 Subject: [PATCH 5/6] test: address code-review nits on constexpr probes and comment Declare the constexpr construction/equality probes in expected.test.cpp as constexpr objects so they still exercise "usable as a constexpr variable", matching unexpected.test.cpp. Fix a stale comment in expected_review_corrections.test.cpp that described a runtime-checked assignment trait as "rejected at compile time". Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/beman/expected/expected.test.cpp | 20 ++++++++++--------- .../expected_review_corrections.test.cpp | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/beman/expected/expected.test.cpp b/tests/beman/expected/expected.test.cpp index 7dedc6a..cacdcb5 100644 --- a/tests/beman/expected/expected.test.cpp +++ b/tests/beman/expected/expected.test.cpp @@ -717,10 +717,12 @@ TEST_CASE("expected: cross-type equality error", "[ExpectedTest]") { // ============================================================================= // Constexpr usage // -// Each of these builds an expected inside a self-contained probe lambda and -// reduces its state to a literal aggregate. `constant_eval` runs the probe -// during translation — so "is this usable in a constant expression?" is still -// answered by the compiler — and hands back the result as an ordinary value, +// Each of these declares the expected as a constexpr object inside a +// self-contained probe lambda and reduces its state to a literal aggregate. The +// constexpr declaration keeps "is this usable as a constexpr variable?" answered +// by the compiler; `constant_eval` runs the probe during translation — so "is +// this usable in a constant expression?" is answered too — and hands back the +// result as an ordinary value, // so "did it produce the right state?" is answered by a reported CHECK. // Calling the same probe directly runs the identical body at runtime, which // is worth doing separately: constant evaluation and ordinary evaluation take @@ -745,7 +747,7 @@ struct int_state { TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { constexpr auto probe = [] { - expt::expected e; + constexpr expt::expected e; return int_state{e.has_value(), *e}; }; CHECK(constant_eval(probe) == int_state{true, 0}); @@ -754,7 +756,7 @@ TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { TEST_CASE("expected: constexpr value construction", "[ExpectedTest]") { constexpr auto probe = [] { - expt::expected e(42); + constexpr expt::expected e(42); return int_state{e.has_value(), *e}; }; CHECK(constant_eval(probe) == int_state{true, 42}); @@ -763,7 +765,7 @@ TEST_CASE("expected: constexpr value construction", "[ExpectedTest]") { TEST_CASE("expected: constexpr error construction", "[ExpectedTest]") { constexpr auto probe = [] { - expt::expected e(expt::unexpect, 7); + constexpr expt::expected e(expt::unexpect, 7); return int_state{e.has_value(), e.error()}; }; CHECK(constant_eval(probe) == int_state{false, 7}); @@ -775,8 +777,8 @@ TEST_CASE("expected: constexpr equality", "[ExpectedTest]") { // the win is that a wrong answer is still a reported failure rather than // a build break, and the rest of the file still runs. constexpr auto probe = [] { - expt::expected a(42); - expt::expected b(42); + constexpr expt::expected a(42); + constexpr expt::expected b(42); return a == b; }; CHECK(constant_eval(probe)); diff --git a/tests/beman/expected/expected_review_corrections.test.cpp b/tests/beman/expected/expected_review_corrections.test.cpp index b875551..29fea32 100644 --- a/tests/beman/expected/expected_review_corrections.test.cpp +++ b/tests/beman/expected/expected_review_corrections.test.cpp @@ -67,8 +67,8 @@ TEST_CASE("reference-error construction from unexpected binds an external ob } // Rebinding assignment from unexpected — allowed for reference E only when G is a reference -// (rebinds the error pointer to an external object; never dangles). Value G and const-drop are -// rejected at compile time, mirroring construction. +// (rebinds the error pointer to an external object; never dangles). Value G and const-drop make +// the assignment ill-formed; that trait is checked at runtime here, mirroring construction. TEST_CASE("F6: rebinding assignment from unexpected is allowed only for reference G", "[ref][unexpected][assign]") { CHECK(std::is_assignable_v&, unexpected>); CHECK(std::is_assignable_v&, unexpected>); From b3b2cdf9ca3c65f19d94d0de1c86a0b64432f4c6 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Mon, 10 Aug 2026 17:37:23 -0400 Subject: [PATCH 6/6] docs: add "Scrap your static_assert" blog post and transclusion machinery Add docs/blog/scrap-your-static_assert.org, an org-mode post covering the two techniques the test suite uses to report compile-time facts through the runtime framework: type_name() for type-identity checks and constant_eval for constexpr probes. Code snippets are pulled from the tree by org-transclusion against UUID-anchored regions, so the post cannot drift from the code. Add the anchor comment pairs the post transcludes to type_name.hpp, constant_eval.hpp, and expected.test.cpp. Copy the conversion machinery from the compile-time-scheme repo: .emacs.d/ (init.el plus the orgit-file-transclusion.el adapter, base URLs repointed at this repo) and the blog-md Makefile targets that export docs/blog/*.org to GFM markdown with transclusions resolved. Pinned orgit-file: links resolve against an annotated tag; docs/blog/pins.md records the post-to-tag mapping. Co-Authored-By: Claude Opus 4.8 (1M context) --- .emacs.d/init.el | 247 ++++++++++++++++++ .emacs.d/lisp/orgit-file-transclusion.el | 146 +++++++++++ .gitignore | 7 + Makefile | 80 ++++++ docs/blog/pins.md | 23 ++ docs/blog/scrap-your-static_assert.md | 85 ++++++ docs/blog/scrap-your-static_assert.org | 95 +++++++ tests/beman/expected/expected.test.cpp | 4 + .../beman/expected/testing/constant_eval.hpp | 2 + tests/beman/expected/testing/type_name.hpp | 2 + 10 files changed, 691 insertions(+) create mode 100644 .emacs.d/init.el create mode 100644 .emacs.d/lisp/orgit-file-transclusion.el create mode 100644 docs/blog/pins.md create mode 100644 docs/blog/scrap-your-static_assert.md create mode 100644 docs/blog/scrap-your-static_assert.org diff --git a/.emacs.d/init.el b/.emacs.d/init.el new file mode 100644 index 0000000..ebf3d9c --- /dev/null +++ b/.emacs.d/init.el @@ -0,0 +1,247 @@ +;; Save any custom set variable in exordium-custom-file rather than at the end of init.el: +(setq custom-file (locate-user-emacs-file "custom.el")) + +(require 'package) +(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(setq package-user-dir + (locate-user-emacs-file (concat "elpa-" emacs-version))) + +(when (fboundp 'native-comp-available-p) + (setq package-native-compile (native-comp-available-p))) +(package-initialize) + +;; Load the packages we need if they are not installed already +(let ((package-pinned-packages (append + '((use-package . "melpa") + (diminish . "melpa") + (bind-key . "melpa")))) + (has-refreshed nil)) + + (defun update-package (p has-refreshed) + (unless (package-installed-p p) + (unless has-refreshed + (message "Refreshing package database...") + (package-refresh-contents) + (setq has-refreshed t) + (message "Done.")) + (package-install p))) + + (dolist (pkg package-pinned-packages) + (let ((p (car pkg))) + (update-package p has-refreshed)))) + +;; This is only needed once, near the top of the file +(eval-when-compile + ;; Following line is not needed if use-package.el is in ~/.emacs.d + (require 'use-package)) + +(require 'use-package-ensure) +(setq use-package-always-ensure t) +(setq use-package-compute-statistics t) +;;; remove a package from the builtin list so it can be upgraded +(defun wg21org-ignore-builtin (pkg) + (assq-delete-all pkg package--builtins) + (assq-delete-all pkg package--builtin-versions)) + + +;;; Org mode + +(use-package org + :commands (org-mode) + :mode (("\\.org\\'" . org-mode)) + :after (flycheck) + :bind + (:map org-mode-map + ([remap org-toggle-comment] . iedit-mode)) + :custom + (org-confirm-babel-evaluate t) + (org-fontify-quote-and-verse-blocks t) + (org-fontify-whole-heading-line t) + (org-log-into-drawer t) + (org-src-fontify-natively t) + (org-src-preserve-indentation t) + (org-startup-folded nil) + (org-startup-indented t) + (org-startup-truncated nil) + (org-startup-with-inline-images t) + (org-support-shift-select :always) + (org-use-sub-superscripts "{}") + + ;; Edit settings + (org-auto-align-tags nil) + (org-tags-column 0) + (org-catch-invisible-edits 'show-and-error) + (org-special-ctrl-a/e t) + (org-insert-heading-respect-content t) + + ;; Org styling, hide markup etc. + (org-hide-emphasis-markers nil) + (org-pretty-entities t) + + ;; ;;;; code blocks + (org-confirm-babel-evaluate nil) + (org-src-window-setup 'reorganize-frame) + (org-edit-src-persistent-message t) + (org-src-fontify-natively t) + (org-src-preserve-indentation t) + (org-src-tab-acts-natively t) + (org-edit-src-content-indentation 0) + + ;; ;;;; export + (org-export-with-toc t) + (org-export-headline-levels 8) + (org-export-dispatch-use-expert-ui nil) + (org-html-htmlize-output-type 'css) + (org-html-head-include-default-style t) + (org-html-head-include-scripts t) + + ;;; visual line mode + (visual-line-mode 1) + + :hook ((org-mode . variable-pitch-mode)) + + :init + (add-hook 'org-src-mode-hool #'(lambda () + (add-to-list 'flycheck-disabled-checkers 'emacs-lisp-checkdoc))) + + :config + ;; Enable org-babel for perl, ruby, sh, python, emacs-lisp, C, C++, etc + ;; TODO: add extra languages configurable by user + (org-babel-do-load-languages + 'org-babel-load-languages + `((perl . t) + (ruby . t) + (shell . t) + (python . t) + (emacs-lisp . t) + (C . t) + (dot . t) + (sql . t)))) + +(use-package htmlize + :ensure t) + + +(use-package graphviz-dot-mode + :config + (setq graphviz-dot-indent-width 4)) + +(org-babel-do-load-languages + 'org-babel-load-languages + (append org-babel-load-languages + '((dot . t)))) + + +(setq plantuml-jar-path "/usr/share/plantuml/plantuml.jar") +(setq plantuml-default-exec-mode 'jar) + +(setq org-plantuml-jar-path (expand-file-name "/usr/share/plantuml/plantuml.jar")) +(add-to-list 'org-src-lang-modes '("plantuml" . plantuml)) +(org-babel-do-load-languages + 'org-babel-load-languages + (append org-babel-load-languages + '((plantuml . t)))) + +(org-babel-do-load-languages + 'org-babel-load-languages + (append org-babel-load-languages + '((ditaa . t)))) + +(setq org-ditaa-jar-path "/usr/share/ditaa/ditaa.jar") + +(setq org-support-shift-select 'always) + + +;; Reveal.js + Org mode +(use-package org-re-reveal + :config + (setq org-re-reveal-root "file:////home/sdowney/bld/reveal.js")) + + +(use-package org-transclusion + :after org + :bind (:map + org-mode-map + ("C-c C-x T" . org-transclusion-mode)) + :config + (org-transclusion-mode 1) + ) + + +(use-package ox-gfm + :after org) + +(use-package with-editor) + +(use-package citeproc :ensure t :after org) + +;; Export orgit links as GitHub source browser URLs +(defvar orgit-base-url "https://github.com/steve-downey/expected/blob/main/" + "Base URL for exporting orgit links.") + +(org-link-set-parameters + "orgit" + :export (lambda (path desc backend) + (let* ((parts (split-string path "::")) + (filepath (if (> (length parts) 1) (cadr parts) path)) + (url (concat orgit-base-url filepath))) + (cond + ((or (eq 'md backend) (eq 'gfm backend)) + (format "[`%s`](%s)" (or desc filepath) url)) + ((eq 'html backend) + (format "%s" url (or desc filepath))) + (t url))))) + + + + + + + + + +;; Allow org-transclusion to resolve orgit links to the local file system +(defun org-transclusion-add-orgit (link plist) + "Resolve orgit links into file links in-place." + (when (string= "orgit" (org-element-property :type link)) + (let* ((full-path (org-element-property :path link)) + (parts (split-string full-path "::")) + (repo-dir (car parts)) + (inner-file (cadr parts)) + (search-uuid (if (> (length parts) 2) (caddr parts) nil)) + (actual-file (expand-file-name inner-file repo-dir)) + (raw-link (concat "file:" actual-file))) + + (when search-uuid + (setq raw-link (concat raw-link "::" search-uuid))) + + ;; CRITICAL: Mutate the original link in-place so downstream plugins + ;; (like org-transclusion-src-lines) see the correct file path! + (org-element-put-property link :type "file") + (org-element-put-property link :path actual-file) + (org-element-put-property link :raw-link raw-link) + (if search-uuid + (org-element-put-property link :search-option search-uuid) + (org-element-put-property link :search-option nil)))) + ;; ALWAYS return nil so the NEXT functions (like org-transclusion-add-src-lines) can handle it! + nil) + +(require 'org-transclusion) +(add-hook 'org-transclusion-add-functions 'org-transclusion-add-orgit) + +;; Pinned transclusion for the epistolary blog posts. +;; +;; Two document categories, two policies (docs/epistolary-pinning-plan.md): +;; - living docs (docs/compiler_architecture.org) transclude via `file:' / +;; `orgit:' and roll forward with the worktree, which is the point of them; +;; - blog posts transclude via `orgit-file:' pinned to a `blog/phase-NN' tag, +;; so a later refactor cannot rewrite the code inside a published entry. +;; +;; Both adapters live on `org-transclusion-add-functions' and coexist; the link +;; type selects the policy. See docs/blog/pins.md for the post-to-tag mapping. +(add-to-list 'load-path + (expand-file-name + "lisp" (or (and load-file-name + (file-name-directory load-file-name)) + user-emacs-directory))) +(require 'orgit-file-transclusion) diff --git a/.emacs.d/lisp/orgit-file-transclusion.el b/.emacs.d/lisp/orgit-file-transclusion.el new file mode 100644 index 0000000..86056be --- /dev/null +++ b/.emacs.d/lisp/orgit-file-transclusion.el @@ -0,0 +1,146 @@ +;;; orgit-file-transclusion.el --- transclude UUID-anchored regions at a git rev -*- lexical-binding: t; -*- +;; SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +;; Provenance: docs/epistolary-pinning-plan.md (section 3), shared across +;; compile-time-scheme sibling repositories by copy, not by load-path coupling. +;; +;; Link form: [[orgit-file:REPO::REV::PATH::UUID]] +;; Used with: :lines 2- :src LANG :end "UUID end" (unchanged conventions) +;; +;; Where the sibling `orgit:' adapter in init.el resolves PATH against the +;; worktree -- showing the file as of now -- this one resolves it against REV, +;; so an epistolary post keeps showing the code its prose was written about. +;; REV is normally an annotated `blog/phase-NN' tag; see docs/blog/pins.md. +;; +;; No magit dependency: the blob comes from a `git show' subprocess, so batch +;; export stays light. The interactive `orgit-file' package (gggion/orgit-file) +;; uses the same link syntax, so installing it makes these links followable in +;; a live Emacs, but nothing here requires it. + +;;; Commentary: +;; +;; Implementation note -- why this extracts a blob to a file instead of +;; slicing the region itself: +;; +;; `org-transclusion-add-src-lines' already implements the exact semantics the +;; posts depend on, and they are fiddlier than they look. `:end' is EXCLUSIVE +;; of the line holding the end marker; `:lines 2-' counts forward from the +;; anchor line found by the search option, not from the top of the file; and +;; `:src cpp' wraps the result in a fenced block. Reimplementing that slicing +;; would duplicate three behaviours that must not drift. +;; +;; So this adapter mirrors `org-transclusion-add-orgit': it materialises the +;; pinned blob, rewrites the link in place into a plain `file:' link with the +;; same `::UUID' search option, and returns nil so the standard src-lines +;; handler does the work. Post attributes keep meaning exactly what they +;; meant under worktree resolution -- by construction, not by imitation. +;; +;; The blob is written under a path that preserves the original repo-relative +;; path, so `find-file-noselect' picks the same major mode it would have for +;; the real file. `org-link-search', which resolves the `::UUID' anchor, is +;; mode-sensitive, so this is what keeps anchor resolution identical. + +;;; Code: + +(require 'org-transclusion) +(require 'subr-x) + +(defgroup orgit-file-transclusion nil + "Transclude UUID-anchored source regions pinned to a git revision." + :group 'org-transclusion) + +(defcustom orgit-file-transclusion-cache-dir + (expand-file-name "orgit-file-transclusion" temporary-file-directory) + "Directory holding blobs extracted from git. +Contents are disposable: each blob is cached under its resolved commit +SHA, so a stale entry cannot be served for a different revision." + :type 'directory + :group 'orgit-file-transclusion) + +(defcustom orgit-file-transclusion-base-url + "https://github.com/steve-downey/expected/blob/" + "Base URL for exporting `orgit-file' links, with a trailing slash. +The pinned REV is appended, so an exported link is a permalink to the +revision the post was written against rather than to a moving branch." + :type 'string + :group 'orgit-file-transclusion) + +(defun orgit-file-transclusion--parse (raw) + "Split RAW into (REPO REV PATH UUID); signal on any other shape." + (let ((parts (split-string raw "::"))) + (unless (= 4 (length parts)) + (error "orgit-file link needs REPO::REV::PATH::UUID, got: %s" raw)) + parts)) + +(defun orgit-file-transclusion--rev-parse (repo rev) + "Resolve REV to a full commit SHA in REPO." + (with-temp-buffer + (unless (zerop (call-process "git" nil t nil + "-C" (expand-file-name repo) + "rev-parse" (concat rev "^{commit}"))) + (error "orgit-file: cannot resolve rev %s in %s (fetch tags?): %s" + rev repo (string-trim (buffer-string)))) + (string-trim (buffer-string)))) + +(defun orgit-file-transclusion--blob-file (repo rev path) + "Materialise PATH at REV in REPO as a local file; return its name. +The returned path ends in PATH, so the buffer gets the major mode it +would have had for the real file." + (let* ((sha (orgit-file-transclusion--rev-parse repo rev)) + (cached (expand-file-name + path (expand-file-name sha orgit-file-transclusion-cache-dir)))) + (unless (file-exists-p cached) + (make-directory (file-name-directory cached) t) + ;; Write via a temp name and rename, so a failed `git show' can never + ;; leave a truncated blob behind for a later run to trust. + (let ((tmp (concat cached ".partial"))) + (with-temp-buffer + (unless (zerop (call-process "git" nil t nil + "-C" (expand-file-name repo) + "show" (format "%s:%s" sha path))) + (error "orgit-file: git show %s:%s failed in %s: %s" + rev path repo (string-trim (buffer-string)))) + (let ((coding-system-for-write 'no-conversion)) + (write-region (point-min) (point-max) tmp nil 'quiet))) + (rename-file tmp cached t))) + cached)) + +(defun orgit-file-transclusion-add (link _plist) + "Resolve an `orgit-file' LINK into a `file' link on the pinned blob. +Mutates LINK in place and returns nil, so `org-transclusion-add-src-lines' +applies the post's :lines/:src/:end attributes exactly as it does for a +worktree-resolved link." + (when (string= "orgit-file" (org-element-property :type link)) + (pcase-let* ((`(,repo ,rev ,path ,uuid) + (orgit-file-transclusion--parse + (org-element-property :path link))) + (blob (orgit-file-transclusion--blob-file repo rev path))) + ;; Mutate in place so downstream handlers see the resolved file path. + (org-element-put-property link :type "file") + (org-element-put-property link :path blob) + (org-element-put-property link :raw-link (concat "file:" blob "::" uuid)) + (org-element-put-property link :search-option uuid))) + ;; Always nil: let the next add-function build the payload. + nil) + +(add-hook 'org-transclusion-add-functions #'orgit-file-transclusion-add) + +;; Export `orgit-file' links as forge permalinks at the pinned revision. +;; Posts currently carry these links only on `#+transclude:' lines, which +;; org-transclusion consumes before export, so this is for future inline use. +(org-link-set-parameters + "orgit-file" + :export + (lambda (path desc backend) + (let* ((parts (split-string path "::")) + (rev (nth 1 parts)) + (filepath (or (nth 2 parts) path)) + (url (concat orgit-file-transclusion-base-url rev "/" filepath))) + (cond + ((memq backend '(md gfm)) (format "[`%s`](%s)" (or desc filepath) url)) + ((eq backend 'html) + (format "%s" url (or desc filepath))) + (t url))))) + +(provide 'orgit-file-transclusion) +;;; orgit-file-transclusion.el ends here diff --git a/.gitignore b/.gitignore index 0e68d18..8f0e1c2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ /uv.lock .build /.claude/ + +# ignore emacs package cache and generated blog artifacts +/.emacs.d/eln-cache/ +/.emacs.d/elpa-*/ +/.emacs.d/elpa/ +/.emacs.d/custom.el +*.deps diff --git a/Makefile b/Makefile index 90a682b..a75cb86 100755 --- a/Makefile +++ b/Makefile @@ -264,6 +264,86 @@ endif install-uv: ## install uv via `pipx install uv` $(install_uv_cmd) +# ------------------------------------------------------------------------------ +# Blog: org-mode -> GFM markdown, with UUID-anchored source transclusion. +# +# docs/blog/*.org posts pull code out of the tree with org-transclusion, +# resolved by the elisp in .emacs.d/. orgit-file: links are pinned to a +# committed git rev, so a published post keeps showing the code its prose was +# written about. See docs/blog/pins.md for the post-to-rev mapping. +# ------------------------------------------------------------------------------ +EMACS := $(shell command -v emacs 2> /dev/null) + +ORGFILES := $(wildcard *.org) + +%.html : %.org + $(EMACS) --init-directory=.emacs.d/ \ + --batch --load .emacs.d/init.el \ + -f package-initialize \ + --eval "(setq enable-local-variables :all)" \ + --visit $< \ + --eval "(org-transclusion-mode t)" \ + --eval "(org-export-to-file 'html \"$@\")" + echo $@ : \\ > $@.deps + echo " $<" \\ >> $@.deps + sed -n "s/^.*\[\[file:\(\S*\)::.*$$/\1/p" < $< | sort -u | xargs printf " %s \\\\\\n" >> $@.deps + +-include $(wildcard $(ORGFILES:%.org=%.html.deps)) + +%-slides.html : %.org + $(EMACS) --init-directory=.emacs.d/ \ + --batch --load .emacs.d/init.el \ + -f package-initialize \ + --eval "(setq enable-local-variables :all)" \ + --visit $< \ + --eval "(org-transclusion-mode t)" \ + --eval "(org-export-to-file 're-reveal \"$@\")" + echo $@ : \\ > $@.deps + echo " $<" \\ >> $@.deps + sed -n "s/^.*\[\[file:\(\S*\)::.*$$/\1/p" < $< | sort -u | xargs printf " %s \\\\\\n" >> $@.deps + +-include $(wildcard $(ORGFILES:%.org=%-slides.html.deps)) + +BLOG_ORGFILES := $(wildcard docs/blog/*.org) + +docs/blog/%.md : docs/blog/%.org + $(EMACS) --init-directory=.emacs.d/ \ + --batch --load .emacs.d/init.el \ + -f package-initialize \ + --eval "(setq enable-local-variables :all)" \ + --visit $< \ + --eval "(org-transclusion-mode t)" \ + --eval "(require 'ox-gfm)" \ + --eval "(org-export-to-file 'gfm \"$(abspath $@)\")" + echo $@ : \\ > $@.deps + echo " $<" \\ >> $@.deps + sed -n \ + -e "s/^.*\[\[file:\(\S*\)::.*$$/\1/p" \ + -e "s/^.*\[\[orgit:[^:]*::\([^:]*\)::.*$$/\1/p" \ + < $< | sort -u | xargs printf " %s \\\\\\n" >> $@.deps + +-include $(wildcard $(BLOG_ORGFILES:.org=.md.deps)) + +.PHONY: blog-md +blog-md: $(BLOG_ORGFILES:.org=.md) ## convert docs/blog/*.org to GFM markdown + +.PHONY: clean-blog-md +clean-blog-md: + -rm -f $(BLOG_ORGFILES:.org=.md) $(BLOG_ORGFILES:.org=.md.deps) +clean: clean-blog-md + + +.PHONY: clean-emacs.d +clean-emacs.d: + -rm -rf .emacs.d/eln-cache + -rm -rf .emacs.d/elpa* + +realclean: clean-emacs.d + +.PHONY: clean-org-deps +clean-org-deps: + -rm $(ORGFILES:%.org=%.org.deps) + # Help target .PHONY: help help: ## Show this help. diff --git a/docs/blog/pins.md b/docs/blog/pins.md new file mode 100644 index 0000000..ed9301c --- /dev/null +++ b/docs/blog/pins.md @@ -0,0 +1,23 @@ +# Blog transclusion pins + +Each post that transcludes live code is pinned to one annotated tag. +`#+transclude:` links resolve against that tag's tree, not against the +worktree, so a later refactor cannot rewrite the code inside an already +published entry. The transclusion machinery is the copy of `.emacs.d/` and the +`blog-md` Makefile target carried over from the `compile-time-scheme` +repository; see `.emacs.d/lisp/orgit-file-transclusion.el`. + +## The mapping + +| Post | Tag | Basis | +|---|---|---| +| `scrap-your-static_assert.org` | `blog/scrap-static-assert` | commit adding the UUID anchors and the post | + +## Notes + +`orgit-file:` links pin to a tag, so a pinned post's `.md.deps` names only its +own `.org` and not the transcluded sources — that is correct, not a bug to +repair. The code comes from an immutable tag, so there is no worktree +dependency to track; rebuilding the post when the working tree changes would be +the defect. The `file:`/`orgit:` dependency extraction in the Makefile stays +useful only for any living document that still resolves against the worktree. diff --git a/docs/blog/scrap-your-static_assert.md b/docs/blog/scrap-your-static_assert.md new file mode 100644 index 0000000..7523930 --- /dev/null +++ b/docs/blog/scrap-your-static_assert.md @@ -0,0 +1,85 @@ +The obvious way to test a compile-time fact is `static_assert`. It's right there, it needs no framework, and for a fact that has to hold it's the right tool. As a *test*, though, it has one bad property: a wrong answer is a translation failure. The build stops at the first one, you get a compiler diagnostic instead of a test result, and every other test in the file goes unrun. The xUnit report is empty. You learn that something is wrong, once, and nothing about the rest. + +There's a second, smaller problem. Even when you write the check as a runtime `CHECK` so that it gets reported, a bare trait doesn't report anything you can use: + +```C++ +CHECK(std::is_same_v); // FAILED: CHECK( false ) +``` + +The expansion is the word `false`. You already knew the two types differed; the framework won't tell you what either of them was. + +Converting the `expected` tests off `static_assert` came down to two header-only components that fix these two problems. Neither is clever. (The title owes Lämmel and Peyton Jones; the debt stops at the title.) + + +# Type identity as a value + +The fix for the second problem is to compare type *identities* that carry their spelling for diagnostics, instead of comparing a bool. See [`type\_name.hpp`](https://github.com/steve-downey/expected/blob/main/tests/beman/expected/testing/type_name.hpp). The comparison is still `std::is_same_v`, so the verdict is exact and a false pass isn't possible: + +```cpp +template +constexpr bool beman::expected::testing::operator==(type_name_t, type_name_t) { + return std::is_same_v; +} +``` + +The spelling is consulted only after a comparison has already failed and the framework needs to explain it. So a failing check explains itself: + +```text +FAILED: CHECK( type_name() == type_name() ) +with expansion: const int& == int& +``` + +And the tests read like the trait they replaced: + +```cpp +TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]") { + using expected_t = expt::expected; + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); +} +``` + + +# Reporting a compile-time value at runtime + +The fix for the first problem is to split the two questions a constexpr test actually asks. "Can this be constant-evaluated at all?" is a property of the code; it stays a hard translation failure, which is correct, because that's a fact that has to hold. "Does it produce the right answer?" is a property of a value, and there's no reason a wrong value should stop the build. + +[`constant\_eval.hpp`](https://github.com/steve-downey/expected/blob/main/tests/beman/expected/testing/constant_eval.hpp) is `consteval`, so a call to it is evaluated during translation. If the probe body isn't usable in a constant expression the program is ill-formed, and the first question is answered by the call itself, with no `static_assert` needed. The result then behaves as an ordinary prvalue, free to be handed to `CHECK`. The whole thing is a one-line wrapper: + +```cpp +template +consteval auto beman::expected::testing::constant_eval(Probe probe) { + return probe(); +} +``` + +A probe is a plain lambda that reduces what it observes to a literal aggregate: + +```cpp +TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { + constexpr auto probe = [] { + constexpr expt::expected e; + return int_state{e.has_value(), *e}; + }; + CHECK(constant_eval(probe) == int_state{true, 0}); + CHECK(probe() == int_state{true, 0}); +} +``` + +Because the probe is a plain lambda and not a `consteval` one, the same body runs in both evaluation modes. Constant evaluation and ordinary evaluation can take different paths through a union-based type like `expected`, so running both earns its second line: + +```C++ +CHECK(constant_eval(probe) == expect); // constant evaluation +CHECK(probe() == expect); // ordinary evaluation +``` + +Give the returned aggregate an `operator<<`. Without one, Catch2 prints `{?} == {?}` and you're back where `static_assert` left you. + + +# What it buys + +Two things. The reporting is better: a mismatch names both types, or prints both states, instead of expanding to `false` or stopping at a diagnostic before it can say anything. And a wrong answer is no longer a compile failure that blocks everything behind it. The suite builds, runs, and reports every case; a broken trait shows up as one red line among the green, with the rest of the run intact. + +None of this abolishes `static_assert`. The genuinely ill-formed cases stay ill-formed, checked in their own negative-compilation files. What moved to runtime is only the part that was a test wearing an assertion's clothes. diff --git a/docs/blog/scrap-your-static_assert.org b/docs/blog/scrap-your-static_assert.org new file mode 100644 index 0000000..2308540 --- /dev/null +++ b/docs/blog/scrap-your-static_assert.org @@ -0,0 +1,95 @@ +#+title: Scrap your ~static_assert~ +#+date: <2026-08-10> +#+author: Steve Downey +#+OPTIONS: toc:nil num:nil ^:nil + +The obvious way to test a compile-time fact is ~static_assert~. It's right +there, it needs no framework, and for a fact that has to hold it's the right +tool. As a /test/, though, it has one bad property: a wrong answer is a +translation failure. The build stops at the first one, you get a compiler +diagnostic instead of a test result, and every other test in the file goes +unrun. The xUnit report is empty. You learn that something is wrong, once, and +nothing about the rest. + +There's a second, smaller problem. Even when you write the check as a runtime +~CHECK~ so that it gets reported, a bare trait doesn't report anything you can +use: + +#+begin_src C++ +CHECK(std::is_same_v); // FAILED: CHECK( false ) +#+end_src + +The expansion is the word ~false~. You already knew the two types differed; the +framework won't tell you what either of them was. + +Converting the =expected= tests off ~static_assert~ came down to two +header-only components that fix these two problems. Neither is clever. +(The title owes Lämmel and Peyton Jones; the debt stops at the title.) + +* Type identity as a value + +The fix for the second problem is to compare type /identities/ that carry their +spelling for diagnostics, instead of comparing a bool. See +[[orgit:~/src/steve-downey/expected/scrap-static-assert::tests/beman/expected/testing/type_name.hpp][type_name.hpp]]. The +comparison is still ~std::is_same_v~, so the verdict is exact and a false pass +isn't possible: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/testing/type_name.hpp::d1c7602e-a42f-46ab-bd53-7adef5646545]] :lines 2- :src cpp :end "d1c7602e-a42f-46ab-bd53-7adef5646545 end" + +The spelling is consulted only after a comparison has already failed and the +framework needs to explain it. So a failing check explains itself: + +#+begin_src text +FAILED: CHECK( type_name() == type_name() ) +with expansion: const int& == int& +#+end_src + +And the tests read like the trait they replaced: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/expected.test.cpp::52697d22-633f-4e07-b364-5b5db41ca2a7]] :lines 2- :src cpp :end "52697d22-633f-4e07-b364-5b5db41ca2a7 end" + +* Reporting a compile-time value at runtime + +The fix for the first problem is to split the two questions a constexpr test +actually asks. "Can this be constant-evaluated at all?" is a property of the +code; it stays a hard translation failure, which is correct, because that's a +fact that has to hold. "Does it produce the right answer?" is a property of a +value, and there's no reason a wrong value should stop the build. + +[[orgit:~/src/steve-downey/expected/scrap-static-assert::tests/beman/expected/testing/constant_eval.hpp][constant_eval.hpp]] +is ~consteval~, so a call to it is evaluated during translation. If the probe +body isn't usable in a constant expression the program is ill-formed, and the +first question is answered by the call itself, with no ~static_assert~ needed. +The result then behaves as an ordinary prvalue, free to be handed to ~CHECK~. +The whole thing is a one-line wrapper: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/testing/constant_eval.hpp::f0b2e22a-6ab4-4112-b826-297a776f01f1]] :lines 2- :src cpp :end "f0b2e22a-6ab4-4112-b826-297a776f01f1 end" + +A probe is a plain lambda that reduces what it observes to a literal aggregate: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/expected.test.cpp::a525eae8-2bb0-4cae-aeaf-b134cbffef1c]] :lines 2- :src cpp :end "a525eae8-2bb0-4cae-aeaf-b134cbffef1c end" + +Because the probe is a plain lambda and not a ~consteval~ one, the same body +runs in both evaluation modes. Constant evaluation and ordinary evaluation can +take different paths through a union-based type like ~expected~, so running both +earns its second line: + +#+begin_src C++ +CHECK(constant_eval(probe) == expect); // constant evaluation +CHECK(probe() == expect); // ordinary evaluation +#+end_src + +Give the returned aggregate an ~operator<<~. Without one, Catch2 prints ~{?} == +{?}~ and you're back where ~static_assert~ left you. + +* What it buys + +Two things. The reporting is better: a mismatch names both types, or prints both +states, instead of expanding to ~false~ or stopping at a diagnostic before it +can say anything. And a wrong answer is no longer a compile failure that blocks +everything behind it. The suite builds, runs, and reports every case; a broken +trait shows up as one red line among the green, with the rest of the run intact. + +None of this abolishes ~static_assert~. The genuinely ill-formed cases stay +ill-formed, checked in their own negative-compilation files. What moved to +runtime is only the part that was a test wearing an assertion's clothes. diff --git a/tests/beman/expected/expected.test.cpp b/tests/beman/expected/expected.test.cpp index cacdcb5..544ae0e 100644 --- a/tests/beman/expected/expected.test.cpp +++ b/tests/beman/expected/expected.test.cpp @@ -80,6 +80,7 @@ TEST_CASE("expected: special member availability and noexcept", "[ExpectedTest]" CHECK(std::is_nothrow_move_assignable_v>); } +// 52697d22-633f-4e07-b364-5b5db41ca2a7 TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]") { using expected_t = expt::expected; CHECK(type_name())>() == type_name()); @@ -87,6 +88,7 @@ TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]" CHECK(type_name())>() == type_name()); CHECK(type_name())>() == type_name()); } +// 52697d22-633f-4e07-b364-5b5db41ca2a7 end TEST_CASE("expected: error() ref-qualification return types", "[ExpectedTest]") { using expected_t = expt::expected; @@ -745,6 +747,7 @@ struct int_state { }; } // namespace +// a525eae8-2bb0-4cae-aeaf-b134cbffef1c TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { constexpr auto probe = [] { constexpr expt::expected e; @@ -753,6 +756,7 @@ TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { CHECK(constant_eval(probe) == int_state{true, 0}); CHECK(probe() == int_state{true, 0}); } +// a525eae8-2bb0-4cae-aeaf-b134cbffef1c end TEST_CASE("expected: constexpr value construction", "[ExpectedTest]") { constexpr auto probe = [] { diff --git a/tests/beman/expected/testing/constant_eval.hpp b/tests/beman/expected/testing/constant_eval.hpp index 50a1c77..aba5f40 100644 --- a/tests/beman/expected/testing/constant_eval.hpp +++ b/tests/beman/expected/testing/constant_eval.hpp @@ -64,9 +64,11 @@ consteval auto constant_eval(Probe probe); } // namespace beman::expected::testing +// f0b2e22a-6ab4-4112-b826-297a776f01f1 template consteval auto beman::expected::testing::constant_eval(Probe probe) { return probe(); } +// f0b2e22a-6ab4-4112-b826-297a776f01f1 end #endif // BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP diff --git a/tests/beman/expected/testing/type_name.hpp b/tests/beman/expected/testing/type_name.hpp index 904c0bb..23bbde4 100644 --- a/tests/beman/expected/testing/type_name.hpp +++ b/tests/beman/expected/testing/type_name.hpp @@ -133,10 +133,12 @@ consteval type_name_t type_name(); } // namespace beman::expected::testing +// d1c7602e-a42f-46ab-bd53-7adef5646545 template constexpr bool beman::expected::testing::operator==(type_name_t, type_name_t) { return std::is_same_v; } +// d1c7602e-a42f-46ab-bd53-7adef5646545 end template consteval beman::expected::testing::type_name_t beman::expected::testing::type_name() {