Skip to content

Commit

Permalink
make FP formatting available to be used at compile-time
Browse files Browse the repository at this point in the history
* works only with FMT_HEADER_ONLY
* works only with float and double types (not long double)
  • Loading branch information
alexezeder committed Aug 28, 2021
1 parent 8ef22f7 commit f645dc0
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 41 deletions.
4 changes: 2 additions & 2 deletions include/fmt/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -1180,8 +1180,8 @@ template <typename Context> 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) {}
Expand Down
36 changes: 23 additions & 13 deletions include/fmt/format-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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];
}

Expand Down Expand Up @@ -2230,8 +2231,8 @@ template <typename T> decimal_fp<T> to_decimal(T x) FMT_NOEXCEPT {
// Floating-Point Printout ((FPP)^2) algorithm by Steele & White:
// https://fmt.dev/papers/p372-steele.pdf.
template <typename Double>
void fallback_format(Double d, int num_digits, bool binary32, buffer<char>& buf,
int& exp10) {
FMT_CONSTEXPR20 void fallback_format(Double d, int num_digits, bool binary32,
buffer<char>& 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.
Expand Down Expand Up @@ -2347,7 +2348,11 @@ void fallback_format(Double d, int num_digits, bool binary32, buffer<char>& buf,
}

template <typename T>
int format_float(T value, int precision, float_specs specs, buffer<char>& buf) {
#ifdef FMT_HEADER_ONLY
FMT_CONSTEXPR20
#endif
int
format_float(T value, int precision, float_specs specs, buffer<char>& buf) {
static_assert(!std::is_same<T, float>::value, "");
FMT_ASSERT(value >= 0, "value is negative");

Expand All @@ -2358,13 +2363,17 @@ int format_float(T value, int precision, float_specs specs, buffer<char>& 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<float>(value));
Expand All @@ -2390,7 +2399,8 @@ int format_float(T value, int precision, float_specs specs, buffer<char>& 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 {
Expand Down
116 changes: 90 additions & 26 deletions include/fmt/format.h
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ constexpr auto exponent_mask() ->

// Writes the exponent exp in the form "[+-]d{2,3}" to buffer.
template <typename Char, typename It>
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<Char>('-');
Expand All @@ -1286,16 +1286,22 @@ auto write_exponent(int exp, It it) -> It {
}

template <typename T>
auto format_float(T value, int precision, float_specs specs, buffer<char>& buf)
-> int;
#ifdef FMT_HEADER_ONLY
FMT_CONSTEXPR20
#endif
auto
format_float(T value, int precision, float_specs specs, buffer<char>& buf)
-> int;

// Formats a floating-point number with snprintf.
template <typename T>
auto snprintf_float(T value, int precision, float_specs specs,
buffer<char>& buf) -> int;

template <typename T> auto promote_float(T value) -> T { return value; }
inline auto promote_float(float value) -> double {
template <typename T> constexpr auto promote_float(T value) -> T {
return value;
}
constexpr auto promote_float(float value) -> double {
return static_cast<double>(value);
}

Expand Down Expand Up @@ -1649,8 +1655,9 @@ FMT_CONSTEXPR auto write(OutputIt out, const Char* s,
}

template <typename Char, typename OutputIt>
auto write_nonfinite(OutputIt out, bool isinf, basic_format_specs<Char> specs,
const float_specs& fspecs) -> OutputIt {
FMT_CONSTEXPR20 auto write_nonfinite(OutputIt out, bool isinf,
basic_format_specs<Char> specs,
const float_specs& fspecs) -> OutputIt {
auto str =
isinf ? (fspecs.upper ? "INF" : "inf") : (fspecs.upper ? "NAN" : "nan");
constexpr size_t str_size = 3;
Expand All @@ -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 <typename T>
Expand All @@ -1682,8 +1689,8 @@ inline auto get_significand_size(const dragonbox::decimal_fp<T>& fp) -> int {
}

template <typename Char, typename OutputIt>
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<Char>(significand, significand + significand_size, out);
}
template <typename Char, typename OutputIt, typename UInt>
Expand Down Expand Up @@ -1736,9 +1743,9 @@ inline auto write_significand(OutputIt out, UInt significand,
}

template <typename OutputIt, typename Char>
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<Char>(significand,
significand + integral_size, out);
if (!decimal_point) return out;
Expand Down Expand Up @@ -1766,12 +1773,13 @@ inline auto write_significand(OutputIt out, T significand, int significand_size,
}

template <typename OutputIt, typename DecimalFP, typename Char>
auto write_float(OutputIt out, const DecimalFP& fp,
const basic_format_specs<Char>& specs, float_specs fspecs,
locale_ref loc) -> OutputIt {
FMT_CONSTEXPR20 auto write_float(OutputIt out, const DecimalFP& fp,
const basic_format_specs<Char>& 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<Char>('0');
constexpr const Char zero = static_cast<Char>('0');
auto sign = fspecs.sign;
size_t size = to_unsigned(significand_size) + (sign ? 1 : 0);
using iterator = reserve_iterator<OutputIt>;
Expand Down Expand Up @@ -1871,22 +1879,75 @@ auto write_float(OutputIt out, const DecimalFP& fp,
});
}

#ifdef __cpp_lib_bit_cast
template <typename T, FMT_ENABLE_IF(is_fast_float<T>::value)>
constexpr bool is_infinite_fast_float(T value) {
using floaty =
std::conditional_t<std::is_same<T, long double>::value, double, T>;
using uint = typename dragonbox::float_info<floaty>::carrier_uint;
constexpr auto significand_bits =
dragonbox::float_info<floaty>::significand_bits;
auto bits = std::bit_cast<uint>(value);
return (bits & exponent_mask<floaty>()) &&
!(bits & ((uint(1) << significand_bits) - 1));
}

template <typename T, FMT_ENABLE_IF(is_fast_float<T>::value)>
constexpr bool is_finite_fast_float(T value) {
using floaty =
std::conditional_t<std::is_same<T, long double>::value, double, T>;
using uint = typename dragonbox::float_info<floaty>::carrier_uint;
auto bits = std::bit_cast<uint>(value);
return (bits & exponent_mask<floaty>()) != exponent_mask<floaty>();
}
#endif

template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value)>
FMT_INLINE FMT_CONSTEXPR bool float_signbit(T value) {
if (is_constant_evaluated()) {
#ifdef __cpp_if_constexpr
if constexpr (is_fast_float<T>::value) {
using floaty =
conditional_t<std::is_same<T, long double>::value, double, T>;
using uint = typename dragonbox::float_info<floaty>::carrier_uint;
auto bits = bit_cast<uint>(value);
return (bits & (uint(1) << (num_bits<uint>() - 1))) != 0;
} else {
FMT_ASSERT(false, "floating point type is not supported");
}
#endif
}
return std::signbit(value);
}

template <typename Char, typename OutputIt, typename T,
FMT_ENABLE_IF(std::is_floating_point<T>::value)>
auto write(OutputIt out, T value, basic_format_specs<Char> specs,
locale_ref loc = {}) -> OutputIt {
FMT_CONSTEXPR20 auto write(OutputIt out, T value,
basic_format_specs<Char> 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<T>::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);
Expand Down Expand Up @@ -1920,21 +1981,24 @@ auto write(OutputIt out, T value, basic_format_specs<Char> specs,

template <typename Char, typename OutputIt, typename T,
FMT_ENABLE_IF(is_fast_float<T>::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<Char>());
}

if (const_check(!is_supported_floating_point(value))) return out;

using floaty = conditional_t<std::is_same<T, long double>::value, double, T>;
using uint = typename dragonbox::float_info<floaty>::carrier_uint;
auto bits = bit_cast<uint>(value);

auto fspecs = float_specs();
auto sign_bit = bits & (uint(1) << (num_bits<uint>() - 1));
if (sign_bit != 0) {
if (float_signbit(value)) {
fspecs.sign = sign::minus;
value = -value;
}

static const auto specs = basic_format_specs<Char>();
constexpr auto specs = basic_format_specs<Char>();
uint mask = exponent_mask<floaty>();
if ((bits & mask) == mask)
return write_nonfinite(out, std::isinf(value), specs, fspecs);
Expand Down
4 changes: 4 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
62 changes: 62 additions & 0 deletions test/compile-fp-test.cc
Original file line number Diff line number Diff line change
@@ -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 <size_t max_string_length, typename Char = char> struct test_string {
template <typename T> constexpr bool operator==(const T& rhs) const noexcept {
return fmt::basic_string_view<Char>(rhs).compare(buffer) == 0;
}
Char buffer[max_string_length]{};
};

template <size_t max_string_length, typename Char = char, typename... Args>
consteval auto test_format(auto format, const Args&... args) {
test_string<max_string_length, Char> 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<double>::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<double>::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

0 comments on commit f645dc0

Please sign in to comment.