tint: Add more helpers to tint::Number

• Add tint::CheckedConvert for converting between Number values and
  checking that the value fits in the target type.
• Quantize the f16 values.
• Add tint::NumberUnwrapper<T> to obtain the underlying type of a
  number.
• Add ostream '<<' operator.
• Add inequality operators.

Bug: tint:1504
Change-Id: I7afa64867a8df0e55ccee16de14ce6a93fbe1965
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/91303
Reviewed-by: David Neto <dneto@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
This commit is contained in:
Ben Clayton 2022-05-25 15:04:24 +00:00 committed by Dawn LUCI CQ
parent 3c83be8a5b
commit c2eccfc887
6 changed files with 380 additions and 12 deletions

View File

@ -360,6 +360,7 @@ libtint_source_set("libtint_core_all_src") {
"inspector/resource_binding.h",
"inspector/scalar.cc",
"inspector/scalar.h",
"number.cc",
"number.h",
"program.cc",
"program.h",

View File

@ -239,6 +239,7 @@ set(TINT_LIB_SRCS
inspector/resource_binding.h
inspector/scalar.cc
inspector/scalar.h
number.cc
number.h
program_builder.cc
program_builder.h
@ -748,21 +749,23 @@ if(TINT_BUILD_TESTS)
diagnostic/diagnostic_test.cc
diagnostic/formatter_test.cc
diagnostic/printer_test.cc
number_test.cc
program_builder_test.cc
program_test.cc
resolver/array_accessor_test.cc
resolver/assignment_validation_test.cc
resolver/atomics_test.cc
resolver/atomics_validation_test.cc
resolver/attribute_validation_test.cc
resolver/bitcast_validation_test.cc
resolver/builtins_validation_test.cc
resolver/builtin_test.cc
resolver/builtin_validation_test.cc
resolver/builtins_validation_test.cc
resolver/call_test.cc
resolver/call_validation_test.cc
resolver/compound_assignment_validation_test.cc
resolver/compound_statement_test.cc
resolver/control_block_validation_test.cc
resolver/attribute_validation_test.cc
resolver/dependency_graph_test.cc
resolver/entry_point_validation_test.cc
resolver/function_validation_test.cc
@ -815,8 +818,8 @@ if(TINT_BUILD_TESTS)
sem/sem_struct_test.cc
sem/storage_texture_test.cc
sem/texture_test.cc
sem/type_test.cc
sem/type_manager_test.cc
sem/type_test.cc
sem/u32_test.cc
sem/vector_test.cc
source_test.cc

59
src/tint/number.cc Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2022 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "src/tint/number.h"
#include <algorithm>
#include <cstring>
#include <ostream>
namespace tint {
std::ostream& operator<<(std::ostream& out, ConversionFailure failure) {
switch (failure) {
case ConversionFailure::kExceedsPositiveLimit:
return out << "value exceeds positive limit for type";
case ConversionFailure::kExceedsNegativeLimit:
return out << "value exceeds negative limit for type";
case ConversionFailure::kTooSmall:
return out << "value is too small for type";
}
return out << "<unknown>";
}
f16::type f16::Quantize(f16::type value) {
if (value > kHighest) {
return std::numeric_limits<f16::type>::infinity();
}
if (value < kLowest) {
return -std::numeric_limits<f16::type>::infinity();
}
// Below value must be within the finite range of a f16.
uint32_t u32;
memcpy(&u32, &value, 4);
if ((u32 & 0x7fffffffu) == 0) { // ~sign
return value; // +/- zero
}
if ((u32 & 0x7f800000) == 0x7f800000) { // exponent all 1's
return value; // inf or nan
}
// f32 bits : 1 sign, 8 exponent, 23 mantissa
// f16 bits : 1 sign, 5 exponent, 10 mantissa
// Mask the value to preserve the sign, exponent and most-significant 10 mantissa bits.
u32 = u32 & 0xffffe000u;
memcpy(&value, &u32, 4);
return value;
}
} // namespace tint

View File

@ -17,18 +17,72 @@
#include <stdint.h>
#include <functional>
#include <limits>
#include <ostream>
#include "src/tint/utils/result.h"
// Forward declaration
namespace tint {
/// Number wraps a integer or floating point number, enforcing explicit casting.
template <typename T>
struct Number;
} // namespace tint
namespace tint::detail {
/// An empty structure used as a unique template type for Number when
/// specializing for the f16 type.
struct NumberKindF16 {};
/// Helper for obtaining the underlying type for a Number.
template <typename T>
struct NumberUnwrapper {
/// When T is not a Number, then type defined to be T.
using type = T;
};
/// NumberUnwrapper specialization for Number<T>.
template <typename T>
struct NumberUnwrapper<Number<T>> {
/// The Number's underlying type.
using type = typename Number<T>::type;
};
} // namespace tint::detail
namespace tint {
/// Evaluates to true iff T is a floating-point type or is NumberKindF16.
template <typename T>
constexpr bool IsFloatingPoint =
std::is_floating_point_v<T> || std::is_same_v<T, detail::NumberKindF16>;
/// Evaluates to true iff T is an integer type.
template <typename T>
constexpr bool IsInteger = std::is_integral_v<T>;
/// Evaluates to true iff T is an integer type, floating-point type or is NumberKindF16.
template <typename T>
constexpr bool IsNumeric = IsInteger<T> || IsFloatingPoint<T>;
/// Number wraps a integer or floating point number, enforcing explicit casting.
template <typename T>
struct Number {
static_assert(IsNumeric<T>, "Number<T> constructed with non-numeric type");
/// type is the underlying type of the Number
using type = T;
/// Highest finite representable value of this type.
static constexpr type kHighest = std::numeric_limits<type>::max();
/// Lowest finite representable value of this type.
static constexpr type kLowest = std::numeric_limits<type>::lowest();
/// Smallest positive normal value of this type.
static constexpr type kSmallest =
std::is_integral_v<type> ? 0 : std::numeric_limits<type>::min();
/// Constructor. The value is zero-initialized.
Number() = default;
@ -59,41 +113,139 @@ struct Number {
}
/// The number value
T value = {};
type value = {};
};
/// Resolves to the underlying type for a Number.
template <typename T>
using UnwrapNumber = typename detail::NumberUnwrapper<T>::type;
/// Writes the number to the ostream.
/// @param out the std::ostream to write to
/// @param num the Number
/// @return the std::ostream so calls can be chained
template <typename T>
inline std::ostream& operator<<(std::ostream& out, Number<T> num) {
return out << num.value;
}
/// Equality operator.
/// @param a the LHS number
/// @param b the RHS number
/// @returns true if the numbers `a` and `b` are exactly equal.
template <typename A, typename B>
bool operator==(Number<A> a, Number<B> b) {
using T = decltype(a.value + b.value);
return std::equal_to<T>()(a.value, b.value);
return std::equal_to<T>()(static_cast<T>(a.value), static_cast<T>(b.value));
}
/// Inequality operator.
/// @param a the LHS number
/// @param b the RHS number
/// @returns true if the numbers `a` and `b` are exactly unequal.
template <typename A, typename B>
bool operator==(Number<A> a, B b) {
bool operator!=(Number<A> a, Number<B> b) {
return !(a == b);
}
/// Equality operator.
/// @param a the LHS number
/// @param b the RHS number
/// @returns true if the numbers `a` and `b` are exactly equal.
template <typename A, typename B>
std::enable_if_t<IsNumeric<B>, bool> operator==(Number<A> a, B b) {
return a == Number<B>(b);
}
/// Inequality operator.
/// @param a the LHS number
/// @param b the RHS number
/// @returns true if the numbers `a` and `b` are exactly unequal.
template <typename A, typename B>
bool operator==(A a, Number<B> b) {
std::enable_if_t<IsNumeric<B>, bool> operator!=(Number<A> a, B b) {
return !(a == b);
}
/// Equality operator.
/// @param a the LHS number
/// @param b the RHS number
/// @returns true if the numbers `a` and `b` are exactly equal.
template <typename A, typename B>
std::enable_if_t<IsNumeric<A>, bool> operator==(A a, Number<B> b) {
return Number<A>(a) == b;
}
/// Inequality operator.
/// @param a the LHS number
/// @param b the RHS number
/// @returns true if the numbers `a` and `b` are exactly unequal.
template <typename A, typename B>
std::enable_if_t<IsNumeric<A>, bool> operator!=(A a, Number<B> b) {
return !(a == b);
}
/// Enumerator of failure reasons when converting from one number to another.
enum class ConversionFailure {
kExceedsPositiveLimit, // The value was too big (+'ve) to fit in the target type
kExceedsNegativeLimit, // The value was too big (-'ve) to fit in the target type
kTooSmall, // The value was too small to fit in the target type
};
/// Writes the conversion failure message to the ostream.
/// @param out the std::ostream to write to
/// @param failure the ConversionFailure
/// @return the std::ostream so calls can be chained
std::ostream& operator<<(std::ostream& out, ConversionFailure failure);
/// Converts a number from one type to another, checking that the value fits in the target type.
/// @returns the resulting value of the conversion, or a failure reason.
template <typename TO, typename FROM>
utils::Result<TO, ConversionFailure> CheckedConvert(Number<FROM> num) {
using T = decltype(UnwrapNumber<TO>() + num.value);
const auto value = static_cast<T>(num.value);
if (value > static_cast<T>(TO::kHighest)) {
return ConversionFailure::kExceedsPositiveLimit;
}
if (value < static_cast<T>(TO::kLowest)) {
return ConversionFailure::kExceedsNegativeLimit;
}
if constexpr (IsFloatingPoint<UnwrapNumber<TO>>) {
if ((value < T(0) && value > static_cast<T>(-TO::kSmallest)) ||
(value > T(0) && value < static_cast<T>(TO::kSmallest))) {
return ConversionFailure::kTooSmall;
}
}
return TO(value); // Success
}
/// The partial specification of Number for f16 type, storing the f16 value as float,
/// and enforcing proper explicit casting.
template <>
struct Number<detail::NumberKindF16> {
/// C++ does not have a native float16 type, so we use a 32-bit float instead.
using type = float;
/// Highest finite representable value of this type.
static constexpr type kHighest = 65504.0f; // 2¹⁵ × (1 + 1023/1024)
/// Lowest finite representable value of this type.
static constexpr type kLowest = -65504.0f;
/// Smallest positive normal value of this type.
static constexpr type kSmallest = 0.00006103515625f; // 2⁻¹⁴
/// Constructor. The value is zero-initialized.
Number() = default;
/// Constructor.
/// @param v the value to initialize this Number to
template <typename U>
explicit Number(U v) : value(static_cast<float>(v)) {}
explicit Number(U v) : value(Quantize(static_cast<type>(v))) {}
/// Constructor.
/// @param v the value to initialize this Number to
template <typename U>
explicit Number(Number<U> v) : value(static_cast<float>(v.value)) {}
explicit Number(Number<U> v) : value(Quantize(static_cast<type>(v.value))) {}
/// Conversion operator
/// @returns the value as the internal representation type of F16
@ -106,13 +258,20 @@ struct Number<detail::NumberKindF16> {
/// Assignment operator with parameter as native floating point type
/// @param v the new value
/// @returns this Number so calls can be chained
Number& operator=(float v) {
value = v;
Number& operator=(type v) {
value = Quantize(v);
return *this;
}
/// @param value the input float32 value
/// @returns the float32 value quantized to the smaller float16 value, through truncation of the
/// mantissa bits (no rounding). If the float32 value is too large (positive or negative) to be
/// represented by a float16 value, then the returned value will be positive or negative
/// infinity.
static type Quantize(type value);
/// The number value, stored as float
float value = {};
type value = {};
};
/// `AInt` is a type alias to `Number<int64_t>`.

145
src/tint/number_test.cc Normal file
View File

@ -0,0 +1,145 @@
// Copyright 2022 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <cmath>
#include "src/tint/program_builder.h"
#include "gtest/gtest.h"
using namespace tint::number_suffixes; // NOLINT
namespace tint {
namespace {
constexpr int64_t kHighestI32 = static_cast<int64_t>(std::numeric_limits<int32_t>::max());
constexpr int64_t kHighestU32 = static_cast<int64_t>(std::numeric_limits<uint32_t>::max());
constexpr int64_t kLowestI32 = static_cast<int64_t>(std::numeric_limits<int32_t>::min());
constexpr int64_t kLowestU32 = static_cast<int64_t>(std::numeric_limits<uint32_t>::min());
// Highest float32 value. Calculated as:
// (2^127)×(1+(0x7fffff÷0x800000))
constexpr double kHighestF32 = 340282346638528859811704183484516925440.0;
// Next ULP up from kHighestF32 for a float64. Calculated as:
// (2^127)×(1+(0xfffffe0000001÷0x10000000000000))
constexpr double kHighestF32NextULP = 340282346638528897590636046441678635008.0;
// Smallest positive normal float32 value. Calculated as:
// 2^-126
constexpr double kSmallestF32 = 1.1754943508222875e-38;
// Next ULP down from kSmallestF32 for a float64. Calculated as:
// (2^-127)×(1+(0xfffffffffffff÷0x10000000000000))
constexpr double kSmallestF32PrevULP = 1.1754943508222874e-38;
// Highest float16 value. Calculated as:
// (2^15)×(1+(0x3ff÷0x400))
constexpr double kHighestF16 = 65504.0;
// Next ULP up from kHighestF16 for a float64. Calculated as:
// (2^15)×(1+(0xffc0000000001÷0x10000000000000))
constexpr double kHighestF16NextULP = 65504.00000000001;
// Smallest positive normal float16 value. Calculated as:
// 2^-14
constexpr double kSmallestF16 = 0.00006103515625;
// Next ULP down from kSmallestF16 for a float64. Calculated as:
// (2^-15)×(1+(0xfffffffffffff÷0x10000000000000))
constexpr double kSmallestF16PrevULP = 0.00006103515624999999;
constexpr double kLowestF32 = -kHighestF32;
constexpr double kLowestF32NextULP = -kHighestF32NextULP;
constexpr double kLowestF16 = -kHighestF16;
constexpr double kLowestF16NextULP = -kHighestF16NextULP;
TEST(NumberTest, CheckedConvertIdentity) {
EXPECT_EQ(CheckedConvert<AInt>(0_a), 0_a);
EXPECT_EQ(CheckedConvert<AFloat>(0_a), 0.0_a);
EXPECT_EQ(CheckedConvert<i32>(0_i), 0_i);
EXPECT_EQ(CheckedConvert<u32>(0_u), 0_u);
EXPECT_EQ(CheckedConvert<f32>(0_f), 0_f);
EXPECT_EQ(CheckedConvert<f16>(0_h), 0_h);
EXPECT_EQ(CheckedConvert<AInt>(1_a), 1_a);
EXPECT_EQ(CheckedConvert<AFloat>(1_a), 1.0_a);
EXPECT_EQ(CheckedConvert<i32>(1_i), 1_i);
EXPECT_EQ(CheckedConvert<u32>(1_u), 1_u);
EXPECT_EQ(CheckedConvert<f32>(1_f), 1_f);
EXPECT_EQ(CheckedConvert<f16>(1_h), 1_h);
}
TEST(NumberTest, CheckedConvertLargestValue) {
EXPECT_EQ(CheckedConvert<i32>(AInt(kHighestI32)), i32(kHighestI32));
EXPECT_EQ(CheckedConvert<u32>(AInt(kHighestU32)), u32(kHighestU32));
EXPECT_EQ(CheckedConvert<f32>(AFloat(kHighestF32)), f32(kHighestF32));
EXPECT_EQ(CheckedConvert<f16>(AFloat(kHighestF16)), f16(kHighestF16));
}
TEST(NumberTest, CheckedConvertLowestValue) {
EXPECT_EQ(CheckedConvert<i32>(AInt(kLowestI32)), i32(kLowestI32));
EXPECT_EQ(CheckedConvert<u32>(AInt(kLowestU32)), u32(kLowestU32));
EXPECT_EQ(CheckedConvert<f32>(AFloat(kLowestF32)), f32(kLowestF32));
EXPECT_EQ(CheckedConvert<f16>(AFloat(kLowestF16)), f16(kLowestF16));
}
TEST(NumberTest, CheckedConvertSmallestValue) {
EXPECT_EQ(CheckedConvert<i32>(AInt(0)), i32(0));
EXPECT_EQ(CheckedConvert<u32>(AInt(0)), u32(0));
EXPECT_EQ(CheckedConvert<f32>(AFloat(kSmallestF32)), f32(kSmallestF32));
EXPECT_EQ(CheckedConvert<f16>(AFloat(kSmallestF16)), f16(kSmallestF16));
}
TEST(NumberTest, CheckedConvertExceedsPositiveLimit) {
EXPECT_EQ(CheckedConvert<i32>(AInt(kHighestI32 + 1)), ConversionFailure::kExceedsPositiveLimit);
EXPECT_EQ(CheckedConvert<u32>(AInt(kHighestU32 + 1)), ConversionFailure::kExceedsPositiveLimit);
EXPECT_EQ(CheckedConvert<f32>(AFloat(kHighestF32NextULP)),
ConversionFailure::kExceedsPositiveLimit);
EXPECT_EQ(CheckedConvert<f16>(AFloat(kHighestF16NextULP)),
ConversionFailure::kExceedsPositiveLimit);
}
TEST(NumberTest, CheckedConvertExceedsNegativeLimit) {
EXPECT_EQ(CheckedConvert<i32>(AInt(kLowestI32 - 1)), ConversionFailure::kExceedsNegativeLimit);
EXPECT_EQ(CheckedConvert<u32>(AInt(kLowestU32 - 1)), ConversionFailure::kExceedsNegativeLimit);
EXPECT_EQ(CheckedConvert<f32>(AFloat(kLowestF32NextULP)),
ConversionFailure::kExceedsNegativeLimit);
EXPECT_EQ(CheckedConvert<f16>(AFloat(kLowestF16NextULP)),
ConversionFailure::kExceedsNegativeLimit);
}
TEST(NumberTest, CheckedConvertTooSmall) {
EXPECT_EQ(CheckedConvert<f32>(AFloat(kSmallestF32PrevULP)), ConversionFailure::kTooSmall);
EXPECT_EQ(CheckedConvert<f16>(AFloat(kSmallestF16PrevULP)), ConversionFailure::kTooSmall);
}
TEST(NumberTest, QuantizeF16) {
constexpr float nan = std::numeric_limits<float>::quiet_NaN();
constexpr float inf = std::numeric_limits<float>::infinity();
EXPECT_EQ(f16(0.0), 0.0f);
EXPECT_EQ(f16(1.0), 1.0f);
EXPECT_EQ(f16(0.00006106496), 0.000061035156f);
EXPECT_EQ(f16(1.0004883), 1.0f);
EXPECT_EQ(f16(-8196), -8192.f);
EXPECT_EQ(f16(65504.003), inf);
EXPECT_EQ(f16(-65504.003), -inf);
EXPECT_EQ(f16(inf), inf);
EXPECT_EQ(f16(-inf), -inf);
EXPECT_TRUE(std::isnan(f16(nan)));
}
} // namespace
} // namespace tint

View File

@ -727,6 +727,7 @@ tint_unittests_source_set("tint_unittests_core_src") {
"../../src/tint/clone_context_test.cc",
"../../src/tint/debug_test.cc",
"../../src/tint/demangler_test.cc",
"../../src/tint/number_test.cc",
"../../src/tint/program_builder_test.cc",
"../../src/tint/program_test.cc",
"../../src/tint/scope_stack_test.cc",