From 4db3249127122b4db0a3270f3a7b30e6d0bff5de Mon Sep 17 00:00:00 2001 From: Jeremy <51220084+jeremy-rifkin@users.noreply.github.com> Date: Mon, 6 May 2024 21:40:32 -0500 Subject: [PATCH] Split up assert.hpp --- include/libassert/assert.hpp | 867 +----------------- .../libassert/expression_decomposition.hpp | 330 +++++++ include/libassert/platform.hpp | 2 +- include/libassert/stringification.hpp | 430 +++++++++ include/libassert/utilities.hpp | 121 +++ 5 files changed, 890 insertions(+), 860 deletions(-) create mode 100644 include/libassert/expression_decomposition.hpp create mode 100644 include/libassert/stringification.hpp create mode 100644 include/libassert/utilities.hpp diff --git a/include/libassert/assert.hpp b/include/libassert/assert.hpp index 60fdfa3f..97148203 100644 --- a/include/libassert/assert.hpp +++ b/include/libassert/assert.hpp @@ -19,42 +19,22 @@ #include #include #include -#ifdef __cpp_lib_expected - #include -#endif #include +#include +#include +#include -#if LIBASSERT_STD_VER >= 20 - #include -#endif +#include -#ifdef LIBASSERT_USE_MAGIC_ENUM - // relative include so that multiple library versions don't clash - // e.g. if both libA and libB have different versions of libassert as a public - // dependency, then any library that consumes both will have both sets of include - // paths. this isn't an issue for #include but becomes an issue - // for includes within the library (libA might include from libB) - #if __has_include() - #include - #else - #include - #endif +#ifdef __cpp_lib_expected + #include #endif -#ifdef LIBASSERT_USE_FMT - #include +#if LIBASSERT_STD_VER >= 20 + #include #endif -#include - -// Ever seen an assertion library with a 1500+ line header? :) -// Block comments are used to create some visual separation and try to break the library into more manageable parts. -// I've tried as much as I can to keep logically connected parts together but there is some bootstrapping necessary. - -// always_false is just convenient to use here -#define LIBASSERT_PHONY_USE(E) ((void)libassert::detail::always_false) - #if LIBASSERT_IS_MSVC #pragma warning(push) // warning C4251: using non-dll-exported type in dll-exported type, firing on std::vector and others for @@ -63,837 +43,6 @@ #pragma warning(disable: 4251; disable: 4275) #endif -// ===================================================================================================================== -// || Core utilities || -// ===================================================================================================================== - -namespace libassert { - // Lightweight helper, eventually may use C++20 std::source_location if this library no longer - // targets C++17. Note: __builtin_FUNCTION only returns the name, so __PRETTY_FUNCTION__ is - // still needed. - struct source_location { - const char* file; - //const char* function; // disabled for now due to static constexpr restrictions - int line; - constexpr source_location( - //const char* _function /*= __builtin_FUNCTION()*/, - const char* _file = __builtin_FILE(), - int _line = __builtin_LINE() - ) : file(_file), /*function(_function),*/ line(_line) {} - }; -} - -namespace libassert::detail { - // bootstrap with primitive implementations - LIBASSERT_EXPORT void primitive_assert_impl( - bool condition, - bool verify, - const char* expression, - const char* signature, - source_location location, - const char* message = nullptr - ); - - [[noreturn]] LIBASSERT_EXPORT void primitive_panic_impl ( - const char* signature, - source_location location, - const char* message - ); - - #ifndef NDEBUG - #define LIBASSERT_PRIMITIVE_ASSERT(c, ...) \ - libassert::detail::primitive_assert_impl(c, false, #c, LIBASSERT_PFUNC, {} LIBASSERT_VA_ARGS(__VA_ARGS__)) - #else - #define LIBASSERT_PRIMITIVE_ASSERT(c, ...) LIBASSERT_PHONY_USE(c) - #endif - - #define LIBASSERT_PRIMITIVE_PANIC(message) libassert::detail::primitive_panic_impl(LIBASSERT_PFUNC, {}, message) -} - -// ===================================================================================================================== -// || Basic formatting and type tools || -// ===================================================================================================================== - -namespace libassert::detail { - [[nodiscard]] LIBASSERT_EXPORT std::string bstringf(const char* format, ...); - - [[nodiscard]] LIBASSERT_EXPORT std::string_view type_name_core(std::string_view signature) noexcept; - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string_view type_name() noexcept { - return type_name_core(LIBASSERT_PFUNC); - } - - [[nodiscard]] LIBASSERT_EXPORT std::string prettify_type(std::string type); -} - -// ===================================================================================================================== -// || Metaprogramming utilities || -// ===================================================================================================================== - -namespace libassert::detail { - struct nothing {}; - - template inline constexpr bool is_nothing = std::is_same_v; - - // Hack to get around static_assert(false); being evaluated before any instantiation, even under - // an if-constexpr branch - // Also used for PHONY_USE - template inline constexpr bool always_false = false; - - template using strip = std::remove_cv_t>; - - // intentionally not stripping B - template inline constexpr bool isa = std::is_same_v, B>; - - // Is integral but not boolean - template inline constexpr bool is_integral_and_not_bool = std::is_integral_v> && !isa; - - template - inline constexpr bool is_arith_not_bool_char = std::is_arithmetic_v> && !isa && !isa; - - template - inline constexpr bool is_c_string = - isa>, char*> // <- covers literals (i.e. const char(&)[N]) too - || isa>, const char*>; - - template - inline constexpr bool is_string_type = - isa - || isa - || is_c_string; - - // char(&)[20], const char(&)[20], const char(&)[] - template inline constexpr bool is_string_literal = - std::is_lvalue_reference_v - && std::is_array_v> - && isa>, char>; - - template typename std::add_lvalue_reference_t decllval() noexcept; -} - -// ===================================================================================================================== -// || Stringification micro-library || -// || Note: There is some stateful stuff behind the scenes related to literal format configuration || -// ===================================================================================================================== - -namespace libassert { - // customization point - template struct stringifier /*{ - std::convertible_to stringify(const T&); - }*/; -} - -namespace libassert::detail { - // What can be stringified - // Base types: - // - anything string-like - // - arithmetic types - // - pointers - // - smart pointers - // - nullptr_t - // - std::error_code/std::error_condition - // - orderings - // - enum values - // User-provided stringifications: - // - std::ostream<< overloads - // - std format TODO - // - fmt TODO - // - libassert::stringifier - // Containers: - // - std::optional - // - std::variant TODO - // - std::expected TODO - // - tuples and tuple-likes - // - anything container-like (std::vector, std::array, std::unordered_map, C arrays, .....) - // Priorities: - // - libassert::stringifier - // - default formatters - // - fmt - // - std fmt TODO - // - osteam format - // - instance of catch all - // Other logistics: - // - Containers are only stringified if their value_type is stringifiable - // TODO Weak pointers? - - template class Ref> - struct is_specialization : std::false_type {}; - - template class Ref, typename... Args> - struct is_specialization, Ref>: std::true_type {}; - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string do_stringify(const T& v); - - namespace stringification { - // - // General traits - // - template class is_tuple_like : public std::false_type {}; - template - class is_tuple_like< - T, - std::void_t< - typename std::tuple_size::type, // TODO: decltype(std::tuple_size_v) ? - decltype(std::get<0>(std::declval())) - > - > : public std::true_type {}; - - namespace adl { - using std::begin, std::end; // ADL - template class is_container : public std::false_type {}; - template - class is_container< - T, - std::void_t< - decltype(begin(decllval())), - decltype(end(decllval())) - > - > : public std::true_type {}; - template class is_begin_deref : public std::false_type {}; - template - class is_begin_deref< - T, - std::void_t< - decltype(*begin(decllval())) - > - > : public std::true_type {}; - } - - template class is_deref : public std::false_type {}; - template - class is_deref< - T, - std::void_t< - decltype(*decllval()) - > - > : public std::true_type {}; - - template class has_ostream_overload : public std::false_type {}; - template - class has_ostream_overload< - T, - std::void_t() << std::declval())> - > : public std::true_type {}; - - template class has_stringifier : public std::false_type {}; - template - class has_stringifier< - T, - std::void_t>{}.stringify(std::declval()))> - > : public std::true_type {}; - - // - // Catch all - // - - template - [[nodiscard]] std::string stringify_unknown() { - return bstringf("", prettify_type(std::string(type_name())).c_str()); - } - - // - // Basic types - // - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::string_view); - // without nullptr_t overload msvc (without /permissive-) will call stringify(bool) and mingw - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::nullptr_t); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(char); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(bool); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(short); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(int); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(long); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(long long); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned short); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned int); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned long); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned long long); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(float); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(double); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(long double); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::error_code ec); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::error_condition ec); - #if __cplusplus >= 202002L - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::strong_ordering); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::weak_ordering); - [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::partial_ordering); - #endif - [[nodiscard]] LIBASSERT_EXPORT - std::string stringify_pointer_value(const void*); - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string stringify_smart_ptr(const T& t) { - if(t) { - return do_stringify(*t); - } else { - return "nullptr"; - } - } - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string stringify_by_ostream(const T& t) { - // clang-tidy bug here - // NOLINTNEXTLINE(misc-const-correctness) - std::ostringstream oss; - oss<>, int> = 0> - LIBASSERT_ATTR_COLD [[nodiscard]] std::string stringify_enum(const T& t) { - std::string_view name = magic_enum::enum_name(t); - if(!name.empty()) { - return std::string(name); - } else { - return bstringf( - "enum %s: %s", - prettify_type(std::string(type_name())).c_str(), - stringify(static_cast::type>(t)).c_str() - ); - } - } - #else - template>, int> = 0> - LIBASSERT_ATTR_COLD [[nodiscard]] std::string stringify_enum(const T& t) { - return bstringf( - "enum %s: %s", - prettify_type(std::string(type_name())).c_str(), - stringify(static_cast>(t)).c_str() - ); - } - #endif - - // - // Compositions of other types - // - // #ifdef __cpp_lib_expected - // template - // [[nodiscard]] std::string stringify(const std::unexpected& x) { - // return "unexpected " + stringify(x.error()); - // } - - // template - // [[nodiscard]] std::string stringify(const std::expected& x) { - // if(x.has_value()) { - // if constexpr(std::is_void_v) { - // return "expected void"; - // } else { - // return "expected " + stringify(*x); - // } - // } else { - // return "unexpected " + stringify(x.error()); - // } - // } - // #endif - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string stringify(const std::optional& t) { - if(t) { - return do_stringify(t.value()); - } else { - return "nullopt"; - } - } - - inline constexpr std::size_t max_container_print_items = 1000; - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string stringify_container(const T& container) { - using std::begin, std::end; // ADL - std::string str = "["; - const auto begin_it = begin(container); - std::size_t count = 0; - for(auto it = begin_it; it != end(container); it++) { - if(it != begin_it) { - str += ", "; - } - str += do_stringify(*it); - if(++count == max_container_print_items) { - str += ", ..."; - break; - } - } - str += "]"; - return str; - } - - // I'm going to assume at least one index because is_tuple_like requires index 0 to exist - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string stringify_tuple_like_impl(const T& t, std::index_sequence) { - return "[" + (do_stringify(std::get<0>(t)) + ... + (", " + do_stringify(std::get(t)))) + "]"; - } - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string stringify_tuple_like(const T& t) { - return stringify_tuple_like_impl(t, std::make_index_sequence::value - 1>{}); - } - } - - template - class has_value_type : public std::false_type {}; - template - class has_value_type< - T, - std::void_t - > : public std::true_type {}; - - template inline constexpr bool is_smart_pointer = - is_specialization::value - || is_specialization::value; // TODO: Handle weak_ptr too? - - template - class can_basic_stringify : public std::false_type {}; - template - class can_basic_stringify< - T, - std::void_t()))> - > : public std::true_type {}; - - template constexpr bool stringifiable_container(); - - template inline constexpr bool stringifiable = - stringification::has_stringifier::value - || std::is_convertible_v - || (std::is_pointer_v || std::is_function_v) - || std::is_enum_v - || (stringification::is_tuple_like::value && stringifiable_container()) - || (stringification::adl::is_container::value && stringifiable_container()) - || can_basic_stringify::value - || stringifiable_container(); - - template constexpr bool tuple_has_stringifiable_args_core(std::index_sequence) { - return ( - stringifiable(std::declval()))> - || ... - || stringifiable(std::declval()))> - ); - } - - template inline constexpr bool tuple_has_stringifiable_args = - tuple_has_stringifiable_args_core(std::make_index_sequence::value - 1>{}); - - template constexpr bool stringifiable_container() { - // TODO: Guard against std::expected....? - if constexpr(has_value_type::value) { - if constexpr(std::is_same_v) { - return false; - } else { - return stringifiable; - } - } else if constexpr(std::is_array_v>) { // C arrays - return stringifiable()[0])>; - } else if constexpr(stringification::is_tuple_like::value) { - return tuple_has_stringifiable_args; - } else { - return false; - } - } - - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string do_stringify(const T& v) { - if constexpr(stringification::has_stringifier::value) { - return stringifier>{}.stringify(v); - } else if constexpr(std::is_convertible_v) { - if constexpr(std::is_pointer_v || std::is_same_v) { - if(v == nullptr) { - return "nullptr"; - } - } - return stringification::stringify(std::string_view(v)); - } else if constexpr(std::is_pointer_v || std::is_function_v) { - return stringification::stringify_pointer_value(reinterpret_cast(v)); - } else if constexpr(is_smart_pointer) { - #ifndef LIBASSERT_NO_STRINGIFY_SMART_POINTER_OBJECTS - if(stringifiable) { - #else - if(false) { - #endif - return stringification::stringify_smart_ptr(v); - } else { - return stringification::stringify_pointer_value(v.get()); - } - } else if constexpr(std::is_enum_v) { - return stringification::stringify_enum(v); - } else if constexpr(stringification::is_tuple_like::value) { - if constexpr(stringifiable_container()) { - return stringification::stringify_tuple_like(v); - } else { - return stringification::stringify_unknown(); - } - } else if constexpr(stringification::adl::is_container::value) { - if constexpr(stringifiable_container()) { - return stringification::stringify_container(v); - } else { - return stringification::stringify_unknown(); - } - } else if constexpr(can_basic_stringify::value) { - return stringification::stringify(v); - } else if constexpr(stringification::has_ostream_overload::value) { - return stringification::stringify_by_ostream(v); - } - #ifdef LIBASSERT_USE_FMT - else if constexpr(fmt::is_formattable::value) { - return fmt::format("{}", v); - } - #endif - else { - return stringification::stringify_unknown(); - } - // TODO fmt / std fmt - } - - // Top-level stringify utility - template - LIBASSERT_ATTR_COLD [[nodiscard]] - std::string generate_stringification(const T& v) { - if constexpr( - stringification::adl::is_container::value - && !is_string_type - && stringifiable_container() - ) { - return prettify_type(std::string(type_name())) + ": " + do_stringify(v); - } else if constexpr(stringification::is_tuple_like::value && stringifiable_container()) { - return prettify_type(std::string(type_name())) + ": " + do_stringify(v); - } else if constexpr((std::is_pointer_v && !is_string_type) || is_smart_pointer) { - return prettify_type(std::string(type_name())) + ": " + do_stringify(v); - } else if constexpr(is_specialization::value) { - return prettify_type(std::string(type_name())) + ": " + do_stringify(v); - } else { - return do_stringify(v); - } - } -} - -// ===================================================================================================================== -// || Expression decomposition micro-library || -// ===================================================================================================================== - -namespace libassert::detail { - // Lots of boilerplate - // Using int comparison functions here to support proper signed comparisons. Need to make sure - // assert(map.count(1) == 2) doesn't produce a warning. It wouldn't under normal circumstances - // but it would in this library due to the parameters being forwarded down a long chain. - // And we want to provide as much robustness as possible anyways. - // Copied and pasted from https://en.cppreference.com/w/cpp/utility/intcmp - // Not using std:: versions because library is targeting C++17 - template - [[nodiscard]] constexpr bool cmp_equal(T t, U u) { - using UT = std::make_unsigned_t; - using UU = std::make_unsigned_t; - if constexpr(std::is_signed_v == std::is_signed_v) { - return t == u; - } else if constexpr(std::is_signed_v) { - return t >= 0 && UT(t) == u; - } else { - return u >= 0 && t == UU(u); - } - } - - template - [[nodiscard]] constexpr bool cmp_not_equal(T t, U u) { - return !cmp_equal(t, u); - } - - template - [[nodiscard]] constexpr bool cmp_less(T t, U u) { - using UT = std::make_unsigned_t; - using UU = std::make_unsigned_t; - if constexpr(std::is_signed_v == std::is_signed_v) { - return t < u; - } else if constexpr(std::is_signed_v) { - return t < 0 || UT(t) < u; - } else { - return u >= 0 && t < UU(u); - } - } - - template - [[nodiscard]] constexpr bool cmp_greater(T t, U u) { - return cmp_less(u, t); - } - - template - [[nodiscard]] constexpr bool cmp_less_equal(T t, U u) { - return !cmp_less(u, t); - } - - template - [[nodiscard]] constexpr bool cmp_greater_equal(T t, U u) { - return !cmp_less(t, u); - } - - // Lots of boilerplate - // std:: implementations don't allow two separate types for lhs/rhs - // Note: is this macro potentially bad when it comes to debugging(?) - namespace ops { - #define LIBASSERT_GEN_OP_BOILERPLATE(name, op) struct name { \ - static constexpr std::string_view op_string = #op; \ - template \ - LIBASSERT_ATTR_COLD [[nodiscard]] \ - constexpr decltype(auto) operator()(A&& lhs, B&& rhs) const { /* no need to forward ints */ \ - return std::forward(lhs) op std::forward(rhs); \ - } \ - } - #define LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(name, op, cmp) struct name { \ - static constexpr std::string_view op_string = #op; \ - template \ - LIBASSERT_ATTR_COLD [[nodiscard]] \ - constexpr decltype(auto) operator()(A&& lhs, B&& rhs) const { /* no need to forward ints */ \ - if constexpr(is_integral_and_not_bool && is_integral_and_not_bool) return cmp(lhs, rhs); \ - else return std::forward(lhs) op std::forward(rhs); \ - } \ - } - LIBASSERT_GEN_OP_BOILERPLATE(shl, <<); - LIBASSERT_GEN_OP_BOILERPLATE(shr, >>); - #if __cplusplus >= 202002L - LIBASSERT_GEN_OP_BOILERPLATE(spaceship, <=>); - #endif - #ifdef LIBASSERT_SAFE_COMPARISONS - // TODO: Make a SAFE() wrapper... - LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(eq, ==, cmp_equal); - LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(neq, !=, cmp_not_equal); - LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(gt, >, cmp_greater); - LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(lt, <, cmp_less); - LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(gteq, >=, cmp_greater_equal); - LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(lteq, <=, cmp_less_equal); - #else - LIBASSERT_GEN_OP_BOILERPLATE(eq, ==); - LIBASSERT_GEN_OP_BOILERPLATE(neq, !=); - LIBASSERT_GEN_OP_BOILERPLATE(gt, >); - LIBASSERT_GEN_OP_BOILERPLATE(lt, <); - LIBASSERT_GEN_OP_BOILERPLATE(gteq, >=); - LIBASSERT_GEN_OP_BOILERPLATE(lteq, <=); - #endif - LIBASSERT_GEN_OP_BOILERPLATE(band, &); - LIBASSERT_GEN_OP_BOILERPLATE(bxor, ^); - LIBASSERT_GEN_OP_BOILERPLATE(bor, |); - #ifdef LIBASSERT_DECOMPOSE_BINARY_LOGICAL - struct land { - static constexpr std::string_view op_string = "&&"; - template - LIBASSERT_ATTR_COLD [[nodiscard]] - constexpr decltype(auto) operator()(A&& lhs, B&& rhs) const { - // Go out of the way to support old-style ASSERT(foo && "Message") - #if LIBASSERT_IS_GCC - if constexpr(is_string_literal) { - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wnonnull-compare" - return std::forward(lhs) && std::forward(rhs); - #pragma GCC diagnostic pop - } else { - return std::forward(lhs) && std::forward(rhs); - } - #else - return std::forward(lhs) && std::forward(rhs); - #endif - } - }; - LIBASSERT_GEN_OP_BOILERPLATE(lor, ||); - #endif - LIBASSERT_GEN_OP_BOILERPLATE(assign, =); - LIBASSERT_GEN_OP_BOILERPLATE(add_assign, +=); - LIBASSERT_GEN_OP_BOILERPLATE(sub_assign, -=); - LIBASSERT_GEN_OP_BOILERPLATE(mul_assign, *=); - LIBASSERT_GEN_OP_BOILERPLATE(div_assign, /=); - LIBASSERT_GEN_OP_BOILERPLATE(mod_assign, %=); - LIBASSERT_GEN_OP_BOILERPLATE(shl_assign, <<=); - LIBASSERT_GEN_OP_BOILERPLATE(shr_assign, >>=); - LIBASSERT_GEN_OP_BOILERPLATE(band_assign, &=); - LIBASSERT_GEN_OP_BOILERPLATE(bxor_assign, ^=); - LIBASSERT_GEN_OP_BOILERPLATE(bor_assign, |=); - #undef LIBASSERT_GEN_OP_BOILERPLATE - #undef LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL - } - - // I learned this automatic expression decomposition trick from lest: - // https://github.com/martinmoene/lest/blob/master/include/lest/lest.hpp#L829-L853 - // - // I have improved upon the trick by supporting more operators and generally improving - // functionality. - // - // Some cases to test and consider: - // - // Expression Parsed as - // Basic: - // false << false - // a == b (<< a) == b - // - // Equal precedence following: - // a << b (<< a) << b - // a << b << c ((<< a) << b) << c - // a << b + c (<< a) << (b + c) - // a << b < c ((<< a) << b) < c // edge case - // - // Higher precedence following: - // a + b << (a + b) - // a + b + c << ((a + b) + c) - // a + b * c << (a + (b * c)) - // a + b < c (<< (a + b)) < c - // - // Lower precedence following: - // a < b (<< a) < b - // a < b < c ((<< a) < b) < c - // a < b + c (<< a) < (b + c) - // a < b == c ((<< a) < b) == c // edge case - - template - struct expression_decomposer { - A a; - B b; - explicit constexpr expression_decomposer() = default; - ~expression_decomposer() = default; - // not copyable - constexpr expression_decomposer(const expression_decomposer&) = delete; - constexpr expression_decomposer& operator=(const expression_decomposer&) = delete; - // allow move construction - constexpr expression_decomposer(expression_decomposer&&) - #if !LIBASSERT_IS_GCC || __GNUC__ >= 10 // gcc 9 has some issue with the move constructor being noexcept - noexcept - #endif - = default; - constexpr expression_decomposer& operator=(expression_decomposer&&) = delete; - // value constructors - template, int> = 0> - // NOLINTNEXTLINE(bugprone-forwarding-reference-overload) // TODO - explicit constexpr expression_decomposer(U&& _a) : a(std::forward(_a)) {} - template - explicit constexpr expression_decomposer(U&& _a, V&& _b) : a(std::forward(_a)), b(std::forward(_b)) {} - /* Ownership logic: - * One of two things can happen to this class - * - Either it is composed with another operation - * + The value of the subexpression represented by this is computed (either get_value() - * or operator bool), either A& or C()(a, b) - * + Or, just the lhs is moved B is nothing - * - Or this class represents the whole expression - * + The value is computed (either A& or C()(a, b)) - * + a and b are referenced freely - * + Either the value is taken or a is moved out - * Ensuring the value is only computed once is left to the assert handler - */ - [[nodiscard]] - constexpr decltype(auto) get_value() { - if constexpr(is_nothing) { - static_assert(is_nothing && !is_nothing); - return (a); - } else { - return C()(a, b); - } - } - [[nodiscard]] - constexpr operator bool() { // for ternary support - return (bool)get_value(); - } - // return true if the lhs should be returned, false if full computed value should be - [[nodiscard]] - constexpr bool ret_lhs() { - static_assert(!is_nothing); - static_assert(is_nothing == is_nothing); - if constexpr(is_nothing) { - // if there is no top-level binary operation, A is the only thing that can be returned - return true; - } else { - // return LHS for the following types; - return C::op_string == "==" - || C::op_string == "!=" - || C::op_string == "<" - || C::op_string == ">" - || C::op_string == "<=" - || C::op_string == ">=" - || C::op_string == "&&" - || C::op_string == "||"; - // don't return LHS for << >> & ^ | and all assignments - } - } - [[nodiscard]] - constexpr A take_lhs() { // should only be called if ret_lhs() == true - if constexpr(std::is_lvalue_reference_v) { - return ((((a)))); - } else { - return std::move(a); - } - } - // Need overloads for operators with precedence <= bitshift - // TODO: spaceship operator? - // Note: Could decompose more than just comparison and boolean operators, but it would take - // a lot of work and I don't think it's beneficial for this library. - template [[nodiscard]] constexpr auto operator<<(O&& operand) && { - using Q = std::conditional_t, std::remove_reference_t, O>; - if constexpr(is_nothing) { - static_assert(is_nothing && is_nothing); - return expression_decomposer(std::forward(operand)); - } else if constexpr(is_nothing) { - static_assert(is_nothing); - return expression_decomposer(std::forward(a), std::forward(operand)); - } else { - static_assert(!is_nothing); - return expression_decomposer( - std::forward(get_value()), - std::forward(operand) - ); - } - } - #define LIBASSERT_GEN_OP_BOILERPLATE(functor, op) \ - template [[nodiscard]] constexpr auto operator op(O&& operand) && { \ - static_assert(!is_nothing); \ - using Q = std::conditional_t, std::remove_reference_t, O>; \ - if constexpr(is_nothing) { \ - static_assert(is_nothing); \ - return expression_decomposer(std::forward(a), std::forward(operand)); \ - } else { \ - static_assert(!is_nothing); \ - using V = decltype(get_value()); \ - return expression_decomposer(std::forward(get_value()), std::forward(operand)); \ - } \ - } - LIBASSERT_GEN_OP_BOILERPLATE(ops::shr, >>) - #if __cplusplus >= 202002L - LIBASSERT_GEN_OP_BOILERPLATE(ops::spaceship, <=>) - #endif - LIBASSERT_GEN_OP_BOILERPLATE(ops::eq, ==) - LIBASSERT_GEN_OP_BOILERPLATE(ops::neq, !=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::gt, >) - LIBASSERT_GEN_OP_BOILERPLATE(ops::lt, <) - LIBASSERT_GEN_OP_BOILERPLATE(ops::gteq, >=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::lteq, <=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::band, &) - LIBASSERT_GEN_OP_BOILERPLATE(ops::bxor, ^) - LIBASSERT_GEN_OP_BOILERPLATE(ops::bor, |) - #ifdef LIBASSERT_DECOMPOSE_BINARY_LOGICAL - LIBASSERT_GEN_OP_BOILERPLATE(ops::land, &&) - LIBASSERT_GEN_OP_BOILERPLATE(ops::lor, ||) - #endif - LIBASSERT_GEN_OP_BOILERPLATE(ops::assign, =) // NOLINT(cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator) - LIBASSERT_GEN_OP_BOILERPLATE(ops::add_assign, +=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::sub_assign, -=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::mul_assign, *=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::div_assign, /=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::mod_assign, %=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::shl_assign, <<=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::shr_assign, >>=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::band_assign, &=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::bxor_assign, ^=) - LIBASSERT_GEN_OP_BOILERPLATE(ops::bor_assign, |=) - #undef LIBASSERT_GEN_OP_BOILERPLATE - }; - - // for ternary support - template - expression_decomposer(U&&) -> expression_decomposer< - std::conditional_t, std::remove_reference_t, U> - >; -} - // ===================================================================================================================== // || Libassert public interface || // ===================================================================================================================== diff --git a/include/libassert/expression_decomposition.hpp b/include/libassert/expression_decomposition.hpp new file mode 100644 index 00000000..446b159e --- /dev/null +++ b/include/libassert/expression_decomposition.hpp @@ -0,0 +1,330 @@ +#ifndef LIBASSERT_EXPRESSION_DECOMPOSITION +#define LIBASSERT_EXPRESSION_DECOMPOSITION + +#include +#include +#include + +#include +#include + +// ===================================================================================================================== +// || Expression decomposition micro-library || +// ===================================================================================================================== + +namespace libassert::detail { + // Lots of boilerplate + // Using int comparison functions here to support proper signed comparisons. Need to make sure + // assert(map.count(1) == 2) doesn't produce a warning. It wouldn't under normal circumstances + // but it would in this library due to the parameters being forwarded down a long chain. + // And we want to provide as much robustness as possible anyways. + // Copied and pasted from https://en.cppreference.com/w/cpp/utility/intcmp + // Not using std:: versions because library is targeting C++17 + template + [[nodiscard]] constexpr bool cmp_equal(T t, U u) { + using UT = std::make_unsigned_t; + using UU = std::make_unsigned_t; + if constexpr(std::is_signed_v == std::is_signed_v) { + return t == u; + } else if constexpr(std::is_signed_v) { + return t >= 0 && UT(t) == u; + } else { + return u >= 0 && t == UU(u); + } + } + + template + [[nodiscard]] constexpr bool cmp_not_equal(T t, U u) { + return !cmp_equal(t, u); + } + + template + [[nodiscard]] constexpr bool cmp_less(T t, U u) { + using UT = std::make_unsigned_t; + using UU = std::make_unsigned_t; + if constexpr(std::is_signed_v == std::is_signed_v) { + return t < u; + } else if constexpr(std::is_signed_v) { + return t < 0 || UT(t) < u; + } else { + return u >= 0 && t < UU(u); + } + } + + template + [[nodiscard]] constexpr bool cmp_greater(T t, U u) { + return cmp_less(u, t); + } + + template + [[nodiscard]] constexpr bool cmp_less_equal(T t, U u) { + return !cmp_less(u, t); + } + + template + [[nodiscard]] constexpr bool cmp_greater_equal(T t, U u) { + return !cmp_less(t, u); + } + + // Lots of boilerplate + // std:: implementations don't allow two separate types for lhs/rhs + // Note: is this macro potentially bad when it comes to debugging(?) + namespace ops { + #define LIBASSERT_GEN_OP_BOILERPLATE(name, op) struct name { \ + static constexpr std::string_view op_string = #op; \ + template \ + LIBASSERT_ATTR_COLD [[nodiscard]] \ + constexpr decltype(auto) operator()(A&& lhs, B&& rhs) const { /* no need to forward ints */ \ + return std::forward(lhs) op std::forward(rhs); \ + } \ + } + #define LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(name, op, cmp) struct name { \ + static constexpr std::string_view op_string = #op; \ + template \ + LIBASSERT_ATTR_COLD [[nodiscard]] \ + constexpr decltype(auto) operator()(A&& lhs, B&& rhs) const { /* no need to forward ints */ \ + if constexpr(is_integral_and_not_bool && is_integral_and_not_bool) return cmp(lhs, rhs); \ + else return std::forward(lhs) op std::forward(rhs); \ + } \ + } + LIBASSERT_GEN_OP_BOILERPLATE(shl, <<); + LIBASSERT_GEN_OP_BOILERPLATE(shr, >>); + #if __cplusplus >= 202002L + LIBASSERT_GEN_OP_BOILERPLATE(spaceship, <=>); + #endif + #ifdef LIBASSERT_SAFE_COMPARISONS + // TODO: Make a SAFE() wrapper... + LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(eq, ==, cmp_equal); + LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(neq, !=, cmp_not_equal); + LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(gt, >, cmp_greater); + LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(lt, <, cmp_less); + LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(gteq, >=, cmp_greater_equal); + LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL(lteq, <=, cmp_less_equal); + #else + LIBASSERT_GEN_OP_BOILERPLATE(eq, ==); + LIBASSERT_GEN_OP_BOILERPLATE(neq, !=); + LIBASSERT_GEN_OP_BOILERPLATE(gt, >); + LIBASSERT_GEN_OP_BOILERPLATE(lt, <); + LIBASSERT_GEN_OP_BOILERPLATE(gteq, >=); + LIBASSERT_GEN_OP_BOILERPLATE(lteq, <=); + #endif + LIBASSERT_GEN_OP_BOILERPLATE(band, &); + LIBASSERT_GEN_OP_BOILERPLATE(bxor, ^); + LIBASSERT_GEN_OP_BOILERPLATE(bor, |); + #ifdef LIBASSERT_DECOMPOSE_BINARY_LOGICAL + struct land { + static constexpr std::string_view op_string = "&&"; + template + LIBASSERT_ATTR_COLD [[nodiscard]] + constexpr decltype(auto) operator()(A&& lhs, B&& rhs) const { + // Go out of the way to support old-style ASSERT(foo && "Message") + #if LIBASSERT_IS_GCC + if constexpr(is_string_literal) { + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wnonnull-compare" + return std::forward(lhs) && std::forward(rhs); + #pragma GCC diagnostic pop + } else { + return std::forward(lhs) && std::forward(rhs); + } + #else + return std::forward(lhs) && std::forward(rhs); + #endif + } + }; + LIBASSERT_GEN_OP_BOILERPLATE(lor, ||); + #endif + LIBASSERT_GEN_OP_BOILERPLATE(assign, =); + LIBASSERT_GEN_OP_BOILERPLATE(add_assign, +=); + LIBASSERT_GEN_OP_BOILERPLATE(sub_assign, -=); + LIBASSERT_GEN_OP_BOILERPLATE(mul_assign, *=); + LIBASSERT_GEN_OP_BOILERPLATE(div_assign, /=); + LIBASSERT_GEN_OP_BOILERPLATE(mod_assign, %=); + LIBASSERT_GEN_OP_BOILERPLATE(shl_assign, <<=); + LIBASSERT_GEN_OP_BOILERPLATE(shr_assign, >>=); + LIBASSERT_GEN_OP_BOILERPLATE(band_assign, &=); + LIBASSERT_GEN_OP_BOILERPLATE(bxor_assign, ^=); + LIBASSERT_GEN_OP_BOILERPLATE(bor_assign, |=); + #undef LIBASSERT_GEN_OP_BOILERPLATE + #undef LIBASSERT_GEN_OP_BOILERPLATE_SPECIAL + } + + // I learned this automatic expression decomposition trick from lest: + // https://github.com/martinmoene/lest/blob/master/include/lest/lest.hpp#L829-L853 + // + // I have improved upon the trick by supporting more operators and generally improving + // functionality. + // + // Some cases to test and consider: + // + // Expression Parsed as + // Basic: + // false << false + // a == b (<< a) == b + // + // Equal precedence following: + // a << b (<< a) << b + // a << b << c ((<< a) << b) << c + // a << b + c (<< a) << (b + c) + // a << b < c ((<< a) << b) < c // edge case + // + // Higher precedence following: + // a + b << (a + b) + // a + b + c << ((a + b) + c) + // a + b * c << (a + (b * c)) + // a + b < c (<< (a + b)) < c + // + // Lower precedence following: + // a < b (<< a) < b + // a < b < c ((<< a) < b) < c + // a < b + c (<< a) < (b + c) + // a < b == c ((<< a) < b) == c // edge case + + template + struct expression_decomposer { + A a; + B b; + explicit constexpr expression_decomposer() = default; + ~expression_decomposer() = default; + // not copyable + constexpr expression_decomposer(const expression_decomposer&) = delete; + constexpr expression_decomposer& operator=(const expression_decomposer&) = delete; + // allow move construction + constexpr expression_decomposer(expression_decomposer&&) + #if !LIBASSERT_IS_GCC || __GNUC__ >= 10 // gcc 9 has some issue with the move constructor being noexcept + noexcept + #endif + = default; + constexpr expression_decomposer& operator=(expression_decomposer&&) = delete; + // value constructors + template, int> = 0> + // NOLINTNEXTLINE(bugprone-forwarding-reference-overload) // TODO + explicit constexpr expression_decomposer(U&& _a) : a(std::forward(_a)) {} + template + explicit constexpr expression_decomposer(U&& _a, V&& _b) : a(std::forward(_a)), b(std::forward(_b)) {} + /* Ownership logic: + * One of two things can happen to this class + * - Either it is composed with another operation + * + The value of the subexpression represented by this is computed (either get_value() + * or operator bool), either A& or C()(a, b) + * + Or, just the lhs is moved B is nothing + * - Or this class represents the whole expression + * + The value is computed (either A& or C()(a, b)) + * + a and b are referenced freely + * + Either the value is taken or a is moved out + * Ensuring the value is only computed once is left to the assert handler + */ + [[nodiscard]] + constexpr decltype(auto) get_value() { + if constexpr(is_nothing) { + static_assert(is_nothing && !is_nothing); + return (a); + } else { + return C()(a, b); + } + } + [[nodiscard]] + constexpr operator bool() { // for ternary support + return (bool)get_value(); + } + // return true if the lhs should be returned, false if full computed value should be + [[nodiscard]] + constexpr bool ret_lhs() { + static_assert(!is_nothing); + static_assert(is_nothing == is_nothing); + if constexpr(is_nothing) { + // if there is no top-level binary operation, A is the only thing that can be returned + return true; + } else { + // return LHS for the following types; + return C::op_string == "==" + || C::op_string == "!=" + || C::op_string == "<" + || C::op_string == ">" + || C::op_string == "<=" + || C::op_string == ">=" + || C::op_string == "&&" + || C::op_string == "||"; + // don't return LHS for << >> & ^ | and all assignments + } + } + [[nodiscard]] + constexpr A take_lhs() { // should only be called if ret_lhs() == true + if constexpr(std::is_lvalue_reference_v) { + return ((((a)))); + } else { + return std::move(a); + } + } + // Need overloads for operators with precedence <= bitshift + // TODO: spaceship operator? + // Note: Could decompose more than just comparison and boolean operators, but it would take + // a lot of work and I don't think it's beneficial for this library. + template [[nodiscard]] constexpr auto operator<<(O&& operand) && { + using Q = std::conditional_t, std::remove_reference_t, O>; + if constexpr(is_nothing) { + static_assert(is_nothing && is_nothing); + return expression_decomposer(std::forward(operand)); + } else if constexpr(is_nothing) { + static_assert(is_nothing); + return expression_decomposer(std::forward(a), std::forward(operand)); + } else { + static_assert(!is_nothing); + return expression_decomposer( + std::forward(get_value()), + std::forward(operand) + ); + } + } + #define LIBASSERT_GEN_OP_BOILERPLATE(functor, op) \ + template [[nodiscard]] constexpr auto operator op(O&& operand) && { \ + static_assert(!is_nothing); \ + using Q = std::conditional_t, std::remove_reference_t, O>; \ + if constexpr(is_nothing) { \ + static_assert(is_nothing); \ + return expression_decomposer(std::forward(a), std::forward(operand)); \ + } else { \ + static_assert(!is_nothing); \ + using V = decltype(get_value()); \ + return expression_decomposer(std::forward(get_value()), std::forward(operand)); \ + } \ + } + LIBASSERT_GEN_OP_BOILERPLATE(ops::shr, >>) + #if __cplusplus >= 202002L + LIBASSERT_GEN_OP_BOILERPLATE(ops::spaceship, <=>) + #endif + LIBASSERT_GEN_OP_BOILERPLATE(ops::eq, ==) + LIBASSERT_GEN_OP_BOILERPLATE(ops::neq, !=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::gt, >) + LIBASSERT_GEN_OP_BOILERPLATE(ops::lt, <) + LIBASSERT_GEN_OP_BOILERPLATE(ops::gteq, >=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::lteq, <=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::band, &) + LIBASSERT_GEN_OP_BOILERPLATE(ops::bxor, ^) + LIBASSERT_GEN_OP_BOILERPLATE(ops::bor, |) + #ifdef LIBASSERT_DECOMPOSE_BINARY_LOGICAL + LIBASSERT_GEN_OP_BOILERPLATE(ops::land, &&) + LIBASSERT_GEN_OP_BOILERPLATE(ops::lor, ||) + #endif + LIBASSERT_GEN_OP_BOILERPLATE(ops::assign, =) // NOLINT(cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator) + LIBASSERT_GEN_OP_BOILERPLATE(ops::add_assign, +=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::sub_assign, -=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::mul_assign, *=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::div_assign, /=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::mod_assign, %=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::shl_assign, <<=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::shr_assign, >>=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::band_assign, &=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::bxor_assign, ^=) + LIBASSERT_GEN_OP_BOILERPLATE(ops::bor_assign, |=) + #undef LIBASSERT_GEN_OP_BOILERPLATE + }; + + // for ternary support + template + expression_decomposer(U&&) -> expression_decomposer< + std::conditional_t, std::remove_reference_t, U> + >; +} + +#endif diff --git a/include/libassert/platform.hpp b/include/libassert/platform.hpp index 178032f4..b7db1c0d 100644 --- a/include/libassert/platform.hpp +++ b/include/libassert/platform.hpp @@ -207,7 +207,7 @@ #define LIBASSERT_HAS_BUILTIN_IS_CONSTANT_EVALUATED #endif -namespace libassert::support { +namespace libassert::detail { // Note: Works with >=C++20 and with C++17 for GCC 9.1+, Clang 9+, and MSVC 19.25+. constexpr bool is_constant_evaluated() noexcept { #if defined(LIBASSERT_HAS_IS_CONSTANT_EVALUATED) diff --git a/include/libassert/stringification.hpp b/include/libassert/stringification.hpp new file mode 100644 index 00000000..e7ca8723 --- /dev/null +++ b/include/libassert/stringification.hpp @@ -0,0 +1,430 @@ +#ifndef LIBASSERT_STRINGIFICATION_HPP +#define LIBASSERT_STRINGIFICATION_HPP + +#include +#include +#include + +#include +#include + +#ifdef LIBASSERT_USE_MAGIC_ENUM + // relative include so that multiple library versions don't clash + // e.g. if both libA and libB have different versions of libassert as a public + // dependency, then any library that consumes both will have both sets of include + // paths. this isn't an issue for #include but becomes an issue + // for includes within the library (libA might include from libB) + #if __has_include() + #include + #else + #include + #endif +#endif + +#ifdef LIBASSERT_USE_FMT + #include +#endif + +// ===================================================================================================================== +// || Stringification micro-library || +// || Note: There is some stateful stuff behind the scenes related to literal format configuration || +// ===================================================================================================================== + +namespace libassert { + // customization point + template struct stringifier /*{ + std::convertible_to stringify(const T&); + }*/; +} + +namespace libassert::detail { + // What can be stringified + // Base types: + // - anything string-like + // - arithmetic types + // - pointers + // - smart pointers + // - nullptr_t + // - std::error_code/std::error_condition + // - orderings + // - enum values + // User-provided stringifications: + // - std::ostream<< overloads + // - std format TODO + // - fmt TODO + // - libassert::stringifier + // Containers: + // - std::optional + // - std::variant TODO + // - std::expected TODO + // - tuples and tuple-likes + // - anything container-like (std::vector, std::array, std::unordered_map, C arrays, .....) + // Priorities: + // - libassert::stringifier + // - default formatters + // - fmt + // - std fmt TODO + // - osteam format + // - instance of catch all + // Other logistics: + // - Containers are only stringified if their value_type is stringifiable + // TODO Weak pointers? + + template class Ref> + struct is_specialization : std::false_type {}; + + template class Ref, typename... Args> + struct is_specialization, Ref>: std::true_type {}; + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string do_stringify(const T& v); + + namespace stringification { + // + // General traits + // + template class is_tuple_like : public std::false_type {}; + template + class is_tuple_like< + T, + std::void_t< + typename std::tuple_size::type, // TODO: decltype(std::tuple_size_v) ? + decltype(std::get<0>(std::declval())) + > + > : public std::true_type {}; + + namespace adl { + using std::begin, std::end; // ADL + template class is_container : public std::false_type {}; + template + class is_container< + T, + std::void_t< + decltype(begin(decllval())), + decltype(end(decllval())) + > + > : public std::true_type {}; + template class is_begin_deref : public std::false_type {}; + template + class is_begin_deref< + T, + std::void_t< + decltype(*begin(decllval())) + > + > : public std::true_type {}; + } + + template class is_deref : public std::false_type {}; + template + class is_deref< + T, + std::void_t< + decltype(*decllval()) + > + > : public std::true_type {}; + + template class has_ostream_overload : public std::false_type {}; + template + class has_ostream_overload< + T, + std::void_t() << std::declval())> + > : public std::true_type {}; + + template class has_stringifier : public std::false_type {}; + template + class has_stringifier< + T, + std::void_t>{}.stringify(std::declval()))> + > : public std::true_type {}; + + // + // Catch all + // + + template + [[nodiscard]] std::string stringify_unknown() { + return bstringf("", prettify_type(std::string(type_name())).c_str()); + } + + // + // Basic types + // + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::string_view); + // without nullptr_t overload msvc (without /permissive-) will call stringify(bool) and mingw + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::nullptr_t); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(char); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(bool); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(short); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(int); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(long); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(long long); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned short); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned int); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned long); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(unsigned long long); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(float); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(double); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(long double); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::error_code ec); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::error_condition ec); + #if __cplusplus >= 202002L + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::strong_ordering); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::weak_ordering); + [[nodiscard]] LIBASSERT_EXPORT std::string stringify(std::partial_ordering); + #endif + [[nodiscard]] LIBASSERT_EXPORT + std::string stringify_pointer_value(const void*); + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string stringify_smart_ptr(const T& t) { + if(t) { + return do_stringify(*t); + } else { + return "nullptr"; + } + } + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string stringify_by_ostream(const T& t) { + // clang-tidy bug here + // NOLINTNEXTLINE(misc-const-correctness) + std::ostringstream oss; + oss<>, int> = 0> + LIBASSERT_ATTR_COLD [[nodiscard]] std::string stringify_enum(const T& t) { + std::string_view name = magic_enum::enum_name(t); + if(!name.empty()) { + return std::string(name); + } else { + return bstringf( + "enum %s: %s", + prettify_type(std::string(type_name())).c_str(), + stringify(static_cast::type>(t)).c_str() + ); + } + } + #else + template>, int> = 0> + LIBASSERT_ATTR_COLD [[nodiscard]] std::string stringify_enum(const T& t) { + return bstringf( + "enum %s: %s", + prettify_type(std::string(type_name())).c_str(), + stringify(static_cast>(t)).c_str() + ); + } + #endif + + // + // Compositions of other types + // + // #ifdef __cpp_lib_expected + // template + // [[nodiscard]] std::string stringify(const std::unexpected& x) { + // return "unexpected " + stringify(x.error()); + // } + + // template + // [[nodiscard]] std::string stringify(const std::expected& x) { + // if(x.has_value()) { + // if constexpr(std::is_void_v) { + // return "expected void"; + // } else { + // return "expected " + stringify(*x); + // } + // } else { + // return "unexpected " + stringify(x.error()); + // } + // } + // #endif + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string stringify(const std::optional& t) { + if(t) { + return do_stringify(t.value()); + } else { + return "nullopt"; + } + } + + inline constexpr std::size_t max_container_print_items = 1000; + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string stringify_container(const T& container) { + using std::begin, std::end; // ADL + std::string str = "["; + const auto begin_it = begin(container); + std::size_t count = 0; + for(auto it = begin_it; it != end(container); it++) { + if(it != begin_it) { + str += ", "; + } + str += do_stringify(*it); + if(++count == max_container_print_items) { + str += ", ..."; + break; + } + } + str += "]"; + return str; + } + + // I'm going to assume at least one index because is_tuple_like requires index 0 to exist + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string stringify_tuple_like_impl(const T& t, std::index_sequence) { + return "[" + (do_stringify(std::get<0>(t)) + ... + (", " + do_stringify(std::get(t)))) + "]"; + } + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string stringify_tuple_like(const T& t) { + return stringify_tuple_like_impl(t, std::make_index_sequence::value - 1>{}); + } + } + + template + class has_value_type : public std::false_type {}; + template + class has_value_type< + T, + std::void_t + > : public std::true_type {}; + + template inline constexpr bool is_smart_pointer = + is_specialization::value + || is_specialization::value; // TODO: Handle weak_ptr too? + + template + class can_basic_stringify : public std::false_type {}; + template + class can_basic_stringify< + T, + std::void_t()))> + > : public std::true_type {}; + + template constexpr bool stringifiable_container(); + + template inline constexpr bool stringifiable = + stringification::has_stringifier::value + || std::is_convertible_v + || (std::is_pointer_v || std::is_function_v) + || std::is_enum_v + || (stringification::is_tuple_like::value && stringifiable_container()) + || (stringification::adl::is_container::value && stringifiable_container()) + || can_basic_stringify::value + || stringifiable_container(); + + template constexpr bool tuple_has_stringifiable_args_core(std::index_sequence) { + return ( + stringifiable(std::declval()))> + || ... + || stringifiable(std::declval()))> + ); + } + + template inline constexpr bool tuple_has_stringifiable_args = + tuple_has_stringifiable_args_core(std::make_index_sequence::value - 1>{}); + + template constexpr bool stringifiable_container() { + // TODO: Guard against std::expected....? + if constexpr(has_value_type::value) { + if constexpr(std::is_same_v) { + return false; + } else { + return stringifiable; + } + } else if constexpr(std::is_array_v>) { // C arrays + return stringifiable()[0])>; + } else if constexpr(stringification::is_tuple_like::value) { + return tuple_has_stringifiable_args; + } else { + return false; + } + } + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string do_stringify(const T& v) { + if constexpr(stringification::has_stringifier::value) { + return stringifier>{}.stringify(v); + } else if constexpr(std::is_convertible_v) { + if constexpr(std::is_pointer_v || std::is_same_v) { + if(v == nullptr) { + return "nullptr"; + } + } + return stringification::stringify(std::string_view(v)); + } else if constexpr(std::is_pointer_v || std::is_function_v) { + return stringification::stringify_pointer_value(reinterpret_cast(v)); + } else if constexpr(is_smart_pointer) { + #ifndef LIBASSERT_NO_STRINGIFY_SMART_POINTER_OBJECTS + if(stringifiable) { + #else + if(false) { + #endif + return stringification::stringify_smart_ptr(v); + } else { + return stringification::stringify_pointer_value(v.get()); + } + } else if constexpr(std::is_enum_v) { + return stringification::stringify_enum(v); + } else if constexpr(stringification::is_tuple_like::value) { + if constexpr(stringifiable_container()) { + return stringification::stringify_tuple_like(v); + } else { + return stringification::stringify_unknown(); + } + } else if constexpr(stringification::adl::is_container::value) { + if constexpr(stringifiable_container()) { + return stringification::stringify_container(v); + } else { + return stringification::stringify_unknown(); + } + } else if constexpr(can_basic_stringify::value) { + return stringification::stringify(v); + } else if constexpr(stringification::has_ostream_overload::value) { + return stringification::stringify_by_ostream(v); + } + #ifdef LIBASSERT_USE_FMT + else if constexpr(fmt::is_formattable::value) { + return fmt::format("{}", v); + } + #endif + else { + return stringification::stringify_unknown(); + } + // TODO std fmt + } + + // Top-level stringify utility + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string generate_stringification(const T& v) { + if constexpr( + stringification::adl::is_container::value + && !is_string_type + && stringifiable_container() + ) { + return prettify_type(std::string(type_name())) + ": " + do_stringify(v); + } else if constexpr(stringification::is_tuple_like::value && stringifiable_container()) { + return prettify_type(std::string(type_name())) + ": " + do_stringify(v); + } else if constexpr((std::is_pointer_v && !is_string_type) || is_smart_pointer) { + return prettify_type(std::string(type_name())) + ": " + do_stringify(v); + } else if constexpr(is_specialization::value) { + return prettify_type(std::string(type_name())) + ": " + do_stringify(v); + } else { + return do_stringify(v); + } + } +} + +#endif diff --git a/include/libassert/utilities.hpp b/include/libassert/utilities.hpp new file mode 100644 index 00000000..53b59291 --- /dev/null +++ b/include/libassert/utilities.hpp @@ -0,0 +1,121 @@ +#ifndef LIBASSERT_UTILITIES_HPP +#define LIBASSERT_UTILITIES_HPP + +#include + +#include + +// ===================================================================================================================== +// || Core utilities || +// ===================================================================================================================== + +namespace libassert { + // Lightweight helper, eventually may use C++20 std::source_location if this library no longer + // targets C++17. Note: __builtin_FUNCTION only returns the name, so __PRETTY_FUNCTION__ is + // still needed. + struct source_location { + const char* file; + //const char* function; // disabled for now due to static constexpr restrictions + int line; + constexpr source_location( + //const char* _function /*= __builtin_FUNCTION()*/, + const char* _file = __builtin_FILE(), + int _line = __builtin_LINE() + ) : file(_file), /*function(_function),*/ line(_line) {} + }; +} + +namespace libassert::detail { + // bootstrap with primitive implementations + LIBASSERT_EXPORT void primitive_assert_impl( + bool condition, + bool verify, + const char* expression, + const char* signature, + source_location location, + const char* message = nullptr + ); + + [[noreturn]] LIBASSERT_EXPORT void primitive_panic_impl ( + const char* signature, + source_location location, + const char* message + ); + + // always_false is just convenient to use here + #define LIBASSERT_PHONY_USE(E) ((void)libassert::detail::always_false) + + #ifndef NDEBUG + #define LIBASSERT_PRIMITIVE_ASSERT(c, ...) \ + libassert::detail::primitive_assert_impl(c, false, #c, LIBASSERT_PFUNC, {} LIBASSERT_VA_ARGS(__VA_ARGS__)) + #else + #define LIBASSERT_PRIMITIVE_ASSERT(c, ...) LIBASSERT_PHONY_USE(c) + #endif + + #define LIBASSERT_PRIMITIVE_PANIC(message) libassert::detail::primitive_panic_impl(LIBASSERT_PFUNC, {}, message) +} + +// ===================================================================================================================== +// || Basic formatting and type tools || +// ===================================================================================================================== + +namespace libassert::detail { + [[nodiscard]] LIBASSERT_EXPORT std::string bstringf(const char* format, ...); + + [[nodiscard]] LIBASSERT_EXPORT std::string_view type_name_core(std::string_view signature) noexcept; + + template + LIBASSERT_ATTR_COLD [[nodiscard]] + std::string_view type_name() noexcept { + return type_name_core(LIBASSERT_PFUNC); + } + + [[nodiscard]] LIBASSERT_EXPORT std::string prettify_type(std::string type); +} + +// ===================================================================================================================== +// || Metaprogramming utilities || +// ===================================================================================================================== + +namespace libassert::detail { + struct nothing {}; + + template inline constexpr bool is_nothing = std::is_same_v; + + // Hack to get around static_assert(false); being evaluated before any instantiation, even under + // an if-constexpr branch + // Also used for PHONY_USE + template inline constexpr bool always_false = false; + + template using strip = std::remove_cv_t>; + + // intentionally not stripping B + template inline constexpr bool isa = std::is_same_v, B>; + + // Is integral but not boolean + template inline constexpr bool is_integral_and_not_bool = std::is_integral_v> && !isa; + + template + inline constexpr bool is_arith_not_bool_char = std::is_arithmetic_v> && !isa && !isa; + + template + inline constexpr bool is_c_string = + isa>, char*> // <- covers literals (i.e. const char(&)[N]) too + || isa>, const char*>; + + template + inline constexpr bool is_string_type = + isa + || isa + || is_c_string; + + // char(&)[20], const char(&)[20], const char(&)[] + template inline constexpr bool is_string_literal = + std::is_lvalue_reference_v + && std::is_array_v> + && isa>, char>; + + template typename std::add_lvalue_reference_t decllval() noexcept; +} + +#endif