From 732fe6295a59eab1bedf918c3c919a7286cc9d88 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 05:34:40 -0400 Subject: [PATCH] Units are constexpr, convert by one compile-time map, and reject mixed dimensions by concept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every LLUnit and LLUnitImplicit operation is constexpr, so a conversion, or a constant built from units, folds at compile time. A unit's conversion from its base unit is an ALUnitConversion, (x * m + c) / d, built from the text the macros already took (ALUnitConversion() * 9 / 5 + 32). Between two units it is one such map through their root unit, kept in lowest terms and, for an integer, worked in 64 bits with one division. That replaces the recursive ll_convert_units chain, the two overloads each macro generated and the linear-ops helpers, and fixes what the chain got wrong: a second offset went in without the first step's divisor (212 F came to 130.35 K), a divisor wider than 32 bits overflowed (U32_MAX microseconds came to 8 days), and a multiplication after a division dropped the divisor (/ 4 * 2 doubled where it should halve). A floating point conversion across levels now multiplies once (3600 for hours to seconds) where it multiplied per level. Operators between units take any mix of LLUnit and LLUnitImplicit and require ALCompatibleUnits, units of one root. What does not mix is a deleted overload rather than one left out, since a unit converts to its storage type and an operator left out would fall to float arithmetic: units of different dimensions, an explicit unit plus or minus a plain value, a product of units, and *= or /= by a unit. The static-assert overloads go with it, and each rejection can now be asked of a concept. Adding an explicit unit to an implicit one no longer truncates it into the implicit one's storage first. Comparisons convert the right operand into the left one's units, with != rewritten from ==. The relational operators stay written out: MSVC compiles a floating point <=> followed by < 0 into a chain of branches. LLUnitImplicit != LLUnit and the valueInUnits(v) setter, which was const, now compile, and compound assignments return the unit. fmt::formatter formats a unit's value by its storage type's spec, then its label. It lives in llunittype.h, on fmt/base.h, because fmt 12 refuses a type with only an implicit conversion, so a formatter in another header could differ between translation units. The Microseconds label was a tab and " --- indra/llcommon/CMakeLists.txt | 2 + indra/llcommon/alunitconversion.h | 156 +++ indra/llcommon/llunits.h | 2 +- indra/llcommon/llunittype.h | 938 +++++++----------- .../llcommon/tests/alunitconversion_test.cpp | 123 +++ indra/llcommon/tests/llunits_test.cpp | 251 +++++ 6 files changed, 918 insertions(+), 554 deletions(-) create mode 100644 indra/llcommon/alunitconversion.h create mode 100644 indra/llcommon/tests/alunitconversion_test.cpp diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 1ec792fb399..b963bfb69e3 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -135,6 +135,7 @@ target_sources( alsavehistory.h alscratcharena.h alsimd.h + alunitconversion.h alwatchedfile.h always_return.h apply.h @@ -317,6 +318,7 @@ if(AL_BUILD_TESTS) tests/alscratcharena_test.cpp tests/alserialworker_test.cpp tests/alsimd_test.cpp + tests/alunitconversion_test.cpp tests/alwatchedfile_test.cpp tests/apply_test.cpp tests/bitpack_test.cpp diff --git a/indra/llcommon/alunitconversion.h b/indra/llcommon/alunitconversion.h new file mode 100644 index 00000000000..a37ebc403f6 --- /dev/null +++ b/indra/llcommon/alunitconversion.h @@ -0,0 +1,156 @@ +/** + * @file alunitconversion.h + * @brief A conversion between units as one affine map, worked out at compile time. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include +#include +#include + +/// A conversion between units: x -> (x * mMultiplier + mOffset) / mDivisor. +/// +/// Built by applying * / + - to ALUnitConversion(), left to right, as they would apply to a value: Fahrenheit from +/// Celsius is ALUnitConversion() * 9 / 5 + 32. While every factor is an integer the terms are integers, kept in +/// lowest terms, so that an integer conversion is exact and divides once; a floating point factor makes them doubles. +template +struct ALUnitConversion +{ + T mMultiplier = 1; + T mOffset = 0; + T mDivisor = 1; +}; + +/// The type of a conversion's terms once a term of type T meets one of type U. +template +using ALUnitConversionTerm = std::conditional_t || std::is_floating_point_v, double, std::intmax_t>; + +template +constexpr ALUnitConversion al_unit_conversion_cast(ALUnitConversion conversion) +{ + return { (TO)conversion.mMultiplier, (TO)conversion.mOffset, (TO)conversion.mDivisor }; +} + +/// conversion with its integer terms divided by their greatest common divisor, and its divisor positive. +template +constexpr ALUnitConversion al_lowest_terms(ALUnitConversion conversion) +{ + if constexpr (std::is_integral_v) + { + if (conversion.mDivisor < 0) + { + conversion = { -conversion.mMultiplier, -conversion.mOffset, -conversion.mDivisor }; + } + const T divisor = std::gcd(std::gcd(conversion.mMultiplier, conversion.mOffset), conversion.mDivisor); + if (divisor > 1) + { + conversion = { conversion.mMultiplier / divisor, conversion.mOffset / divisor, conversion.mDivisor / divisor }; + } + } + return conversion; +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator * (ALUnitConversion conversion, FACTOR factor) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mMultiplier *= factor; + result.mOffset *= factor; + return al_lowest_terms(result); +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator / (ALUnitConversion conversion, FACTOR factor) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mDivisor *= factor; + return al_lowest_terms(result); +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator + (ALUnitConversion conversion, FACTOR offset) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mOffset += offset * result.mDivisor; + return al_lowest_terms(result); +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator - (ALUnitConversion conversion, FACTOR offset) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mOffset -= offset * result.mDivisor; + return al_lowest_terms(result); +} + +/// The conversion applying first, then second. +template +constexpr ALUnitConversion> al_compose_unit_conversions(ALUnitConversion first, ALUnitConversion second) +{ + // second((x * m1 + c1) / d1) = (x * m1 * m2 + c1 * m2 + c2 * d1) / (d1 * d2) + const auto a = al_unit_conversion_cast>(first); + const auto b = al_unit_conversion_cast>(second); + return al_lowest_terms(ALUnitConversion>{ a.mMultiplier * b.mMultiplier, + a.mOffset * b.mMultiplier + b.mOffset * a.mDivisor, + a.mDivisor * b.mDivisor }); +} + +/// The conversion undoing conversion: x = (y * mDivisor - mOffset) / mMultiplier. +template +constexpr ALUnitConversion al_invert_unit_conversion(ALUnitConversion conversion) +{ + return al_lowest_terms(ALUnitConversion{ conversion.mDivisor, -conversion.mOffset, conversion.mMultiplier }); +} + +/// Applies CONVERSION::value, a constant ALUnitConversion, to value. An integer value under an integer conversion is +/// worked in 64 bits and divided once, so that a factor T cannot hold, such as the 86400000000 microseconds of a day, +/// still divides it; otherwise the conversion is worked in T, or in double for an integer T. A multiplier or divisor of +/// 1 and an offset of 0 are not applied at all, so a floating point value is not rounded, nor -0 made +0, by them. +template +constexpr T al_apply_unit_conversion(T value) +{ + constexpr auto conversion = CONVERSION::value; + using term_t = std::remove_cv_t; + using work_t = std::conditional_t && std::is_integral_v, + std::conditional_t, std::intmax_t, std::uintmax_t>, + std::conditional_t, T, double>>; + + work_t result = (work_t)value; + if constexpr (conversion.mMultiplier != 1) + { + result *= (work_t)conversion.mMultiplier; + } + if constexpr (conversion.mOffset != 0) + { + result += (work_t)conversion.mOffset; + } + if constexpr (conversion.mDivisor != 1) + { + result /= (work_t)conversion.mDivisor; + } + return (T)result; +} diff --git a/indra/llcommon/llunits.h b/indra/llcommon/llunits.h index 122d4557bfe..4e9e1116585 100644 --- a/indra/llcommon/llunits.h +++ b/indra/llcommon/llunits.h @@ -69,7 +69,7 @@ LL_DECLARE_DERIVED_UNIT(Minutes, "min", Seconds, / 60); LL_DECLARE_DERIVED_UNIT(Hours, "h", Minutes, / 60); LL_DECLARE_DERIVED_UNIT(Days, "d", Hours, / 24); LL_DECLARE_DERIVED_UNIT(Milliseconds, "ms", Seconds, * 1000); -LL_DECLARE_DERIVED_UNIT(Microseconds, "\x09\x3cs", Milliseconds, * 1000); +LL_DECLARE_DERIVED_UNIT(Microseconds, "\xC2\xB5s", Milliseconds, * 1000); LL_DECLARE_DERIVED_UNIT(Nanoseconds, "ns", Microseconds, * 1000); } diff --git a/indra/llcommon/llunittype.h b/indra/llcommon/llunittype.h index bb1408609a8..87297d068dc 100644 --- a/indra/llcommon/llunittype.h +++ b/indra/llcommon/llunittype.h @@ -1,5 +1,5 @@ /** - * @file llunit.h + * @file llunittype.h * @brief Unit conversion classes * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ @@ -30,208 +30,198 @@ #include "stdtypes.h" #include "llpreprocessor.h" #include "llerror.h" +#include "alunitconversion.h" + +#include + +#include +#include #ifdef LL_WINDOWS #pragma warning(push) #pragma warning(disable : 4244) // possible loss of data on conversions #endif -//lightweight replacement of type traits for simple type equality check +// the storage type an operation on two storage types yields template -struct LLIsSameType -{ - static const bool value = false; -}; +using LLResultTypeAdd = decltype(std::declval() + std::declval()); -template -struct LLIsSameType -{ - static const bool value = true; -}; +template +using LLResultTypeSubtract = decltype(std::declval() - std::declval()); -// workaround for decltype() not existing and typeof() not working inline in gcc 4.2 template -struct LLResultTypeAdd -{ - typedef LL_TYPEOF(S() + T()) type_t; -}; +using LLResultTypeMultiply = decltype(std::declval() * std::declval()); template -struct LLResultTypeSubtract -{ - typedef LL_TYPEOF(S() - T()) type_t; -}; +using LLResultTypeDivide = decltype(std::declval() / std::declval()); +// the storage type a conversion between two storage types is worked in template -struct LLResultTypeMultiply +using LLResultTypePromote = std::common_type_t; + +// LLUnit and LLUnitImplicit, whatever their storage type and units +template +concept ALUnitType = requires { typename T::is_unit_t; }; + +template +concept ALImplicitUnitType = ALUnitType && requires { typename T::is_implicit_unit_t; }; + +template +concept ALExplicitUnitType = ALUnitType && !ALImplicitUnitType; + +// a plain value, such as an F32, or anything else that is not a unit +template +concept ALUnitlessType = !ALUnitType; + +// the unit UNITS is defined from, through as many units as it takes: Bytes for Gigabits +template> +struct ALRootUnit { - typedef LL_TYPEOF(S() * T()) type_t; + using type = typename ALRootUnit::type; }; -template -struct LLResultTypeDivide +template +struct ALRootUnit { - typedef LL_TYPEOF(S() / T(1)) type_t; + using type = UNITS; }; -template -struct LLResultTypePromote +// units of one dimension, which convert into each other +template +concept ALCompatibleUnits = std::is_same_v::type, typename ALRootUnit::type>; + +// the conversion from the root unit of UNITS, through each unit it is defined from, to UNITS +template +constexpr auto al_unit_conversion_from_root() { - typedef LL_TYPEOF((true) ? S() : T()) type_t; + if constexpr (std::is_same_v) + { + return ALUnitConversion(); + } + else + { + return al_compose_unit_conversions(al_unit_conversion_from_root(), UNITS::sConversion); + } +} + +// the conversion from FROM_UNITS to TO_UNITS, as one map through their root unit +template +struct ALUnitConversionBetween +{ + static constexpr auto value = al_compose_unit_conversions(al_invert_unit_conversion(al_unit_conversion_from_root()), + al_unit_conversion_from_root()); }; template struct LLUnit { - typedef LLUnit self_t; - typedef STORAGE_TYPE storage_t; - typedef void is_unit_t; + using self_t = LLUnit; + using storage_t = STORAGE_TYPE; + using unit_t = UNITS; + using is_unit_t = void; // value initialization - LL_FORCE_INLINE explicit LLUnit(storage_t value = storage_t()) + LL_FORCE_INLINE constexpr explicit LLUnit(storage_t value = storage_t()) : mValue(value) {} - - LL_FORCE_INLINE static self_t convert(self_t v) - { - return v; - } - - template - LL_FORCE_INLINE static self_t convert(LLUnit v) - { - self_t result; - result.mValue = (STORAGE_TYPE)v.value(); - return result; - } - - template - LL_FORCE_INLINE static self_t convert(LLUnit v) - { - self_t result; - STORAGE_TYPE divisor = ll_convert_units(v, result); - result.mValue /= divisor; - return result; - } - template - LL_FORCE_INLINE static self_t convert(LLUnit v) - { - typedef typename LLResultTypePromote::type_t result_storage_t; - LLUnit result; - result_storage_t divisor = ll_convert_units(v, result); - result.value(result.value() / divisor); - return self_t(result.value()); + requires ALCompatibleUnits + [[nodiscard]] LL_FORCE_INLINE static constexpr self_t convert(LLUnit v) + { + if constexpr (std::is_same_v) + { + return self_t((storage_t)v.value()); + } + else + { + // worked in the promoted storage type, so that an integer unit converted to a floating point one keeps + // its fraction until it is stored + using result_storage_t = LLResultTypePromote; + return self_t((storage_t)al_apply_unit_conversion>((result_storage_t)v.value())); + } } - // unit initialization and conversion template - LL_FORCE_INLINE LLUnit(LLUnit other) + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr LLUnit(LLUnit other) : mValue(convert(other).mValue) {} - LL_FORCE_INLINE storage_t value() const + // nor does a unit of another dimension pass for this one through the storage type + template + requires (!ALCompatibleUnits) + LLUnit(LLUnit other) = delete; + + [[nodiscard]] LL_FORCE_INLINE constexpr storage_t value() const { return mValue; } - LL_FORCE_INLINE void value(const storage_t& value) + LL_FORCE_INLINE constexpr void value(const storage_t& value) { mValue = value; } template - storage_t valueInUnits() const + requires ALCompatibleUnits + [[nodiscard]] constexpr storage_t valueInUnits() const { return LLUnit(*this).value(); } template - void valueInUnits(const storage_t& value) const + requires ALCompatibleUnits + constexpr void valueInUnits(const storage_t& value) { *this = LLUnit(value); } - LL_FORCE_INLINE operator storage_t() const + LL_FORCE_INLINE constexpr operator storage_t() const { return value(); } - /*LL_FORCE_INLINE self_t& operator= (storage_t v) + LL_FORCE_INLINE constexpr self_t& operator += (self_t other) { - value(v); + mValue += other.mValue; return *this; - }*/ - - LL_FORCE_INLINE void operator += (self_t other) - { - mValue += convert(other).mValue; } - LL_FORCE_INLINE void operator -= (self_t other) - { - mValue -= convert(other).mValue; - } + template + requires (!ALCompatibleUnits) + self_t& operator += (const OTHER& other) = delete; - LL_FORCE_INLINE void operator *= (const storage_t& multiplicand) + LL_FORCE_INLINE constexpr self_t& operator -= (self_t other) { - mValue *= multiplicand; - } - - LL_FORCE_INLINE void operator *= (const self_t& multiplicand) - { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "Multiplication of unit types not supported."); - } - - LL_FORCE_INLINE void operator /= (const storage_t& divisor) - { - mValue /= divisor; - } - - void operator /= (const self_t& divisor) - { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "Illegal in-place division of unit types."); + mValue -= other.mValue; + return *this; } - template - LL_FORCE_INLINE bool operator == (const LLUnit& other) const - { - return mValue == convert(other).value(); - } + template + requires (!ALCompatibleUnits) + self_t& operator -= (const OTHER& other) = delete; - template - LL_FORCE_INLINE bool operator != (const LLUnit& other) const + LL_FORCE_INLINE constexpr self_t& operator *= (const storage_t& multiplicand) { - return mValue != convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator < (const LLUnit& other) const - { - return mValue < convert(other).value(); + mValue *= multiplicand; + return *this; } - template - LL_FORCE_INLINE bool operator <= (const LLUnit& other) const - { - return mValue <= convert(other).value(); - } + // a product of units is a new kind of unit + template + self_t& operator *= (const OTHER& multiplicand) = delete; - template - LL_FORCE_INLINE bool operator > (const LLUnit& other) const + LL_FORCE_INLINE constexpr self_t& operator /= (const storage_t& divisor) { - return mValue > convert(other).value(); + mValue /= divisor; + return *this; } - template - LL_FORCE_INLINE bool operator >= (const LLUnit& other) const - { - return mValue >= convert(other).value(); - } + // a unit divided by a unit is a plain value, not this unit + template + self_t& operator /= (const OTHER& divisor) = delete; protected: storage_t mValue; @@ -256,161 +246,101 @@ std::istream& operator >>(std::istream& s, LLUnit& unit) template struct LLUnitImplicit : public LLUnit { - typedef LLUnitImplicit self_t; - typedef typename LLUnit::storage_t storage_t; - typedef LLUnit base_t; + using self_t = LLUnitImplicit; + using storage_t = typename LLUnit::storage_t; + using base_t = LLUnit; + using is_implicit_unit_t = void; - LL_FORCE_INLINE LLUnitImplicit(storage_t value = storage_t()) + LL_FORCE_INLINE constexpr LLUnitImplicit(storage_t value = storage_t()) : base_t(value) {} template - LL_FORCE_INLINE LLUnitImplicit(LLUnit other) + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr LLUnitImplicit(LLUnit other) : base_t(other) {} - // unlike LLUnit, LLUnitImplicit is *implicitly* convertable to a POD value (F32, S32, etc) - // this allows for interoperability with legacy code - LL_FORCE_INLINE operator storage_t() const - { - return base_t::value(); - } - - using base_t::operator +=; - LL_FORCE_INLINE void operator += (storage_t value) - { - base_t::mValue += value; - } - - // this overload exists to explicitly catch use of another implicit unit - // without ambiguity between conversion to storage_t vs conversion to base_t - template - LL_FORCE_INLINE void operator += (LLUnitImplicit other) - { - base_t::mValue += base_t::convert(other).value(); - } - - using base_t::operator -=; - LL_FORCE_INLINE void operator -= (storage_t value) - { - base_t::mValue -= value; - } - - // this overload exists to explicitly catch use of another implicit unit - // without ambiguity between conversion to storage_t vs conversion to base_t - template - LL_FORCE_INLINE void operator -= (LLUnitImplicit other) - { - base_t::mValue -= base_t::convert(other).value(); - } - template - LL_FORCE_INLINE bool operator == (LLUnit other) const - { - return base_t::mValue == base_t::convert(other).value(); - } + requires (!ALCompatibleUnits) + LLUnitImplicit(LLUnit other) = delete; - template - LL_FORCE_INLINE bool operator == (LLUnitImplicit other) const + // unlike LLUnit, LLUnitImplicit is also *implicitly* constructed from a POD value (F32, S32, etc) + // and takes part in arithmetic with one; this allows for interoperability with legacy code + LL_FORCE_INLINE constexpr operator storage_t() const { - return base_t::mValue == base_t::convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator == (STORAGE_T other) const - { - return base_t::mValue == other; - } - - template - LL_FORCE_INLINE bool operator != (LLUnit other) const - { - return base_t::mValue != convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator != (LLUnitImplicit other) const - { - return base_t::mValue != base_t::convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator != (STORAGE_T other) const - { - return base_t::mValue != other; + return base_t::value(); } - template - LL_FORCE_INLINE bool operator < (LLUnit other) const + LL_FORCE_INLINE constexpr self_t& operator += (base_t other) { - return base_t::mValue < base_t::convert(other).value(); + base_t::operator += (other); + return *this; } - template - LL_FORCE_INLINE bool operator < (LLUnitImplicit other) const + LL_FORCE_INLINE constexpr self_t& operator += (storage_t value) { - return base_t::mValue < base_t::convert(other).value(); + base_t::mValue += value; + return *this; } - template - LL_FORCE_INLINE bool operator < (STORAGE_T other) const + // this overload exists to explicitly catch use of another unit + // without ambiguity between conversion to storage_t vs conversion to base_t + template + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr self_t& operator += (const OTHER& other) { - return base_t::mValue < other; + base_t::mValue += base_t::convert(other).value(); + return *this; } - template - LL_FORCE_INLINE bool operator <= (LLUnit other) const - { - return base_t::mValue <= base_t::convert(other).value(); - } + template + requires (!ALCompatibleUnits) + self_t& operator += (const OTHER& other) = delete; - template - LL_FORCE_INLINE bool operator <= (LLUnitImplicit other) const + LL_FORCE_INLINE constexpr self_t& operator -= (base_t other) { - return base_t::mValue <= base_t::convert(other).value(); + base_t::operator -= (other); + return *this; } - template - LL_FORCE_INLINE bool operator <= (STORAGE_T other) const + LL_FORCE_INLINE constexpr self_t& operator -= (storage_t value) { - return base_t::mValue <= other; + base_t::mValue -= value; + return *this; } - template - LL_FORCE_INLINE bool operator > (LLUnit other) const + // this overload exists to explicitly catch use of another unit + // without ambiguity between conversion to storage_t vs conversion to base_t + template + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr self_t& operator -= (const OTHER& other) { - return base_t::mValue > base_t::convert(other).value(); + base_t::mValue -= base_t::convert(other).value(); + return *this; } - template - LL_FORCE_INLINE bool operator > (LLUnitImplicit other) const - { - return base_t::mValue > base_t::convert(other).value(); - } + template + requires (!ALCompatibleUnits) + self_t& operator -= (const OTHER& other) = delete; - template - LL_FORCE_INLINE bool operator > (STORAGE_T other) const + LL_FORCE_INLINE constexpr self_t& operator *= (const storage_t& multiplicand) { - return base_t::mValue > other; + base_t::operator *= (multiplicand); + return *this; } - template - LL_FORCE_INLINE bool operator >= (LLUnit other) const - { - return base_t::mValue >= base_t::convert(other).value(); - } + template + self_t& operator *= (const OTHER& multiplicand) = delete; - template - LL_FORCE_INLINE bool operator >= (LLUnitImplicit other) const + LL_FORCE_INLINE constexpr self_t& operator /= (const storage_t& divisor) { - return base_t::mValue >= base_t::convert(other).value(); + base_t::operator /= (divisor); + return *this; } - template - LL_FORCE_INLINE bool operator >= (STORAGE_T other) const - { - return base_t::mValue >= other; - } + template + self_t& operator /= (const OTHER& divisor) = delete; }; template @@ -429,427 +359,329 @@ std::istream& operator >>(std::istream& s, LLUnitImplicit& return s; } -template -LL_FORCE_INLINE S2 ll_convert_units(LLUnit in, LLUnit& out) -{ - S2 divisor(1); - - LL_STATIC_ASSERT((LLIsSameType::value - || !LLIsSameType::value - || !LLIsSameType::value), - "conversion requires compatible units"); +// what an operation on two units yields: in the first one's units, and implicit when either of them is +template +using ALUnitResult = std::conditional_t || ALImplicitUnitType, + LLUnitImplicit, + LLUnit>; - if (LLIsSameType::value) - { - // T1 and T2 same type, just assign - out.value((S2)in.value()); - } - else if (T1::sLevel > T2::sLevel) - { - // reduce T1 - LLUnit new_in; - divisor *= (S2)ll_convert_units(in, new_in); - divisor *= (S2)ll_convert_units(new_in, out); - } - else - { - // reduce T2 - LLUnit new_out; - divisor *= (S2)ll_convert_units(in, new_out); - divisor *= (S2)ll_convert_units(new_out, out); - } - return divisor; -} - -template -struct LLStorageType -{ - typedef T type_t; -}; - -template -struct LLStorageType > -{ - typedef STORAGE_TYPE type_t; -}; - -// all of these operators need to perform type promotion on the storage type of the units, so they -// cannot be expressed as operations on identical types with implicit unit conversion -// e.g. typeof(S32Bytes(x) + F32Megabytes(y)) <==> F32Bytes +// what an operation on a unit and a plain value yields: the unit's kind, in STORAGE_TYPE +template +using ALUnitLike = std::conditional_t, + LLUnitImplicit, + LLUnit>; // -// operator + +// Operators between two units take any mix of LLUnit and LLUnitImplicit, and convert the second into the first's +// units. An operation the units do not support is deleted rather than left out: a unit converts to its storage type, +// so an operator left out would be taken by the storage type's own and drop the units without a word. // -template -LL_FORCE_INLINE LLUnit::type_t, UNITS1> operator + (LLUnit first, LLUnit second) -{ - LLUnit::type_t, UNITS1> result(first); - result += second; - return result; -} -template -LLUnit operator + (LLUnit first, UNITLESS second) +// +// operator + and -: two units of one dimension, or an implicit unit and a plain value +// +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitResult> operator + (const UNIT1& first, const UNIT2& second) { - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator + requires compatible unit types"); - return LLUnit(0); + using result_t = ALUnitResult>; + return result_t(result_t::convert(first).value() + result_t::convert(second).value()); } -template -LLUnit operator + (UNITLESS first, LLUnit second) -{ - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator + requires compatible unit types"); - return LLUnit(0); -} +template + requires (!ALCompatibleUnits) +void operator + (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator + (LLUnitImplicit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator + (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t, UNITS1> result(first); + LLUnitImplicit, typename UNIT::unit_t> result(first); result += second; return result; } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator + (LLUnit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator + (UNITLESS_TYPE first, const UNIT& second) { - LLUnitImplicit::type_t, UNITS1> result(first); + LLUnitImplicit, typename UNIT::unit_t> result(first); result += second; return result; } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator + (LLUnitImplicit first, LLUnit second) -{ - LLUnitImplicit::type_t, UNITS1> result(first); - result += LLUnitImplicit(second); - return result; -} +// an explicit unit and a plain value do not add +template +void operator + (const UNIT& first, const UNITLESS_TYPE& second) = delete; + +template +void operator + (const UNITLESS_TYPE& first, const UNIT& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator + (LLUnitImplicit first, UNITLESS_TYPE second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitResult> operator - (const UNIT1& first, const UNIT2& second) { - LLUnitImplicit::type_t>::type_t, UNITS> result(first); - result += second; - return result; + using result_t = ALUnitResult>; + return result_t(result_t::convert(first).value() - result_t::convert(second).value()); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, STORAGE_TYPE>:: - type_t, UNITS> operator + (UNITLESS_TYPE first, LLUnitImplicit second) +template + requires (!ALCompatibleUnits) +void operator - (const UNIT1& first, const UNIT2& second) = delete; + +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator - (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> result(first); - result += second; + LLUnitImplicit, typename UNIT::unit_t> result(first); + result -= second; return result; } -// -// operator - -// -template -LL_FORCE_INLINE LLUnit::type_t, UNITS1> operator - (LLUnit first, LLUnit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator - (UNITLESS_TYPE first, const UNIT& second) { - LLUnit::type_t, UNITS1> result(first); + LLUnitImplicit, typename UNIT::unit_t> result(first); result -= second; return result; } -template -LLUnit operator - (LLUnit first, UNITLESS second) -{ - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator - requires compatible unit types"); - return LLUnit(0); -} +template +void operator - (const UNIT& first, const UNITLESS_TYPE& second) = delete; -template -LLUnit operator - (UNITLESS first, LLUnit second) -{ - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator - requires compatible unit types"); - return LLUnit(0); -} +template +void operator - (const UNITLESS_TYPE& first, const UNIT& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator - (LLUnitImplicit first, LLUnitImplicit second) +// +// operator *: a unit and a plain value +// +template +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitLike> operator * (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t, UNITS1> result(first); - result -= second; - return result; + return ALUnitLike>(first.value() * second); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator - (LLUnit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitLike> operator * (UNITLESS_TYPE first, const UNIT& second) { - LLUnitImplicit::type_t, UNITS1> result(first); - result -= second; - return result; + return ALUnitLike>(first * second.value()); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator - (LLUnitImplicit first, LLUnit second) -{ - LLUnitImplicit::type_t, UNITS1> result(first); - result -= LLUnitImplicit(second); - return result; -} +// a product of units is a new kind of unit +template +void operator * (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator - (LLUnitImplicit first, UNITLESS_TYPE second) +// +// operator /: a unit by a plain value, or by a unit of its dimension, which leaves a plain value +// +template +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitLike> operator / (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t>::type_t, UNITS> result(first); - result -= second; - return result; + return ALUnitLike>(first.value() / second); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> operator - (UNITLESS_TYPE first, LLUnitImplicit second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr LLResultTypeDivide operator / (const UNIT1& first, const UNIT2& second) { - LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> result(first); - result -= second; - return result; + using first_t = LLUnit; + return (LLResultTypeDivide)(first.value() / first_t::convert(second).value()); } +template + requires (!ALCompatibleUnits) +void operator / (const UNIT1& first, const UNIT2& second) = delete; + // -// operator * +// comparisons between two units of one dimension, or between an implicit unit and a plain value // -template -LLUnit operator * (LLUnit, LLUnit) +// != is rewritten from ==. The relational operators are not rewritten from <=>, which MSVC compiles to a chain of +// branches for a floating point storage type. +// +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator == (const UNIT1& first, const UNIT2& second) { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE1, "multiplication of unit types results in new unit type - not supported."); - return LLUnit(); + return first.value() == LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnit::type_t>::type_t, UNITS> operator * (LLUnit first, UNITLESS_TYPE second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator < (const UNIT1& first, const UNIT2& second) { - return LLUnit::type_t>::type_t, UNITS>(first.value() * second); + return first.value() < LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnit::type_t, STORAGE_TYPE>::type_t, UNITS> operator * (UNITLESS_TYPE first, LLUnit second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator <= (const UNIT1& first, const UNIT2& second) { - return LLUnit::type_t, STORAGE_TYPE>::type_t, UNITS>(first * second.value()); + return first.value() <= LLUnit::convert(second).value(); } -template -LLUnitImplicit operator * (LLUnitImplicit, LLUnitImplicit) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator > (const UNIT1& first, const UNIT2& second) { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE1, "multiplication of unit types results in new unit type - not supported."); - return LLUnitImplicit(); + return first.value() > LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator * (LLUnitImplicit first, UNITLESS_TYPE second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator >= (const UNIT1& first, const UNIT2& second) { - return LLUnitImplicit::type_t, UNITS>(first.value() * second); + return first.value() >= LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> operator * (UNITLESS_TYPE first, LLUnitImplicit second) -{ - return LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS>(first * second.value()); -} +template + requires (!ALCompatibleUnits) +bool operator == (const UNIT1& first, const UNIT2& second) = delete; +template + requires (!ALCompatibleUnits) +bool operator < (const UNIT1& first, const UNIT2& second) = delete; -// -// operator / -// +template + requires (!ALCompatibleUnits) +bool operator <= (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE LLUnit::type_t>::type_t, UNITS> operator / (LLUnit first, UNITLESS_TYPE second) -{ - return LLUnit::type_t>::type_t, UNITS>(first.value() / second); -} +template + requires (!ALCompatibleUnits) +bool operator > (const UNIT1& first, const UNIT2& second) = delete; + +template + requires (!ALCompatibleUnits) +bool operator >= (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnit first, LLUnit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator == (const UNIT& first, const UNITLESS_TYPE& second) { - return first.value() / first.convert(second).value(); + return first.value() == second; } -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator / (LLUnitImplicit first, UNITLESS_TYPE second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator < (const UNIT& first, const UNITLESS_TYPE& second) { - return LLUnitImplicit::type_t>::type_t, UNITS>(first.value() / second); + return first.value() < second; } -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnitImplicit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator <= (const UNIT& first, const UNITLESS_TYPE& second) { - return (typename LLResultTypeDivide::type_t)(first.value() / first.convert(second).value()); + return first.value() <= second; } -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator > (const UNIT& first, const UNITLESS_TYPE& second) { - return (typename LLResultTypeDivide::type_t)(first.value() / first.convert(second).value()); + return first.value() > second; } -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnitImplicit first, LLUnit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator >= (const UNIT& first, const UNITLESS_TYPE& second) { - return (typename LLResultTypeDivide::type_t)(first.value() / first.convert(second).value()); + return first.value() >= second; } template struct LLGetUnitLabel { - static const char* getUnitLabel() { return ""; } + static constexpr const char* getUnitLabel() { return ""; } }; template struct LLGetUnitLabel > { - static const char* getUnitLabel() { return T::getUnitLabel(); } + static constexpr const char* getUnitLabel() { return T::getUnitLabel(); } }; -template -struct LLUnitLinearOps +// formats the value as its storage type would be, then the unit's label: fmt::format("{:.1f}", F32Seconds(2.f)) is "2.0s" +template +struct fmt::formatter> : fmt::formatter { - typedef LLUnitLinearOps self_t; - - LLUnitLinearOps(T val) - : mValue(val), - mDivisor(1) - {} - - template - self_t operator * (OTHER_T other) - { - return mValue * other; - } - - template - self_t operator / (OTHER_T other) - { - mDivisor *= other; - return *this; - } - - template - self_t operator + (OTHER_T other) - { - mValue += other * mDivisor; - return *this; - } - - template - self_t operator - (OTHER_T other) + template + auto format(const LLUnit& unit, FORMAT_CONTEXT& ctx) const { - mValue -= other * mDivisor; - return *this; + auto out = fmt::formatter::format(unit.value(), ctx); + for (const char* label = UNITS::getUnitLabel(); *label; ++label) + { + *out++ = *label; + } + return out; } - - T mValue; - T mDivisor; }; -template -struct LLUnitInverseLinearOps +template +struct fmt::formatter> : fmt::formatter> { - typedef LLUnitInverseLinearOps self_t; - - LLUnitInverseLinearOps(T val) - : mValue(val), - mDivisor(1), - mMultiplicand(1) - {} - - template - self_t operator * (OTHER_T other) - { - mDivisor *= other; - return *this; - } - - template - self_t operator / (OTHER_T other) - { - mValue *= other; - mMultiplicand *= other; - return *this; - } +}; - template - self_t operator + (OTHER_T other) +// +// unit definitions +// +// A unit is a type naming its base unit and its label, and, for a unit defined from another, sConversion: the +// ALUnitConversion taking a value in the base unit to one in this unit. +// +// struct Kilobytes : ALDerivedUnit +// { +// static constexpr const char* getUnitLabel() { return "KB"; } +// static constexpr auto sConversion = ALUnitConversion() / 1024; +// }; +// +// Units of one dimension convert into each other through the base unit they share, as one map worked out at compile +// time. LL_DECLARE_BASE_UNIT and LL_DECLARE_DERIVED_UNIT write these out. +// +template +struct ALUnitDefinition +{ + template + [[nodiscard]] static constexpr LLUnit fromValue(T value) { - mValue -= other * mMultiplicand; - return *this; + return LLUnit(value); } - template - self_t operator - (OTHER_T other) + template + [[nodiscard]] static constexpr LLUnit fromValue(LLUnit value) { - mValue += other * mMultiplicand; - return *this; + return LLUnit(value); } - - T mValue; - T mDivisor; - T mMultiplicand; }; -#define LL_DECLARE_BASE_UNIT(base_unit_name, unit_label) \ -struct base_unit_name \ -{ \ - static const int sLevel = 0; \ - typedef base_unit_name base_unit_t; \ - static const char* getUnitLabel() { return unit_label; } \ - template \ - static LLUnit fromValue(T value) { return LLUnit(value); } \ - template \ - static LLUnit fromValue(LLUnit value) \ - { return LLUnit(value); } \ -} - +template +struct ALBaseUnit : ALUnitDefinition +{ + using base_unit_t = SELF; +}; -#define LL_DECLARE_DERIVED_UNIT(unit_name, unit_label, base_unit_name, conversion_operation) \ -struct unit_name \ -{ \ - static const int sLevel = base_unit_name::sLevel + 1; \ - typedef base_unit_name base_unit_t; \ - static const char* getUnitLabel() { return unit_label; } \ - template \ - static LLUnit fromValue(T value) { return LLUnit(value); } \ - template \ - static LLUnit fromValue(LLUnit value) \ - { return LLUnit(value); } \ -}; \ - \ -template \ -LL_FORCE_INLINE S2 ll_convert_units(LLUnit in, LLUnit& out) \ -{ \ - typedef typename LLResultTypePromote::type_t result_storage_t; \ - LLUnitInverseLinearOps result = \ - LLUnitInverseLinearOps(in.value()) conversion_operation; \ - out = LLUnit((S2)result.mValue); \ - return result.mDivisor; \ -} \ - \ -template \ -LL_FORCE_INLINE S2 ll_convert_units(LLUnit in, LLUnit& out) \ -{ \ - typedef typename LLResultTypePromote::type_t result_storage_t; \ - LLUnitLinearOps result = \ - LLUnitLinearOps(in.value()) conversion_operation; \ - out = LLUnit((S2)result.mValue); \ - return result.mDivisor; \ -} +template +struct ALDerivedUnit : ALUnitDefinition +{ + using base_unit_t = BASE_UNIT; +}; -#define LL_DECLARE_UNIT_TYPEDEFS(ns, unit_name) \ - typedef LLUnit F32##unit_name; \ - typedef LLUnitImplicit F32##unit_name##Implicit;\ - typedef LLUnit F64##unit_name; \ - typedef LLUnitImplicit F64##unit_name##Implicit;\ - typedef LLUnit S32##unit_name; \ - typedef LLUnitImplicit S32##unit_name##Implicit;\ - typedef LLUnit S64##unit_name; \ - typedef LLUnitImplicit S64##unit_name##Implicit;\ - typedef LLUnit U32##unit_name; \ - typedef LLUnitImplicit U32##unit_name##Implicit;\ - typedef LLUnit U64##unit_name; \ - typedef LLUnitImplicit U64##unit_name##Implicit +#define LL_DECLARE_BASE_UNIT(base_unit_name, unit_label) \ +struct base_unit_name : ALBaseUnit \ +{ \ + static constexpr const char* getUnitLabel() { return unit_label; } \ +} + +#define LL_DECLARE_DERIVED_UNIT(unit_name, unit_label, base_unit_name, conversion_operation) \ +struct unit_name : ALDerivedUnit \ +{ \ + static constexpr const char* getUnitLabel() { return unit_label; } \ + static constexpr auto sConversion = ALUnitConversion() conversion_operation; \ +} + +#define LL_DECLARE_UNIT_TYPEDEFS(ns, unit_name) \ + using F32##unit_name = LLUnit; \ + using F32##unit_name##Implicit = LLUnitImplicit; \ + using F64##unit_name = LLUnit; \ + using F64##unit_name##Implicit = LLUnitImplicit; \ + using S32##unit_name = LLUnit; \ + using S32##unit_name##Implicit = LLUnitImplicit; \ + using S64##unit_name = LLUnit; \ + using S64##unit_name##Implicit = LLUnitImplicit; \ + using U32##unit_name = LLUnit; \ + using U32##unit_name##Implicit = LLUnitImplicit; \ + using U64##unit_name = LLUnit; \ + using U64##unit_name##Implicit = LLUnitImplicit #ifdef LL_WINDOWS #pragma warning(pop) diff --git a/indra/llcommon/tests/alunitconversion_test.cpp b/indra/llcommon/tests/alunitconversion_test.cpp new file mode 100644 index 00000000000..c6dfd97af9d --- /dev/null +++ b/indra/llcommon/tests/alunitconversion_test.cpp @@ -0,0 +1,123 @@ +/** + * @file alunitconversion_test.cpp + * @brief Tests of ALUnitConversion. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../alunitconversion.h" + +#include + +#include +#include +#include + +namespace +{ + template + constexpr bool has_terms(ALUnitConversion conversion, T multiplier, T offset, T divisor) + { + return conversion.mMultiplier == multiplier && conversion.mOffset == offset && conversion.mDivisor == divisor; + } + + struct ToFahrenheit + { + static constexpr auto value = ALUnitConversion() * 9 / 5 + 32; + }; + + struct FromFahrenheit + { + static constexpr auto value = al_invert_unit_conversion(ToFahrenheit::value); + }; + + // microseconds to days, by a divisor no 32 bit integer holds + struct ToDays + { + static constexpr auto value = ALUnitConversion() / 86400000000LL; + }; + + struct ToDegrees + { + static constexpr auto value = ALUnitConversion() * 57.29578f; + }; + + struct Doubled + { + static constexpr auto value = ALUnitConversion() * 2; + }; + + TEST_CASE("ALUnitConversion: operations build the map left to right in lowest terms", "[ALUnitConversion]") + { + STATIC_REQUIRE(has_terms(ALUnitConversion(), 1, 0, 1)); + STATIC_REQUIRE(has_terms(ALUnitConversion() * 9 / 5 + 32, 9, 160, 5)); + STATIC_REQUIRE(has_terms(ALUnitConversion() / 4 * 2, 1, 0, 2)); + STATIC_REQUIRE(has_terms(ALUnitConversion() * 8 / 1024, 1, 0, 128)); + STATIC_REQUIRE(has_terms(ALUnitConversion() / 2 - 3, 1, -6, 2)); + } + + TEST_CASE("ALUnitConversion: a floating point factor makes the terms doubles", "[ALUnitConversion]") + { + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(has_terms(ALUnitConversion() * 9 / 5 + 32.5, 9.0, 162.5, 5.0)); + } + + TEST_CASE("ALUnitConversion: compositions and inverses reduce to one map", "[ALUnitConversion]") + { + STATIC_REQUIRE(has_terms(FromFahrenheit::value, 5, -160, 9)); + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(ToFahrenheit::value, FromFahrenheit::value), 1, 0, 1)); + + constexpr auto kilobytes = ALUnitConversion() / 1024; + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(kilobytes, kilobytes), 1, 0, 1048576)); + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(al_invert_unit_conversion(kilobytes), ALUnitConversion() * 8), 8192, 0, 1)); + + // an offset in the first conversion is scaled by the second, one in the second is not + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(ALUnitConversion() + 1, ALUnitConversion() * 3 + 2), 3, 5, 1)); + } + + TEST_CASE("ALUnitConversion: an integer conversion is exact and divides once", "[ALUnitConversion]") + { + STATIC_REQUIRE(al_apply_unit_conversion(100) == 212); + STATIC_REQUIRE(al_apply_unit_conversion(212) == 100); + // (0 * 5 - 160) / 9 is -17.8, which truncates toward zero + STATIC_REQUIRE(al_apply_unit_conversion(0) == -17); + STATIC_REQUIRE(al_apply_unit_conversion(std::uint32_t(4294967295u)) == 0u); + STATIC_REQUIRE(al_apply_unit_conversion(std::int64_t(172800000000)) == 2); + } + + TEST_CASE("ALUnitConversion: a floating point conversion is worked in the value's own type", "[ALUnitConversion]") + { + STATIC_REQUIRE(al_apply_unit_conversion(37.f) == (37.f * 9.f + 160.f) / 5.f); + STATIC_REQUIRE(al_apply_unit_conversion(2.f) == 2.f * 57.29578f); + STATIC_REQUIRE(al_apply_unit_conversion(2.0) == 2.0 * (double)57.29578f); + // an integer value under a floating point conversion is worked in double, then truncated + STATIC_REQUIRE(al_apply_unit_conversion(2) == 114); + } + + TEST_CASE("ALUnitConversion: a conversion with no offset keeps negative zero", "[ALUnitConversion]") + { + CHECK(std::signbit(al_apply_unit_conversion(-0.f))); + CHECK(std::signbit(al_apply_unit_conversion(-0.0))); + } +} diff --git a/indra/llcommon/tests/llunits_test.cpp b/indra/llcommon/tests/llunits_test.cpp index 036aaef30cd..f7f9c06a54f 100644 --- a/indra/llcommon/tests/llunits_test.cpp +++ b/indra/llcommon/tests/llunits_test.cpp @@ -30,6 +30,11 @@ #include "llunits.h" #include +#include + +#include +#include + namespace LLUnits { namespace @@ -38,12 +43,15 @@ namespace LLUnits LL_DECLARE_BASE_UNIT(Quatloos, "Quat"); LL_DECLARE_DERIVED_UNIT(Latinum, "Lat", Quatloos, / 4); LL_DECLARE_DERIVED_UNIT(Solari, "Sol", Latinum, * 16); + // a multiplication after a division, which must keep the division's divisor + LL_DECLARE_DERIVED_UNIT(Ducats, "Duc", Quatloos, / 4 * 2); } // anonymous namespace } LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Quatloos); LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Latinum); LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Solari); +LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Ducats); namespace LLUnits { @@ -377,4 +385,247 @@ namespace int_celcius = int_fahrenheit; CHECK(int_celcius.value() == 100); } + + // a conversion's operations after a division are applied over the division's divisor + TEST_CASE("LLUnit: a conversion that multiplies after dividing keeps its divisor", "[LLUnit]") + { + F32Ducats ducats = F32Quatloos(8.f); + CHECK(ducats.value() == 4.f); + + F32Quatloos quatloos = F32Ducats(4.f); + CHECK(quatloos.value() == 8.f); + + S32Ducats int_ducats = S32Quatloos(8); + CHECK(int_ducats.value() == 4); + } + + // comparisons + TEST_CASE("LLUnit: comparisons convert the right operand into the storage of the left", "[LLUnit]") + { + // 1.5 truncates to 1 in S32 storage, so which operand is converted decides each answer + S32Quatloos int_quatloos(1); + F32QuatloosImplicit float_quatloos(1.5f); + + CHECK(int_quatloos == float_quatloos); + CHECK_FALSE(int_quatloos != float_quatloos); + CHECK_FALSE(int_quatloos < float_quatloos); + CHECK(int_quatloos >= float_quatloos); + + CHECK(float_quatloos != int_quatloos); + CHECK(float_quatloos > int_quatloos); + } + + constexpr F32Quatloos accumulate_quatloos() + { + F32Quatloos quatloos; + quatloos += S32Latinum(1); + quatloos *= 2.f; + quatloos -= F32Quatloos(1.f); + quatloos /= 7.f; + quatloos.valueInUnits(quatloos.value()); + return quatloos; + } + + constexpr F32QuatloosImplicit accumulate_implicit_quatloos() + { + F32QuatloosImplicit quatloos; + quatloos += 2; + quatloos += F32Quatloos(4.f); + quatloos += F32LatinumImplicit(1.f); + quatloos -= 4; + quatloos -= F32LatinumImplicit(1.f); + return quatloos; + } + + // constant expressions + TEST_CASE("LLUnit: conversions and arithmetic are constant expressions", "[LLUnit]") + { + constexpr F32Quatloos quatloos(1024.f); + STATIC_REQUIRE(F32Solari(quatloos).value() == 4096.f); + STATIC_REQUIRE(F32Latinum(quatloos) == S32Latinum(256)); + STATIC_REQUIRE(F32Quatloos(S32Solari(1)) == F32Quatloos(0.25f)); + STATIC_REQUIRE(quatloos.valueInUnits() == 256.f); + STATIC_REQUIRE(U32Kilobytes(U32Gigabytes(1)).value() == 1024 * 1024); + STATIC_REQUIRE(S32Fahrenheit(S32Celcius(100)).value() == 212); + STATIC_REQUIRE(S32Celcius(S32Fahrenheit(212)).value() == 100); + STATIC_REQUIRE(F32Ducats(F32Quatloos(8.f)).value() == 4.f); + STATIC_REQUIRE(Ratio::fromValue(0.5f) == F32Percent(50.f)); + + STATIC_REQUIRE(F32Seconds(1.f) + F32Milliseconds(500.f) == F32Seconds(1.5f)); + STATIC_REQUIRE(F32Seconds(1.f) - F32Milliseconds(250.f) == F32Milliseconds(750.f)); + STATIC_REQUIRE(F32Seconds(2.f) * 3 == F32Seconds(6.f)); + STATIC_REQUIRE(3 * F32Seconds(2.f) == F32Seconds(6.f)); + STATIC_REQUIRE(F32Seconds(2.f) / 4 == F32Seconds(0.5f)); + STATIC_REQUIRE(F32Seconds(1.f) / F32Milliseconds(250.f) == 4.f); + STATIC_REQUIRE(accumulate_quatloos() == F32Quatloos(4.f)); + + STATIC_REQUIRE(F32QuatloosImplicit(1.f) + 3 == 4); + STATIC_REQUIRE(10 - F32QuatloosImplicit(4.f) == 6); + STATIC_REQUIRE(2 * F32QuatloosImplicit(4.f) / 4 == 2); + STATIC_REQUIRE(F32QuatloosImplicit(4.f) / F32LatinumImplicit(1.f) == 1); + STATIC_REQUIRE(accumulate_implicit_quatloos() == 2); + + constexpr F32 plain = F32QuatloosImplicit(4.f); + STATIC_REQUIRE(plain == 4.f); + } + + TEST_CASE("LLUnit: comparisons are constant expressions", "[LLUnit]") + { + STATIC_REQUIRE(S32Quatloos(1) < S32Latinum(1)); + STATIC_REQUIRE(S32Latinum(1) > S32Quatloos(1)); + STATIC_REQUIRE(S32Quatloos(4) <= S32Latinum(1)); + STATIC_REQUIRE(S32Quatloos(4) >= F32Latinum(1.f)); + STATIC_REQUIRE(F32Quatloos(1.f) != F32Latinum(1.f)); + STATIC_REQUIRE(F32QuatloosImplicit(4.f) == F32Latinum(1.f)); + + STATIC_REQUIRE(F32QuatloosImplicit(1.f) < 2); + STATIC_REQUIRE(F32QuatloosImplicit(2.f) >= 2); + STATIC_REQUIRE(1 == F32QuatloosImplicit(1.f)); + STATIC_REQUIRE(1 != F32QuatloosImplicit(2.f)); + STATIC_REQUIRE(2 > F32QuatloosImplicit(1.f)); + } + + TEST_CASE("LLUnit: unit labels and levels are constant expressions", "[LLUnit]") + { + STATIC_REQUIRE(std::string_view(Quatloos::getUnitLabel()) == "Quat"); + STATIC_REQUIRE(std::string_view(LLGetUnitLabel::getUnitLabel()) == "Sol"); + STATIC_REQUIRE(std::string_view(LLGetUnitLabel::getUnitLabel()).empty()); + STATIC_REQUIRE(std::string_view(Microseconds::getUnitLabel()) == "\xC2\xB5s"); + } + + // result types + TEST_CASE("LLUnit: arithmetic promotes storage as the storage types promote", "[LLUnit]") + { + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + // conversions through the root unit + TEST_CASE("LLUnit: a conversion between two derived units applies both offsets", "[LLUnit]") + { + // Fahrenheit to Kelvin goes through Celsius, with an offset each way + F32Kelvin kelvin = F32Fahrenheit(212.f); + CHECK(value_near(kelvin.value(), 373.15f, 0.01f)); + + F32Fahrenheit fahrenheit = F32Kelvin(273.15f); + CHECK(value_near(fahrenheit.value(), 32.f, 0.01f)); + } + + TEST_CASE("LLUnit: an integer conversion divides by a divisor its storage cannot hold", "[LLUnit]") + { + // a day is 86400000000 microseconds, more than a U32 holds + CHECK(U32Days(U32Microseconds(U32_MAX)).value() == 0); + CHECK(U32Microseconds(U32Seconds(4000)).value() == 4000000000u); + CHECK(S64Microseconds(S32Days(1)).value() == 86400000000); + } + + TEST_CASE("LLUnit: adding an explicit unit to an implicit one keeps the explicit one's fraction", "[LLUnit]") + { + // 0.375 latinum is 1.5 quatloos, which S32 storage would truncate to 1 + F32 sum = S32QuatloosImplicit(1) + F32Latinum(0.375f); + CHECK(sum == 2.5f); + + F32 difference = S32QuatloosImplicit(1) - F32Latinum(0.375f); + CHECK(difference == -0.5f); + } + + // what the units reject, asked of a concept, since an expression that does not compile cannot be tested otherwise + template + concept CanAdd = requires (const A& a, const B& b) { a + b; }; + + template + concept CanSubtract = requires (const A& a, const B& b) { a - b; }; + + template + concept CanMultiply = requires (const A& a, const B& b) { a * b; }; + + template + concept CanDivide = requires (const A& a, const B& b) { a / b; }; + + template + concept CanOrder = requires (const A& a, const B& b) { a < b; }; + + template + concept CanEqual = requires (const A& a, const B& b) { a == b; }; + + template + concept CanAddTo = requires (A& a, const B& b) { a += b; }; + + template + concept CanMultiplyBy = requires (A& a, const B& b) { a *= b; }; + + TEST_CASE("LLUnit: units of different dimensions do not mix", "[LLUnit]") + { + STATIC_REQUIRE(CanAdd); + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanSubtract); + STATIC_REQUIRE_FALSE(CanDivide); + STATIC_REQUIRE_FALSE(CanOrder); + STATIC_REQUIRE_FALSE(CanEqual); + STATIC_REQUIRE_FALSE(CanAddTo); + STATIC_REQUIRE_FALSE(CanAddTo); + + STATIC_REQUIRE(std::is_constructible_v); + STATIC_REQUIRE_FALSE(std::is_constructible_v); + STATIC_REQUIRE_FALSE(std::is_constructible_v); + STATIC_REQUIRE_FALSE(std::is_convertible_v); + } + + TEST_CASE("LLUnit: an explicit unit takes a plain value only as a factor", "[LLUnit]") + { + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanSubtract); + STATIC_REQUIRE(CanMultiply); + STATIC_REQUIRE(CanMultiply); + STATIC_REQUIRE(CanDivide); + STATIC_REQUIRE(CanMultiplyBy); + STATIC_REQUIRE_FALSE(std::is_convertible_v); + + STATIC_REQUIRE(CanAdd); + STATIC_REQUIRE(CanSubtract); + STATIC_REQUIRE(std::is_convertible_v); + } + + TEST_CASE("LLUnit: a product of units is not a unit", "[LLUnit]") + { + STATIC_REQUIRE_FALSE(CanMultiply); + STATIC_REQUIRE_FALSE(CanMultiply); + STATIC_REQUIRE_FALSE(CanMultiplyBy); + STATIC_REQUIRE_FALSE(CanMultiplyBy); + } + + TEST_CASE("LLUnit: compound assignments return the unit", "[LLUnit]") + { + F32QuatloosImplicit quatloos(1.f); + (quatloos += 1) *= 3; + CHECK(quatloos == 6); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + F32Quatloos explicit_quatloos(1.f); + STATIC_REQUIRE(std::is_same_v); + } + + // formatting + TEST_CASE("LLUnit: fmt formats the value as its storage type then the label", "[LLUnit]") + { + CHECK(fmt::format("{}", F32Seconds(1.5f)) == "1.5s"); + CHECK(fmt::format("{:.2f}", F32Seconds(1.5f)) == "1.50s"); + CHECK(fmt::format("{:>4}", S32Bytes(12)) == " 12B"); + CHECK(fmt::format("{}", F32QuatloosImplicit(2.f)) == "2Quat"); + CHECK(fmt::format("{}", U32Microseconds(3)) == "3\xC2\xB5s"); + } }