diff options
author | Martok <martok@martoks-place.de> | 2023-06-17 16:05:46 +0200 |
---|---|---|
committer | Martok <martok@martoks-place.de> | 2023-06-29 22:27:28 +0200 |
commit | bed754c21c712013b425417a9be3b18125695252 (patch) | |
tree | bd71d33ac3448298b5f7ec7c840c7981292cfe02 /mfbt | |
parent | 85efbc0c1e4d62098b385ffa141add07b811429d (diff) | |
download | uxp-bed754c21c712013b425417a9be3b18125695252.tar.gz |
Issue #2259 - Add mozilla::Result<V, E> and JS::Result<> for fallible return values
Based-on: m-c 1283562, 1277368/1, 1324828
Diffstat (limited to 'mfbt')
-rw-r--r-- | mfbt/Assertions.h | 14 | ||||
-rw-r--r-- | mfbt/Result.h | 313 | ||||
-rw-r--r-- | mfbt/moz.build | 1 | ||||
-rw-r--r-- | mfbt/tests/TestResult.cpp | 213 | ||||
-rw-r--r-- | mfbt/tests/moz.build | 1 |
5 files changed, 542 insertions, 0 deletions
diff --git a/mfbt/Assertions.h b/mfbt/Assertions.h index cc4735071d..df41795c12 100644 --- a/mfbt/Assertions.h +++ b/mfbt/Assertions.h @@ -597,6 +597,8 @@ struct AssertionConditionType /* Do nothing. */ \ } \ } while (0) +# define MOZ_ALWAYS_OK(expr) MOZ_ASSERT((expr).isOk()) +# define MOZ_ALWAYS_ERR(expr) MOZ_ASSERT((expr).isErr()) #else # define MOZ_ALWAYS_TRUE(expr) \ do { \ @@ -610,6 +612,18 @@ struct AssertionConditionType /* Silence MOZ_MUST_USE. */ \ } \ } while (0) +# define MOZ_ALWAYS_OK(expr) \ + do { \ + if ((expr).isOk()) { \ + /* Silence MOZ_MUST_USE. */ \ + } \ + } while (0) +# define MOZ_ALWAYS_ERR(expr) \ + do { \ + if ((expr).isErr()) { \ + /* Silence MOZ_MUST_USE. */ \ + } \ + } while (0) #endif #undef MOZ_DUMP_ASSERTION_STACK diff --git a/mfbt/Result.h b/mfbt/Result.h new file mode 100644 index 0000000000..85063c8815 --- /dev/null +++ b/mfbt/Result.h @@ -0,0 +1,313 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* A type suitable for returning either a value or an error from a function. */ + +#ifndef mozilla_Result_h +#define mozilla_Result_h + +#include "mozilla/Alignment.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Types.h" +#include "mozilla/TypeTraits.h" +#include "mozilla/Variant.h" + +namespace mozilla { + +/** + * Empty struct, indicating success for operations that have no return value. + * For example, if you declare another empty struct `struct OutOfMemory {};`, + * then `Result<Ok, OutOfMemory>` represents either success or OOM. + */ +struct Ok {}; + +template <typename E> class GenericErrorResult; + +namespace detail { + +enum class VEmptiness { IsEmpty, IsNotEmpty }; +enum class Alignedness { IsAligned, IsNotAligned }; + +template <typename V, typename E, VEmptiness EmptinessOfV, Alignedness Aligned> +class ResultImplementation +{ + mozilla::Variant<V, E> mStorage; + +public: + explicit ResultImplementation(V aValue) : mStorage(aValue) {} + explicit ResultImplementation(E aErrorValue) : mStorage(aErrorValue) {} + + bool isOk() const { return mStorage.template is<V>(); } + + // The callers of these functions will assert isOk() has the proper value, so + // these functions (in all ResultImplementation specializations) don't need + // to do so. + V unwrap() const { return mStorage.template as<V>(); } + E unwrapErr() const { return mStorage.template as<E>(); } +}; + +/** + * mozilla::Variant doesn't like storing a reference. This is a specialization + * to store E as pointer if it's a reference. + */ +template <typename V, typename E, VEmptiness EmptinessOfV, Alignedness Aligned> +class ResultImplementation<V, E&, EmptinessOfV, Aligned> +{ + mozilla::Variant<V, E*> mStorage; + +public: + explicit ResultImplementation(V aValue) : mStorage(aValue) {} + explicit ResultImplementation(E& aErrorValue) : mStorage(&aErrorValue) {} + + bool isOk() const { return mStorage.template is<V>(); } + V unwrap() const { return mStorage.template as<V>(); } + E& unwrapErr() const { return *mStorage.template as<E*>(); } +}; + +/** + * Specialization for when the success type is Ok (or another empty class) and + * the error type is a reference. + */ +template <typename V, typename E, Alignedness Aligned> +class ResultImplementation<V, E&, VEmptiness::IsEmpty, Aligned> +{ + E* mErrorValue; + +public: + explicit ResultImplementation(V) : mErrorValue(nullptr) {} + explicit ResultImplementation(E& aErrorValue) : mErrorValue(&aErrorValue) {} + + bool isOk() const { return mErrorValue == nullptr; } + + V unwrap() const { return V(); } + E& unwrapErr() const { return *mErrorValue; } +}; + +/** + * Specialization for when alignment permits using the least significant bit as + * a tag bit. + */ +template <typename V, typename E, VEmptiness EmptinessOfV> +class ResultImplementation<V*, E&, EmptinessOfV, Alignedness::IsAligned> +{ + uintptr_t mBits; + +public: + explicit ResultImplementation(V* aValue) + : mBits(reinterpret_cast<uintptr_t>(aValue)) + { + MOZ_ASSERT((uintptr_t(aValue) % MOZ_ALIGNOF(V)) == 0, + "Result value pointers must not be misaligned"); + } + explicit ResultImplementation(E& aErrorValue) + : mBits(reinterpret_cast<uintptr_t>(&aErrorValue) | 1) + { + MOZ_ASSERT((uintptr_t(&aErrorValue) % MOZ_ALIGNOF(E)) == 0, + "Result errors must not be misaligned"); + } + + bool isOk() const { return (mBits & 1) == 0; } + + V* unwrap() const { return reinterpret_cast<V*>(mBits); } + E& unwrapErr() const { return *reinterpret_cast<E*>(mBits & ~uintptr_t(1)); } +}; + +// A bit of help figuring out which of the above specializations to use. +// +// We begin by safely assuming types don't have a spare bit. +template <typename T> struct HasFreeLSB { static const bool value = false; }; + +// The lowest bit of a properly-aligned pointer is always zero if the pointee +// type is greater than byte-aligned. That bit is free to use if it's masked +// out of such pointers before they're dereferenced. +template <typename T> struct HasFreeLSB<T*> { + static const bool value = (MOZ_ALIGNOF(T) & 1) == 0; +}; + +// We store references as pointers, so they have a free bit if a pointer would +// have one. +template <typename T> struct HasFreeLSB<T&> { + static const bool value = HasFreeLSB<T*>::value; +}; + +} // namespace detail + +/** + * Result<V, E> represents the outcome of an operation that can either succeed + * or fail. It contains either a success value of type V or an error value of + * type E. + * + * All Result methods are const, so results are basically immutable. + * This is just like Variant<V, E> but with a slightly different API, and the + * following cases are optimized so Result can be stored more efficiently: + * + * - If the success type is Ok (or another empty class) and the error type is a + * reference, Result<V, E&> is guaranteed to be pointer-sized and all zero + * bits on success. Do not change this representation! There is JIT code that + * depends on it. + * + * - If the success type is a pointer type and the error type is a reference + * type, and the least significant bit is unused for both types when stored + * as a pointer (due to alignment rules), Result<V*, E&> is guaranteed to be + * pointer-sized. In this case, we use the lowest bit as tag bit: 0 to + * indicate the Result's bits are a V, 1 to indicate the Result's bits (with + * the 1 masked out) encode an E*. + * + * The purpose of Result is to reduce the screwups caused by using `false` or + * `nullptr` to indicate errors. + * What screwups? See <https://bugzilla.mozilla.org/show_bug.cgi?id=912928> for + * a partial list. + */ +template <typename V, typename E> +class MOZ_MUST_USE_TYPE Result final +{ + using Impl = + detail::ResultImplementation<V, E, + IsEmpty<V>::value + ? detail::VEmptiness::IsEmpty + : detail::VEmptiness::IsNotEmpty, + (detail::HasFreeLSB<V>::value && + detail::HasFreeLSB<E>::value) + ? detail::Alignedness::IsAligned + : detail::Alignedness::IsNotAligned>; + Impl mImpl; + +public: + /** + * Create a success result. + */ + MOZ_IMPLICIT Result(V aValue) : mImpl(aValue) { MOZ_ASSERT(isOk()); } + + /** + * Create an error result. + */ + explicit Result(E aErrorValue) : mImpl(aErrorValue) { MOZ_ASSERT(isErr()); } + + /** + * Implementation detail of MOZ_TRY(). + * Create an error result from another error result. + */ + template <typename E2> + MOZ_IMPLICIT Result(const GenericErrorResult<E2>& aErrorResult) + : mImpl(aErrorResult.mErrorValue) + { + static_assert(mozilla::IsConvertible<E2, E>::value, + "E2 must be convertible to E"); + MOZ_ASSERT(isErr()); + } + + Result(const Result&) = default; + Result& operator=(const Result&) = default; + + /** True if this Result is a success result. */ + bool isOk() const { return mImpl.isOk(); } + + /** True if this Result is an error result. */ + bool isErr() const { return !mImpl.isOk(); } + + /** Get the success value from this Result, which must be a success result. */ + V unwrap() const { + MOZ_ASSERT(isOk()); + return mImpl.unwrap(); + } + + /** Get the error value from this Result, which must be an error result. */ + E unwrapErr() const { + MOZ_ASSERT(isErr()); + return mImpl.unwrapErr(); + } + + /** + * Map a function V -> W over this result's success variant. If this result is + * an error, do not invoke the function and return a copy of the error. + * + * Mapping over success values invokes the function to produce a new success + * value: + * + * // Map Result<int, E> to another Result<int, E> + * Result<int, E> res(5); + * Result<int, E> res2 = res.map([](int x) { return x * x; }); + * MOZ_ASSERT(res2.unwrap() == 25); + * + * // Map Result<const char*, E> to Result<size_t, E> + * Result<const char*, E> res("hello, map!"); + * Result<size_t, E> res2 = res.map(strlen); + * MOZ_ASSERT(res2.unwrap() == 11); + * + * Mapping over an error does not invoke the function and copies the error: + * + * Result<V, int> res(5); + * MOZ_ASSERT(res.isErr()); + * Result<W, int> res2 = res.map([](V v) { ... }); + * MOZ_ASSERT(res2.isErr()); + * MOZ_ASSERT(res2.unwrapErr() == 5); + */ + template<typename F> + auto map(F f) const -> Result<decltype(f(*((V*) nullptr))), E> { + using RetResult = Result<decltype(f(*((V*) nullptr))), E>; + return isOk() ? RetResult(f(unwrap())) : RetResult(unwrapErr()); + } +}; + +/** + * A type that auto-converts to an error Result. This is like a Result without + * a success type. It's the best return type for functions that always return + * an error--functions designed to build and populate error objects. It's also + * useful in error-handling macros; see MOZ_TRY for an example. + */ +template <typename E> +class MOZ_MUST_USE_TYPE GenericErrorResult +{ + E mErrorValue; + + template<typename V, typename E2> friend class Result; + +public: + explicit GenericErrorResult(E aErrorValue) : mErrorValue(aErrorValue) {} +}; + +template <typename E> +inline GenericErrorResult<E> +MakeGenericErrorResult(E&& aErrorValue) +{ + return GenericErrorResult<E>(aErrorValue); +} + +} // namespace mozilla + +/** + * MOZ_TRY(expr) is the C++ equivalent of Rust's `try!(expr);`. First, it + * evaluates expr, which must produce a Result value. On success, it + * discards the result altogether. On error, it immediately returns an error + * Result from the enclosing function. + */ +#define MOZ_TRY(expr) \ + do { \ + auto mozTryTempResult_ = (expr); \ + if (mozTryTempResult_.isErr()) { \ + return ::mozilla::MakeGenericErrorResult(mozTryTempResult_.unwrapErr()); \ + } \ + } while (0) + +/** + * MOZ_TRY_VAR(target, expr) is the C++ equivalent of Rust's `target = try!(expr);`. + * First, it evaluates expr, which must produce a Result value. + * On success, the result's success value is assigned to target. + * On error, immediately returns the error result. + * |target| must evaluate to a reference without any side effects. + */ +#define MOZ_TRY_VAR(target, expr) \ + do { \ + auto mozTryVarTempResult_ = (expr); \ + if (mozTryVarTempResult_.isErr()) { \ + return ::mozilla::MakeGenericErrorResult( \ + mozTryVarTempResult_.unwrapErr()); \ + } \ + (target) = mozTryVarTempResult_.unwrap(); \ + } while (0) + +#endif // mozilla_Result_h diff --git a/mfbt/moz.build b/mfbt/moz.build index 4c2f903fb9..20c7234dd3 100644 --- a/mfbt/moz.build +++ b/mfbt/moz.build @@ -73,6 +73,7 @@ EXPORTS.mozilla = [ 'RefCounted.h', 'RefCountType.h', 'RefPtr.h', + 'Result.h', 'ReverseIterator.h', 'RollingMean.h', 'Saturate.h', diff --git a/mfbt/tests/TestResult.cpp b/mfbt/tests/TestResult.cpp new file mode 100644 index 0000000000..da269a9e8e --- /dev/null +++ b/mfbt/tests/TestResult.cpp @@ -0,0 +1,213 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <string.h> +#include "mozilla/Result.h" + +using mozilla::GenericErrorResult; +using mozilla::MakeGenericErrorResult; +using mozilla::Ok; +using mozilla::Result; + +struct Failed +{ + int x; +}; + +static_assert(sizeof(Result<Ok, Failed&>) == sizeof(uintptr_t), + "Result with empty value type should be pointer-sized"); +static_assert(sizeof(Result<int*, Failed&>) == sizeof(uintptr_t), + "Result with two aligned pointer types should be pointer-sized"); +static_assert(sizeof(Result<char*, Failed*>) > sizeof(char*), + "Result with unaligned success type `char*` must not be pointer-sized"); +static_assert(sizeof(Result<int*, char*>) > sizeof(char*), + "Result with unaligned error type `char*` must not be pointer-sized"); + +static GenericErrorResult<Failed&> +Fail() +{ + static Failed failed; + return MakeGenericErrorResult<Failed&>(failed); +} + +static Result<Ok, Failed&> +Task1(bool pass) +{ + if (!pass) { + return Fail(); // implicit conversion from GenericErrorResult to Result + } + return Ok(); +} + +static Result<int, Failed&> +Task2(bool pass, int value) +{ + MOZ_TRY(Task1(pass)); // converts one type of result to another in the error case + return value; // implicit conversion from T to Result<T, E> +} + +static Result<int, Failed&> +Task3(bool pass1, bool pass2, int value) +{ + int x, y; + MOZ_TRY_VAR(x, Task2(pass1, value)); + MOZ_TRY_VAR(y, Task2(pass2, value)); + return x + y; +} + +static void +BasicTests() +{ + MOZ_RELEASE_ASSERT(Task1(true).isOk()); + MOZ_RELEASE_ASSERT(!Task1(true).isErr()); + MOZ_RELEASE_ASSERT(!Task1(false).isOk()); + MOZ_RELEASE_ASSERT(Task1(false).isErr()); + + // MOZ_TRY works. + MOZ_RELEASE_ASSERT(Task2(true, 3).isOk()); + MOZ_RELEASE_ASSERT(Task2(true, 3).unwrap() == 3); + MOZ_RELEASE_ASSERT(Task2(false, 3).isErr()); + + // MOZ_TRY_VAR works. + MOZ_RELEASE_ASSERT(Task3(true, true, 3).isOk()); + MOZ_RELEASE_ASSERT(Task3(true, true, 3).unwrap() == 6); + MOZ_RELEASE_ASSERT(Task3(true, false, 3).isErr()); + MOZ_RELEASE_ASSERT(Task3(false, true, 3).isErr()); + + // Lvalues should work too. + { + Result<Ok, Failed&> res = Task1(true); + MOZ_RELEASE_ASSERT(res.isOk()); + MOZ_RELEASE_ASSERT(!res.isErr()); + + res = Task1(false); + MOZ_RELEASE_ASSERT(!res.isOk()); + MOZ_RELEASE_ASSERT(res.isErr()); + } + + { + Result<int, Failed&> res = Task2(true, 3); + MOZ_RELEASE_ASSERT(res.isOk()); + MOZ_RELEASE_ASSERT(res.unwrap() == 3); + + res = Task2(false, 4); + MOZ_RELEASE_ASSERT(res.isErr()); + } + + // Some tests for pointer tagging. + { + int i = 123; + double d = 3.14; + + Result<int*, double&> res = &i; + static_assert(sizeof(res) == sizeof(uintptr_t), + "should use pointer tagging to fit in a word"); + + MOZ_RELEASE_ASSERT(res.isOk()); + MOZ_RELEASE_ASSERT(*res.unwrap() == 123); + + res = MakeGenericErrorResult(d); + MOZ_RELEASE_ASSERT(res.isErr()); + MOZ_RELEASE_ASSERT(&res.unwrapErr() == &d); + MOZ_RELEASE_ASSERT(res.unwrapErr() == 3.14); + } +} + + +/* * */ + +struct Snafu : Failed {}; + +static Result<Ok, Snafu*> +Explode() +{ + static Snafu snafu; + return MakeGenericErrorResult(&snafu); +} + +static Result<Ok, Failed*> +ErrorGeneralization() +{ + MOZ_TRY(Explode()); // change error type from Snafu* to more general Failed* + return Ok(); +} + +static void +TypeConversionTests() +{ + MOZ_RELEASE_ASSERT(ErrorGeneralization().isErr()); +} + +static void +EmptyValueTest() +{ + struct Fine {}; + mozilla::Result<Fine, int&> res((Fine())); + res.unwrap(); + MOZ_RELEASE_ASSERT(res.isOk()); + static_assert(sizeof(res) == sizeof(uintptr_t), + "Result with empty value type should be pointer-sized"); +} + +static void +ReferenceTest() +{ + struct MyError { int x = 0; }; + MyError merror; + Result<int, MyError&> res(merror); + MOZ_RELEASE_ASSERT(&res.unwrapErr() == &merror); +} + +static void +MapTest() +{ + struct MyError { + int x; + + explicit MyError(int y) : x(y) { } + }; + + // Mapping over success values. + Result<int, MyError> res(5); + bool invoked = false; + auto res2 = res.map([&invoked](int x) { + MOZ_RELEASE_ASSERT(x == 5); + invoked = true; + return "hello"; + }); + MOZ_RELEASE_ASSERT(res2.isOk()); + MOZ_RELEASE_ASSERT(invoked); + MOZ_RELEASE_ASSERT(strcmp(res2.unwrap(), "hello") == 0); + + // Mapping over error values. + MyError err(1); + Result<char, MyError> res3(err); + MOZ_RELEASE_ASSERT(res3.isErr()); + Result<char, MyError> res4 = res3.map([](int x) { + MOZ_RELEASE_ASSERT(false); + return 'a'; + }); + MOZ_RELEASE_ASSERT(res4.isErr()); + MOZ_RELEASE_ASSERT(res4.unwrapErr().x == err.x); + + // Function pointers instead of lamdbas as the mapping function. + Result<const char*, MyError> res5("hello"); + auto res6 = res5.map(strlen); + MOZ_RELEASE_ASSERT(res6.isOk()); + MOZ_RELEASE_ASSERT(res6.unwrap() == 5); +} + +/* * */ + +int main() +{ + BasicTests(); + TypeConversionTests(); + EmptyValueTest(); + ReferenceTest(); + MapTest(); + return 0; +} diff --git a/mfbt/tests/moz.build b/mfbt/tests/moz.build index 491e4f3396..1e1ac6975f 100644 --- a/mfbt/tests/moz.build +++ b/mfbt/tests/moz.build @@ -39,6 +39,7 @@ CppUnitTests([ 'TestPair', 'TestRange', 'TestRefPtr', + 'TestResult', 'TestRollingMean', 'TestSaturate', 'TestScopeExit', |