Skip to content

Commit 81578cd

Browse files
committed
Add float classify()
1 parent a905cc8 commit 81578cd

File tree

7 files changed

+228
-39
lines changed

7 files changed

+228
-39
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ add_library(subspace STATIC
6969
"num/__private/signed_integer_macros.h"
7070
"num/__private/unsigned_integer_macros.h"
7171
"num/float.h"
72+
"num/fp_category.h"
7273
"num/signed_integer.h"
7374
"num/integer_concepts.h"
7475
"num/num_concepts.h"

num/__private/float_macros.h

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "marker/unsafe.h"
2222
#include "num/__private/float_ordering.h"
2323
#include "num/__private/intrinsics.h"
24+
#include "num/fp_category.h"
2425
#include "num/integer_concepts.h"
2526
#include "num/signed_integer.h"
2627
#include "num/unsigned_integer.h"
@@ -546,33 +547,44 @@
546547
} \
547548
static_assert(true)
548549

549-
#define _sus__float_bytes(T, UnsignedIntT) \
550-
/** Raw transmutation from `##UnsignedIntT##`. \
551-
* \
552-
* Note that this function is distinct from Into<##T##>, which attempts to \
553-
* preserve the numeric value, and not the bitwise value. \
554-
* \
555-
* # Examples \
556-
* ``` \
557-
* auto v = f32::from_bits(0x41480000); \
558-
* sus::check!(v, 12.5); \
559-
* ``` \
560-
* \
561-
* This function is not constexpr, as converting to a float does not always \
562-
* preserve the exact bits in a NaN in a constexpr context. \
563-
*/ \
564-
static T from_bits(const UnsignedIntT& v) noexcept { \
565-
return std::bit_cast<T>(v); \
566-
} \
567-
/** Raw transmutation to ##UnsignedT##. \
568-
* \
569-
* Note that this function is distinct from Into<##UnsignedIntT##>, which \
570-
* attempts to preserve the numeric value, and not the bitwise value. \
571-
*/ \
572-
constexpr inline UnsignedIntT to_bits() const& noexcept { \
573-
return std::bit_cast<decltype(UnsignedIntT::primitive_value)>( \
574-
primitive_value); \
575-
} \
550+
#define _sus__float_bytes(T, UnsignedIntT) \
551+
/** Raw transmutation from `##UnsignedIntT##`. \
552+
* \
553+
* Note that this function is distinct from Into<##T##>, which attempts to \
554+
* preserve the numeric value, and not the bitwise value. \
555+
* \
556+
* # Examples \
557+
* ``` \
558+
* auto v = f32::from_bits(0x41480000); \
559+
* sus::check!(v, 12.5); \
560+
* ``` \
561+
* \
562+
* This function is not constexpr, as converting a NaN does not preserve the \
563+
* exact bits in a constexpr context. \
564+
*/ \
565+
static T from_bits(const UnsignedIntT& v) noexcept { \
566+
return std::bit_cast<T>(v); \
567+
} \
568+
/** Raw transmutation to ##UnsignedT##. \
569+
* \
570+
* Note that this function is distinct from Into<##UnsignedIntT##>, which \
571+
* attempts to preserve the numeric value, and not the bitwise value. \
572+
*/ \
573+
constexpr inline UnsignedIntT to_bits() const& noexcept { \
574+
return std::bit_cast<decltype(UnsignedIntT::primitive_value)>( \
575+
primitive_value); \
576+
} \
577+
static_assert(true)
578+
579+
#define _sus__float_category(T) \
580+
/** Returns the floating point category of the number. \
581+
* \
582+
* If only one property is going to be tested, it is generally faster to use \
583+
* the specific predicate instead. \
584+
*/ \
585+
constexpr inline FpCategory classify() const& noexcept { \
586+
return __private::float_category(primitive_value); \
587+
} \
576588
static_assert(true)
577589

578590
// clamp
@@ -596,4 +608,5 @@
596608
_sus__float_fract_trunc(T); \
597609
_sus__float_convert_to(T, PrimitiveT); \
598610
_sus__float_bytes(T, UnsignedIntT); \
611+
_sus__float_category(T); \
599612
static_assert(true)

num/__private/intrinsics.h

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "assertions/check.h"
12
// Copyright 2022 Google LLC
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,8 +26,11 @@
2526
#include <intrin.h>
2627
#endif
2728

29+
#include "assertions/builtin.h"
30+
#include "assertions/unreachable.h"
2831
#include "macros/always_inline.h"
2932
#include "marker/unsafe.h"
33+
#include "num/fp_category.h"
3034

