From f645dc06b5277caaed206e9743320e8be848dfb4 Mon Sep 17 00:00:00 2001 From: Alexey Ochapov Date: Thu, 15 Jul 2021 01:45:46 +0300 Subject: [PATCH] make FP formatting available to be used at compile-time * works only with FMT_HEADER_ONLY * works only with float and double types (not long double) --- include/fmt/core.h | 4 +- include/fmt/format-inl.h | 36 +++++++----- include/fmt/format.h | 116 ++++++++++++++++++++++++++++++--------- test/CMakeLists.txt | 4 ++ test/compile-fp-test.cc | 62 +++++++++++++++++++++ 5 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 test/compile-fp-test.cc diff --git a/include/fmt/core.h b/include/fmt/core.h index 1bfb2e15b2a7c..a007c491f24b1 100644 --- a/include/fmt/core.h +++ b/include/fmt/core.h @@ -1180,8 +1180,8 @@ template class value { constexpr FMT_INLINE value(unsigned long long val) : ulong_long_value(val) {} FMT_INLINE value(int128_t val) : int128_value(val) {} FMT_INLINE value(uint128_t val) : uint128_value(val) {} - FMT_INLINE value(float val) : float_value(val) {} - FMT_INLINE value(double val) : double_value(val) {} + constexpr FMT_INLINE value(float val) : float_value(val) {} + constexpr FMT_INLINE value(double val) : double_value(val) {} FMT_INLINE value(long double val) : long_double_value(val) {} constexpr FMT_INLINE value(bool val) : bool_value(val) {} constexpr FMT_INLINE value(char_type val) : char_value(val) {} diff --git a/include/fmt/format-inl.h b/include/fmt/format-inl.h index 5a5fbea7ef71f..3180c26593bb0 100644 --- a/include/fmt/format-inl.h +++ b/include/fmt/format-inl.h @@ -278,10 +278,11 @@ FMT_CONSTEXPR inline fp operator*(fp x, fp y) { // Returns a cached power of 10 `c_k = c_k.f * pow(2, c_k.e)` such that its // (binary) exponent satisfies `min_exponent <= c_k.e <= min_exponent + 28`. -inline fp get_cached_power(int min_exponent, int& pow10_exponent) { +FMT_CONSTEXPR inline fp get_cached_power(int min_exponent, + int& pow10_exponent) { // Normalized 64-bit significands of pow(10, k), for k = -348, -340, ..., 340. // These are generated by support/compute-powers.py. - static constexpr const uint64_t pow10_significands[] = { + constexpr const uint64_t pow10_significands[] = { 0xfa8fd5a0081c0288, 0xbaaee17fa23ebf76, 0x8b16fb203055ac76, 0xcf42894a5dce35ea, 0x9a6bb0aa55653b2d, 0xe61acf033d1a45df, 0xab70fe17c79ac6ca, 0xff77b1fcbebcdc4f, 0xbe5691ef416bd60c, @@ -315,7 +316,7 @@ inline fp get_cached_power(int min_exponent, int& pow10_exponent) { // Binary exponents of pow(10, k), for k = -348, -340, ..., 340, corresponding // to significands above. - static constexpr const int16_t pow10_exponents[] = { + constexpr const int16_t pow10_exponents[] = { -1220, -1193, -1166, -1140, -1113, -1087, -1060, -1034, -1007, -980, -954, -927, -901, -874, -847, -821, -794, -768, -741, -715, -688, -661, -635, -608, -582, -555, -529, -502, -475, -449, -422, -396, -369, @@ -639,10 +640,10 @@ enum result { }; } -inline uint64_t power_of_10_64(int exp) { - static constexpr const uint64_t data[] = {1, FMT_POWERS_OF_10(1), - FMT_POWERS_OF_10(1000000000ULL), - 10000000000000000000ULL}; +FMT_CONSTEXPR inline uint64_t power_of_10_64(int exp) { + constexpr const uint64_t data[] = {1, FMT_POWERS_OF_10(1), + FMT_POWERS_OF_10(1000000000ULL), + 10000000000000000000ULL}; return data[exp]; } @@ -2230,8 +2231,8 @@ template decimal_fp to_decimal(T x) FMT_NOEXCEPT { // Floating-Point Printout ((FPP)^2) algorithm by Steele & White: // https://fmt.dev/papers/p372-steele.pdf. template -void fallback_format(Double d, int num_digits, bool binary32, buffer& buf, - int& exp10) { +FMT_CONSTEXPR20 void fallback_format(Double d, int num_digits, bool binary32, + buffer& buf, int& exp10) { bigint numerator; // 2 * R in (FPP)^2. bigint denominator; // 2 * S in (FPP)^2. // lower and upper are differences between value and corresponding boundaries. @@ -2347,7 +2348,11 @@ void fallback_format(Double d, int num_digits, bool binary32, buffer& buf, } template -int format_float(T value, int precision, float_specs specs, buffer& buf) { +#ifdef FMT_HEADER_ONLY +FMT_CONSTEXPR20 +#endif + int + format_float(T value, int precision, float_specs specs, buffer& buf) { static_assert(!std::is_same::value, ""); FMT_ASSERT(value >= 0, "value is negative"); @@ -2358,13 +2363,17 @@ int format_float(T value, int precision, float_specs specs, buffer& buf) { return 0; } buf.try_resize(to_unsigned(precision)); - std::uninitialized_fill_n(buf.data(), precision, '0'); + if (is_constant_evaluated()) { + fill_n(buf.data(), precision, '0'); + } else { + std::uninitialized_fill_n(buf.data(), precision, '0'); + } return -precision; } if (!specs.use_grisu) return snprintf_float(value, precision, specs, buf); - if (precision < 0) { + if (!is_constant_evaluated() && precision < 0) { // Use Dragonbox for the shortest format. if (specs.binary32) { auto dec = dragonbox::to_decimal(static_cast(value)); @@ -2390,7 +2399,8 @@ int format_float(T value, int precision, float_specs specs, buffer& buf) { const int max_double_digits = 767; if (precision > max_double_digits) precision = max_double_digits; fixed_handler handler{buf.data(), 0, precision, -cached_exp10, fixed}; - if (grisu_gen_digits(normalized, 1, exp, handler) == digits::error) { + if (grisu_gen_digits(normalized, 1, exp, handler) == digits::error || + is_constant_evaluated()) { exp += handler.size - cached_exp10 - 1; fallback_format(value, handler.precision, specs.binary32, buf, exp); } else { diff --git a/include/fmt/format.h b/include/fmt/format.h index 82ddb407d098e..04fe47f32348a 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -1265,7 +1265,7 @@ constexpr auto exponent_mask() -> // Writes the exponent exp in the form "[+-]d{2,3}" to buffer. template -auto write_exponent(int exp, It it) -> It { +FMT_CONSTEXPR auto write_exponent(int exp, It it) -> It { FMT_ASSERT(-10000 < exp && exp < 10000, "exponent out of range"); if (exp < 0) { *it++ = static_cast('-'); @@ -1286,16 +1286,22 @@ auto write_exponent(int exp, It it) -> It { } template -auto format_float(T value, int precision, float_specs specs, buffer& buf) - -> int; +#ifdef FMT_HEADER_ONLY +FMT_CONSTEXPR20 +#endif + auto + format_float(T value, int precision, float_specs specs, buffer& buf) + -> int; // Formats a floating-point number with snprintf. template auto snprintf_float(T value, int precision, float_specs specs, buffer& buf) -> int; -template auto promote_float(T value) -> T { return value; } -inline auto promote_float(float value) -> double { +template constexpr auto promote_float(T value) -> T { + return value; +} +constexpr auto promote_float(float value) -> double { return static_cast(value); } @@ -1649,8 +1655,9 @@ FMT_CONSTEXPR auto write(OutputIt out, const Char* s, } template -auto write_nonfinite(OutputIt out, bool isinf, basic_format_specs specs, - const float_specs& fspecs) -> OutputIt { +FMT_CONSTEXPR20 auto write_nonfinite(OutputIt out, bool isinf, + basic_format_specs specs, + const float_specs& fspecs) -> OutputIt { auto str = isinf ? (fspecs.upper ? "INF" : "inf") : (fspecs.upper ? "NAN" : "nan"); constexpr size_t str_size = 3; @@ -1673,7 +1680,7 @@ struct big_decimal_fp { int exponent; }; -inline auto get_significand_size(const big_decimal_fp& fp) -> int { +constexpr auto get_significand_size(const big_decimal_fp& fp) -> int { return fp.significand_size; } template @@ -1682,8 +1689,8 @@ inline auto get_significand_size(const dragonbox::decimal_fp& fp) -> int { } template -inline auto write_significand(OutputIt out, const char* significand, - int significand_size) -> OutputIt { +constexpr auto write_significand(OutputIt out, const char* significand, + int significand_size) -> OutputIt { return copy_str(significand, significand + significand_size, out); } template @@ -1736,9 +1743,9 @@ inline auto write_significand(OutputIt out, UInt significand, } template -inline auto write_significand(OutputIt out, const char* significand, - int significand_size, int integral_size, - Char decimal_point) -> OutputIt { +FMT_CONSTEXPR auto write_significand(OutputIt out, const char* significand, + int significand_size, int integral_size, + Char decimal_point) -> OutputIt { out = detail::copy_str_noinline(significand, significand + integral_size, out); if (!decimal_point) return out; @@ -1766,12 +1773,13 @@ inline auto write_significand(OutputIt out, T significand, int significand_size, } template -auto write_float(OutputIt out, const DecimalFP& fp, - const basic_format_specs& specs, float_specs fspecs, - locale_ref loc) -> OutputIt { +FMT_CONSTEXPR20 auto write_float(OutputIt out, const DecimalFP& fp, + const basic_format_specs& specs, + float_specs fspecs, locale_ref loc) + -> OutputIt { auto significand = fp.significand; int significand_size = get_significand_size(fp); - static const Char zero = static_cast('0'); + constexpr const Char zero = static_cast('0'); auto sign = fspecs.sign; size_t size = to_unsigned(significand_size) + (sign ? 1 : 0); using iterator = reserve_iterator; @@ -1871,22 +1879,75 @@ auto write_float(OutputIt out, const DecimalFP& fp, }); } +#ifdef __cpp_lib_bit_cast +template ::value)> +constexpr bool is_infinite_fast_float(T value) { + using floaty = + std::conditional_t::value, double, T>; + using uint = typename dragonbox::float_info::carrier_uint; + constexpr auto significand_bits = + dragonbox::float_info::significand_bits; + auto bits = std::bit_cast(value); + return (bits & exponent_mask()) && + !(bits & ((uint(1) << significand_bits) - 1)); +} + +template ::value)> +constexpr bool is_finite_fast_float(T value) { + using floaty = + std::conditional_t::value, double, T>; + using uint = typename dragonbox::float_info::carrier_uint; + auto bits = std::bit_cast(value); + return (bits & exponent_mask()) != exponent_mask(); +} +#endif + +template ::value)> +FMT_INLINE FMT_CONSTEXPR bool float_signbit(T value) { + if (is_constant_evaluated()) { +#ifdef __cpp_if_constexpr + if constexpr (is_fast_float::value) { + using floaty = + conditional_t::value, double, T>; + using uint = typename dragonbox::float_info::carrier_uint; + auto bits = bit_cast(value); + return (bits & (uint(1) << (num_bits() - 1))) != 0; + } else { + FMT_ASSERT(false, "floating point type is not supported"); + } +#endif + } + return std::signbit(value); +} + template ::value)> -auto write(OutputIt out, T value, basic_format_specs specs, - locale_ref loc = {}) -> OutputIt { +FMT_CONSTEXPR20 auto write(OutputIt out, T value, + basic_format_specs specs, locale_ref loc = {}) + -> OutputIt { if (const_check(!is_supported_floating_point(value))) return out; float_specs fspecs = parse_float_type_spec(specs); fspecs.sign = specs.sign; - if (std::signbit(value)) { // value < 0 is false for NaN so use signbit. + if (float_signbit(value)) { fspecs.sign = sign::minus; value = -value; } else if (fspecs.sign == sign::minus) { fspecs.sign = sign::none; } - if (!std::isfinite(value)) - return write_nonfinite(out, std::isinf(value), specs, fspecs); +#if defined(__cpp_lib_bit_cast) && defined(__cpp_if_constexpr) + if (is_constant_evaluated()) { + if constexpr (is_fast_float::value) { + if (!is_finite_fast_float(value)) + return write_nonfinite(out, is_infinite_fast_float(value), specs, + fspecs); + } + } else +#endif + { + if (!std::isfinite(value)) + return write_nonfinite(out, std::isinf(value), specs, fspecs); + } if (specs.align == align::numeric && fspecs.sign) { auto it = reserve(out, 1); @@ -1920,7 +1981,11 @@ auto write(OutputIt out, T value, basic_format_specs specs, template ::value)> -auto write(OutputIt out, T value) -> OutputIt { +FMT_CONSTEXPR20 auto write(OutputIt out, T value) -> OutputIt { + if (is_constant_evaluated()) { + return write(out, value, basic_format_specs()); + } + if (const_check(!is_supported_floating_point(value))) return out; using floaty = conditional_t::value, double, T>; @@ -1928,13 +1993,12 @@ auto write(OutputIt out, T value) -> OutputIt { auto bits = bit_cast(value); auto fspecs = float_specs(); - auto sign_bit = bits & (uint(1) << (num_bits() - 1)); - if (sign_bit != 0) { + if (float_signbit(value)) { fspecs.sign = sign::minus; value = -value; } - static const auto specs = basic_format_specs(); + constexpr auto specs = basic_format_specs(); uint mask = exponent_mask(); if ((bits & mask) == mask) return write_nonfinite(out, std::isinf(value), specs, fspecs); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bf3785a65d726..d719865ac39d8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -84,6 +84,10 @@ if (NOT (MSVC AND BUILD_SHARED_LIBS)) endif () add_fmt_test(ostream-test) add_fmt_test(compile-test) +add_fmt_test(compile-fp-test HEADER_ONLY) +if (MSVC) + target_compile_options(compile-fp-test PRIVATE /Zc:__cplusplus) +endif() add_fmt_test(printf-test) add_fmt_test(ranges-test) add_fmt_test(scan-test) diff --git a/test/compile-fp-test.cc b/test/compile-fp-test.cc new file mode 100644 index 0000000000000..afedc26d0611f --- /dev/null +++ b/test/compile-fp-test.cc @@ -0,0 +1,62 @@ +// Formatting library for C++ - formatting library tests +// +// Copyright (c) 2012 - present, Victor Zverovich +// All rights reserved. +// +// For the license information refer to format.h. + +#include "fmt/compile.h" +#include "gmock/gmock.h" + +#if defined(__cpp_lib_bit_cast) && __cpp_lib_bit_cast >= 201806 && \ + defined(__cpp_constexpr) && __cpp_constexpr >= 201907 && \ + defined(__cpp_constexpr_dynamic_alloc) && \ + __cpp_constexpr_dynamic_alloc >= 201907 && __cplusplus >= 202002L +template struct test_string { + template constexpr bool operator==(const T& rhs) const noexcept { + return fmt::basic_string_view(rhs).compare(buffer) == 0; + } + Char buffer[max_string_length]{}; +}; + +template +consteval auto test_format(auto format, const Args&... args) { + test_string string{}; + fmt::format_to(string.buffer, format, args...); + return string; +} + +TEST(compile_time_formatting_test, floating_point) { + EXPECT_EQ("0", test_format<2>(FMT_COMPILE("{}"), 0.0f)); + EXPECT_EQ("392.500000", test_format<11>(FMT_COMPILE("{0:f}"), 392.5f)); + + EXPECT_EQ("0", test_format<2>(FMT_COMPILE("{:}"), 0.0)); + EXPECT_EQ("0.000000", test_format<9>(FMT_COMPILE("{:f}"), 0.0)); + EXPECT_EQ("0", test_format<2>(FMT_COMPILE("{:g}"), 0.0)); + EXPECT_EQ("392.65", test_format<7>(FMT_COMPILE("{:}"), 392.65)); + EXPECT_EQ("392.65", test_format<7>(FMT_COMPILE("{:g}"), 392.65)); + EXPECT_EQ("392.65", test_format<7>(FMT_COMPILE("{:G}"), 392.65)); + EXPECT_EQ("4.9014e+06", test_format<11>(FMT_COMPILE("{:g}"), 4.9014e6)); + EXPECT_EQ("-392.650000", test_format<12>(FMT_COMPILE("{:f}"), -392.65)); + EXPECT_EQ("-392.650000", test_format<12>(FMT_COMPILE("{:F}"), -392.65)); + + EXPECT_EQ("3.926500e+02", test_format<13>(FMT_COMPILE("{0:e}"), 392.65)); + EXPECT_EQ("3.926500E+02", test_format<13>(FMT_COMPILE("{0:E}"), 392.65)); + EXPECT_EQ("+0000392.6", test_format<11>(FMT_COMPILE("{0:+010.4g}"), 392.65)); + EXPECT_EQ("9223372036854775808.000000", + test_format<27>(FMT_COMPILE("{:f}"), 9223372036854775807.0)); + + constexpr double nan = std::numeric_limits::quiet_NaN(); + EXPECT_EQ("nan", test_format<4>(FMT_COMPILE("{}"), nan)); + EXPECT_EQ("+nan", test_format<5>(FMT_COMPILE("{:+}"), nan)); + if (std::signbit(-nan)) + EXPECT_EQ("-nan", test_format<5>(FMT_COMPILE("{}"), -nan)); + else + fmt::print("Warning: compiler doesn't handle negative NaN correctly"); + + constexpr double inf = std::numeric_limits::infinity(); + EXPECT_EQ("inf", test_format<4>(FMT_COMPILE("{}"), inf)); + EXPECT_EQ("+inf", test_format<5>(FMT_COMPILE("{:+}"), inf)); + EXPECT_EQ("-inf", test_format<5>(FMT_COMPILE("{}"), -inf)); +} +#endif