Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add floating-point utils #454

Merged
merged 2 commits into from
Aug 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/unittests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ target_sources(
execute_floating_point_test.cpp
execute_numeric_test.cpp
execute_test.cpp
floating_point_utils_test.cpp
instantiate_test.cpp
leb128_test.cpp
module_test.cpp
Expand Down
192 changes: 192 additions & 0 deletions test/unittests/floating_point_utils_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Fizzy: A fast WebAssembly interpreter
// Copyright 2020 The Fizzy Authors.
// SPDX-License-Identifier: Apache-2.0

#include <gtest/gtest.h>
#include <test/utils/floating_point_utils.hpp>

using namespace fizzy::test;

TEST(floating_point_utils, double_as_uint)
{
EXPECT_EQ(FP(0.0).as_uint(), 0x0000000000000000);
EXPECT_EQ(FP(-0.0).as_uint(), 0x8000000000000000);
EXPECT_EQ(FP(FP64::Limits::infinity()).as_uint(), 0x7FF'0000000000000);
EXPECT_EQ(FP(-FP64::Limits::infinity()).as_uint(), 0xFFF'0000000000000);
EXPECT_EQ(FP(FP64::Limits::max()).as_uint(), 0x7FE'FFFFFFFFFFFFF);
EXPECT_EQ(FP(-FP64::Limits::max()).as_uint(), 0xFFE'FFFFFFFFFFFFF);
EXPECT_EQ(FP(FP64::Limits::min()).as_uint(), 0x001'0000000000000);
EXPECT_EQ(FP(-FP64::Limits::min()).as_uint(), 0x801'0000000000000);
EXPECT_EQ(FP(FP64::Limits::denorm_min()).as_uint(), 0x000'0000000000001);
EXPECT_EQ(FP(-FP64::Limits::denorm_min()).as_uint(), 0x800'0000000000001);
EXPECT_EQ(FP(1.0).as_uint(), 0x3FF'0000000000000);
EXPECT_EQ(FP(-1.0).as_uint(), 0xBFF'0000000000000);
EXPECT_EQ(FP(std::nextafter(1.0, 0.0)).as_uint(), 0x3FE'FFFFFFFFFFFFF);
Copy link
Member

@axic axic Aug 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn these helpers, so this is the 1.0 - <smallest representable value> (e.g. 0.999999999999999...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly. This is the next representable value after 1.0 towards 0.0. But should be something like 0.9999....

EXPECT_EQ(FP(std::nextafter(-1.0, 0.0)).as_uint(), 0xBFE'FFFFFFFFFFFFF);
EXPECT_EQ(FP(FP64::nan(FP64::canon)).as_uint(), 0x7FF'8000000000000);
EXPECT_EQ(FP(-FP64::nan(FP64::canon)).as_uint(), 0xFFF'8000000000000);
}

TEST(floating_point_utils, binary_representation_implementation_defined)
{
EXPECT_EQ(FP(FP64::Limits::quiet_NaN()).as_uint(), 0x7FF'8000000000000);
EXPECT_EQ(FP(FP64::Limits::quiet_NaN()).nan_payload(), 0x8000000000000);
EXPECT_EQ(FP(FP64::Limits::signaling_NaN()).nan_payload(), 0x4000000000000);

EXPECT_EQ(FP(FP32::Limits::quiet_NaN()).as_uint(), 0x7FC00000);
EXPECT_EQ(FP(FP32::Limits::quiet_NaN()).nan_payload(), 0x400000);
EXPECT_EQ(FP(FP32::Limits::signaling_NaN()).nan_payload(), 0x200000);
}

TEST(floating_point_utils, float_as_uint)
{
EXPECT_EQ(FP(0.0f).as_uint(), 0x00000000);
EXPECT_EQ(FP(-0.0f).as_uint(), 0x80000000);
EXPECT_EQ(FP(FP32::Limits::infinity()).as_uint(), 0x7F800000);
EXPECT_EQ(FP(-FP32::Limits::infinity()).as_uint(), 0xFF800000);
EXPECT_EQ(FP(FP32::Limits::max()).as_uint(), 0x7F7FFFFF);
EXPECT_EQ(FP(-FP32::Limits::max()).as_uint(), 0xFF7FFFFF);
EXPECT_EQ(FP(FP32::Limits::min()).as_uint(), 0x00800000);
EXPECT_EQ(FP(-FP32::Limits::min()).as_uint(), 0x80800000);
EXPECT_EQ(FP(FP32::Limits::denorm_min()).as_uint(), 0x00000001);
EXPECT_EQ(FP(-FP32::Limits::denorm_min()).as_uint(), 0x80000001);
EXPECT_EQ(FP(1.0f).as_uint(), 0x3F800000);
EXPECT_EQ(FP(-1.0f).as_uint(), 0xBF800000);
EXPECT_EQ(FP(std::nextafter(1.0f, 0.0f)).as_uint(), 0x3F7FFFFF);
EXPECT_EQ(FP(std::nextafter(-1.0f, 0.0f)).as_uint(), 0xBF7FFFFF);
EXPECT_EQ(FP(FP32::nan(FP32::canon)).as_uint(), 0x7FC00000);
EXPECT_EQ(FP(-FP32::nan(FP32::canon)).as_uint(), 0xFFC00000);
}

TEST(floating_point_utils, double_from_uint)
{
EXPECT_EQ(FP(uint64_t{0x0000000000000000}).value, 0.0);
EXPECT_EQ(FP(uint64_t{0x8000000000000000}).value, -0.0);
EXPECT_EQ(FP(uint64_t{0x3FF'000000000DEAD}).value, 0x1.000000000DEADp0);
EXPECT_EQ(FP(uint64_t{0xBFF'000000000DEAD}).value, -0x1.000000000DEADp0);
EXPECT_EQ(FP(uint64_t{0x7FF'0000000000000}).value, FP64::Limits::infinity());
EXPECT_EQ(FP(uint64_t{0xFFF'0000000000000}).value, -FP64::Limits::infinity());
}

TEST(floating_point_utils, float_from_uint)
{
EXPECT_EQ(FP(uint32_t{0x00000000}).value, 0.0f);
EXPECT_EQ(FP(uint32_t{0x80000000}).value, -0.0f);
EXPECT_EQ(FP(uint32_t{0x3FEF5680}).value, 0x1.DEADp0f);
EXPECT_EQ(FP(uint32_t{0xBFEF5680}).value, -0x1.DEADp0f);
EXPECT_EQ(FP(uint32_t{0x7F800000}).value, FP32::Limits::infinity());
EXPECT_EQ(FP(uint32_t{0xFF800000}).value, -FP32::Limits::infinity());
}

TEST(floating_point_utils, double_nan_payload)
{
constexpr auto inf = FP64::Limits::infinity();
const auto qnan = FP64::nan(FP64::canon);

EXPECT_EQ(FP(0.0).nan_payload(), 0);
EXPECT_EQ(FP(FP64::nan(1)).nan_payload(), 1);
EXPECT_EQ(FP(FP64::nan(FP64::canon + 1)).nan_payload(), FP64::canon + 1);
EXPECT_EQ(FP(qnan).nan_payload(), FP64::canon);
EXPECT_EQ(FP(qnan + 1.0).nan_payload(), FP64::canon);
EXPECT_EQ(FP(inf - inf).nan_payload(), FP64::canon);
EXPECT_EQ(FP(inf * 0.0).nan_payload(), FP64::canon);
}

TEST(floating_point_utils, float_nan_payload)
{
constexpr auto inf = FP32::Limits::infinity();
const auto qnan = FP32::nan(FP32::canon);

EXPECT_EQ(FP(0.0f).nan_payload(), 0);
EXPECT_EQ(FP(FP32::nan(1)).nan_payload(), 1);
EXPECT_EQ(FP(FP32::nan(FP32::canon + 1)).nan_payload(), FP32::canon + 1);
EXPECT_EQ(FP(qnan).nan_payload(), FP32::canon);
EXPECT_EQ(FP(qnan + 1.0f).nan_payload(), FP32::canon);
EXPECT_EQ(FP(inf - inf).nan_payload(), FP32::canon);
EXPECT_EQ(FP(inf * 0.0f).nan_payload(), FP32::canon);
}

TEST(floating_point_utils, double_nan)
{
EXPECT_TRUE(std::isnan(FP64::nan(FP64::canon)));
EXPECT_TRUE(std::isnan(FP64::nan(1)));
EXPECT_TRUE(std::isnan(FP64::nan(0xDEADBEEF)));
EXPECT_TRUE(std::isnan(FP64::nan(0xDEADBEEFBEEEF)));
EXPECT_FALSE(std::isnan(FP64::nan(0)));

EXPECT_EQ(FP{FP64::nan(FP64::canon)}.nan_payload(), FP64::canon);

EXPECT_EQ(FP{FP64::nan(FP64::canon)}.as_uint(), 0x7FF'8000000000000);
EXPECT_EQ(FP{FP64::nan(0xDEADBEEF)}.as_uint(), 0x7FF'00000DEADBEEF);
EXPECT_EQ(FP{FP64::nan(0xDEADBEEFBEEEF)}.as_uint(), 0x7FF'DEADBEEFBEEEF);
}

TEST(floating_point_utils, float_nan)
{
EXPECT_TRUE(std::isnan(FP32::nan(FP32::canon)));
EXPECT_TRUE(std::isnan(FP32::nan(1)));
EXPECT_TRUE(std::isnan(FP32::nan(0x7fffff)));
EXPECT_TRUE(std::isnan(FP32::nan(0x400001)));
EXPECT_FALSE(std::isnan(FP32::nan(0)));

EXPECT_EQ(FP{FP32::nan(FP32::canon)}.nan_payload(), FP32::canon);

EXPECT_EQ(FP{FP32::nan(FP32::canon)}.as_uint(), 0x7FC00000);
EXPECT_EQ(FP{FP32::nan(0x7FFFFF)}.as_uint(), 0x7FFFFFFF);
EXPECT_EQ(FP{FP32::nan(0x400001)}.as_uint(), 0x7FC00001);
}

TEST(floating_point_utils, std_nan)
{
EXPECT_EQ(FP(std::nan("")).nan_payload(), FP64::canon);
EXPECT_EQ(FP(std::nan("1")).nan_payload(), FP64::canon + 1);
EXPECT_EQ(FP(std::nan("0xDEAD")).nan_payload(), FP64::canon + 0xDEAD);
}

TEST(floating_point_utils, compare_double)
{
const auto one = 1.0;
const auto inf = FP64::Limits::infinity();
const auto cnan = FP64::nan(FP64::canon);
const auto snan = FP64::nan(1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is snan?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signaling nan


EXPECT_EQ(FP{one}, FP{one});
EXPECT_EQ(FP{one}, one);
EXPECT_EQ(one, FP{one});

EXPECT_EQ(FP{inf}, FP{inf});
EXPECT_EQ(FP{inf}, inf);
EXPECT_EQ(inf, FP{inf});

EXPECT_EQ(FP{cnan}, FP{cnan});
EXPECT_EQ(FP{cnan}, cnan);
EXPECT_EQ(cnan, FP{cnan});

EXPECT_EQ(FP{snan}, FP{snan});
EXPECT_EQ(FP{snan}, snan);
EXPECT_EQ(snan, FP{snan});

EXPECT_NE(FP{one}, FP{inf});
EXPECT_NE(FP{one}, inf);
EXPECT_NE(one, FP{inf});

EXPECT_NE(FP{one}, FP{cnan});
EXPECT_NE(FP{one}, cnan);
EXPECT_NE(one, FP{cnan});

EXPECT_NE(FP{one}, FP{snan});
EXPECT_NE(FP{one}, snan);
EXPECT_NE(one, FP{snan});

EXPECT_NE(FP{inf}, FP{cnan});
EXPECT_NE(FP{inf}, cnan);
EXPECT_NE(inf, FP{cnan});

EXPECT_NE(FP{inf}, FP{snan});
EXPECT_NE(FP{inf}, snan);
EXPECT_NE(inf, FP{snan});

EXPECT_NE(FP{cnan}, FP{snan});
EXPECT_NE(FP{cnan}, snan);
EXPECT_NE(cnan, FP{snan});
}
1 change: 1 addition & 0 deletions test/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ target_sources(
asserts.hpp
execute_helpers.hpp
fizzy_engine.cpp
floating_point_utils.hpp
hex.cpp
hex.hpp
leb128_encode.cpp
Expand Down
7 changes: 3 additions & 4 deletions test/utils/asserts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "execute.hpp"
#include <gmock/gmock.h>
#include <test/utils/floating_point_utils.hpp>
#include <iosfwd>

MATCHER(Traps, "") // NOLINT(readability-redundant-string-init)
Expand All @@ -23,10 +24,8 @@ MATCHER_P(Result, value, "") // NOLINT(readability-redundant-string-init)
if (arg.trapped || !arg.has_value)
return false;

if constexpr (std::is_same_v<value_type, float>)
return arg.value.f32 == value;
else if constexpr (std::is_same_v<value_type, double>)
return arg.value.f64 == value;
if constexpr (std::is_floating_point_v<value_type>)
return arg.value.template as<value_type>() == fizzy::test::FP{value};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This C++ syntax of template just to be able to use as<value_type> is terribly, but it is what it is.

Copy link
Member

@axic axic Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this beneficial over the previous one, btw? Wouldn't it be simpler here just comparing the binary representation?

I understand the FP class will come handy later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FP is comparing binary representation.

The template is a mandatory hint to the compiler that the < in as<value_type> is a template bracket. Fortunately, GCC is giving nice hints when you forget it.

else // always check 64 bit of result for all integers, including 32-bit results
return arg.value.i64 == static_cast<std::make_unsigned_t<value_type>>(value);
}
Expand Down
95 changes: 95 additions & 0 deletions test/utils/floating_point_utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Fizzy: A fast WebAssembly interpreter
// Copyright 2020 The Fizzy Authors.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <cmath>
#include <cstdint>
#include <limits>
#include <type_traits>

namespace fizzy::test
{
/// Simple implementation of C++20's std::bit_cast.
template <typename DstT, typename SrcT>
DstT bit_cast(SrcT x) noexcept
{
DstT z;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x, z are an interesting choice of combination :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took it from Go's BigInt, where z is always the return value. Want me to change it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine, does not matter too much. Would have used something like from/to, a/b, x/y, in/out -- but many of these could be subject to some clashes with certain compilers, so not worth the hassle.

static_assert(sizeof(x) == sizeof(z));
__builtin_memcpy(&z, &x, sizeof(x));
return z;
}

/// A wrapper for floating-point types with inspection/construction/comparison utilities.
template <typename T>
struct FP
{
static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, do you also want to add a static_assert for float/double being iee754 or fine as we have it in value.h and/or the limits check below?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static_assert(Limits::is_iec559); does exactly that, except only when FP is instantiated.


/// Shortcut to numeric_limits.
using Limits = std::numeric_limits<T>;
static_assert(Limits::is_iec559);

/// The unsigned integer type matching the size of this floating-point type.
using UintType = std::conditional_t<std::is_same_v<T, float>, uint32_t, uint64_t>;
static_assert(sizeof(T) == sizeof(UintType));

/// The number of mantissa bits in the binary representation.
static constexpr auto num_mantissa_bits = Limits::digits - 1;

/// The binary mask of the mantissa part of the binary representation.
static constexpr auto mantissa_mask = (UintType{1} << num_mantissa_bits) - 1;

/// The number of exponent bits in the binary representation.
static constexpr auto num_exponent_bits = int{sizeof(T) * 8} - num_mantissa_bits - 1;

/// The exponent value (all exponent bits set) for NaNs.
static constexpr auto nan_exponent = (UintType{1} << num_exponent_bits) - 1;

/// The payload of the canonical NaN (only the top bit set).
/// See: https://webassembly.github.io/spec/core/syntax/values.html#canonical-nan.
static constexpr auto canon = UintType{1} << (num_mantissa_bits - 1);

T value{};

explicit FP(T v) noexcept : value{v} {};

explicit FP(UintType u) noexcept : value{bit_cast<T>(u)} {};

/// Return unsigned integer with the binary representation of the value.
UintType as_uint() const noexcept { return bit_cast<UintType>(value); }

/// Returns NaN payload if the value is a NaN, otherwise 0 (NaN payload is never 0).
UintType nan_payload() const noexcept
{
return std::isnan(value) ? (as_uint() & mantissa_mask) : 0;
}

/// Build the NaN value with the given payload.
Copy link
Member

@axic axic Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the canonical one?

Perhaps add a comment it is canonical by default? Though from a reading-tests-perspective perhaps having nan() and cnan() or canonical_nan() is better.

Copy link
Collaborator Author

@chfast chfast Aug 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can make the argument explicit, as they are coming in groups, e.g. canon, canon+1, 1, ....

///
/// The NaN values have any sign, all exponent bits set, and non-zero mantissa (otherwise they
/// would be infinities).
/// The IEEE 754 defines quiet NaN as having the top bit of the mantissa set to 1. Wasm calls
/// this NaN _arithmetic_. The arithmetic NaN with the lowest mantissa (the top bit set, all
/// other zeros) is the _canonical_ NaN.
static T nan(UintType payload) noexcept
{
return FP{(nan_exponent << num_mantissa_bits) | (payload & mantissa_mask)}.value;
}

friend bool operator==(FP a, FP b) noexcept { return a.as_uint() == b.as_uint(); }
friend bool operator==(FP a, T b) noexcept { return a == FP{b}; }
friend bool operator==(T a, FP b) noexcept { return FP{a} == b; }

friend bool operator!=(FP a, FP b) noexcept { return !(a == b); }
friend bool operator!=(FP a, T b) noexcept { return a != FP{b}; }
friend bool operator!=(T a, FP b) noexcept { return FP{a} != b; }
};

FP(uint32_t)->FP<float>;
FP(uint64_t)->FP<double>;

using FP32 = FP<float>;
using FP64 = FP<double>;
} // namespace fizzy::test