diff --git a/mfbt/Assertions.h b/mfbt/Assertions.h index e978af3d6359..ae873c42ae7c 100644 --- a/mfbt/Assertions.h +++ b/mfbt/Assertions.h @@ -564,6 +564,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 { \ @@ -577,6 +579,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 000000000000..93abd2490b00 --- /dev/null +++ b/mfbt/Result.h @@ -0,0 +1,282 @@ +/* -*- 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` represents either success or OOM. + */ +struct Ok {}; + +template class GenericErrorResult; + +namespace detail { + +enum class VEmptiness { IsEmpty, IsNotEmpty }; +enum class Alignedness { IsAligned, IsNotAligned }; + +template +class ResultImplementation +{ + mozilla::Variant mStorage; + +public: + explicit ResultImplementation(V aValue) : mStorage(aValue) {} + explicit ResultImplementation(E aErrorValue) : mStorage(aErrorValue) {} + + bool isOk() const { return mStorage.template is(); } + + // 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(); } + E unwrapErr() const { return mStorage.template as(); } +}; + +/** + * mozilla::Variant doesn't like storing a reference. This is a specialization + * to store E as pointer if it's a reference. + */ +template +class ResultImplementation +{ + mozilla::Variant mStorage; + +public: + explicit ResultImplementation(V aValue) : mStorage(aValue) {} + explicit ResultImplementation(E& aErrorValue) : mStorage(&aErrorValue) {} + + bool isOk() const { return mStorage.template is(); } + V unwrap() const { return mStorage.template as(); } + E& unwrapErr() const { return *mStorage.template as(); } +}; + +/** + * Specialization for when the success type is Ok (or another empty class) and + * the error type is a reference. + */ +template +class ResultImplementation +{ + 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 +class ResultImplementation +{ + uintptr_t mBits; + +public: + explicit ResultImplementation(V* aValue) + : mBits(reinterpret_cast(aValue)) + { + MOZ_ASSERT((uintptr_t(aValue) % MOZ_ALIGNOF(V)) == 0, + "Result value pointers must not be misaligned"); + } + explicit ResultImplementation(E& aErrorValue) + : mBits(reinterpret_cast(&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(mBits); } + E& unwrapErr() const { return *reinterpret_cast(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 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 struct HasFreeLSB { + 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 struct HasFreeLSB { + static const bool value = HasFreeLSB::value; +}; + +} // namespace detail + +/** + * Result 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 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 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 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 for + * a partial list. + */ +template +class MOZ_MUST_USE_TYPE Result final +{ + using Impl = + detail::ResultImplementation::value + ? detail::VEmptiness::IsEmpty + : detail::VEmptiness::IsNotEmpty, + (detail::HasFreeLSB::value && + detail::HasFreeLSB::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 + MOZ_IMPLICIT Result(const GenericErrorResult& aErrorResult) + : mImpl(aErrorResult.mErrorValue) + { + static_assert(mozilla::IsConvertible::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(); + } +}; + +/** + * 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 +class MOZ_MUST_USE_TYPE GenericErrorResult +{ + E mErrorValue; + + template friend class Result; + +public: + explicit GenericErrorResult(E aErrorValue) : mErrorValue(aErrorValue) {} +}; + +template +inline GenericErrorResult +MakeGenericErrorResult(E&& aErrorValue) +{ + return GenericErrorResult(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 f23a3b6f5d86..428b531dd4a5 100644 --- a/mfbt/moz.build +++ b/mfbt/moz.build @@ -74,6 +74,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 000000000000..1e133edeb872 --- /dev/null +++ b/mfbt/tests/TestResult.cpp @@ -0,0 +1,172 @@ +/* -*- 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 "mozilla/Result.h" + +using mozilla::GenericErrorResult; +using mozilla::MakeGenericErrorResult; +using mozilla::Ok; +using mozilla::Result; + +struct Failed +{ + int x; +}; + +static_assert(sizeof(Result) == sizeof(uintptr_t), + "Result with empty value type should be pointer-sized"); +static_assert(sizeof(Result) == sizeof(uintptr_t), + "Result with two aligned pointer types should be pointer-sized"); +static_assert(sizeof(Result) > sizeof(char*), + "Result with unaligned success type `char*` must not be pointer-sized"); +static_assert(sizeof(Result) > sizeof(char*), + "Result with unaligned error type `char*` must not be pointer-sized"); + +static GenericErrorResult +Fail() +{ + static Failed failed; + return MakeGenericErrorResult(failed); +} + +static Result +Task1(bool pass) +{ + if (!pass) { + return Fail(); // implicit conversion from GenericErrorResult to Result + } + return Ok(); +} + +static Result +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 +} + +static Result +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 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 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 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 +Explode() +{ + static Snafu snafu; + return MakeGenericErrorResult(&snafu); +} + +static Result +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 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 res(merror); + MOZ_RELEASE_ASSERT(&res.unwrapErr() == &merror); +} + +/* * */ + +int main() +{ + BasicTests(); + TypeConversionTests(); + EmptyValueTest(); + ReferenceTest(); + return 0; +} diff --git a/mfbt/tests/moz.build b/mfbt/tests/moz.build index f96117e038d9..3750a55bfa29 100644 --- a/mfbt/tests/moz.build +++ b/mfbt/tests/moz.build @@ -35,6 +35,7 @@ CppUnitTests([ 'TestPair', 'TestRange', 'TestRefPtr', + 'TestResult', 'TestRollingMean', 'TestSaturate', 'TestScopeExit',