/// @file grotto/hexfloat.hpp /// @author Ryan Henry /// @brief Hexadecimal floating-point formatting for IEEE doubles and wide fixed-point words. /// @details /// @copyright Copyright (c) 2019-2023 Ryan Henry and others /// @license Released under a GNU General Public v2.0 (GPLv2) license; /// see [LICENSE.md](@ref GPLv2) for details. #ifndef LIBDPF_INCLUDE_GROTTO_HEXFLOAT_HPP__ #define LIBDPF_INCLUDE_GROTTO_HEXFLOAT_HPP__ #include #include #include #include #include #include #include #include #include #include #include #include #include "grotto/fixedpoint.hpp" namespace grotto { template , class Allocator = std::allocator> std::basic_string to_hexfloat(double x) { static constexpr auto base_indicator = "0x"; if (std::isnan(x)) { return "NaN"; } if (std::isinf(x)) { return std::string(std::signbit(x) ? "-" : "+") + "Infinity"; } if (iszero(x)) { return std::string(std::signbit(x) ? "-" : "+") + "0x0.0000000000000p+0000"; } uint32_t X[2]; std::memcpy(&X[0], &x, sizeof(X)); const int32_t sign_mask = 0x7fffffff; const int32_t exponent_mask = 0x7ff00000; #if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) constexpr unsigned hi = 0; #else constexpr unsigned hi = 1; #endif constexpr unsigned lo = 1u - hi; // extract and clear the exponent int32_t exponent = ((X[hi] & exponent_mask) >> 20) - 0x3ff; if (exponent == -1023) { x *= std::exp2(1023); std::memcpy(&X[0], &x, sizeof(X)); exponent += ((X[hi] & exponent_mask) >> 20) - 0x3ff; } X[hi] = (X[hi] & ~exponent_mask) ^ 0x3ff00000; // extract and clear the sign bit std::string sign_prefix = !!(X[hi] & ~sign_mask) ? "-" : "+"; X[hi] &= sign_mask; uint64_t mantissa = X[lo] | (uint64_t(X[hi]) & 0xfffff) << 32; char buf[13+1]; snprintf(buf, 13+1, "%013lx", mantissa); std::string hex_mantissa = std::string("1.") + std::string(buf); std::string expsign = exponent < 0 ? "p-" : "p+"; exponent = std::abs(exponent); std::string expnum = std::to_string(exponent); std::string exponent_suffix = expsign + std::string(4 - std::min(size_t(4), expnum.length()), '0') + expnum; return sign_prefix + base_indicator + hex_mantissa + exponent_suffix; } namespace detail { template constexpr bool is_hexfloat_integer = std::is_integral_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v; // libstdc++ reports std::is_signed_v<__int128> as false. template constexpr bool hexfloat_is_signed = std::is_signed_v || std::is_same_v; template constexpr bool is_hexfloat_fixedpoint = false; template constexpr bool is_hexfloat_fixedpoint> = true; constexpr void mask_low_bits(std::uint64_t * limbs, unsigned width) noexcept { if (width >= 256u) { return; } const unsigned limb = width / 64u; const unsigned rem = width % 64u; if (rem == 0u) { for (unsigned i = limb; i < 4u; ++i) { limbs[i] = 0; } return; } limbs[limb] &= (std::uint64_t{1} << rem) - 1u; for (unsigned i = limb + 1u; i < 4u; ++i) { limbs[i] = 0; } } constexpr bool bit_is_set(const std::uint64_t * limbs, unsigned bit) noexcept { return ((limbs[bit / 64u] >> (bit % 64u)) & 1u) != 0u; } constexpr void negate_low_bits(std::uint64_t * limbs, unsigned width) noexcept { for (unsigned i = 0; i < 4u; ++i) { limbs[i] = ~limbs[i]; } std::uint64_t carry = 1; for (unsigned i = 0; i < 4u && carry != 0u; ++i) { const std::uint64_t old = limbs[i]; limbs[i] = old + carry; carry = limbs[i] < old ? 1u : 0u; } mask_low_bits(limbs, width); } template constexpr void store_integer_bits(const T & value, std::uint64_t out[4]) noexcept { out[0] = out[1] = out[2] = out[3] = 0; if constexpr (std::is_same_v) { out[0] = value.lower().lower(); out[1] = value.lower().upper(); out[2] = value.upper().lower(); out[3] = value.upper().upper(); } else if constexpr (std::is_same_v) { out[0] = value.lower(); out[1] = value.upper(); } else if constexpr (std::is_same_v || std::is_same_v) { const simde_uint128 bits = static_cast(value); out[0] = static_cast(bits); out[1] = static_cast(bits >> 64); } else { using unsigned_same = std::make_unsigned_t; out[0] = static_cast(static_cast(value)); } } template constexpr T load_integer_bits(const std::uint64_t * limbs) noexcept { if constexpr (std::is_same_v) { return uint256_t{uint128_t{limbs[3], limbs[2]}, uint128_t{limbs[1], limbs[0]}}; } else if constexpr (std::is_same_v) { return uint128_t{limbs[1], limbs[0]}; } else if constexpr (std::is_same_v || std::is_same_v) { const simde_uint128 bits = simde_uint128(limbs[0]) | (simde_uint128(limbs[1]) << 64); return static_cast(bits); } else { using unsigned_same = std::make_unsigned_t; return static_cast(static_cast(limbs[0])); } } inline int highest_bit(const std::uint64_t * limbs) noexcept { for (int i = 3; i >= 0; --i) { if (limbs[i] != 0u) { return i * 64 + 63 - __builtin_clzll(limbs[i]); } } return -1; } /// Exact hexfloat of a two's-complement word whose binary point sits `fractional_bits` /// below bit 0. The fraction keeps every bit below the leading 1, so a 256-bit word /// round-trips. template std::string format_hexfloat(const T & value, int fractional_bits) { const unsigned width = static_cast(dpf::utils::bitlength_of_v); std::uint64_t mag[4]; store_integer_bits(value, mag); mask_low_bits(mag, width); const bool negative = hexfloat_is_signed && width > 0u && bit_is_set(mag, width - 1u); if (negative) { negate_low_bits(mag, width); } const int msb = highest_bit(mag); if (msb < 0) { return std::string(negative ? "-" : "+") + "0x0.0p+0000"; } std::string fraction; for (int bit = msb - 1; bit >= 0; ) { unsigned nibble = 0; for (int k = 0; k < 4; ++k, --bit) { nibble <<= 1u; if (bit >= 0 && bit_is_set(mag, static_cast(bit))) { nibble |= 1u; } } fraction.push_back("0123456789abcdef"[nibble]); } while (fraction.size() > 1u && fraction.back() == '0') { fraction.pop_back(); } if (fraction.empty()) { fraction = "0"; } const long exponent = static_cast(msb) - static_cast(fractional_bits); char expbuf[32]; std::snprintf(expbuf, sizeof expbuf, "%c%04ld", exponent < 0 ? '-' : '+', std::labs(exponent)); return std::string(negative ? "-" : "+") + "0x1." + fraction + "p" + expbuf; } inline int hex_value(char c) noexcept { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } inline void shift_left_limbs(std::vector & limbs, unsigned shift) { if (shift == 0u) { return; } const std::size_t limb_shift = shift / 64u; const unsigned bit_shift = shift % 64u; std::vector out(limbs.size() + limb_shift + 1u, 0); for (std::size_t i = 0; i < limbs.size(); ++i) { out[i + limb_shift] |= limbs[i] << bit_shift; if (bit_shift != 0u) { out[i + limb_shift + 1u] |= limbs[i] >> (64u - bit_shift); } } limbs.swap(out); } inline void shift_right_limbs(std::vector & limbs, unsigned shift) { if (shift == 0u) { return; } const std::size_t limb_shift = shift / 64u; const unsigned bit_shift = shift % 64u; if (limb_shift >= limbs.size()) { limbs.assign(1, 0); return; } std::vector out(limbs.size() - limb_shift, 0); for (std::size_t i = 0; i < out.size(); ++i) { out[i] = limbs[i + limb_shift] >> bit_shift; if (bit_shift != 0u && i + limb_shift + 1u < limbs.size()) { out[i] |= limbs[i + limb_shift + 1u] << (64u - bit_shift); } } limbs.swap(out); } /// Parse `±0x.p±` into the low `width` bits of /// significand * 2^(exponent + fractional_bits), floored toward -inf on the /// discarded fraction and wrapped modulo 2^width. template T integer_from_hexfloat(std::string_view text, int fractional_bits) { const unsigned width = static_cast(dpf::utils::bitlength_of_v); std::size_t i = 0; while (i < text.size() && std::isspace(static_cast(text[i]))) { ++i; } std::size_t end = text.size(); while (end > i && std::isspace(static_cast(text[end - 1]))) { --end; } if (i >= end) { throw std::invalid_argument("empty hexfloat"); } bool negative = false; if (text[i] == '+' || text[i] == '-') { negative = text[i] == '-'; ++i; } if (i + 1 < end && text[i] == '0' && (text[i + 1] == 'x' || text[i + 1] == 'X')) { i += 2; } std::vector nibbles; bool saw_digit = false; bool after_point = false; int fraction_digits = 0; for (; i < end; ++i) { const char c = text[i]; if (c == '.') { if (after_point) { throw std::invalid_argument("malformed hexfloat"); } after_point = true; continue; } const int digit = hex_value(c); if (digit < 0) { break; } nibbles.push_back(digit); saw_digit = true; if (after_point) { ++fraction_digits; } } if (!saw_digit || i >= end || (text[i] != 'p' && text[i] != 'P')) { throw std::invalid_argument("malformed hexfloat"); } ++i; int exp_sign = 1; if (i < end && (text[i] == '+' || text[i] == '-')) { exp_sign = text[i] == '-' ? -1 : 1; ++i; } if (i >= end || !std::isdigit(static_cast(text[i]))) { throw std::invalid_argument("malformed hexfloat exponent"); } long exponent = 0; for (; i < end && std::isdigit(static_cast(text[i])); ++i) { exponent = std::min(1000000000L, exponent * 10 + (text[i] - '0')); } if (i != end) { throw std::invalid_argument("malformed hexfloat"); } exponent *= exp_sign; std::vector sig(1, 0); for (int nibble : nibbles) { std::uint64_t carry = static_cast(nibble); for (std::size_t k = 0; k < sig.size(); ++k) { const unsigned __int128 prod = static_cast(sig[k]) * 16u + carry; sig[k] = static_cast(prod); carry = static_cast(prod >> 64); } if (carry != 0u) { sig.push_back(carry); } } const long long placed = static_cast(exponent) - 4LL * fraction_digits + static_cast(fractional_bits); std::uint64_t bits[4] = {}; const bool sig_zero = sig.size() == 1u && sig[0] == 0u; if (!sig_zero && placed < static_cast(width)) { if (placed >= 0) { shift_left_limbs(sig, static_cast(placed)); } else { const unsigned down = static_cast(std::min(-placed, 1000000000LL)); shift_right_limbs(sig, down); } for (std::size_t k = 0; k < 4u && k < sig.size(); ++k) { bits[k] = sig[k]; } mask_low_bits(bits, width); if (negative && highest_bit(bits) >= 0) { negate_low_bits(bits, width); } } return load_integer_bits(bits); } } // namespace detail template std::enable_if_t, std::string> to_hexfloat(T value) { return detail::format_hexfloat(value, 0); } template std::string to_hexfloat(fixedpoint value) { return detail::format_hexfloat(value.integral_representation(), FractionalBits); } template T from_hexfloat(std::string_view text) { if constexpr (std::is_same_v) { const std::string owned{text}; char * end = nullptr; const double parsed = std::strtod(owned.c_str(), &end); if (end == owned.c_str()) { throw std::invalid_argument("malformed hexfloat"); } while (*end != '\0' && std::isspace(static_cast(*end))) { ++end; } if (*end != '\0') { throw std::invalid_argument("malformed hexfloat"); } return parsed; } else if constexpr (detail::is_hexfloat_fixedpoint) { const auto bits = detail::integer_from_hexfloat( text, T::fractional_bits); return make_fixed_from_integral_type(bits); } else { static_assert(detail::is_hexfloat_integer, "from_hexfloat expects a floating, integral, or fixedpoint type"); return detail::integer_from_hexfloat(text, 0); } } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_HEXFLOAT_HPP__