diff --git a/BUCK b/BUCK index aeeac450..4087a183 100644 --- a/BUCK +++ b/BUCK @@ -20,6 +20,7 @@ TEST_COMPILER_FLAGS = BASE_COMPILER_FLAGS + GMOCK_OVERRIDE_FLAGS + [ yoga_cxx_library( name = "yoga", srcs = glob(["yoga/*.cpp"]), + headers = subdir_glob([("", "yoga/**/*.h")]), header_namespace = "", exported_headers = subdir_glob([("", "yoga/*.h")]), compiler_flags = COMPILER_FLAGS, @@ -34,6 +35,7 @@ yoga_cxx_library( yoga_cxx_test( name = "YogaTests", srcs = glob(["tests/*.cpp"]), + headers = subdir_glob([("", "yoga/**/*.h")]), compiler_flags = TEST_COMPILER_FLAGS, contacts = ["emilsj@fb.com"], visibility = ["PUBLIC"], diff --git a/tests/CompactValueTest.cpp b/tests/CompactValueTest.cpp new file mode 100644 index 00000000..7d2afc13 --- /dev/null +++ b/tests/CompactValueTest.cpp @@ -0,0 +1,349 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#define YOGA_COMPACT_VALUE_TEST + +#include +#include +#include + +using facebook::yoga::detail::CompactValue; + +const auto tooSmall = nextafterf(CompactValue::LOWER_BOUND, -INFINITY); +const auto tooLargePoints = + nextafterf(CompactValue::UPPER_BOUND_POINT, INFINITY); +const auto tooLargePercent = + nextafterf(CompactValue::UPPER_BOUND_PERCENT, INFINITY); + +TEST(YogaTest, compact_value_can_represent_undefined) { + auto c = CompactValue{YGValue{12.5f, YGUnitUndefined}}; + YGValue v = c; + ASSERT_EQ(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{-1.25, YGUnitPoint})); + ASSERT_NE(v, (YGValue{25, YGUnitPercent})); + ASSERT_TRUE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_auto) { + auto c = CompactValue{YGValue{0, YGUnitAuto}}; + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_EQ(v, YGValueAuto); + ASSERT_NE(v, (YGValue{-1.25, YGUnitPoint})); + ASSERT_NE(v, (YGValue{25, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_TRUE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_zero_points) { + auto c = CompactValue{YGValue{0, YGUnitPoint}}; + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{0, YGUnitPoint})); + ASSERT_NE(v, (YGValue{0, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_lower_bound_points) { + auto c = CompactValue({YGValue{CompactValue::LOWER_BOUND, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{CompactValue::LOWER_BOUND, YGUnitPoint})); + ASSERT_NE(v, (YGValue{CompactValue::LOWER_BOUND, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_negative_lower_bound_points) { + auto c = CompactValue({YGValue{-CompactValue::LOWER_BOUND, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{-CompactValue::LOWER_BOUND, YGUnitPoint})); + ASSERT_NE(v, (YGValue{-CompactValue::LOWER_BOUND, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_clamps_smaller_than_lower_bound_points_to_zero) { + auto c = CompactValue({YGValue{tooSmall, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{0, YGUnitPoint})); + ASSERT_NE(v, (YGValue{0, YGUnitPercent})); +} + +TEST( + YogaTest, + compact_value_clamps_greater_than_negative_lower_bound_points_to_zero) { + auto c = CompactValue({YGValue{-tooSmall, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{0, YGUnitPoint})); + ASSERT_NE(v, (YGValue{0, YGUnitPercent})); +} + +TEST(YogaTest, compact_value_can_represent_upper_bound_points) { + auto c = + CompactValue({YGValue{CompactValue::UPPER_BOUND_POINT, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{CompactValue::UPPER_BOUND_POINT, YGUnitPoint})); + ASSERT_NE(v, (YGValue{CompactValue::UPPER_BOUND_POINT, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_negative_upper_bound_points) { + auto c = + CompactValue({YGValue{-CompactValue::UPPER_BOUND_POINT, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{-CompactValue::UPPER_BOUND_POINT, YGUnitPoint})); + ASSERT_NE(v, (YGValue{-CompactValue::UPPER_BOUND_POINT, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST( + YogaTest, + compact_value_clamps_greater_than__upper_bound_points_to_upper_bound) { + auto c = CompactValue({YGValue{tooLargePoints, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{CompactValue::UPPER_BOUND_POINT, YGUnitPoint})); + ASSERT_NE(v, (YGValue{CompactValue::UPPER_BOUND_POINT, YGUnitPercent})); +} + +TEST( + YogaTest, + compact_value_clamps_smaller_than_negative_upper_bound_points_to_upper_bound) { + auto c = CompactValue({YGValue{-tooLargePoints, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{-CompactValue::UPPER_BOUND_POINT, YGUnitPoint})); + ASSERT_NE(v, (YGValue{-CompactValue::UPPER_BOUND_POINT, YGUnitPercent})); +} + +TEST(YogaTest, compact_value_can_represent_one_point) { + auto c = CompactValue({YGValue{1, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{1, YGUnitPoint})); + ASSERT_NE(v, (YGValue{1, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_negative_one_point) { + auto c = CompactValue({YGValue{-1, YGUnitPoint}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_EQ(v, (YGValue{-1, YGUnitPoint})); + ASSERT_NE(v, (YGValue{-1, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_zero_percent) { + auto c = CompactValue{YGValue{0, YGUnitPercent}}; + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{0, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{0, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_lower_bound_percent) { + auto c = CompactValue({YGValue{CompactValue::LOWER_BOUND, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{CompactValue::LOWER_BOUND, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{CompactValue::LOWER_BOUND, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_negative_lower_bound_percent) { + auto c = CompactValue({YGValue{-CompactValue::LOWER_BOUND, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{-CompactValue::LOWER_BOUND, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{-CompactValue::LOWER_BOUND, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_clamps_smaller_than_lower_bound_percent_to_zero) { + auto c = CompactValue({YGValue{tooSmall, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{0, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{0, YGUnitPercent})); +} + +TEST( + YogaTest, + compact_value_clamps_greater_than_negative_lower_bound_percent_to_zero) { + auto c = CompactValue({YGValue{-tooSmall, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{0, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{0, YGUnitPercent})); +} + +TEST(YogaTest, compact_value_can_represent_upper_bound_percent) { + auto c = + CompactValue({YGValue{CompactValue::UPPER_BOUND_PERCENT, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{CompactValue::UPPER_BOUND_PERCENT, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{CompactValue::UPPER_BOUND_PERCENT, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_negative_upper_bound_percent) { + auto c = CompactValue( + {YGValue{-CompactValue::UPPER_BOUND_PERCENT, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{-CompactValue::UPPER_BOUND_PERCENT, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{-CompactValue::UPPER_BOUND_PERCENT, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST( + YogaTest, + compact_value_clamps_greater_than_upper_bound_percent_to_upper_bound) { + auto c = CompactValue({YGValue{tooLargePercent, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{CompactValue::UPPER_BOUND_PERCENT, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{CompactValue::UPPER_BOUND_PERCENT, YGUnitPercent})); +} + +TEST( + YogaTest, + compact_value_clamps_smaller_than_negative_upper_bound_percent_to_upper_bound) { + auto c = CompactValue({YGValue{-tooLargePercent, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{-CompactValue::UPPER_BOUND_PERCENT, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{-CompactValue::UPPER_BOUND_PERCENT, YGUnitPercent})); +} + +TEST(YogaTest, compact_value_can_represent_one_percent) { + auto c = CompactValue({YGValue{1, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{1, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{1, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, compact_value_can_represent_negative_one_percent) { + auto c = CompactValue({YGValue{-1, YGUnitPercent}}); + YGValue v = c; + ASSERT_NE(v, YGValueUndefined); + ASSERT_NE(v, YGValueAuto); + ASSERT_NE(v, (YGValue{-1, YGUnitPoint})); + ASSERT_EQ(v, (YGValue{-1, YGUnitPercent})); + ASSERT_FALSE(c.isUndefined()); + ASSERT_FALSE(c.isAuto()); +} + +TEST(YogaTest, dedicated_unit_factories) { + ASSERT_EQ(CompactValue::ofUndefined(), CompactValue(YGValueUndefined)); + ASSERT_EQ(CompactValue::ofAuto(), CompactValue(YGValueAuto)); + ASSERT_EQ( + CompactValue::of(-9876.5f), + CompactValue(YGValue{-9876.5f, YGUnitPoint})); + ASSERT_EQ( + CompactValue::of(123.456f), + CompactValue(YGValue{123.456f, YGUnitPercent})); +} + +TEST(YogaTest, dedicated_unit_maybe_factories) { + ASSERT_EQ( + CompactValue::ofMaybe(-9876.5f), + CompactValue(YGValue{-9876.5f, YGUnitPoint})); + ASSERT_EQ( + CompactValue::ofMaybe(YGUndefined), + CompactValue(YGValueUndefined)); + ASSERT_EQ( + CompactValue::ofMaybe(123.456f), + CompactValue(YGValue{123.456f, YGUnitPercent})); + ASSERT_EQ( + CompactValue::ofMaybe(YGUndefined), + CompactValue(YGValueUndefined)); +} + +TEST(YogaTest, can_be_assigned_from_YGValue) { + CompactValue c{}; + + YGValue v{2.0f, YGUnitPercent}; + c = v; + ASSERT_EQ((YGValue)c, v); + + c = YGValue{123, YGUnitPoint}; + ASSERT_EQ((YGValue)c, (YGValue{123, YGUnitPoint})); +} + +TEST(YogaTest, compact_value_bound_representations) { + ASSERT_EQ( + CompactValue::of(CompactValue::LOWER_BOUND).repr(), + uint32_t{0}); + ASSERT_EQ( + CompactValue::of(CompactValue::UPPER_BOUND_POINT).repr(), + uint32_t{0x3fffffff}); + ASSERT_EQ( + CompactValue::of(CompactValue::LOWER_BOUND).repr(), + uint32_t{0x40000000}); + ASSERT_EQ( + CompactValue::of(CompactValue::UPPER_BOUND_PERCENT).repr(), + uint32_t{0x7f7fffff}); + + ASSERT_EQ( + CompactValue::of(-CompactValue::LOWER_BOUND).repr(), + uint32_t{0x80000000}); + ASSERT_EQ( + CompactValue::of(-CompactValue::UPPER_BOUND_POINT).repr(), + uint32_t{0xbfffffff}); + ASSERT_EQ( + CompactValue::of(-CompactValue::LOWER_BOUND).repr(), + uint32_t{0xc0000000}); + ASSERT_EQ( + CompactValue::of(-CompactValue::UPPER_BOUND_PERCENT) + .repr(), + uint32_t{0xff7fffff}); +} diff --git a/yoga/CompactValue.h b/yoga/CompactValue.h new file mode 100644 index 00000000..1ca9d619 --- /dev/null +++ b/yoga/CompactValue.h @@ -0,0 +1,182 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#pragma once + +#include + +#include +#include +#include + +static_assert( + std::numeric_limits::is_iec559, + "facebook::yoga::detail::CompactValue only works with IEEE754 floats"); + +#ifdef YOGA_COMPACT_VALUE_TEST +#define VISIBLE_FOR_TESTING public: +#else +#define VISIBLE_FOR_TESTING private: +#endif + +namespace facebook { +namespace yoga { +namespace detail { + +// This class stores YGValue in 32 bits. +// - The value does not matter for Undefined and Auto. NaNs are used for their +// representation. +// - To differentiate between Point and Percent, one exponent bit is used. +// Supported the range [0x40, 0xbf] (0xbf is inclusive for point, but +// exclusive for percent). +// - Value ranges: +// points: 1.08420217e-19f to 36893485948395847680 +// 0x00000000 0x3fffffff +// percent: 1.08420217e-19f to 18446742974197923840 +// 0x40000000 0x7f7fffff +// - Zero is supported, negative zero is not +// - values outside of the representable range are clamped +class CompactValue { + friend constexpr bool operator==(CompactValue, CompactValue) noexcept; + + public: + static constexpr auto LOWER_BOUND = 1.08420217e-19f; + static constexpr auto UPPER_BOUND_POINT = 36893485948395847680.0f; + static constexpr auto UPPER_BOUND_PERCENT = 18446742974197923840.0f; + + template + static CompactValue of(float value) noexcept { + if (value == 0.0f || (value < LOWER_BOUND && value > -LOWER_BOUND)) { + constexpr auto zero = + Unit == YGUnitPercent ? ZERO_BITS_PERCENT : ZERO_BITS_POINT; + return {Payload{zero}}; + } + + constexpr auto upperBound = + Unit == YGUnitPercent ? UPPER_BOUND_PERCENT : UPPER_BOUND_POINT; + if (value > upperBound || value < -upperBound) { + value = copysignf(upperBound, value); + } + + uint32_t unitBit = Unit == YGUnitPercent ? PERCENT_BIT : 0; + auto data = Payload{value}; + data.repr -= BIAS; + data.repr |= unitBit; + return {data}; + } + + template + static CompactValue ofMaybe(float value) noexcept { + return std::isnan(value) ? ofUndefined() : of(value); + } + + static constexpr CompactValue ofUndefined() noexcept { + return CompactValue{}; + } + + static constexpr CompactValue ofAuto() noexcept { + return CompactValue{Payload{AUTO_BITS}}; + } + + constexpr CompactValue() noexcept + : payload_(std::numeric_limits::quiet_NaN()) {} + + CompactValue(const YGValue& x) noexcept : payload_(uint32_t{0}) { + switch (x.unit) { + case YGUnitUndefined: + *this = ofUndefined(); + break; + case YGUnitAuto: + *this = ofAuto(); + break; + case YGUnitPoint: + *this = of(x.value); + break; + case YGUnitPercent: + *this = of(x.value); + break; + } + } + + operator YGValue() const noexcept { + switch (payload_.repr) { + case AUTO_BITS: + return YGValueAuto; + case ZERO_BITS_POINT: + return YGValue{0.0f, YGUnitPoint}; + case ZERO_BITS_PERCENT: + return YGValue{0.0f, YGUnitPercent}; + } + + if (std::isnan(payload_.value)) { + return YGValueUndefined; + } + + auto data = payload_; + data.repr &= ~PERCENT_BIT; + data.repr += BIAS; + + return YGValue{data.value, + payload_.repr & 0x40000000 ? YGUnitPercent : YGUnitPoint}; + } + + bool isUndefined() const noexcept { + return ( + payload_.repr != AUTO_BITS && payload_.repr != ZERO_BITS_POINT && + payload_.repr != ZERO_BITS_PERCENT && std::isnan(payload_.value)); + } + + bool isAuto() const noexcept { + return payload_.repr == AUTO_BITS; + } + + private: + union Payload { + float value; + uint32_t repr; + Payload() = delete; + constexpr Payload(uint32_t r) : repr(r) {} + constexpr Payload(float v) : value(v) {} + }; + + static constexpr uint32_t BIAS = 0x20000000; + static constexpr uint32_t PERCENT_BIT = 0x40000000; + + // these are signaling NaNs with specific bit pattern as payload + // they will be silenced whenever going through an FPU operation on ARM + x86 + static constexpr uint32_t AUTO_BITS = 0x7faaaaaa; + static constexpr uint32_t ZERO_BITS_POINT = 0x7f8f0f0f; + static constexpr uint32_t ZERO_BITS_PERCENT = 0x7f80f0f0; + + constexpr CompactValue(Payload data) noexcept : payload_(data) {} + + Payload payload_; + + VISIBLE_FOR_TESTING uint32_t repr() { + return payload_.repr; + } +}; + +template <> +CompactValue CompactValue::of(float) noexcept = delete; +template <> +CompactValue CompactValue::of(float) noexcept = delete; +template <> +CompactValue CompactValue::ofMaybe(float) noexcept = delete; +template <> +CompactValue CompactValue::ofMaybe(float) noexcept = delete; + +constexpr bool operator==(CompactValue a, CompactValue b) noexcept { + return a.payload_.repr == b.payload_.repr; +} + +constexpr bool operator!=(CompactValue a, CompactValue b) noexcept { + return !(a == b); +} + +} // namespace detail +} // namespace yoga +} // namespace facebook