Repository: ZestCommunity/ZestCode
Pull request #63: feat: :sparkles: Result class
Author: login: ion098
Created: 2025-03-28T18:22:23Z | Merged: 2025-05-03T22:33:39Z

#### Overview
<!-- Provide a concise description of your changes -->

#### Motivation
<!-- Provide a brief description of why you think these changes need to be made -->

#### References (optional)
Closes #29

#### Implementation Details (optional)
<!-- Optionally provide details of your implementation, such as why certain decisions were made and what problems had to be solved -->

#### Test Plan:
<!-- Provide a list of steps that can be taken to verify these changes work as intended -->


Linked issue #29: ♻ Rework Error Types
Using errno is slightly obtuse of a way to check what type of error occured, and we currently don't follow convention with "Success" being `0` and errors being non-zero types. 

Here's what I'm thinking: 
- Enumerate error types to be a more approachable and readable format. 
- The new "Success" type should be 0 instead of 1. 
- Make a lookup table for the errors to provide some form of error print function. 

Looking forwards to seeing how this project goes. 
Linked issue comments: user: login: liam-teale
created_at: 2025-02-24T20:32:11Z
body: My concern with modern error handling in C++ is that incorrectly handled errors will throw an exception, and as a result crash the thread on which the exception was thrown. So, here's one solution:

```c++
// wraps any value. Can be implicitly cast to its wrapped value
template <typename T>
class Maybe {
  public:
    Maybe(T val,
          std::optional<Error> err = std::nullopt)
              : m_val(val),
                m_err(err) {}

    operator T() {
        return m_val;
    }

    std::optional<Error> get_error() {
        return m_err;
    }

    T get_val() {
        return m_val;
    }

    T m_val;
    std::optional<Error> m_err;
};
```

an example function using this class:

```c++
// example function
Maybe<double> Motor::get_temp() {
  constexpr auto ERROR = 0;
  // 0 is the error code
  if (error) return { ERROR, Error("a specific error has occurred", 0) };
  else return temp;
}
```

most of the time, users will do this:

```c++
double temp = Motor::get_temp();
```

but they can do this if they want to:

```c++
if (auto err = motor.move(100).get_error()) {
    std::cout << *err << std::endl; // prints the error string and code
}
```

it does mean that function signatures get slightly more complex, but this isn't a big deal imo

```c++
Maybe<double> get_temp();
// vs
double get_temp();
```
author_association: MEMBER

