diff --git a/include/common/result.hpp b/include/common/result.hpp new file mode 100644 index 00000000..799c0c5b --- /dev/null +++ b/include/common/result.hpp @@ -0,0 +1,314 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace zest { + +/** + * @brief Base class for custom error types used in the Result class. + * + */ +class ResultError { + public: + /** + * @brief struct containing data that can only be known at runtime. + * + */ + struct RuntimeData { + std::stacktrace stacktrace; + std::chrono::time_point time; + }; + + /** + * @brief Construct a new ResultError object. + * + * @details Captures the current stacktrace and system time if called at runtime. + */ + constexpr ResultError() { + if !consteval { + runtime_data = { + .stacktrace = std::stacktrace::current(), + .time = std::chrono::system_clock::now() + }; + } + } + + std::optional runtime_data; +}; + +/** + * @brief Trait to define a "sentinel" value for types indicating an error state. + * @tparam T Type to provide a sentinel value for. + * @note Specialize this template for custom types if needed. + */ +template +class SentinelValue; + +/** + * @brief Concept to check if a type has a defined sentinel value. + * @tparam T Type to check. + */ +template +concept Sentinel = requires(const T& val) { SentinelValue::value; }; + +/** + * @brief Helper variable to simplify access to a type's sentinel value. + * @tparam T Type with a defined sentinel (must satisfy Sentinel concept). + */ +template +constexpr T sentinel_v = SentinelValue::value; + +/** + * @brief Partial specialization of SentinelValue for integral and floating-point types. + * @tparam T Integral or floating-point type. + * @details Uses infinity for floating-point types if available; otherwise uses max value. + */ +template + requires(std::integral || std::floating_point) +class SentinelValue { + public: + static constexpr T get() { + if constexpr (std::numeric_limits::has_infinity) { + return std::numeric_limits::infinity(); + } else { + return std::numeric_limits::max(); + } + } + + static constexpr T value = get(); ///< Precomputed sentinel value for type T. +}; + +/** + * @brief Result class for expected value or error handling (similar to std::expected). + * @tparam T Type of the expected value. + * @tparam Errs List of possible error types (must inherit from ResultError). + * @note Errors are stored in a variant, and the value is always initialized. + */ +template + requires(sizeof...(Errs) > 0) && (std::derived_from && ...) +class Result { + public: + /** + * @brief Construct a Result with a normal value (no error). + * @tparam U Type convertible to T. + * @param value Value to initialize the result with. + */ + template + requires std::constructible_from + constexpr Result(U&& value) + : error(std::monostate()), + value(std::forward(value)) {} + + /** + * @brief Construct a Result with a value and an error. + * @tparam U Type convertible to T. + * @tparam E Error type (must be in Errs). + * @param value Value to store. + * @param error Error to store. + */ + template + requires std::constructible_from + && (std::same_as, Errs> || ...) + constexpr Result(U&& value, E&& error) + : value(std::forward(value)), + error(std::forward(error)) {} + + /** + * @brief Construct a Result with an error, initializing the value to its sentinel. + * @tparam E Error type (must be in Errs). + * @param error Error to store. + * @note Requires T to have a defined sentinel value (via SentinelValue). + */ + template + requires Sentinel && (std::same_as, Errs> || ...) + constexpr Result(E&& error) + : error(std::forward(error)), + value(sentinel_v) {} + + /** + * @brief Get an error of type E if present (const-qualified overload). + * @tparam E Error type to retrieve. + * @return std::optional Contains the error if present; otherwise nullopt. + */ + template + requires(std::same_as || ...) + constexpr std::optional get() const& { + if (std::holds_alternative(error)) { + return std::get(error); + } else { + return std::nullopt; + } + } + + /** + * @brief Get an error of type E if present (rvalue overload). + * @tparam E Error type to retrieve. + * @return std::optional Contains the error if present; otherwise nullopt. + */ + template + requires(std::same_as || ...) + constexpr std::optional get() && { + if (std::holds_alternative(error)) { + return std::move(std::get(error)); + } else { + return std::nullopt; + } + } + + /** + * @brief Get an error of type E if present (const rvalue overload). + * @tparam E Error type to retrieve. + * @return std::optional Contains the error if present; otherwise nullopt. + */ + template + requires(std::same_as || ...) + constexpr const std::optional get() const&& { + if (std::holds_alternative(error)) { + return std::move(std::get(error)); + } else { + return std::nullopt; + } + } + + /** + * @brief Get the stored value (const-qualified overload). + * @return T Copy of the stored value. + */ + template + requires std::same_as + constexpr T get() const& { + return value; + } + + /** + * @brief Get the stored value (rvalue overload). + * @return T Moved value. + */ + template + requires std::same_as + constexpr T get() && { + return std::move(value); + } + + constexpr operator T&() & { + return value; + } + + constexpr operator const T&() const& { + return value; + }; + + constexpr operator T&&() && { + return std::move(value); + } + + constexpr operator const T&&() const&& { + return std::move(value); + } + + /** + * @brief error value + * @details instead of wrapping the variant in std::optional, it's more efficient to use + * std::monostate. since we have to use std::variant in any case. + */ + std::variant error; + T value; +}; + +/** + * @brief compare Result instances with comparable normal values + * + * @tparam LhsT the normal value type of the left-hand side argument + * @tparam RhsT the normal value type of the right-hand side argument + * @tparam LhsErrs the error value types of the left-hand side argument + * @tparam RhsErrs the error value types of the right-hand side argument + * @param lhs the left-hand side of the expression + * @param rhs the right-hand side of the expression + * @return true if the values are equal + * @return false if the values are not equal + */ +template + requires std::equality_comparable_with +constexpr bool +operator==(const Result& lhs, const Result& rhs) { + return lhs.value == rhs.value; +} + +/** + * @brief Result specialization for void value type (no stored value). + * @tparam Errs List of possible error types (must inherit from ResultError). + */ +template + requires(sizeof...(Errs) > 0) && (std::derived_from && ...) +class Result { + public: + /** + * @brief Construct a Result with an error. + * @tparam E Error type (must be in Errs). + * @param error Error to store. + */ + template + requires(std::same_as, Errs> || ...) + constexpr Result(E&& error) + : error(std::forward(error)) {} + + /** + * @brief Construct a Result with no error (success state). + */ + constexpr Result() + : error(std::monostate()) {} + + /** + * @brief Get an error of type E if present (const-qualified overload). + * @tparam E Error type to retrieve. + * @return std::optional Contains the error if present; otherwise nullopt. + */ + template + requires(std::same_as || ...) + constexpr std::optional get() const& { + if (std::holds_alternative(error)) { + return std::get(error); + } else { + return std::nullopt; + } + } + + /** + * @brief Get an error of type E if present (rvalue overload). + * @tparam E Error type to retrieve. + * @return std::optional Contains the error if present; otherwise nullopt. + */ + template + requires(std::same_as || ...) + constexpr std::optional get() && { + if (std::holds_alternative(error)) { + return std::move(std::get(error)); + } else { + return std::nullopt; + } + } + + /** + * @brief Get an error of type E if present (const rvalue overload). + * @tparam E Error type to retrieve. + * @return std::optional Contains the error if present; otherwise nullopt. + */ + template + requires(std::same_as || ...) + constexpr const std::optional get() const&& { + if (std::holds_alternative(error)) { + return std::move(std::get(error)); + } else { + return std::nullopt; + } + } + + std::variant error; ///< Variant holding an error or monostate. +}; + +} // namespace zest \ No newline at end of file diff --git a/meson.build b/meson.build index af4d8e78..c666f6a5 100644 --- a/meson.build +++ b/meson.build @@ -64,6 +64,7 @@ linker_flags = [ # system libraries we depend on system_deps = [ '-nostartfiles', # we still need to implement some newlib stubs + '-lstdc++exp', ] add_global_link_arguments( system_deps, language: 'c') add_global_link_arguments(system_deps, language: 'cpp') @@ -73,11 +74,16 @@ stdlib_conf = [ '-D_POSIX_MONOTONIC_CLOCK', # enable the POSIX monotonic clock ] +# warning flags +warning_flags = [ + '-Wno-psabi' # all libraries (except libv5) are compiled from source, making this warning useless +] + # apply all these flags and configs -add_global_arguments(optimization_flags, formatting_flags, stdlib_conf, language: 'c') -add_global_arguments(optimization_flags, formatting_flags, stdlib_conf, language: 'cpp') -add_global_link_arguments(optimization_flags, linker_flags, formatting_flags, system_deps, language: 'c') -add_global_link_arguments(optimization_flags, linker_flags, formatting_flags, system_deps, language: 'cpp') +add_global_arguments(optimization_flags, formatting_flags, warning_flags, stdlib_conf, language: 'c') +add_global_arguments(optimization_flags, formatting_flags, warning_flags, stdlib_conf, language: 'cpp') +add_global_link_arguments(optimization_flags, linker_flags, formatting_flags, warning_flags, system_deps, language: 'c') +add_global_link_arguments(optimization_flags, linker_flags, formatting_flags, warning_flags, system_deps, language: 'cpp') # include directories. # we only specify the top level in order to enforce paths in include directives. @@ -109,4 +115,4 @@ custom_target( input: elf, build_by_default: true, # otherwise it won't be built command: [objcopy, ['-O', 'binary', '-S', '@INPUT@', '@OUTPUT@']], -) \ No newline at end of file +) diff --git a/tests/result.cpp b/tests/result.cpp new file mode 100644 index 00000000..e04378a4 --- /dev/null +++ b/tests/result.cpp @@ -0,0 +1,61 @@ +#include "common/result.hpp" + +#include +#include + +// there'll be a lot of unused variables, since we just want to see if it compiles +#pragma GCC diagnostic ignored "-Wunused-variable" + +class MyError : public zest::ResultError {}; + +class MyError2 : public zest::ResultError {}; + +zest::Result test_function_1() { + // return nothing + return {}; + // return MyError + return MyError(); +} + +zest::Result test_function_2() { + // return an integer + return 1; + // return MyError + return MyError(); +} + +constexpr void compile_time_tests() { + // test sentinel values + static_assert(zest::sentinel_v == INT32_MAX); + static_assert(zest::sentinel_v == std::numeric_limits::infinity()); + + { + // test comparison operator + static_assert(zest::Result(2) == zest::Result(2)); + static_assert(zest::Result(3) == zest::Result(3)); + static_assert(zest::Result(0) == 0); + static_assert(0 == zest::Result(0)); + static_assert(0.0 == zest::Result(0)); + static_assert(zest::Result(0) == 0.0); + } + + { + // test conversion operators + zest::Result a = 2; + int& b = a; + const int& c = a; + int&& d = zest::Result(2); + const int&& e = zest::Result(2); + int f = a; + } + + { + // test error getting + static_assert(zest::Result(MyError()).get()); + static_assert(!zest::Result(1).get()); + static_assert(zest::Result(1).get()); + static_assert(zest::Result(1).get()); + } +} + +void runtime_tests() {}