3135
namespace sus::num::__private {
3236

@@ -1121,30 +1125,44 @@ sus_always_inline constexpr T nan() noexcept {
11211125
template <class T>
11221126
requires(std::is_floating_point_v<T>)
11231127
sus_always_inline constexpr T infinity() noexcept {
1128+
// SAFETY: The value being constructed is non a NaN so we can do this in a
1129+
// constexpr way.
11241130
if constexpr (sizeof(T) == sizeof(float))
1125-
return into_float(uint32_t{0x7f800000});
1131+
return into_float_constexpr(unsafe_fn, uint32_t{0x7f800000});
11261132
else
1127-
return into_float(uint64_t{0x7ff0000000000000});
1133+
return into_float_constexpr(unsafe_fn, uint64_t{0x7ff0000000000000});
11281134
}
11291135

11301136
template <class T>
11311137
requires(std::is_floating_point_v<T>)
11321138
sus_always_inline constexpr T negative_infinity() noexcept {
1139+
// SAFETY: The value being constructed is non a NaN so we can do this in a
1140+
// constexpr way.
11331141
if constexpr (sizeof(T) == sizeof(float))
1134-
return into_float(uint32_t{0xff800000});
1142+
return into_float_constexpr(unsafe_fn, uint32_t{0xff800000});
11351143
else
1136-
return into_float(uint64_t{0xfff0000000000000});
1144+
return into_float_constexpr(unsafe_fn, uint64_t{0xfff0000000000000});
11371145
}
11381146

1139-
constexpr int32_t exponent(float x) noexcept {
1147+
constexpr int32_t exponent_bits(float x) noexcept {
11401148
constexpr uint32_t mask = 0b01111111100000000000000000000000;
1141-
return ((into_unsigned_integer(x) & mask) >> 23) - int32_t{127};
1149+
return static_cast<int32_t>(
1150+
unchecked_shr(into_unsigned_integer(x) & mask, 23));
11421151
}
11431152

1144-
constexpr int32_t exponent(double x) noexcept {
1153+
constexpr int32_t exponent_bits(double x) noexcept {
11451154
constexpr uint64_t mask =
11461155
0b0111111111110000000000000000000000000000000000000000000000000000;
1147-
return ((into_unsigned_integer(x) & mask) >> 52) - int32_t{1023};
1156+
return static_cast<int32_t>(
1157+
unchecked_shr(into_unsigned_integer(x) & mask, 52));
1158+
}
1159+
1160+
constexpr int32_t exponent_value(float x) noexcept {
1161+
return exponent_bits(x) - int32_t{127};
1162+
}
1163+
1164+
constexpr int32_t exponent_value(double x) noexcept {
1165+
return exponent_bits(x) - int32_t{1023};
11481166
}
11491167

11501168
constexpr uint32_t mantissa(float x) noexcept {
@@ -1175,15 +1193,23 @@ constexpr bool float_is_inf_or_nan(double x) noexcept {
11751193
}
11761194

11771195
constexpr bool float_is_nan(float x) noexcept {
1196+
#if __has_builtin(__builtin_isnan)
1197+
return __builtin_isnan(x);
1198+
#else
11781199
constexpr auto inf_mask = uint32_t{0x7f800000};
11791200
constexpr auto nan_mask = uint32_t{0x7fffffff};
11801201
return (into_unsigned_integer(x) & nan_mask) > inf_mask;
1202+
#endif
11811203
}
11821204

11831205
constexpr bool float_is_nan(double x) noexcept {
1206+
#if __has_builtin(__builtin_isnan)
1207+
return __builtin_isnan(x);
1208+
#else
11841209
constexpr auto inf_mask = uint64_t{0x7ff0000000000000};
11851210
constexpr auto nan_mask = uint64_t{0x7fffffffffffffff};
11861211
return (into_unsigned_integer(x) & nan_mask) > inf_mask;
1212+
#endif
11871213
}
11881214

11891215
// Assumes that x is a NaN.
@@ -1200,6 +1226,12 @@ constexpr bool float_is_nan_quiet(double x) noexcept {
12001226
return (into_unsigned_integer(x) & quiet_mask) != 0;
12011227
}
12021228

1229+
template <class T>
1230+
requires(std::is_floating_point_v<T> && sizeof(T) <= 8)
1231+
constexpr bool float_is_subnormal(T x) noexcept {
1232+
return exponent_bits(x) == int32_t{0};
1233+
}
1234+
12031235
template <class T>
12041236
requires(std::is_floating_point_v<T> && sizeof(T) <= 8)
12051237
constexpr T truncate_float(T x) noexcept {
@@ -1208,21 +1240,21 @@ constexpr T truncate_float(T x) noexcept {
12081240

12091241
if (float_is_inf_or_nan(x) || float_is_zero(x)) return x;
12101242

1211-
const int32_t exp = exponent(x);
1243+
const int32_t exponent = exponent_value(x);
12121244

12131245
// If the exponent is greater than the most negative mantissa
12141246
// exponent, then x is already an integer.
1215-
if (exp >= static_cast<int32_t>(mantissa_width)) return x;
1247+
if (exponent >= static_cast<int32_t>(mantissa_width)) return x;
12161248

12171249
// If the exponent is such that abs(x) is less than 1, then return 0.
1218-
if (exp <= -1) {
1250+
if (exponent <= -1) {
12191251
if ((into_unsigned_integer(x) & high_bit<T>()) != 0)
12201252
return T{-0.0};
12211253
else
12221254
return T{0.0};
12231255
}
12241256

1225-
const uint32_t trim_bits = mantissa_width - static_cast<uint32_t>(exp);
1257+
const uint32_t trim_bits = mantissa_width - static_cast<uint32_t>(exponent);
12261258
const auto shr = unchecked_shr(into_unsigned_integer(x), trim_bits);
12271259
const auto shl = unchecked_shl(shr, trim_bits);
12281260
// SAFETY: The value here is not a NaN, so will give the same value in
@@ -1252,4 +1284,46 @@ inline T float_round(T x) noexcept {
12521284
(into_unsigned_integer(x) & high_bit<T>()));
12531285
}
12541286

1287+
#if __has_builtin(__builtin_fpclassify)
1288+
template <class T>
1289+
requires(std::is_floating_point_v<T> && sizeof(T) <= 8)
1290+
constexpr inline ::sus::num::FpCategory float_category(T x) noexcept {
1291+
constexpr auto nan = 1;
1292+
constexpr auto inf = 2;
1293+
constexpr auto norm = 3;
1294+
constexpr auto subnorm = 4;
1295+
constexpr auto zero = 5;
1296+
switch (__builtin_fpclassify(nan, inf, norm, subnorm, zero, x)) {
1297+
case nan: return ::sus::num::FpCategory::Nan;
1298+
case inf: return ::sus::num::FpCategory::Infinite;
1299+
case norm: return ::sus::num::FpCategory::Normal;
1300+
case subnorm: return ::sus::num::FpCategory::Subnormal;
1301+
case zero: return ::sus::num::FpCategory::Zero;
1302+
default: ::sus::unreachable_unchecked(unsafe_fn);
1303+
}
1304+
}
1305+
#else
1306+
template <class T>
1307+
requires(std::is_floating_point_v<T> && sizeof(T) <= 8)
1308+
constexpr inline ::sus::num::FpCategory float_category(T x) noexcept {
1309+
if (std::is_constant_evaluated()) {
1310+
if (float_is_nan(x)) return ::sus::num::FpCategory::Nan;
1311+
if (float_is_inf_or_nan(x)) return ::sus::num::FpCategory::Infinite;
1312+
if (float_is_zero(x)) return ::sus::num::FpCategory::Zero;
1313+
if (float_is_subnormal(x)) return ::sus::num::FpCategory::Subnormal;
1314+
return ::sus::num::FpCategory::Normal;
1315+
} else {
1316+
// C++23 requires a constexpr way to do this.
1317+
switch (::fpclassify(x)) {
1318+
case FP_NAN: return ::sus::num::FpCategory::Nan;
1319+
case FP_INFINITE: return ::sus::num::FpCategory::Infinite;
1320+
case FP_NORMAL: return ::sus::num::FpCategory::Normal;
1321+
case FP_SUBNORMAL: return ::sus::num::FpCategory::Subnormal;
1322+
case FP_ZERO: return ::sus::num::FpCategory::Zero;
1323+
default: ::sus::unreachable_unchecked(unsafe_fn);
1324+
}
1325+
}
1326+
}
1327+
#endif
1328+
12551329
} // namespace sus::num::__private

num/f32_unittest.cc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
namespace {
2828

29+
using sus::num::FpCategory;
30+
2931
TEST(f32, Traits) {
3032
static_assert(sus::num::Neg<f32>);
3133
static_assert(sus::num::Add<f32, f32>);
@@ -819,4 +821,34 @@ TEST(f32, ToBits) {
819821
EXPECT_EQ(a, 0x41480000_u32);
820822
}
821823

824+
TEST(f32, Classify) {
825+
EXPECT_EQ(f32::TODO_NAN().classify(), FpCategory::Nan);
826+
EXPECT_EQ(f32::TODO_INFINITY().classify(), FpCategory::Infinite);
827+
EXPECT_EQ(f32::NEG_INFINITY().classify(), FpCategory::Infinite);
828+
EXPECT_EQ((0_f32).classify(), FpCategory::Zero);
829+
EXPECT_EQ((-0_f32).classify(), FpCategory::Zero);
830+
EXPECT_EQ(
831+
f32(std::numeric_limits<decltype(f32::primitive_value)>::denorm_min())
832+
.classify(),
833+
FpCategory::Subnormal);
834+
EXPECT_EQ((123_f32).classify(), FpCategory::Normal);
835+
836+
constexpr auto a = f32::TODO_NAN().classify();
837+
EXPECT_EQ(a, FpCategory::Nan);
838+
constexpr auto b = f32::TODO_INFINITY().classify();
839+
EXPECT_EQ(b, FpCategory::Infinite);
840+
constexpr auto c = f32::NEG_INFINITY().classify();
841+
EXPECT_EQ(c, FpCategory::Infinite);
842+
constexpr auto d = (0_f32).classify();
843+
EXPECT_EQ(d, FpCategory::Zero);
844+
constexpr auto e = (-0_f32).classify();
845+
EXPECT_EQ(e, FpCategory::Zero);
846+
constexpr auto f =
847+
f32(std::numeric_limits<decltype(f32::primitive_value)>::denorm_min())
848+
.classify();
849+
EXPECT_EQ(f, FpCategory::Subnormal);
850+
constexpr auto g = (123_f32).classify();
851+
EXPECT_EQ(g, FpCategory::Normal);
852+
}
853+
822854
} // namespace

num/f64_unittest.cc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
namespace {
2424

25+
using sus::num::FpCategory;
26+
2527
TEST(f64, Traits) {
2628
static_assert(sus::num::Neg<f64>);
2729
static_assert(sus::num::Add<f64, f64>);
@@ -808,4 +810,34 @@ TEST(f64, ToBits) {
808810
EXPECT_EQ(a, 0x4029000000000000_u64);
809811
}
810812

813+
TEST(f64, Classify) {
814+
EXPECT_EQ(f64::TODO_NAN().classify(), FpCategory::Nan);
815+
EXPECT_EQ(f64::TODO_INFINITY().classify(), FpCategory::Infinite);
816+
EXPECT_EQ(f64::NEG_INFINITY().classify(), FpCategory::Infinite);
817+
EXPECT_EQ((0_f64).classify(), FpCategory::Zero);
818+
EXPECT_EQ((-0_f64).classify(), FpCategory::Zero);
819+
EXPECT_EQ(
820+
f64(std::numeric_limits<decltype(f64::primitive_value)>::denorm_min())
821+
.classify(),
822+
FpCategory::Subnormal);
823+
EXPECT_EQ((123_f64).classify(), FpCategory::Normal);
824+
825+
constexpr auto a = f64::TODO_NAN().classify();
826+
EXPECT_EQ(a, FpCategory::Nan);
827+
constexpr auto b = f64::TODO_INFINITY().classify();
828+
EXPECT_EQ(b, FpCategory::Infinite);
829+
constexpr auto c = f64::NEG_INFINITY().classify();
830+
EXPECT_EQ(c, FpCategory::Infinite);
831+
constexpr auto d = (0_f64).classify();
832+
EXPECT_EQ(d, FpCategory::Zero);
833+
constexpr auto e = (-0_f64).classify();
834+
EXPECT_EQ(e, FpCategory::Zero);
835+
constexpr auto f =
836+
f64(std::numeric_limits<decltype(f64::primitive_value)>::denorm_min())
837+
.classify();
838+
EXPECT_EQ(f, FpCategory::Subnormal);
839+
constexpr auto g = (123_f64).classify();
840+
EXPECT_EQ(g, FpCategory::Normal);
841+
}
842+
811843
} // namespace

num/float.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,23 @@
2020

2121
namespace sus::num {
2222

23+
/// A 32-bit floating point type (specifically, the “binary32” type defined in
24+
/// IEEE 754-2008).
25+
///
26+
/// This type can represent a wide range of decimal numbers, like 3.5, 27,
27+
/// -113.75, 0.0078125, 34359738368, 0, -1. So unlike integer types (such as
28+
/// i32), floating point types can represent non-integer numbers, too.
2329
struct f32 final {
2430
_sus__float_consts(f32, f);
2531
_sus__float(f32, float, u32);
2632
};
2733

34+
/// A 64-bit floating point type (specifically, the “binary64” type defined in
35+
/// IEEE 754-2008).
36+
///
37+
/// This type is very similar to `f32`, but has increased precision by using
38+
/// twice as many bits. Please see the documentation for `f32` for more
39+
/// information.
2840
struct f64 final {
2941
_sus__float_consts(f64, );
3042
_sus__float(f64, double, u64);

0 commit comments

Comments
 (0)