Diff:
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 <chrono>
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <variant>
+
+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<std::chrono::system_clock> 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<RuntimeData> 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<typename T>
+class SentinelValue;
+
+/**
+ * @brief Concept to check if a type has a defined sentinel value.
+ * @tparam T Type to check.
+ */
+template<typename T>
+concept Sentinel = requires(const T& val) { SentinelValue<T>::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<Sentinel T>
+constexpr T sentinel_v = SentinelValue<T>::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<typename T>
+    requires(std::integral<T> || std::floating_point<T>)
+class SentinelValue<T> {
+  public:
+    static constexpr T get() {
+        if constexpr (std::numeric_limits<T>::has_infinity) {
+            return std::numeric_limits<T>::infinity();
+        } else {
+            return std::numeric_limits<T>::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<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
+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<typename U>
+        requires std::constructible_from<T, U>
+    constexpr Result(U&& value)
+        : error(std::monostate()),
+          value(std::forward<U>(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<typename U, typename E>
+        requires std::constructible_from<T, U>
+                     && (std::same_as<std::remove_cvref_t<E>, Errs> || ...)
+    constexpr Result(U&& value, E&& error)
+        : value(std::forward<U>(value)),
+          error(std::forward<E>(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<T>).
+     */
+    template<typename E>
+        requires Sentinel<T> && (std::same_as<std::remove_cvref_t<E>, Errs> || ...)
+    constexpr Result(E&& error)
+        : error(std::forward<E>(error)),
+          value(sentinel_v<T>) {}
+
+    /**
+     * @brief Get an error of type E if present (const-qualified overload).
+     * @tparam E Error type to retrieve.
+     * @return std::optional<E> Contains the error if present; otherwise nullopt.
+     */
+    template<typename E>
+        requires(std::same_as<E, Errs> || ...)
+    constexpr std::optional<E> get() const& {
+        if (std::holds_alternative<E>(error)) {
+            return std::get<E>(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<E> Contains the error if present; otherwise nullopt.
+     */
+    template<typename E>
+        requires(std::same_as<E, Errs> || ...)
+    constexpr std::optional<E> get() && {
+        if (std::holds_alternative<E>(error)) {
+            return std::move(std::get<E>(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<E> Contains the error if present; otherwise nullopt.
+     */
+    template<typename E>
+        requires(std::same_as<E, Errs> || ...)
+    constexpr const std::optional<E> get() const&& {
+        if (std::holds_alternative<E>(error)) {
+            return std::move(std::get<E>(error));
+        } else {
+            return std::nullopt;
+        }
+    }
+
+    /**
+     * @brief Get the stored value (const-qualified overload).
+     * @return T Copy of the stored value.
+     */
+    template<typename U = T>
+        requires std::same_as<U, T>
+    constexpr T get() const& {
+        return value;
+    }
+
+    /**
+     * @brief Get the stored value (rvalue overload).
+     * @return T Moved value.
+     */
+    template<typename U = T>
+        requires std::same_as<U, T>
+    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<std::monostate, Errs...> 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<typename LhsT, typename RhsT, typename... LhsErrs, typename... RhsErrs>
+    requires std::equality_comparable_with<LhsT, RhsT>
+constexpr bool
+operator==(const Result<LhsT, LhsErrs...>& lhs, const Result<RhsT, RhsErrs...>& 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<typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
+class Result<void, Errs...> {
+  public:
+    /**
+     * @brief Construct a Result with an error.
+     * @tparam E Error type (must be in Errs).
+     * @param error Error to store.
+     */
+    template<typename E>
+        requires(std::same_as<std::remove_cvref_t<E>, Errs> || ...)
+    constexpr Result(E&& error)
+        : error(std::forward<E>(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<E> Contains the error if present; otherwise nullopt.
+     */
+    template<typename E>
+        requires(std::same_as<E, Errs> || ...)
+    constexpr std::optional<E> get() const& {
+        if (std::holds_alternative<E>(error)) {
+            return std::get<E>(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<E> Contains the error if present; otherwise nullopt.
+     */
+    template<typename E>
+        requires(std::same_as<E, Errs> || ...)
+    constexpr std::optional<E> get() && {
+        if (std::holds_alternative<E>(error)) {
+            return std::move(std::get<E>(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<E> Contains the error if present; otherwise nullopt.
+     */
+    template<typename E>
+        requires(std::same_as<E, Errs> || ...)
+    constexpr const std::optional<E> get() const&& {
+        if (std::holds_alternative<E>(error)) {
+            return std::move(std::get<E>(error));
+        } else {
+            return std::nullopt;
+        }
+    }
+
+    std::variant<std::monostate, Errs...> 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 <cstdint>
+#include <limits>
+
+// 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<void, MyError> test_function_1() {
+    // return nothing
+    return {};
+    // return MyError
+    return MyError();
+}
+
+zest::Result<int, MyError> 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_t> == INT32_MAX);
+    static_assert(zest::sentinel_v<float> == std::numeric_limits<float>::infinity());
+
+    {
+        // test comparison operator
+        static_assert(zest::Result<int, MyError>(2) == zest::Result<int, MyError>(2));
+        static_assert(zest::Result<int, MyError>(3) == zest::Result<float, MyError2>(3));
+        static_assert(zest::Result<int, MyError>(0) == 0);
+        static_assert(0 == zest::Result<int, MyError>(0));
+        static_assert(0.0 == zest::Result<int, MyError>(0));
+        static_assert(zest::Result<int, MyError>(0) == 0.0);
+    }
+
+    {
+        // test conversion operators
+        zest::Result<int, MyError> a = 2;
+        int& b = a;
+        const int& c = a;
+        int&& d = zest::Result<int, MyError>(2);
+        const int&& e = zest::Result<int, MyError>(2);
+        int f = a;
+    }
+
+    {
+        // test error getting
+        static_assert(zest::Result<int, MyError>(MyError()).get<MyError>());
+        static_assert(!zest::Result<int, MyError>(1).get<MyError>());
+        static_assert(zest::Result<int, MyError>(1).get<int>());
+        static_assert(zest::Result<int, MyError>(1).get());
+    }
+}
+
+void runtime_tests() {}


Reviews:
user: login: ion098
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T18:37:49Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T20:08:59Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T20:10:04Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T20:10:45Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T23:29:05Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T23:32:19Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T23:37:17Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:33:36Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T21:22:48Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T21:32:52Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T21:34:47Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T21:51:09Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T00:09:38Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:57:06Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:59:19Z
user: login: djava
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T14:44:20Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T02:43:00Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T02:51:12Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T03:22:52Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T03:23:07Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T03:50:04Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T12:27:03Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T12:30:38Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T12:36:40Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T12:37:03Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T20:51:28Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T21:07:21Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T21:11:21Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:39:57Z
user: login: djava
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:40:12Z
user: login: djava
body: I'd like to see some more tests before this merges, I think. In particular, I think that the multiple-errors case is probably where most of the snags lie, and the tests don't currently cover that.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:45:40Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T16:55:57Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T22:34:44Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T22:35:31Z
user: login: liam-teale
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T23:22:43Z

Review comments:
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
path: include/common/result.hpp
user: login: ion098
body: Is there a reason this doesn't just use `= default`?
created_at: 2025-03-28T18:24:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
path: include/common/result.hpp
user: login: ion098
body: Why are these `const`? `const` member variables are generally an antipattern.
created_at: 2025-03-28T18:29:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
+    const std::optional<const ResultError<E>> error;
+};
+
+} // namespace zest
+
+template<typename E>
+struct std::formatter<zest::ResultError<E>> {
+    constexpr auto parse(std::format_parse_context& ctx) {
+        return ctx.begin();
+    }
+
+    auto format(const zest::ResultError<E>& error, std::format_context& ctx) const {
+        return std::format_to(
+            ctx.out(),
+            "{}\n"
+            "begin stacktrace\n"
+            "{}\n"
+            "end stacktrace",
+            error.message,
+            error.stacktrace
+        );
+    }
+};
+
+template<typename E>
+std::ostream& operator<<(std::ostream& os, const zest::ResultError<E>& error) {
+    os << error.message << std::endl
path: include/common/result.hpp
user: login: ion098
body: This should be implemented in terms of `std::format`.
created_at: 2025-03-28T18:29:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
+    const std::optional<const ResultError<E>> error;
+};
+
+} // namespace zest
+
+template<typename E>
+struct std::formatter<zest::ResultError<E>> {
+    constexpr auto parse(std::format_parse_context& ctx) {
path: include/common/result.hpp
user: login: ion098
body: ```suggestion
    constexpr auto parse(std::format_parse_context& ctx) const {
```
created_at: 2025-03-28T18:31:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
path: include/common/result.hpp
user: login: ion098
body: This is pretty minor, but moving the `E type;` to the end of the struct *might* allow for identical code folding in the `std::formatter<zest::ResultError<E>>::format` function.
created_at: 2025-03-28T18:36:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
path: include/common/result.hpp
user: login: djava
body: Agreed - const member variables make a type non-assignable and non-moveable
created_at: 2025-03-28T20:08:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
+    const std::optional<const ResultError<E>> error;
path: include/common/result.hpp
user: login: djava
body: I doubt that internal type parameter should be const
created_at: 2025-03-28T20:10:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
+    const std::optional<const ResultError<E>> error;
+};
+
+} // namespace zest
+
+template<typename E>
+struct std::formatter<zest::ResultError<E>> {
+    constexpr auto parse(std::format_parse_context& ctx) {
+        return ctx.begin();
+    }
+
+    auto format(const zest::ResultError<E>& error, std::format_context& ctx) const {
+        return std::format_to(
+            ctx.out(),
+            "{}\n"
+            "begin stacktrace\n"
+            "{}\n"
+            "end stacktrace",
+            error.message,
+            error.stacktrace
+        );
+    }
+};
+
+template<typename E>
+std::ostream& operator<<(std::ostream& os, const zest::ResultError<E>& error) {
+    os << error.message << std::endl
path: include/common/result.hpp
user: login: djava
body: and maybe some more flair around the stacktrace delimeters!
created_at: 2025-03-28T20:10:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
path: include/common/result.hpp
user: login: liam-teale
body: this will have a drastic effect on fishing season
created_at: 2025-03-28T23:29:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
+    const std::optional<const ResultError<E>> error;
path: include/common/result.hpp
user: login: liam-teale
body: fixed
created_at: 2025-03-28T23:32:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
+        : val(std::move(other.val)),
+          error(std::move(other.error)) {}
+
+    template<typename U, typename F>
+        requires std::constructible_from<T, U> && std::constructible_from<ResultError<E>, F>
+    Result(U&& val, F&& error)
+        : val(std::forward<T>(val)),
+          error(std::forward<ResultError>(error)) {}
+
+    template<typename U, typename... Args>
+        requires std::constructible_from<T, U>
+    Result(U&& val, E type, std::format_string<Args...> fmt, Args&&... args)
+        : val(std::forward<T>(val)),
+          error({type, std::format(fmt, std::forward<Args>(args)...), std::stacktrace::current()}) {
+    }
+
+    operator T() const& {
+        return val;
+    }
+
+    operator T() && {
+        return std::move(val);
+    }
+
+    const T val;
+    const std::optional<const ResultError<E>> error;
+};
+
+} // namespace zest
+
+template<typename E>
+struct std::formatter<zest::ResultError<E>> {
+    constexpr auto parse(std::format_parse_context& ctx) {
+        return ctx.begin();
+    }
+
+    auto format(const zest::ResultError<E>& error, std::format_context& ctx) const {
+        return std::format_to(
+            ctx.out(),
+            "{}\n"
+            "begin stacktrace\n"
+            "{}\n"
+            "end stacktrace",
+            error.message,
+            error.stacktrace
+        );
+    }
+};
+
+template<typename E>
+std::ostream& operator<<(std::ostream& os, const zest::ResultError<E>& error) {
+    os << error.message << std::endl
path: include/common/result.hpp
user: login: liam-teale
body: done
created_at: 2025-03-28T23:37:17Z
author_association: MEMBER
diff_hunk: @@ -64,6 +64,7 @@ linker_flags = [
 # system libraries we depend on
 system_deps = [
   '-nostartfiles', # we still need to implement some newlib stubs
+  '-lstdc++exp',
path: meson.build
user: login: djava
body: Hrm, whats the experimental stdlib for? Would probably prefer not to ship that
created_at: 2025-04-28T12:54:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
path: include/common/result.hpp
user: login: djava
body: As discussed on discord, I think having a timestamp on here would reasonably useful. Up for discussion though
created_at: 2025-04-28T12:56:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: djava
body: youre missing the spec for `template <std::floating_point>`.

Note that when youre writing that, probably do like `value = limits::has_infinity ? limits::infinity() : limits::max();`.  Technically not all float types necessarily have infinity, and its UB to call `limits::infinity()` if `has_infinity` is false.
created_at: 2025-04-28T13:16:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: djava
body: Technically you can combine the integral and floating_point partial specs into one with that ternary if you want i guess
created_at: 2025-04-28T13:16:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
+// Result class.
+// An alternative to std::expected, where an expected value can be contained alongside an unexpected
+// error value.
+// This means that Result will always contain an expected value.
+// Therefore, if the user does not check if a function returned an error, an exception will NOT be
+// thrown, and therefore the thread won't crash.
+template<typename T, CustomError... Errs>
+class Result {
+  public:
+    // Construct a Result with a value and no error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    template<typename U>
+        requires std::constructible_from<T, U>
+    constexpr Result(U&& value)
+        : value(std::forward<U>(value)) {}
+
+    // Construct a Result with a value and an error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<typename U, CustomError E>
+        requires std::constructible_from<T, U>
+                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    constexpr Result(U&& value, E&& error)
+        : value(std::forward<U>(value)),
+          error(std::forward<E>(error)) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: the type of the normal value must have a specialized sentinel value.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<CustomError U>
+        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr Result(U&& error)
+        : value(sentinel_v<T>),
+          error(std::forward<U>(error)) {}
path: include/common/result.hpp
user: login: djava
body: Maybeeee add a class-level template constraint of
```cpp
!(std::constructible_from<T, Errs> && ...)
```
or else the ctors will be ambiguous?
created_at: 2025-04-28T13:19:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
+// Result class.
+// An alternative to std::expected, where an expected value can be contained alongside an unexpected
+// error value.
+// This means that Result will always contain an expected value.
+// Therefore, if the user does not check if a function returned an error, an exception will NOT be
+// thrown, and therefore the thread won't crash.
+template<typename T, CustomError... Errs>
+class Result {
+  public:
+    // Construct a Result with a value and no error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    template<typename U>
+        requires std::constructible_from<T, U>
+    constexpr Result(U&& value)
+        : value(std::forward<U>(value)) {}
+
+    // Construct a Result with a value and an error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<typename U, CustomError E>
+        requires std::constructible_from<T, U>
+                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    constexpr Result(U&& value, E&& error)
+        : value(std::forward<U>(value)),
+          error(std::forward<E>(error)) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: the type of the normal value must have a specialized sentinel value.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<CustomError U>
+        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr Result(U&& error)
path: include/common/result.hpp
user: login: djava
body: I'm not sure if you should or not, but at least think about making this ctor explicit? IDK it's probably fine because of the contraints blocking multi-hop implicit conversion chains but worth a thought
created_at: 2025-04-28T13:21:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
path: include/common/result.hpp
user: login: djava
body: Think about changing the naming here - maybe `SentinelValue_t<T>` and then the concept can be `HasSentinel` and the value `sentinel_value` or maybe still `sentinel_v`. `HasSentinel` class just sounds like its only a boolean, which it's not
created_at: 2025-04-28T13:25:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
+// Result class.
+// An alternative to std::expected, where an expected value can be contained alongside an unexpected
+// error value.
+// This means that Result will always contain an expected value.
+// Therefore, if the user does not check if a function returned an error, an exception will NOT be
+// thrown, and therefore the thread won't crash.
+template<typename T, CustomError... Errs>
+class Result {
+  public:
+    // Construct a Result with a value and no error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    template<typename U>
+        requires std::constructible_from<T, U>
+    constexpr Result(U&& value)
+        : value(std::forward<U>(value)) {}
+
+    // Construct a Result with a value and an error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<typename U, CustomError E>
+        requires std::constructible_from<T, U>
+                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    constexpr Result(U&& value, E&& error)
+        : value(std::forward<U>(value)),
+          error(std::forward<E>(error)) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: the type of the normal value must have a specialized sentinel value.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<CustomError U>
+        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr Result(U&& error)
+        : value(sentinel_v<T>),
+          error(std::forward<U>(error)) {}
+
+    // Check whether the result contains an error of the given type
+    // Constraint: the custom error type to check for must be able to be contained in the error
+    // member variable.
+    template<CustomError U>
+        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr U has() const& {
+        if (!error.has_value()) {
+            return false;
+        } else {
+            return std::holds_alternative<U>(error);
+        }
+    };
+
+    // return an optional wrapping the given error type.
+    // Constraint: the custom error type to get must be able to be contained in the error
+    // member variable.
+    template<CustomError U>
+        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr std::optional<U> get() const& {
+        if (this->has<U>()) {
+            return std::get<U>(error.value());
+        } else {
+            return std::nullopt;
+        }
+    }
+
+    // return an optional wrapping the given error type.
+    // Constraint: the custom error type to get must be able to be contained in the error
+    // member variable.
+    template<CustomError U>
+        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr std::optional<U> get() && {
+        if (this->has<U>()) {
+            return std::get<U>(std::move(error.value()));
+        } else {
+            return std::nullopt;
+        }
+    }
+
+    // implicit conversion operator, on an l-value
+    constexpr operator T() const& {
+        return value;
+    };
+
+    // implicit conversion operator, on an r-value
+    constexpr operator T() && {
+        return std::move(value);
+    }
+
+    // the optional error value.
+    // a variant that could contain any of the specified error types.
+    std::optional<std::variant<Errs...>> error;
path: include/common/result.hpp
user: login: djava
body: I think its more space efficient for this to be `std::variant<std::monotype, Errs...>`, thats sort of what std::monotype is for. 

(unless stdlib already has a partial spec of `std::optional<std::variant<Ts...>>` optimizing for this... but i dont think so). 
created_at: 2025-04-28T13:29:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
+// Result class.
+// An alternative to std::expected, where an expected value can be contained alongside an unexpected
+// error value.
+// This means that Result will always contain an expected value.
+// Therefore, if the user does not check if a function returned an error, an exception will NOT be
+// thrown, and therefore the thread won't crash.
+template<typename T, CustomError... Errs>
+class Result {
+  public:
+    // Construct a Result with a value and no error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    template<typename U>
+        requires std::constructible_from<T, U>
+    constexpr Result(U&& value)
+        : value(std::forward<U>(value)) {}
+
+    // Construct a Result with a value and an error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<typename U, CustomError E>
+        requires std::constructible_from<T, U>
+                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    constexpr Result(U&& value, E&& error)
+        : value(std::forward<U>(value)),
+          error(std::forward<E>(error)) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: the type of the normal value must have a specialized sentinel value.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<CustomError U>
+        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr Result(U&& error)
+        : value(sentinel_v<T>),
+          error(std::forward<U>(error)) {}
+
+    // Check whether the result contains an error of the given type
+    // Constraint: the custom error type to check for must be able to be contained in the error
+    // member variable.
+    template<CustomError U>
+        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr U has() const& {
+        if (!error.has_value()) {
+            return false;
+        } else {
+            return std::holds_alternative<U>(error);
+        }
+    };
+
+    // return an optional wrapping the given error type.
+    // Constraint: the custom error type to get must be able to be contained in the error
+    // member variable.
+    template<CustomError U>
+        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr std::optional<U> get() const& {
+        if (this->has<U>()) {
+            return std::get<U>(error.value());
+        } else {
+            return std::nullopt;
+        }
+    }
path: include/common/result.hpp
user: login: djava
body: I probably want to see `get<T>` specialization as well. I know you have the implicit conversions but might as well have that too for symmetry.
created_at: 2025-04-28T13:33:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,6 +64,7 @@ linker_flags = [
 # system libraries we depend on
 system_deps = [
   '-nostartfiles', # we still need to implement some newlib stubs
+  '-lstdc++exp',
path: meson.build
user: login: liam-teale
body: It's for `std::stacktrace` support. We could opt not to use it, but considering how much time that would take it is my opinion that linking against the experimental libstdc++ is worth it
created_at: 2025-04-28T21:22:48Z
author_association: MEMBER
diff_hunk: @@ -64,6 +64,7 @@ linker_flags = [
 # system libraries we depend on
 system_deps = [
   '-nostartfiles', # we still need to implement some newlib stubs
+  '-lstdc++exp',
path: meson.build
user: login: liam-teale
body: unfortunately it seems that `std::stacktrace` is not supported at all by clang's `libc++`, so we won't be able to use `std::stacktrace` if we want to switch to clang. This will need to be resolved before this PR can be merged
created_at: 2025-04-28T21:32:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: liam-teale
body: wouldn't `std::numeric_limits<T>::max()` return the maximum value for these special types? If the type doesn't have infinity it should return its maximum value, whatever that may be
created_at: 2025-04-28T21:34:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
path: include/common/result.hpp
user: login: liam-teale
body: renamed `HasSentinel` to `SentinelValue`. Postfixing with `_t` does not conform to the styleguide
created_at: 2025-04-28T21:51:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
+// Result class.
+// An alternative to std::expected, where an expected value can be contained alongside an unexpected
+// error value.
+// This means that Result will always contain an expected value.
+// Therefore, if the user does not check if a function returned an error, an exception will NOT be
+// thrown, and therefore the thread won't crash.
+template<typename T, CustomError... Errs>
+class Result {
+  public:
+    // Construct a Result with a value and no error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    template<typename U>
+        requires std::constructible_from<T, U>
+    constexpr Result(U&& value)
+        : value(std::forward<U>(value)) {}
+
+    // Construct a Result with a value and an error value.
+    // Constraint: the value member variable must be able to be constructed with the value
+    // parameter.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<typename U, CustomError E>
+        requires std::constructible_from<T, U>
+                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    constexpr Result(U&& value, E&& error)
+        : value(std::forward<U>(value)),
+          error(std::forward<E>(error)) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: the type of the normal value must have a specialized sentinel value.
+    // Constraint: the error parameter must be of a type that may be contained in the error member
+    // variable.
+    template<CustomError U>
+        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    constexpr Result(U&& error)
+        : value(sentinel_v<T>),
+          error(std::forward<U>(error)) {}
path: include/common/result.hpp
user: login: liam-teale
body: Added some function-level constraints instead which don't harm the DX
created_at: 2025-04-29T00:09:38Z
author_association: MEMBER
diff_hunk: @@ -64,6 +64,7 @@ linker_flags = [
 # system libraries we depend on
 system_deps = [
   '-nostartfiles', # we still need to implement some newlib stubs
+  '-lstdc++exp',
path: meson.build
user: login: djava
body: I believe you can use `libstdc++` with clang still
created_at: 2025-04-29T13:57:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: djava
body: i mean, yes, but `numeric_limits<float>::max() != numeric_limits<float>::infinity()`. You could use either, I guess, but I think infinity probably works better. In general I would use that ternary (`has_infinity() ? infinity() : max()`) for all numeric types IMO
created_at: 2025-04-29T13:59:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
path: include/common/result.hpp
user: login: djava
body: This either doesnt work at all now, or is confusing (which can be demonstrated by how im not sure which)

So, it seems like this will invoke [variant constructor 4](https://en.cppreference.com/w/cpp/utility/variant/variant), which will do (checks notes) normal overload resolution of the argument over the contained types' converting constructors, excluding narrowing constructions? Anyway, I think that means it fails to resolve this constructor if the overload resolution is ambiguous, and otherwise requires you to know the overload resolution rules (which nobody does).

My proposed solution is either:
```suggestion
    template<typename U, typename E>
        requires std::constructible_from<T, U> && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
    constexpr Result(U&& value, E&& error)
        : value(std::forward<U>(value)),
          error(std::forward<E>(error)) {}
```
(which is what you had before this most recent batch of commits)

or (honestly what i'd prefer) ditch the public error constructors and replace them with public static functions 
 where you can explicitly specify the error type and a private constructor where the variant is already populated, something like:

```cpp
private:
template <typename U>
Result(U&& value, std::variant<Errs...>&& error) : value{std::forward<U>(value)), error(std::move(err)) {}

public:
// requires explicitly specifying E, allows converting constructor U->E
// ie call would be `MotorResult::make_error<PortError>("port not found!!");`
template <typename E, typename U>
requires (std::is_same_v<E, Errs> || ...) && std::constructible_from<E, U>
Result<T, Errs...> make_error(U&& err_value) {
  return {sentinel_v<T>, std::variant<Errs...>(std::in_place_type<E>, std::forward<U>(err_value))};
}

// requires explicitly specifying E, allows converting constructor U->E and V->T
// ie call would be `MotorResult::make_error<PortError>("port not found!!", 7)`
template <typename E, typename U, typename V>
requires (std::is_same_v<E, Errs> || ...) && std::constructible_from<E, U> && std::constructible_from<T, V>
Result<T, Errs...> make_error_with_value(U&& err_value, V&& value) {
  return {std::forward<V>(value), std::variant<Errs...>(std::in_place_type<E>, std::forward<U>(err_value))};
}
```

My point is that you if you want to have converting constructors into the error type, you have to specify which error type you're converting into. Personally, I prefer calling out specifically when youre making an error instead of just using constructor overloads anyway.

(All this applies to the other error ctor as well)
created_at: 2025-04-29T14:33:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
path: include/common/result.hpp
user: login: djava
body: ```suggestion
    // Constraint: U is in Errs
```
created_at: 2025-04-29T14:37:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: djava
body: No - the old logic was closer. Just because error is non-monostate doesn't mean it's a `U`.

I'd do, like
```
auto* ptr = std::get_if<U>(error);
return ptr ? *ptr : std::nullopt;
```


created_at: 2025-04-29T14:42:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: djava
body: Applies to rvalue get<> as well
created_at: 2025-04-29T14:42:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: liam-teale
body: oh I see. I was under the impression that `numeric_limits<float>::max()` would return `numeric_limits<float>::infinity()`
created_at: 2025-04-30T02:43:00Z
author_association: MEMBER
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
path: include/common/result.hpp
user: login: liam-teale
body: While a static function would be more explicit and arguably a better design choice, but the simplicity illustrated by the following example is appealing

```c++
Result<int, MyError> foo(bool bar) {
  if (bar) return 1;
  else return MyError();
}
```

But yea the previous constraint we had on the `E` template type would actually be better, the current constraint would allow for implicit conversion of an error argument which isn't ideal
created_at: 2025-04-30T02:51:12Z
author_association: MEMBER
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
path: include/common/result.hpp
user: login: liam-teale
body: should be resolved in https://github.com/ZestCommunity/ZestCode/pull/63/commits/3628c58195fe15aa758a48c1718e16ddd2f803b2
created_at: 2025-04-30T03:22:52Z
author_association: MEMBER
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
path: include/common/result.hpp
user: login: liam-teale
body: another attempt in https://github.com/ZestCommunity/ZestCode/pull/63/commits/3628c58195fe15aa758a48c1718e16ddd2f803b2
created_at: 2025-04-30T03:23:07Z
author_association: MEMBER
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: liam-teale
body: I fail to see how this logic is any different, or is this simply a nitpick?

Also I don't quite understand what you mean by

> No - the old logic was closer. Just because error is non-monostate doesn't mean it's a `U`
created_at: 2025-04-30T03:50:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: djava
body: nope - https://godbolt.org/z/7jMMWTqrW
created_at: 2025-04-30T12:27:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
path: include/common/result.hpp
user: login: djava
body: I'm okay with converting constructor from an is_same<E, Err>, LGTM
created_at: 2025-04-30T12:30:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: djava
body: There can be multiple error types.

The existing logic says "if `error` contains a `monostate` then return `nullopt`, else return the contained `U`" - which is wrong because there are other things it can contain than just `monostate` and `U`, all the other error types. If `error` contains one of the other error types that isnt `U` then this will throw `std::bad_variant_access`.

 The correct logic is "If it contains a `U` then return the contained `U`, else return `nullopt`".
created_at: 2025-04-30T12:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
path: include/common/result.hpp
user: login: djava
body: LGTM
created_at: 2025-04-30T12:37:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: liam-teale
body: I see, now I feel stupid
created_at: 2025-04-30T20:51:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: liam-teale
body: this has been implemented in https://github.com/ZestCommunity/ZestCode/pull/63/commits/d5a4999d590678ddc9aa3ce86e223c1a6fe3778e
created_at: 2025-04-30T21:07:21Z
author_association: MEMBER
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: liam-teale
body: should be fixed in https://github.com/ZestCommunity/ZestCode/pull/63/commits/479f1c4be0c27b8becf65b91311b0e6216515f19
created_at: 2025-04-30T21:11:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: djava
body: LGTM. Bro hates ternaries lol (whatever idrc)
created_at: 2025-05-01T13:39:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,93 +53,146 @@ class HasSentinel<T> {
 // This means that Result will always contain an expected value.
 // Therefore, if the user does not check if a function returned an error, an exception will NOT be
 // thrown, and therefore the thread won't crash.
-template<typename T, CustomError... Errs>
+template<typename T, typename... Errs>
+    requires(sizeof...(Errs) > 0) && (std::derived_from<Errs, ResultError> && ...)
 class Result {
   public:
     // Construct a Result with a value and no error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
+    // Constraint: type T can be constructed from type U
     template<typename U>
         requires std::constructible_from<T, U>
     constexpr Result(U&& value)
-        : value(std::forward<U>(value)) {}
+        : error(std::monostate()),
+          value(std::forward<U>(value)) {}
 
     // Construct a Result with a value and an error value.
-    // Constraint: the value member variable must be able to be constructed with the value
-    // parameter.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<typename U, CustomError E>
-        requires std::constructible_from<T, U>
-                     && (std::is_same_v<Errs, std::remove_cvref<E>> || ...)
+    // Constraint: type T can be constructed from type U
+    // Constraint: some type in Errs can be constructed from type E
+    template<typename U, typename E>
+        requires std::constructible_from<T, U> && (std::constructible_from<E, Errs> || ...)
     constexpr Result(U&& value, E&& error)
         : value(std::forward<U>(value)),
           error(std::forward<E>(error)) {}
 
     // Construct a Result with an error, initializing the normal value to its sentinel value.
-    // Constraint: the type of the normal value must have a specialized sentinel value.
-    // Constraint: the error parameter must be of a type that may be contained in the error member
-    // variable.
-    template<CustomError U>
-        requires Sentinel<T> && (std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    // Constraint: type U can't be implicitly converted to type T
+    template<typename U>
+        requires Sentinel<T> && (std::constructible_from<U, Errs> || ...)
+                     && (!std::convertible_to<U, T>)
     constexpr Result(U&& error)
-        : value(sentinel_v<T>),
-          error(std::forward<U>(error)) {}
-
-    // Check whether the result contains an error of the given type
-    // Constraint: the custom error type to check for must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
-    constexpr U has() const& {
-        if (!error.has_value()) {
-            return false;
-        } else {
-            return std::holds_alternative<U>(error);
-        }
-    };
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
+
+    // Construct a Result with an error, initializing the normal value to its sentinel value.
+    // Constraint: type T has a sentinel value
+    // Constraint: some type in Errs can be constructed from type U
+    template<typename U>
+        requires Sentinel<U> && (std::constructible_from<U, Errs> || ...)
+    explicit constexpr Result<U>(U&& error)
+        : error(std::forward<U>(error)),
+          value(sentinel_v<T>) {}
 
-    // return an optional wrapping the given error type.
-    // Constraint: the custom error type to get must be able to be contained in the error
-    // member variable.
-    template<CustomError U>
-        requires(std::is_same_v<Errs, std::remove_cvref<U>> || ...)
+    // Get the given error type, if it exists
+    // Constraint: some type Errs can be constructed from type U
+    template<typename U>
+        requires(std::same_as<U, Errs> || ...)
     constexpr std::optional<U> get() const& {
-        if (this->has<U>()) {
-            return std::get<U>(error.value());
-        } else {
+        if (std::holds_alternative<std::monostate>(error)) {
             return std::nullopt;
+        } else {
+            return std::get<U>(error);
         }
-    }
+    };
path: include/common/result.hpp
user: login: djava
body: LGTM. no worries, happens to everyone!
created_at: 2025-05-01T13:40:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,202 @@
+#pragma once
+
+#include <chrono>
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()),
+          time(std::chrono::system_clock::now()) {}
+
+    std::stacktrace stacktrace;
+    std::chrono::time_point<std::chrono::system_clock> time;
+};
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class SentinelValue;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { SentinelValue<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = SentinelValue<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value.
+// floating-point numbers with infinity support have sentinel values equal to infinity.
+template<std::integral T>
+class SentinelValue<T> {
path: include/common/result.hpp
user: login: djava
body: ```suggestion
// partial specialization for HasSentinel.
// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value.
// floating-point numbers with infinity support have sentinel values equal to infinity.
template<typename T>
    requires (std::integral<T> || std::floating_point<T>)
class SentinelValue<T> {
```
created_at: 2025-05-01T13:43:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,6 +64,7 @@ linker_flags = [
 # system libraries we depend on
 system_deps = [
   '-nostartfiles', # we still need to implement some newlib stubs
+  '-lstdc++exp',
path: meson.build
user: login: liam-teale
body: It looks like we can link against libbacktrace too which also solves the problem.

I think we should stick with `std::stacktrace` if it ends up working, or we can get it to work. It's part of the C++23 standard which has been finalized
created_at: 2025-05-01T16:55:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+#pragma once
+
+#include <format>
+#include <iostream>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+
+namespace zest {
+
+template<typename E>
+    requires std::is_scoped_enum_v<E>
+struct ResultError {
+    E type;
+    std::string message;
+    std::stacktrace stacktrace;
+};
+
+/**
+ * @brief Result class, used for error handling.
+ *
+ * The Result class can contain a value and optionally an error. Unlike std::expected, the value is
+ * ALWAYS valid. This means that a failure to handle an error by the user will not crash the thread.
+ *
+ * Instances of this class can be implicitly converted to the "normal" type, so UX is not
+ * complicated at all.
+ *
+ * @tparam T the "normal" value.
+ * @tparam E the error type, has to be an enum class.
+ *
+ * @b Example
+ * @code
+ * enum class ExampleError {
+ *     ExampleA,
+ *     ExampleB,
+ * };
+ *
+ * zest::Result<double, ExampleError> get_double() {
+ *     // for the purposes of this example, this function will always return an error
+ *     return {INFINITY, ExampleError::ExampleA, "I can be formatted{}", "!!!"};
+ * }
+ *
+ * void initialize() {
+ * }
+ *
+ * @endcode
+ */
+template<typename T, typename E>
+    requires std::is_scoped_enum_v<E>
+class Result {
+  public:
+    template<typename U>
+        requires std::constructible_from<T, U>
+    Result(U&& val)
+        : val(std::forward<T>(val)) {}
+
+    Result(Result&& other)
path: include/common/result.hpp
user: login: liam-teale
body: turns out we don't need this at all
created_at: 2025-05-01T22:34:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,146 @@
+#pragma once
+
+#include <concepts>
+#include <limits>
+#include <optional>
+#include <stacktrace>
+#include <type_traits>
+#include <variant>
+
+namespace zest {
+
+// custom error types inherit from the ResultError class, which enforces that custom error types
+// have some shared functionality.
+class ResultError {
+  public:
+    // since this constructor has no arguments, it'll be called implicitly by the constructor of any
+    // child class.
+    ResultError()
+        : stacktrace(std::stacktrace::current()) {}
+
+    std::stacktrace stacktrace;
+};
+
+// concept used to enforce that error types inherit from the ResultError class
+template<typename T>
+concept CustomError = std::derived_from<T, ResultError>;
+
+// Some primitive types such as float, double, etc may have a standardized "sentinel" value.
+// For example, a function that returns a float may return INFINITY if an error occurs.
+// Usually the maximum value of the type is returned if there is an error.
+// This trait helps simplify DX.
+// This trait can also be specialized for custom types (e.g unitized types)
+template<typename T>
+class HasSentinel;
+
+// concept which can be used to determine if a type has a sentinel value
+template<typename T>
+concept Sentinel = requires(const T& val) { HasSentinel<T>::value; };
+
+// templated variable which can be used to simplify usage of HasSentinel
+template<Sentinel T>
+constexpr T sentinel_v = HasSentinel<T>::value;
+
+// partial specialization for HasSentinel.
+// any integral type (e.g double, int, uint8, etc) has a sentinel value equal to its maximum value
+template<std::integral T>
+class HasSentinel<T> {
+    static constexpr T value = std::numeric_limits<T>::max();
+};
+
path: include/common/result.hpp
user: login: liam-teale
body: i didn't use a ternary operator for this because the line would've been really, really long with all the namespace prefixes
created_at: 2025-05-01T22:35:30Z
author_association: MEMBER
diff_hunk: @@ -64,6 +64,7 @@ linker_flags = [
 # system libraries we depend on
 system_deps = [
   '-nostartfiles', # we still need to implement some newlib stubs
+  '-lstdc++exp',
path: meson.build
user: login: liam-teale
body: We're going to stick with `std::stacktrace` as it greatly simplifies the implementation of this feature.
created_at: 2025-05-02T23:22:43Z
author_association: MEMBER

PR comments:
user: login: djava
created_at: 2025-04-29T15:21:36Z
body: BTW I would prefer that if you make a change related to one of my comments, just respond with the hash of the commit that fixes it and don't resolve - i'll either resolve or suggest a different fix

And feel free to argue and stuff if you disagree as well, that's the point of all this
author_association: CONTRIBUTOR
user: login: liam-teale
created_at: 2025-04-30T02:54:21Z
body: > BTW I would prefer that if you make a change related to one of my comments, just respond with the hash of the commit that fixes it and don't resolve - i'll either resolve or suggest a different fix
> 
> And feel free to argue and stuff if you disagree as well, that's the point of all this

this is a much better approach, i'll do this from now on
author_association: MEMBER
user: login: djava
created_at: 2025-04-30T12:37:38Z
body: It turns out I literally cant resolve my own threads? I really hate github for code review - gitlab is much much better for this.

 I guess consider any "LGTM"s to be resolved
author_association: CONTRIBUTOR
user: login: liam-teale
created_at: 2025-05-01T16:51:42Z
body: > I'd like to see some more tests before this merges, I think. In particular, I think that the multiple-errors case is probably where most of the snags lie, and the tests don't currently cover that.

Yea that's next on the to-do list, along with better docs and formatting
author_association: MEMBER
user: login: liam-teale
created_at: 2025-05-02T23:21:33Z
body: I'm marking this as ready for review. I wasn't able to implement *all* the features I've planned to (notably std::format support), but what we have right now is enough to get started with other tasks.
author_association: MEMBER
user: login: liam-teale
created_at: 2025-05-03T22:33:36Z
body: Everyone is busy, all the tests passed, and the design has been debated to death. So this is getting merged
author_association: MEMBER