diff --git a/src/core/include/units/magnitude.h b/src/core/include/units/magnitude.h index 3cec3b98..ffd86e7e 100644 --- a/src/core/include/units/magnitude.h +++ b/src/core/include/units/magnitude.h @@ -28,6 +28,7 @@ #include #include #include +#include #include namespace units { @@ -282,6 +283,20 @@ constexpr bool is_valid_base_power(const BasePower auto& bp) } if constexpr (std::is_same_v) { + // Some prime numbers are so big, that we can't check their primality without exhausting limits on constexpr steps + // and/or iterations. We can still _perform_ the factorization for these by using the `known_first_factor` + // workaround. However, we can't _check_ that they are prime, because this workaround depends on the input being + // usable in a constexpr expression. This is true for `prime_factorization` (below), where the input `N` is a + // template parameter, but is not true for our case, where the input `bp.get_base()` is a function parameter. (See + // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1045r1.html for some background reading on this + // distinction.) + // + // In our case: we simply give up on excluding every possible ill-formed base power, and settle for catching the + // most likely and common mistakes. + if (const bool too_big_to_check = (bp.get_base() > 1'000'000'000)) { + return true; + } + return is_prime(bp.get_base()); } else { return bp.get_base() > 0; @@ -468,12 +483,30 @@ constexpr auto operator/(Magnitude auto l, Magnitude auto r) { return l * pow<-1 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // `as_magnitude()` implementation. +// Sometimes we need to give the compiler a "shortcut" when factorizing large numbers (specifically, numbers whose +// _first factor_ is very large). If we don't, we can run into limits on the number of constexpr steps or iterations. +// +// To provide the first factor for a given number, specialize this variable template. +// +// WARNING: The program behaviour will be undefined if you provide a wrong answer, so check your math! +template +inline constexpr std::optional known_first_factor = std::nullopt; + namespace detail { // Helper to perform prime factorization at compile time. template requires(N > 0) struct prime_factorization { - static constexpr std::intmax_t first_base = static_cast(Factorizer::find_first_factor(N)); + static constexpr std::intmax_t get_or_compute_first_factor() + { + if constexpr (known_first_factor.has_value()) { + return known_first_factor.value(); + } else { + return static_cast(Factorizer::find_first_factor(N)); + } + } + + static constexpr std::intmax_t first_base = get_or_compute_first_factor(); static constexpr std::intmax_t first_power = multiplicity(first_base, N); static constexpr std::intmax_t remainder = remove_power(first_base, first_power, N); diff --git a/test/unit_test/runtime/magnitude_test.cpp b/test/unit_test/runtime/magnitude_test.cpp index 40c8c1b1..6df348d4 100644 --- a/test/unit_test/runtime/magnitude_test.cpp +++ b/test/unit_test/runtime/magnitude_test.cpp @@ -28,6 +28,9 @@ using namespace units; using namespace units::detail; +template<> +inline constexpr std::optional units::known_first_factor<9223372036854775783> = 9223372036854775783; + namespace { // A set of non-standard bases for testing purposes. @@ -149,7 +152,17 @@ TEST_CASE("make_ratio performs prime factorization correctly") // all odd numbers up to sqrt(N), will exceed this limit for the following prime. Thus, for this test to pass, we // need to be using a more efficient algorithm. (We could increase the limit, but we don't want users to have to // mess with compiler flags just to compile the code.) - as_magnitude<334524384739>(); + as_magnitude<334'524'384'739>(); + } + + SECTION ("Can bypass computing primes by providing known_first_factor") { + // Sometimes, even wheel factorization isn't enough to handle the compilers' limits on constexpr steps and/or + // iterations. To work around these cases, we can explicitly provide the correct answer directly to the compiler. + // + // In this case, we test that we can represent the largest prime that fits in a signed 64-bit int. The reason this + // test can pass is that we have provided the answer, by specializing the `known_first_factor` variable template + // above in this file. + as_magnitude<9'223'372'036'854'775'783>(); } }