diff --git a/test/unittests/CMakeLists.txt b/test/unittests/CMakeLists.txt index 6c2ea9951..ca372ac69 100644 --- a/test/unittests/CMakeLists.txt +++ b/test/unittests/CMakeLists.txt @@ -19,7 +19,9 @@ target_sources( execute_call_test.cpp execute_control_test.cpp execute_death_test.cpp + execute_floating_point_conversion_test.cpp execute_floating_point_test.cpp + execute_floating_point_test.hpp execute_numeric_test.cpp execute_test.cpp floating_point_utils_test.cpp diff --git a/test/unittests/execute_floating_point_conversion_test.cpp b/test/unittests/execute_floating_point_conversion_test.cpp new file mode 100644 index 000000000..bbdb67781 --- /dev/null +++ b/test/unittests/execute_floating_point_conversion_test.cpp @@ -0,0 +1,559 @@ +// Fizzy: A fast WebAssembly interpreter +// Copyright 2020 The Fizzy Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "execute.hpp" +#include "execute_floating_point_test.hpp" +#include "instructions.hpp" +#include "parser.hpp" +#include "trunc_boundaries.hpp" +#include +#include +#include + +using namespace fizzy; +using namespace fizzy::test; + +TEST(execute_floating_point_conversion, f64_promote_f32) +{ + /* wat2wasm + (func (param f32) (result f64) + local.get 0 + f64.promote_f32 + ) + */ + const auto wasm = from_hex("0061736d0100000001060160017d017c030201000a070105002000bb0b"); + auto instance = instantiate(parse(wasm)); + + const std::pair test_cases[] = { + {0.0f, 0.0}, + {-0.0f, -0.0}, + {1.0f, 1.0}, + {-1.0f, -1.0}, + {FP32::Limits::lowest(), double{FP32::Limits::lowest()}}, + {FP32::Limits::max(), double{FP32::Limits::max()}}, + {FP32::Limits::min(), double{FP32::Limits::min()}}, + {FP32::Limits::denorm_min(), double{FP32::Limits::denorm_min()}}, + {FP32::Limits::infinity(), FP64::Limits::infinity()}, + {-FP32::Limits::infinity(), -FP64::Limits::infinity()}, + + // The canonical NaN must result in canonical NaN (only the top bit set). + {FP32::nan(FP32::canon), FP64::nan(FP64::canon)}, + {-FP32::nan(FP32::canon), -FP64::nan(FP64::canon)}, + }; + + ASSERT_EQ(std::fegetround(), FE_TONEAREST); + for (const auto rounding_direction : all_rounding_directions) + { + ASSERT_EQ(std::fesetround(rounding_direction), 0); + SCOPED_TRACE(rounding_direction); + + for (const auto& [arg, expected] : test_cases) + { + EXPECT_THAT(execute(*instance, 0, {arg}), Result(expected)) + << arg << " -> " << expected; + } + + // Check arithmetic NaNs (payload >= canonical payload). + // The following check expect arithmetic NaNs. Canonical NaNs are arithmetic NaNs + // and are allowed by the spec in these situations, but our checks are more restrictive + + // An arithmetic NaN must result in any arithmetic NaN. + const auto res1 = execute(*instance, 0, {FP32::nan(FP32::canon + 1)}); + ASSERT_TRUE(!res1.trapped && res1.has_value); + EXPECT_EQ(std::signbit(res1.value.f64), 0); + EXPECT_GT(FP{res1.value.f64}.nan_payload(), FP64::canon); + const auto res2 = execute(*instance, 0, {-FP32::nan(FP32::canon + 1)}); + ASSERT_TRUE(!res2.trapped && res2.has_value); + EXPECT_EQ(std::signbit(res2.value.f64), 1); + EXPECT_GT(FP{res2.value.f64}.nan_payload(), FP64::canon); + + // Other NaN must also result in arithmetic NaN. + const auto res3 = execute(*instance, 0, {FP32::nan(1)}); + ASSERT_TRUE(!res3.trapped && res3.has_value); + EXPECT_EQ(std::signbit(res3.value.f64), 0); + EXPECT_GT(FP{res3.value.f64}.nan_payload(), FP64::canon); + const auto res4 = execute(*instance, 0, {-FP32::nan(1)}); + ASSERT_TRUE(!res4.trapped && res4.has_value); + EXPECT_EQ(std::signbit(res4.value.f64), 1); + EXPECT_GT(FP{res4.value.f64}.nan_payload(), FP64::canon); + + // Any input NaN other than canonical must result in an arithmetic NaN. + for (const auto nan : TestValues::positive_noncanonical_nans()) + { + EXPECT_THAT(execute(*instance, 0, {nan}), ArithmeticNaN(double{})); + EXPECT_THAT(execute(*instance, 0, {-nan}), ArithmeticNaN(double{})); + } + } + ASSERT_EQ(std::fesetround(FE_TONEAREST), 0); +} + +TEST(execute_floating_point_conversion, f32_demote_f64) +{ + /* wat2wasm + (func (param f64) (result f32) + local.get 0 + f32.demote_f64 + ) + */ + const auto wasm = from_hex("0061736d0100000001060160017c017d030201000a070105002000b60b"); + auto instance = instantiate(parse(wasm)); + + constexpr double f32_max = FP32::Limits::max(); + ASSERT_EQ(f32_max, 0x1.fffffep127); + + // The "artificial" f32 range limit: the next f32 number that could be represented + // if the exponent had a larger range. + // Wasm spec Rounding section denotes this as the limit_N in the float_N function (for N=32). + // https://webassembly.github.io/spec/core/exec/numerics.html#rounding + constexpr double f32_limit = 0x1p128; // 2**128. + + // The lower boundary input value that results in the infinity. The number is midway between + // f32_max and f32_limit. For this value rounding prefers infinity, because f32_limit is even. + constexpr double lowest_to_inf = (f32_max + f32_limit) / 2; + ASSERT_EQ(lowest_to_inf, 0x1.ffffffp127); + + const std::pair test_cases[] = { + // demote(+-0) = +-0 + {0.0, 0.0f}, + {-0.0, -0.0f}, + + {1.0, 1.0f}, + {-1.0, -1.0f}, + {double{FP32::Limits::lowest()}, FP32::Limits::lowest()}, + {double{FP32::Limits::max()}, FP32::Limits::max()}, + {double{FP32::Limits::min()}, FP32::Limits::min()}, + {double{-FP32::Limits::min()}, -FP32::Limits::min()}, + {double{FP32::Limits::denorm_min()}, FP32::Limits::denorm_min()}, + {double{-FP32::Limits::denorm_min()}, -FP32::Limits::denorm_min()}, + + // Some special f64 values. + {FP64::Limits::lowest(), -FP32::Limits::infinity()}, + {FP64::Limits::max(), FP32::Limits::infinity()}, + {FP64::Limits::min(), 0.0f}, + {-FP64::Limits::min(), -0.0f}, + {FP64::Limits::denorm_min(), 0.0f}, + {-FP64::Limits::denorm_min(), -0.0f}, + + // Out of range values rounded to max/lowest. + {std::nextafter(f32_max, FP64::Limits::infinity()), FP32::Limits::max()}, + {std::nextafter(double{FP32::Limits::lowest()}, -FP64::Limits::infinity()), + FP32::Limits::lowest()}, + + {std::nextafter(lowest_to_inf, 0.0), FP32::Limits::max()}, + {std::nextafter(-lowest_to_inf, 0.0), FP32::Limits::lowest()}, + + // The smallest of range values rounded to infinity. + {lowest_to_inf, FP32::Limits::infinity()}, + {-lowest_to_inf, -FP32::Limits::infinity()}, + + {std::nextafter(lowest_to_inf, FP64::Limits::infinity()), FP32::Limits::infinity()}, + {std::nextafter(-lowest_to_inf, -FP64::Limits::infinity()), -FP32::Limits::infinity()}, + + // float_32(r) = +inf (if r >= +limit_32) + {f32_limit, FP32::Limits::infinity()}, + + // float_32(r) = -inf (if r <= -limit_32) + {-f32_limit, -FP32::Limits::infinity()}, + + // demote(+-inf) = +-inf + {FP64::Limits::infinity(), FP32::Limits::infinity()}, + {-FP64::Limits::infinity(), -FP32::Limits::infinity()}, + + // Rounding. + {0x1.fffffefffffffp0, 0x1.fffffep0f}, // round down + {0x1.fffffe0000000p0, 0x1.fffffep0f}, // exact (odd) + {0x1.fffffd0000001p0, 0x1.fffffep0f}, // round up + + {0x1.fffff8p0, 0x1.fffff8p0f}, // exact (even) + {(0x1.fffff8p0 + 0x1.fffffap0) / 2, 0x1.fffff8p0f}, // tie-to-even down + {0x1.fffffap0, 0x1.fffffap0f}, // exact (odd) + {(0x1.fffffap0 + 0x1.fffffcp0) / 2, 0x1.fffffcp0f}, // tie-to-even up + {0x1.fffffcp0, 0x1.fffffcp0f}, // exact (even) + + // The canonical NaN must result in canonical NaN (only the top bit of payload set). + {FP32::nan(FP32::canon), FP64::nan(FP64::canon)}, + {-FP32::nan(FP32::canon), -FP64::nan(FP64::canon)}, + }; + + for (const auto& [arg, expected] : test_cases) + { + EXPECT_THAT(execute(*instance, 0, {arg}), Result(expected)) << arg << " -> " << expected; + } + + // Any input NaN other than canonical must result in an arithmetic NaN. + for (const auto nan : TestValues::positive_noncanonical_nans()) + { + EXPECT_THAT(execute(*instance, 0, {nan}), ArithmeticNaN(float{})); + EXPECT_THAT(execute(*instance, 0, {-nan}), ArithmeticNaN(float{})); + } +} + +TYPED_TEST(execute_floating_point_types, reinterpret) +{ + /* wat2wasm + (func (param f32) (result i32) (i32.reinterpret_f32 (local.get 0))) + (func (param f64) (result i64) (i64.reinterpret_f64 (local.get 0))) + (func (param i32) (result f32) (f32.reinterpret_i32 (local.get 0))) + (func (param i64) (result f64) (f64.reinterpret_i64 (local.get 0))) + */ + const auto wasm = from_hex( + "0061736d0100000001150460017d017f60017c017e60017f017d60017e017c030504000102030a190405002000" + "bc0b05002000bd0b05002000be0b05002000bf0b"); + auto instance = instantiate(parse(wasm)); + const auto func_float_to_int = std::is_same_v ? 0 : 1; + const auto func_int_to_float = std::is_same_v ? 2 : 3; + + ASSERT_EQ(std::fegetround(), FE_TONEAREST); + for (const auto rounding_direction : all_rounding_directions) + { + ASSERT_EQ(std::fesetround(rounding_direction), 0); + SCOPED_TRACE(rounding_direction); + + const auto& ordered_values = TestValues::ordered_and_nans(); + for (const auto float_value : ordered_values) + { + const auto uint_value = FP{float_value}.as_uint(); + EXPECT_THAT(execute(*instance, func_float_to_int, {float_value}), Result(uint_value)); + EXPECT_THAT(execute(*instance, func_int_to_float, {uint_value}), Result(float_value)); + } + } + ASSERT_EQ(std::fesetround(FE_TONEAREST), 0); +} + + +template +struct ConversionPairWasmTraits; + +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i32_trunc_f32_s"; + static constexpr auto opcode = Instr::i32_trunc_f32_s; + static constexpr auto src_valtype = ValType::f32; + static constexpr auto dst_valtype = ValType::i32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i32_trunc_f32_u"; + static constexpr auto opcode = Instr::i32_trunc_f32_u; + static constexpr auto src_valtype = ValType::f32; + static constexpr auto dst_valtype = ValType::i32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i32_trunc_f64_s"; + static constexpr auto opcode = Instr::i32_trunc_f64_s; + static constexpr auto src_valtype = ValType::f64; + static constexpr auto dst_valtype = ValType::i32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i32_trunc_f64_u"; + static constexpr auto opcode = Instr::i32_trunc_f64_u; + static constexpr auto src_valtype = ValType::f64; + static constexpr auto dst_valtype = ValType::i32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i64_trunc_f32_s"; + static constexpr auto opcode = Instr::i64_trunc_f32_s; + static constexpr auto src_valtype = ValType::f32; + static constexpr auto dst_valtype = ValType::i64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i64_trunc_f32_u"; + static constexpr auto opcode = Instr::i64_trunc_f32_u; + static constexpr auto src_valtype = ValType::f32; + static constexpr auto dst_valtype = ValType::i64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i64_trunc_f64_s"; + static constexpr auto opcode = Instr::i64_trunc_f64_s; + static constexpr auto src_valtype = ValType::f64; + static constexpr auto dst_valtype = ValType::i64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "i64_trunc_f64_u"; + static constexpr auto opcode = Instr::i64_trunc_f64_u; + static constexpr auto src_valtype = ValType::f64; + static constexpr auto dst_valtype = ValType::i64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f32_convert_i32_s"; + static constexpr auto opcode = Instr::f32_convert_i32_s; + static constexpr auto src_valtype = ValType::i32; + static constexpr auto dst_valtype = ValType::f32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f32_convert_i32_u"; + static constexpr auto opcode = Instr::f32_convert_i32_u; + static constexpr auto src_valtype = ValType::i32; + static constexpr auto dst_valtype = ValType::f32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f32_convert_i64_s"; + static constexpr auto opcode = Instr::f32_convert_i64_s; + static constexpr auto src_valtype = ValType::i64; + static constexpr auto dst_valtype = ValType::f32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f32_convert_i64_u"; + static constexpr auto opcode = Instr::f32_convert_i64_u; + static constexpr auto src_valtype = ValType::i64; + static constexpr auto dst_valtype = ValType::f32; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f64_convert_i32_s"; + static constexpr auto opcode = Instr::f64_convert_i32_s; + static constexpr auto src_valtype = ValType::i32; + static constexpr auto dst_valtype = ValType::f64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f64_convert_i32_u"; + static constexpr auto opcode = Instr::f64_convert_i32_u; + static constexpr auto src_valtype = ValType::i32; + static constexpr auto dst_valtype = ValType::f64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f64_convert_i64_s"; + static constexpr auto opcode = Instr::f64_convert_i64_s; + static constexpr auto src_valtype = ValType::i64; + static constexpr auto dst_valtype = ValType::f64; +}; +template <> +struct ConversionPairWasmTraits +{ + static constexpr auto opcode_name = "f64_convert_i64_u"; + static constexpr auto opcode = Instr::f64_convert_i64_u; + static constexpr auto src_valtype = ValType::i64; + static constexpr auto dst_valtype = ValType::f64; +}; + +template +struct ConversionPair : ConversionPairWasmTraits +{ + using src_type = SrcT; + using dst_type = DstT; +}; + +struct ConversionName +{ + template + static std::string GetName(int /*unused*/) + { + return T::opcode_name; + } +}; + +template +class execute_floating_point_trunc : public testing::Test +{ +}; + +using TruncPairs = testing::Types, ConversionPair, + ConversionPair, ConversionPair, + ConversionPair, ConversionPair, + ConversionPair, ConversionPair>; +TYPED_TEST_SUITE(execute_floating_point_trunc, TruncPairs, ConversionName); + +TYPED_TEST(execute_floating_point_trunc, trunc) +{ + using FloatT = typename TypeParam::src_type; + using IntT = typename TypeParam::dst_type; + using FloatLimits = std::numeric_limits; + using IntLimits = std::numeric_limits; + + /* wat2wasm + (func (param f32) (result i32) + local.get 0 + i32.trunc_f32_s + ) + */ + auto wasm = from_hex("0061736d0100000001060160017d017f030201000a070105002000a80b"); + + // Find and replace changeable values: types and the trunc instruction. + constexpr auto param_type = static_cast(ValType::f32); + constexpr auto result_type = static_cast(ValType::i32); + constexpr auto opcode = static_cast(Instr::i32_trunc_f32_s); + ASSERT_EQ(std::count(wasm.begin(), wasm.end(), param_type), 1); + ASSERT_EQ(std::count(wasm.begin(), wasm.end(), result_type), 1); + ASSERT_EQ(std::count(wasm.begin(), wasm.end(), opcode), 1); + *std::find(wasm.begin(), wasm.end(), param_type) = static_cast(TypeParam::src_valtype); + *std::find(wasm.begin(), wasm.end(), result_type) = + static_cast(TypeParam::dst_valtype); + *std::find(wasm.begin(), wasm.end(), opcode) = static_cast(TypeParam::opcode); + + auto instance = instantiate(parse(wasm)); + + // Zero. + EXPECT_THAT(execute(*instance, 0, {FloatT{0}}), Result(IntT{0})); + EXPECT_THAT(execute(*instance, 0, {-FloatT{0}}), Result(IntT{0})); + + // Something around 0.0. + EXPECT_THAT(execute(*instance, 0, {FloatLimits::denorm_min()}), Result(IntT{0})); + EXPECT_THAT(execute(*instance, 0, {-FloatLimits::denorm_min()}), Result(IntT{0})); + + // Something smaller than 2.0. + EXPECT_THAT(execute(*instance, 0, {std::nextafter(FloatT{2}, FloatT{0})}), Result(IntT{1})); + + // Something bigger than -1.0. + EXPECT_THAT(execute(*instance, 0, {std::nextafter(FloatT{-1}, FloatT{0})}), Result(IntT{0})); + + { + // BOUNDARIES OF DEFINITION + // + // Here we want to identify and test the boundary values of the defined behavior of the + // trunc instructions. For undefined results the execution must trap. + // Note that floating point type can represent any power of 2. + + using expected_boundaries = trunc_boundaries; + + // For iN with max value 2^N-1 the float(2^N) exists and trunc(float(2^N)) to iN + // is undefined. + const auto upper_boundary = std::pow(FloatT{2}, FloatT{IntLimits::digits}); + EXPECT_EQ(upper_boundary, expected_boundaries::upper); + EXPECT_THAT(execute(*instance, 0, {upper_boundary}), Traps()); + + // But the trunc() of the next float value smaller than 2^N is defined. + // Depending on the resolution of the floating point type, the result integer value may + // be other than 2^(N-1). + const auto max_defined = std::nextafter(upper_boundary, FloatT{0}); + const auto max_defined_int = static_cast(max_defined); + EXPECT_THAT(execute(*instance, 0, {max_defined}), Result(max_defined_int)); + + // The lower boundary is: + // - for signed integers: -2^N - 1, + // - for unsigned integers: -1. + // However, the -2^N - 1 may be not representative in a float type so we compute it as + // floor(-2^N - epsilon). + const auto min_defined_int = IntLimits::min(); + const auto lower_boundary = + std::floor(std::nextafter(FloatT{min_defined_int}, -FloatLimits::infinity())); + EXPECT_EQ(lower_boundary, expected_boundaries::lower); + EXPECT_THAT(execute(*instance, 0, {lower_boundary}), Traps()); + + const auto min_defined = std::nextafter(lower_boundary, FloatT{0}); + EXPECT_THAT(execute(*instance, 0, {min_defined}), Result(min_defined_int)); + } + + { + // NaNs. + EXPECT_THAT(execute(*instance, 0, {FloatLimits::quiet_NaN()}), Traps()); + EXPECT_THAT(execute(*instance, 0, {FloatLimits::signaling_NaN()}), Traps()); + EXPECT_THAT(execute(*instance, 0, {FP::nan(FP::canon)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {-FP::nan(FP::canon)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {FP::nan(FP::canon + 1)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {-FP::nan(FP::canon + 1)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {FP::nan(1)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {-FP::nan(1)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {FP::nan(0xdead)}), Traps()); + EXPECT_THAT(execute(*instance, 0, {-FP::nan(0xdead)}), Traps()); + const auto signaling_nan = FP::nan(FP::canon >> 1); + EXPECT_THAT(execute(*instance, 0, {signaling_nan}), Traps()); + EXPECT_THAT(execute(*instance, 0, {-signaling_nan}), Traps()); + + const auto inf = FloatLimits::infinity(); + EXPECT_THAT(execute(*instance, 0, {inf}), Traps()); + EXPECT_THAT(execute(*instance, 0, {-inf}), Traps()); + + EXPECT_THAT(execute(*instance, 0, {FloatLimits::max()}), Traps()); + EXPECT_THAT(execute(*instance, 0, {FloatLimits::lowest()}), Traps()); + } + + if constexpr (IntLimits::is_signed) + { + // Something bigger than -2.0. + const auto arg = std::nextafter(FloatT{-2}, FloatT{0}); + const auto result = execute(*instance, 0, {arg}); + EXPECT_EQ(result.value.template as(), FloatT{-1}); + } +} + + +template +class execute_floating_point_convert : public testing::Test +{ +}; + +using ConvertPairs = testing::Types, ConversionPair, + ConversionPair, ConversionPair, + ConversionPair, ConversionPair, + ConversionPair, ConversionPair>; +TYPED_TEST_SUITE(execute_floating_point_convert, ConvertPairs, ConversionName); + +TYPED_TEST(execute_floating_point_convert, convert) +{ + using IntT = typename TypeParam::src_type; + using FloatT = typename TypeParam::dst_type; + using IntLimits = std::numeric_limits; + using FloatLimits = std::numeric_limits; + + /* wat2wasm + (func (param i32) (result f32) + local.get 0 + f32.convert_i32_s + ) + */ + auto wasm = from_hex("0061736d0100000001060160017f017d030201000a070105002000b20b"); + + // Find and replace changeable values: types and the convert instruction. + constexpr auto param_type = static_cast(ValType::i32); + constexpr auto result_type = static_cast(ValType::f32); + constexpr auto opcode = static_cast(Instr::f32_convert_i32_s); + ASSERT_EQ(std::count(wasm.begin(), wasm.end(), param_type), 1); + ASSERT_EQ(std::count(wasm.begin(), wasm.end(), result_type), 1); + ASSERT_EQ(std::count(wasm.begin(), wasm.end(), opcode), 1); + *std::find(wasm.begin(), wasm.end(), param_type) = static_cast(TypeParam::src_valtype); + *std::find(wasm.begin(), wasm.end(), result_type) = + static_cast(TypeParam::dst_valtype); + *std::find(wasm.begin(), wasm.end(), opcode) = static_cast(TypeParam::opcode); + + auto instance = instantiate(parse(wasm)); + + EXPECT_THAT(execute(*instance, 0, {IntT{0}}), Result(FloatT{0})); + EXPECT_THAT(execute(*instance, 0, {IntT{1}}), Result(FloatT{1})); + + // Max integer value: 2^N - 1. + constexpr auto max = IntLimits::max(); + // Can the FloatT represent all values of IntT? + constexpr auto exact = IntLimits::digits < FloatLimits::digits; + // For "exact" the result is just 2^N - 1, for "not exact" the nearest to 2^N - 1 is 2^N. + const auto max_expected = std::pow(FloatT{2}, FloatT{IntLimits::digits}) - FloatT{exact}; + EXPECT_THAT(execute(*instance, 0, {max}), Result(max_expected)); + + if constexpr (IntLimits::is_signed) + { + EXPECT_THAT(execute(*instance, 0, {-IntT{1}}), Result(-FloatT{1})); + + static_assert(std::is_same_v); + EXPECT_THAT(execute(*instance, 0, {-max}), Result(-max_expected)); + + const auto min_expected = -std::pow(FloatT{2}, FloatT{IntLimits::digits}); + EXPECT_THAT(execute(*instance, 0, {IntLimits::min()}), Result(min_expected)); + } +} diff --git a/test/unittests/execute_floating_point_test.cpp b/test/unittests/execute_floating_point_test.cpp index 82ffef464..ea7b79d1f 100644 --- a/test/unittests/execute_floating_point_test.cpp +++ b/test/unittests/execute_floating_point_test.cpp @@ -2,183 +2,17 @@ // Copyright 2020 The Fizzy Authors. // SPDX-License-Identifier: Apache-2.0 +#include "execute_floating_point_test.hpp" #include "execute.hpp" -#include "instructions.hpp" #include "parser.hpp" #include "trunc_boundaries.hpp" -#include #include -#include -#include #include -#include -#include #include -// Enables access and modification of the floating-point environment. -// Here we use it to change rounding direction in tests. -// Although required by the C standard, neither GCC nor Clang supports it. -#pragma STDC FENV_ACCESS ON - using namespace fizzy; using namespace fizzy::test; -MATCHER_P(CanonicalNaN, value, "result with a canonical NaN") -{ - (void)value; - if (arg.trapped || !arg.has_value) - return false; - - const auto result_value = arg.value.template as(); - return FP{result_value}.is_canonical_nan(); -} - -MATCHER_P(ArithmeticNaN, value, "result with an arithmetic NaN") -{ - (void)value; - if (arg.trapped || !arg.has_value) - return false; - - const auto result_value = arg.value.template as(); - return FP{result_value}.is_arithmetic_nan(); -} - -namespace -{ -constexpr auto all_rounding_directions = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; - -template -class TestValues -{ - using Limits = typename FP::Limits; - - inline static const std::array m_values{ - T{0.0}, - - Limits::denorm_min(), - std::nextafter(Limits::denorm_min(), Limits::infinity()), - std::nextafter(Limits::min(), T{0}), - Limits::min(), - std::nextafter(Limits::min(), Limits::infinity()), - std::nextafter(T{1.0}, T{0}), - T{1.0}, - std::nextafter(T{1.0}, Limits::infinity()), - std::nextafter(Limits::max(), T{0}), - Limits::max(), - - Limits::infinity(), - - // Canonical NaN: - FP::nan(FP::canon), - - // Arithmetic NaNs: - FP::nan((FP::canon << 1) - 1), // All bits set. - FP::nan(FP::canon | (FP::canon >> 1)), // Two top bits set. - FP::nan(FP::canon + 1), - - // Signaling (not arithmetic) NaNs: - FP::nan(FP::canon >> 1), // "Standard" signaling NaN. - FP::nan(2), - FP::nan(1), - }; - -public: - using Iterator = typename decltype(m_values)::const_iterator; - - static constexpr auto num_nans = 7; - static constexpr auto num_positive = m_values.size() - num_nans; - - static constexpr Iterator first_non_zero = &m_values[1]; - static constexpr Iterator canonical_nan = &m_values[num_positive]; - static constexpr Iterator first_noncanonical_nan = canonical_nan + 1; - static constexpr Iterator infinity = &m_values[num_positive - 1]; - - class Range - { - Iterator m_begin; - Iterator m_end; - - public: - constexpr Range(Iterator begin, Iterator end) noexcept : m_begin{begin}, m_end{end} {} - - constexpr Iterator begin() const { return m_begin; } - constexpr Iterator end() const { return m_end; } - - [[gnu::no_sanitize("pointer-subtract")]] constexpr size_t size() const - { - // The "pointer-subtract" sanitizer is disabled because GCC fails to compile - // constexpr function with pointer subtraction. - // The bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=97145 - // is to be fixed in GCC 10.3. - return static_cast(m_end - m_begin); - } - }; - - // The list of positive floating-point values without zero, infinity and NaNs. - static constexpr Range positive_nonzero_finite() noexcept { return {first_non_zero, infinity}; } - - // The list of positive floating-point values without zero and NaNs (includes infinity). - static constexpr Range positive_nonzero_infinite() noexcept - { - return {first_non_zero, canonical_nan}; - } - - // The list of positive NaN values. - static constexpr Range positive_nans() noexcept { return {canonical_nan, m_values.end()}; } - - // The list of positive non-canonical NaN values (including signaling NaNs). - static constexpr Range positive_noncanonical_nans() noexcept - { - return {first_noncanonical_nan, m_values.end()}; - } - - // The list of positive floating-point values with zero, infinity and NaNs. - static constexpr Range positive_all() noexcept { return {m_values.begin(), m_values.end()}; } - - // The list of floating-point values, including infinities. - // They are strictly ordered (ordered_values[i] < ordered_values[j] for i a; - - auto it = std::begin(a); - it = std::transform(std::make_reverse_iterator(std::end(ps)), - std::make_reverse_iterator(std::begin(ps)), it, std::negate{}); - *it++ = T{0.0}; - std::copy(std::begin(ps), std::end(ps), it); - return a; - }(); - - return ordered_values; - } - - // The list of floating-point values, including infinities and NaNs. - // They are strictly ordered (ordered_values[i] < ordered_values[j] for i a; - - auto it = std::begin(a); - it = std::copy(std::begin(without_nans), std::end(without_nans), it); - it = std::copy(std::begin(nans), std::end(nans), it); - std::transform(std::begin(nans), std::end(nans), it, std::negate{}); - return a; - }(); - - return ordered_values; - } -}; -} // namespace - TEST(execute_floating_point, f32_const) { /* wat2wasm @@ -206,148 +40,6 @@ TEST(execute_floating_point, f64_const) EXPECT_THAT(execute(*instance, 0, {}), Result(8589934592.1)); } -/// Compile-time information about a Wasm type. -template -struct WasmTypeTraits; - -template <> -struct WasmTypeTraits -{ - static constexpr auto name = "f32"; - static constexpr auto valtype = ValType::f32; -}; -template <> -struct WasmTypeTraits -{ - static constexpr auto name = "f64"; - static constexpr auto valtype = ValType::f64; -}; - -struct WasmTypeName -{ - template - static std::string GetName(int) - { - return WasmTypeTraits::name; - } -}; - -template -class execute_floating_point_types : public testing::Test -{ -public: - using L = typename FP::Limits; - - // The [int_only_begin; int_only_end) is the range of floating-point numbers, where each - // representable number is an integer and there are no fractional numbers between them. - // These numbers are represented as mantissa [0x1.00..0; 0x1.ff..f] - // and exponent 2**(mantissa digits without implicit leading 1). - // The main point of using int_only_begin is to tests nearest() as for int_only_begin - 0.5 and - // int_only_begin - 1.5 we have equal distance to nearby integer values. - // (Integer shift is used instead of std::pow() because of the clang compiler bug). - static constexpr auto int_only_begin = T{uint64_t{1} << (L::digits - 1)}; - static constexpr auto int_only_end = T{uint64_t{1} << L::digits}; - - // The list of rounding test cases as pairs (input, expected_trunc) with only positive inputs. - inline static const std::pair positive_trunc_tests[] = { - - // Checks the following rule common for all rounding instructions: - // f(+-0) = +-0 - {0, 0}, - - {L::denorm_min(), 0}, - {L::min(), 0}, - - {T{0.1f}, T{0}}, - - {std::nextafter(T{0.5}, T{0}), T{0}}, - {T{0.5}, T{0}}, - {std::nextafter(T{0.5}, T{1}), T{0}}, - - {std::nextafter(T{1}, T{0}), T{0}}, - {T{1}, T{1}}, - {std::nextafter(T{1}, T{2}), T{1}}, - - {std::nextafter(T{1.5}, T{1}), T{1}}, - {T{1.5}, T{1}}, - {std::nextafter(T{1.5}, T{2}), T{1}}, - - {std::nextafter(T{2}, T{1}), T{1}}, - {T{2}, T{2}}, - {std::nextafter(T{2}, T{3}), T{2}}, - - {int_only_begin - T{2}, int_only_begin - T{2}}, - {int_only_begin - T{1.5}, int_only_begin - T{2}}, - {int_only_begin - T{1}, int_only_begin - T{1}}, - {int_only_begin - T{0.5}, int_only_begin - T{1}}, - {int_only_begin, int_only_begin}, - {int_only_begin + T{1}, int_only_begin + T{1}}, - - {int_only_end - T{1}, int_only_end - T{1}}, - {int_only_end, int_only_end}, - {int_only_end + T{2}, int_only_end + T{2}}, - - {L::max(), L::max()}, - - // Checks the following rule common for all rounding instructions: - // f(+-inf) = +-inf - {L::infinity(), L::infinity()}, - }; - - /// Creates a wasm module with a single function for the given instructions opcode. - /// The opcode is converted to match the type, e.g. f32_add -> f64_add. - static bytes get_numeric_instruction_code( - bytes_view template_code, Instr template_opcode, Instr opcode) - { - constexpr auto f64_variant_offset = - static_cast(Instr::f64_add) - static_cast(Instr::f32_add); - - // Convert to f64 variant if needed. - const auto typed_opcode = - std::is_same_v ? - static_cast(static_cast(opcode) + f64_variant_offset) : - static_cast(opcode); - - bytes wasm{template_code}; - constexpr auto template_type = static_cast(ValType::f32); - const auto template_opcode_byte = static_cast(template_opcode); - const auto opcode_arity = get_instruction_type_table()[template_opcode_byte].inputs.size(); - - EXPECT_EQ(std::count(wasm.begin(), wasm.end(), template_type), opcode_arity + 1); - EXPECT_EQ(std::count(wasm.begin(), wasm.end(), template_opcode_byte), 1); - - std::replace(wasm.begin(), wasm.end(), template_type, - static_cast(WasmTypeTraits::valtype)); - std::replace(wasm.begin(), wasm.end(), template_opcode_byte, typed_opcode); - return wasm; - } - - static bytes get_unop_code(Instr opcode) - { - /* wat2wasm - (func (param f32) (result f32) - (f32.abs (local.get 0)) - ) - */ - auto wasm = from_hex("0061736d0100000001060160017d017d030201000a0701050020008b0b"); - return get_numeric_instruction_code(wasm, Instr::f32_abs, opcode); - } - - static bytes get_binop_code(Instr opcode) - { - /* wat2wasm - (func (param f32 f32) (result f32) - (f32.add (local.get 0) (local.get 1)) - ) - */ - auto wasm = from_hex("0061736d0100000001070160027d7d017d030201000a0901070020002001920b"); - return get_numeric_instruction_code(wasm, Instr::f32_add, opcode); - } -}; - -using FloatingPointTypes = testing::Types; -TYPED_TEST_SUITE(execute_floating_point_types, FloatingPointTypes, WasmTypeName); - TYPED_TEST(execute_floating_point_types, test_values_selftest) { using TV = TestValues; @@ -1291,551 +983,6 @@ TYPED_TEST(execute_floating_point_types, copysign) } -TEST(execute_floating_point, f64_promote_f32) -{ - /* wat2wasm - (func (param f32) (result f64) - local.get 0 - f64.promote_f32 - ) - */ - const auto wasm = from_hex("0061736d0100000001060160017d017c030201000a070105002000bb0b"); - auto instance = instantiate(parse(wasm)); - - const std::pair test_cases[] = { - {0.0f, 0.0}, - {-0.0f, -0.0}, - {1.0f, 1.0}, - {-1.0f, -1.0}, - {FP32::Limits::lowest(), double{FP32::Limits::lowest()}}, - {FP32::Limits::max(), double{FP32::Limits::max()}}, - {FP32::Limits::min(), double{FP32::Limits::min()}}, - {FP32::Limits::denorm_min(), double{FP32::Limits::denorm_min()}}, - {FP32::Limits::infinity(), FP64::Limits::infinity()}, - {-FP32::Limits::infinity(), -FP64::Limits::infinity()}, - - // The canonical NaN must result in canonical NaN (only the top bit set). - {FP32::nan(FP32::canon), FP64::nan(FP64::canon)}, - {-FP32::nan(FP32::canon), -FP64::nan(FP64::canon)}, - }; - - ASSERT_EQ(std::fegetround(), FE_TONEAREST); - for (const auto rounding_direction : all_rounding_directions) - { - ASSERT_EQ(std::fesetround(rounding_direction), 0); - SCOPED_TRACE(rounding_direction); - - for (const auto& [arg, expected] : test_cases) - { - EXPECT_THAT(execute(*instance, 0, {arg}), Result(expected)) - << arg << " -> " << expected; - } - - // Check arithmetic NaNs (payload >= canonical payload). - // The following check expect arithmetic NaNs. Canonical NaNs are arithmetic NaNs - // and are allowed by the spec in these situations, but our checks are more restrictive - - // An arithmetic NaN must result in any arithmetic NaN. - const auto res1 = execute(*instance, 0, {FP32::nan(FP32::canon + 1)}); - ASSERT_TRUE(!res1.trapped && res1.has_value); - EXPECT_EQ(std::signbit(res1.value.f64), 0); - EXPECT_GT(FP{res1.value.f64}.nan_payload(), FP64::canon); - const auto res2 = execute(*instance, 0, {-FP32::nan(FP32::canon + 1)}); - ASSERT_TRUE(!res2.trapped && res2.has_value); - EXPECT_EQ(std::signbit(res2.value.f64), 1); - EXPECT_GT(FP{res2.value.f64}.nan_payload(), FP64::canon); - - // Other NaN must also result in arithmetic NaN. - const auto res3 = execute(*instance, 0, {FP32::nan(1)}); - ASSERT_TRUE(!res3.trapped && res3.has_value); - EXPECT_EQ(std::signbit(res3.value.f64), 0); - EXPECT_GT(FP{res3.value.f64}.nan_payload(), FP64::canon); - const auto res4 = execute(*instance, 0, {-FP32::nan(1)}); - ASSERT_TRUE(!res4.trapped && res4.has_value); - EXPECT_EQ(std::signbit(res4.value.f64), 1); - EXPECT_GT(FP{res4.value.f64}.nan_payload(), FP64::canon); - - // Any input NaN other than canonical must result in an arithmetic NaN. - for (const auto nan : TestValues::positive_noncanonical_nans()) - { - EXPECT_THAT(execute(*instance, 0, {nan}), ArithmeticNaN(double{})); - EXPECT_THAT(execute(*instance, 0, {-nan}), ArithmeticNaN(double{})); - } - } - ASSERT_EQ(std::fesetround(FE_TONEAREST), 0); -} - -TEST(execute_floating_point, f32_demote_f64) -{ - /* wat2wasm - (func (param f64) (result f32) - local.get 0 - f32.demote_f64 - ) - */ - const auto wasm = from_hex("0061736d0100000001060160017c017d030201000a070105002000b60b"); - auto instance = instantiate(parse(wasm)); - - constexpr double f32_max = FP32::Limits::max(); - ASSERT_EQ(f32_max, 0x1.fffffep127); - - // The "artificial" f32 range limit: the next f32 number that could be represented - // if the exponent had a larger range. - // Wasm spec Rounding section denotes this as the limit_N in the float_N function (for N=32). - // https://webassembly.github.io/spec/core/exec/numerics.html#rounding - constexpr double f32_limit = 0x1p128; // 2**128. - - // The lower boundary input value that results in the infinity. The number is midway between - // f32_max and f32_limit. For this value rounding prefers infinity, because f32_limit is even. - constexpr double lowest_to_inf = (f32_max + f32_limit) / 2; - ASSERT_EQ(lowest_to_inf, 0x1.ffffffp127); - - const std::pair test_cases[] = { - // demote(+-0) = +-0 - {0.0, 0.0f}, - {-0.0, -0.0f}, - - {1.0, 1.0f}, - {-1.0, -1.0f}, - {double{FP32::Limits::lowest()}, FP32::Limits::lowest()}, - {double{FP32::Limits::max()}, FP32::Limits::max()}, - {double{FP32::Limits::min()}, FP32::Limits::min()}, - {double{-FP32::Limits::min()}, -FP32::Limits::min()}, - {double{FP32::Limits::denorm_min()}, FP32::Limits::denorm_min()}, - {double{-FP32::Limits::denorm_min()}, -FP32::Limits::denorm_min()}, - - // Some special f64 values. - {FP64::Limits::lowest(), -FP32::Limits::infinity()}, - {FP64::Limits::max(), FP32::Limits::infinity()}, - {FP64::Limits::min(), 0.0f}, - {-FP64::Limits::min(), -0.0f}, - {FP64::Limits::denorm_min(), 0.0f}, - {-FP64::Limits::denorm_min(), -0.0f}, - - // Out of range values rounded to max/lowest. - {std::nextafter(f32_max, FP64::Limits::infinity()), FP32::Limits::max()}, - {std::nextafter(double{FP32::Limits::lowest()}, -FP64::Limits::infinity()), - FP32::Limits::lowest()}, - - {std::nextafter(lowest_to_inf, 0.0), FP32::Limits::max()}, - {std::nextafter(-lowest_to_inf, 0.0), FP32::Limits::lowest()}, - - // The smallest of range values rounded to infinity. - {lowest_to_inf, FP32::Limits::infinity()}, - {-lowest_to_inf, -FP32::Limits::infinity()}, - - {std::nextafter(lowest_to_inf, FP64::Limits::infinity()), FP32::Limits::infinity()}, - {std::nextafter(-lowest_to_inf, -FP64::Limits::infinity()), -FP32::Limits::infinity()}, - - // float_32(r) = +inf (if r >= +limit_32) - {f32_limit, FP32::Limits::infinity()}, - - // float_32(r) = -inf (if r <= -limit_32) - {-f32_limit, -FP32::Limits::infinity()}, - - // demote(+-inf) = +-inf - {FP64::Limits::infinity(), FP32::Limits::infinity()}, - {-FP64::Limits::infinity(), -FP32::Limits::infinity()}, - - // Rounding. - {0x1.fffffefffffffp0, 0x1.fffffep0f}, // round down - {0x1.fffffe0000000p0, 0x1.fffffep0f}, // exact (odd) - {0x1.fffffd0000001p0, 0x1.fffffep0f}, // round up - - {0x1.fffff8p0, 0x1.fffff8p0f}, // exact (even) - {(0x1.fffff8p0 + 0x1.fffffap0) / 2, 0x1.fffff8p0f}, // tie-to-even down - {0x1.fffffap0, 0x1.fffffap0f}, // exact (odd) - {(0x1.fffffap0 + 0x1.fffffcp0) / 2, 0x1.fffffcp0f}, // tie-to-even up - {0x1.fffffcp0, 0x1.fffffcp0f}, // exact (even) - - // The canonical NaN must result in canonical NaN (only the top bit of payload set). - {FP32::nan(FP32::canon), FP64::nan(FP64::canon)}, - {-FP32::nan(FP32::canon), -FP64::nan(FP64::canon)}, - }; - - for (const auto& [arg, expected] : test_cases) - { - EXPECT_THAT(execute(*instance, 0, {arg}), Result(expected)) << arg << " -> " << expected; - } - - // Any input NaN other than canonical must result in an arithmetic NaN. - for (const auto nan : TestValues::positive_noncanonical_nans()) - { - EXPECT_THAT(execute(*instance, 0, {nan}), ArithmeticNaN(float{})); - EXPECT_THAT(execute(*instance, 0, {-nan}), ArithmeticNaN(float{})); - } -} - -TYPED_TEST(execute_floating_point_types, reinterpret) -{ - /* wat2wasm - (func (param f32) (result i32) (i32.reinterpret_f32 (local.get 0))) - (func (param f64) (result i64) (i64.reinterpret_f64 (local.get 0))) - (func (param i32) (result f32) (f32.reinterpret_i32 (local.get 0))) - (func (param i64) (result f64) (f64.reinterpret_i64 (local.get 0))) - */ - const auto wasm = from_hex( - "0061736d0100000001150460017d017f60017c017e60017f017d60017e017c030504000102030a190405002000" - "bc0b05002000bd0b05002000be0b05002000bf0b"); - auto instance = instantiate(parse(wasm)); - const auto func_float_to_int = std::is_same_v ? 0 : 1; - const auto func_int_to_float = std::is_same_v ? 2 : 3; - - ASSERT_EQ(std::fegetround(), FE_TONEAREST); - for (const auto rounding_direction : all_rounding_directions) - { - ASSERT_EQ(std::fesetround(rounding_direction), 0); - SCOPED_TRACE(rounding_direction); - - const auto& ordered_values = TestValues::ordered_and_nans(); - for (const auto float_value : ordered_values) - { - const auto uint_value = FP{float_value}.as_uint(); - EXPECT_THAT(execute(*instance, func_float_to_int, {float_value}), Result(uint_value)); - EXPECT_THAT(execute(*instance, func_int_to_float, {uint_value}), Result(float_value)); - } - } - ASSERT_EQ(std::fesetround(FE_TONEAREST), 0); -} - - -template -struct ConversionPairWasmTraits; - -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i32_trunc_f32_s"; - static constexpr auto opcode = Instr::i32_trunc_f32_s; - static constexpr auto src_valtype = ValType::f32; - static constexpr auto dst_valtype = ValType::i32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i32_trunc_f32_u"; - static constexpr auto opcode = Instr::i32_trunc_f32_u; - static constexpr auto src_valtype = ValType::f32; - static constexpr auto dst_valtype = ValType::i32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i32_trunc_f64_s"; - static constexpr auto opcode = Instr::i32_trunc_f64_s; - static constexpr auto src_valtype = ValType::f64; - static constexpr auto dst_valtype = ValType::i32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i32_trunc_f64_u"; - static constexpr auto opcode = Instr::i32_trunc_f64_u; - static constexpr auto src_valtype = ValType::f64; - static constexpr auto dst_valtype = ValType::i32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i64_trunc_f32_s"; - static constexpr auto opcode = Instr::i64_trunc_f32_s; - static constexpr auto src_valtype = ValType::f32; - static constexpr auto dst_valtype = ValType::i64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i64_trunc_f32_u"; - static constexpr auto opcode = Instr::i64_trunc_f32_u; - static constexpr auto src_valtype = ValType::f32; - static constexpr auto dst_valtype = ValType::i64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i64_trunc_f64_s"; - static constexpr auto opcode = Instr::i64_trunc_f64_s; - static constexpr auto src_valtype = ValType::f64; - static constexpr auto dst_valtype = ValType::i64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "i64_trunc_f64_u"; - static constexpr auto opcode = Instr::i64_trunc_f64_u; - static constexpr auto src_valtype = ValType::f64; - static constexpr auto dst_valtype = ValType::i64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f32_convert_i32_s"; - static constexpr auto opcode = Instr::f32_convert_i32_s; - static constexpr auto src_valtype = ValType::i32; - static constexpr auto dst_valtype = ValType::f32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f32_convert_i32_u"; - static constexpr auto opcode = Instr::f32_convert_i32_u; - static constexpr auto src_valtype = ValType::i32; - static constexpr auto dst_valtype = ValType::f32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f32_convert_i64_s"; - static constexpr auto opcode = Instr::f32_convert_i64_s; - static constexpr auto src_valtype = ValType::i64; - static constexpr auto dst_valtype = ValType::f32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f32_convert_i64_u"; - static constexpr auto opcode = Instr::f32_convert_i64_u; - static constexpr auto src_valtype = ValType::i64; - static constexpr auto dst_valtype = ValType::f32; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f64_convert_i32_s"; - static constexpr auto opcode = Instr::f64_convert_i32_s; - static constexpr auto src_valtype = ValType::i32; - static constexpr auto dst_valtype = ValType::f64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f64_convert_i32_u"; - static constexpr auto opcode = Instr::f64_convert_i32_u; - static constexpr auto src_valtype = ValType::i32; - static constexpr auto dst_valtype = ValType::f64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f64_convert_i64_s"; - static constexpr auto opcode = Instr::f64_convert_i64_s; - static constexpr auto src_valtype = ValType::i64; - static constexpr auto dst_valtype = ValType::f64; -}; -template <> -struct ConversionPairWasmTraits -{ - static constexpr auto opcode_name = "f64_convert_i64_u"; - static constexpr auto opcode = Instr::f64_convert_i64_u; - static constexpr auto src_valtype = ValType::i64; - static constexpr auto dst_valtype = ValType::f64; -}; - -template -struct ConversionPair : ConversionPairWasmTraits -{ - using src_type = SrcT; - using dst_type = DstT; -}; - -struct ConversionName -{ - template - static std::string GetName(int /*unused*/) - { - return T::opcode_name; - } -}; - -template -class execute_floating_point_trunc : public testing::Test -{ -}; - -using TruncPairs = testing::Types, ConversionPair, - ConversionPair, ConversionPair, - ConversionPair, ConversionPair, - ConversionPair, ConversionPair>; -TYPED_TEST_SUITE(execute_floating_point_trunc, TruncPairs, ConversionName); - -TYPED_TEST(execute_floating_point_trunc, trunc) -{ - using FloatT = typename TypeParam::src_type; - using IntT = typename TypeParam::dst_type; - using FloatLimits = std::numeric_limits; - using IntLimits = std::numeric_limits; - - /* wat2wasm - (func (param f32) (result i32) - local.get 0 - i32.trunc_f32_s - ) - */ - auto wasm = from_hex("0061736d0100000001060160017d017f030201000a070105002000a80b"); - - // Find and replace changeable values: types and the trunc instruction. - constexpr auto param_type = static_cast(ValType::f32); - constexpr auto result_type = static_cast(ValType::i32); - constexpr auto opcode = static_cast(Instr::i32_trunc_f32_s); - ASSERT_EQ(std::count(wasm.begin(), wasm.end(), param_type), 1); - ASSERT_EQ(std::count(wasm.begin(), wasm.end(), result_type), 1); - ASSERT_EQ(std::count(wasm.begin(), wasm.end(), opcode), 1); - *std::find(wasm.begin(), wasm.end(), param_type) = static_cast(TypeParam::src_valtype); - *std::find(wasm.begin(), wasm.end(), result_type) = - static_cast(TypeParam::dst_valtype); - *std::find(wasm.begin(), wasm.end(), opcode) = static_cast(TypeParam::opcode); - - auto instance = instantiate(parse(wasm)); - - // Zero. - EXPECT_THAT(execute(*instance, 0, {FloatT{0}}), Result(IntT{0})); - EXPECT_THAT(execute(*instance, 0, {-FloatT{0}}), Result(IntT{0})); - - // Something around 0.0. - EXPECT_THAT(execute(*instance, 0, {FloatLimits::denorm_min()}), Result(IntT{0})); - EXPECT_THAT(execute(*instance, 0, {-FloatLimits::denorm_min()}), Result(IntT{0})); - - // Something smaller than 2.0. - EXPECT_THAT(execute(*instance, 0, {std::nextafter(FloatT{2}, FloatT{0})}), Result(IntT{1})); - - // Something bigger than -1.0. - EXPECT_THAT(execute(*instance, 0, {std::nextafter(FloatT{-1}, FloatT{0})}), Result(IntT{0})); - - { - // BOUNDARIES OF DEFINITION - // - // Here we want to identify and test the boundary values of the defined behavior of the - // trunc instructions. For undefined results the execution must trap. - // Note that floating point type can represent any power of 2. - - using expected_boundaries = trunc_boundaries; - - // For iN with max value 2^N-1 the float(2^N) exists and trunc(float(2^N)) to iN - // is undefined. - const auto upper_boundary = std::pow(FloatT{2}, FloatT{IntLimits::digits}); - EXPECT_EQ(upper_boundary, expected_boundaries::upper); - EXPECT_THAT(execute(*instance, 0, {upper_boundary}), Traps()); - - // But the trunc() of the next float value smaller than 2^N is defined. - // Depending on the resolution of the floating point type, the result integer value may - // be other than 2^(N-1). - const auto max_defined = std::nextafter(upper_boundary, FloatT{0}); - const auto max_defined_int = static_cast(max_defined); - EXPECT_THAT(execute(*instance, 0, {max_defined}), Result(max_defined_int)); - - // The lower boundary is: - // - for signed integers: -2^N - 1, - // - for unsigned integers: -1. - // However, the -2^N - 1 may be not representative in a float type so we compute it as - // floor(-2^N - epsilon). - const auto min_defined_int = IntLimits::min(); - const auto lower_boundary = - std::floor(std::nextafter(FloatT{min_defined_int}, -FloatLimits::infinity())); - EXPECT_EQ(lower_boundary, expected_boundaries::lower); - EXPECT_THAT(execute(*instance, 0, {lower_boundary}), Traps()); - - const auto min_defined = std::nextafter(lower_boundary, FloatT{0}); - EXPECT_THAT(execute(*instance, 0, {min_defined}), Result(min_defined_int)); - } - - { - // NaNs. - EXPECT_THAT(execute(*instance, 0, {FloatLimits::quiet_NaN()}), Traps()); - EXPECT_THAT(execute(*instance, 0, {FloatLimits::signaling_NaN()}), Traps()); - EXPECT_THAT(execute(*instance, 0, {FP::nan(FP::canon)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {-FP::nan(FP::canon)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {FP::nan(FP::canon + 1)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {-FP::nan(FP::canon + 1)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {FP::nan(1)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {-FP::nan(1)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {FP::nan(0xdead)}), Traps()); - EXPECT_THAT(execute(*instance, 0, {-FP::nan(0xdead)}), Traps()); - const auto signaling_nan = FP::nan(FP::canon >> 1); - EXPECT_THAT(execute(*instance, 0, {signaling_nan}), Traps()); - EXPECT_THAT(execute(*instance, 0, {-signaling_nan}), Traps()); - - const auto inf = FloatLimits::infinity(); - EXPECT_THAT(execute(*instance, 0, {inf}), Traps()); - EXPECT_THAT(execute(*instance, 0, {-inf}), Traps()); - - EXPECT_THAT(execute(*instance, 0, {FloatLimits::max()}), Traps()); - EXPECT_THAT(execute(*instance, 0, {FloatLimits::lowest()}), Traps()); - } - - if constexpr (IntLimits::is_signed) - { - // Something bigger than -2.0. - const auto arg = std::nextafter(FloatT{-2}, FloatT{0}); - const auto result = execute(*instance, 0, {arg}); - EXPECT_EQ(result.value.template as(), FloatT{-1}); - } -} - - -template -class execute_floating_point_convert : public testing::Test -{ -}; - -using ConvertPairs = testing::Types, ConversionPair, - ConversionPair, ConversionPair, - ConversionPair, ConversionPair, - ConversionPair, ConversionPair>; -TYPED_TEST_SUITE(execute_floating_point_convert, ConvertPairs, ConversionName); - -TYPED_TEST(execute_floating_point_convert, convert) -{ - using IntT = typename TypeParam::src_type; - using FloatT = typename TypeParam::dst_type; - using IntLimits = std::numeric_limits; - using FloatLimits = std::numeric_limits; - - /* wat2wasm - (func (param i32) (result f32) - local.get 0 - f32.convert_i32_s - ) - */ - auto wasm = from_hex("0061736d0100000001060160017f017d030201000a070105002000b20b"); - - // Find and replace changeable values: types and the convert instruction. - constexpr auto param_type = static_cast(ValType::i32); - constexpr auto result_type = static_cast(ValType::f32); - constexpr auto opcode = static_cast(Instr::f32_convert_i32_s); - ASSERT_EQ(std::count(wasm.begin(), wasm.end(), param_type), 1); - ASSERT_EQ(std::count(wasm.begin(), wasm.end(), result_type), 1); - ASSERT_EQ(std::count(wasm.begin(), wasm.end(), opcode), 1); - *std::find(wasm.begin(), wasm.end(), param_type) = static_cast(TypeParam::src_valtype); - *std::find(wasm.begin(), wasm.end(), result_type) = - static_cast(TypeParam::dst_valtype); - *std::find(wasm.begin(), wasm.end(), opcode) = static_cast(TypeParam::opcode); - - auto instance = instantiate(parse(wasm)); - - EXPECT_THAT(execute(*instance, 0, {IntT{0}}), Result(FloatT{0})); - EXPECT_THAT(execute(*instance, 0, {IntT{1}}), Result(FloatT{1})); - - // Max integer value: 2^N - 1. - constexpr auto max = IntLimits::max(); - // Can the FloatT represent all values of IntT? - constexpr auto exact = IntLimits::digits < FloatLimits::digits; - // For "exact" the result is just 2^N - 1, for "not exact" the nearest to 2^N - 1 is 2^N. - const auto max_expected = std::pow(FloatT{2}, FloatT{IntLimits::digits}) - FloatT{exact}; - EXPECT_THAT(execute(*instance, 0, {max}), Result(max_expected)); - - if constexpr (IntLimits::is_signed) - { - EXPECT_THAT(execute(*instance, 0, {-IntT{1}}), Result(-FloatT{1})); - - static_assert(std::is_same_v); - EXPECT_THAT(execute(*instance, 0, {-max}), Result(-max_expected)); - - const auto min_expected = -std::pow(FloatT{2}, FloatT{IntLimits::digits}); - EXPECT_THAT(execute(*instance, 0, {IntLimits::min()}), Result(min_expected)); - } -} - - TEST(execute_floating_point, f32_load) { /* wat2wasm diff --git a/test/unittests/execute_floating_point_test.hpp b/test/unittests/execute_floating_point_test.hpp new file mode 100644 index 000000000..6713661da --- /dev/null +++ b/test/unittests/execute_floating_point_test.hpp @@ -0,0 +1,314 @@ +// Fizzy: A fast WebAssembly interpreter +// Copyright 2020 The Fizzy Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "instructions.hpp" +#include "types.hpp" +#include +#include +#include +#include +#include + +// Enables access and modification of the floating-point environment. +// Here we use it to change rounding direction in tests. +// Although required by the C standard, neither GCC nor Clang supports it. +#pragma STDC FENV_ACCESS ON + +MATCHER_P(CanonicalNaN, value, "result with a canonical NaN") +{ + (void)value; + if (arg.trapped || !arg.has_value) + return false; + + const auto result_value = arg.value.template as(); + return fizzy::test::FP{result_value}.is_canonical_nan(); +} + +MATCHER_P(ArithmeticNaN, value, "result with an arithmetic NaN") +{ + (void)value; + if (arg.trapped || !arg.has_value) + return false; + + const auto result_value = arg.value.template as(); + return fizzy::test::FP{result_value}.is_arithmetic_nan(); +} + +namespace fizzy::test +{ +constexpr auto all_rounding_directions = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + +template +class TestValues +{ + using Limits = typename FP::Limits; + + inline static const std::array m_values{ + T{0.0}, + + Limits::denorm_min(), + std::nextafter(Limits::denorm_min(), Limits::infinity()), + std::nextafter(Limits::min(), T{0}), + Limits::min(), + std::nextafter(Limits::min(), Limits::infinity()), + std::nextafter(T{1.0}, T{0}), + T{1.0}, + std::nextafter(T{1.0}, Limits::infinity()), + std::nextafter(Limits::max(), T{0}), + Limits::max(), + + Limits::infinity(), + + // Canonical NaN: + FP::nan(FP::canon), + + // Arithmetic NaNs: + FP::nan((FP::canon << 1) - 1), // All bits set. + FP::nan(FP::canon | (FP::canon >> 1)), // Two top bits set. + FP::nan(FP::canon + 1), + + // Signaling (not arithmetic) NaNs: + FP::nan(FP::canon >> 1), // "Standard" signaling NaN. + FP::nan(2), + FP::nan(1), + }; + +public: + using Iterator = typename decltype(m_values)::const_iterator; + + static constexpr auto num_nans = 7; + static constexpr auto num_positive = m_values.size() - num_nans; + + static constexpr Iterator first_non_zero = &m_values[1]; + static constexpr Iterator canonical_nan = &m_values[num_positive]; + static constexpr Iterator first_noncanonical_nan = canonical_nan + 1; + static constexpr Iterator infinity = &m_values[num_positive - 1]; + + class Range + { + Iterator m_begin; + Iterator m_end; + + public: + constexpr Range(Iterator begin, Iterator end) noexcept : m_begin{begin}, m_end{end} {} + + constexpr Iterator begin() const { return m_begin; } + constexpr Iterator end() const { return m_end; } + + [[gnu::no_sanitize("pointer-subtract")]] constexpr size_t size() const + { + // The "pointer-subtract" sanitizer is disabled because GCC fails to compile + // constexpr function with pointer subtraction. + // The bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=97145 + // is to be fixed in GCC 10.3. + return static_cast(m_end - m_begin); + } + }; + + // The list of positive floating-point values without zero, infinity and NaNs. + static constexpr Range positive_nonzero_finite() noexcept { return {first_non_zero, infinity}; } + + // The list of positive floating-point values without zero and NaNs (includes infinity). + static constexpr Range positive_nonzero_infinite() noexcept + { + return {first_non_zero, canonical_nan}; + } + + // The list of positive NaN values. + static constexpr Range positive_nans() noexcept { return {canonical_nan, m_values.end()}; } + + // The list of positive non-canonical NaN values (including signaling NaNs). + static constexpr Range positive_noncanonical_nans() noexcept + { + return {first_noncanonical_nan, m_values.end()}; + } + + // The list of positive floating-point values with zero, infinity and NaNs. + static constexpr Range positive_all() noexcept { return {m_values.begin(), m_values.end()}; } + + // The list of floating-point values, including infinities. + // They are strictly ordered (ordered_values[i] < ordered_values[j] for i a; + + auto it = std::begin(a); + it = std::transform(std::make_reverse_iterator(std::end(ps)), + std::make_reverse_iterator(std::begin(ps)), it, std::negate{}); + *it++ = T{0.0}; + std::copy(std::begin(ps), std::end(ps), it); + return a; + }(); + + return ordered_values; + } + + // The list of floating-point values, including infinities and NaNs. + // They are strictly ordered (ordered_values[i] < ordered_values[j] for i a; + + auto it = std::begin(a); + it = std::copy(std::begin(without_nans), std::end(without_nans), it); + it = std::copy(std::begin(nans), std::end(nans), it); + std::transform(std::begin(nans), std::end(nans), it, std::negate{}); + return a; + }(); + + return ordered_values; + } +}; + +/// Compile-time information about a Wasm type. +template +struct WasmTypeTraits; + +template <> +struct WasmTypeTraits +{ + static constexpr auto name = "f32"; + static constexpr auto valtype = ValType::f32; +}; +template <> +struct WasmTypeTraits +{ + static constexpr auto name = "f64"; + static constexpr auto valtype = ValType::f64; +}; + +struct WasmTypeName +{ + template + static std::string GetName(int) + { + return WasmTypeTraits::name; + } +}; + +template +class execute_floating_point_types : public testing::Test +{ +public: + using L = typename FP::Limits; + + // The [int_only_begin; int_only_end) is the range of floating-point numbers, where each + // representable number is an integer and there are no fractional numbers between them. + // These numbers are represented as mantissa [0x1.00..0; 0x1.ff..f] + // and exponent 2**(mantissa digits without implicit leading 1). + // The main point of using int_only_begin is to tests nearest() as for int_only_begin - 0.5 and + // int_only_begin - 1.5 we have equal distance to nearby integer values. + // (Integer shift is used instead of std::pow() because of the clang compiler bug). + static constexpr auto int_only_begin = T{uint64_t{1} << (L::digits - 1)}; + static constexpr auto int_only_end = T{uint64_t{1} << L::digits}; + + // The list of rounding test cases as pairs (input, expected_trunc) with only positive inputs. + inline static const std::pair positive_trunc_tests[] = { + + // Checks the following rule common for all rounding instructions: + // f(+-0) = +-0 + {0, 0}, + + {L::denorm_min(), 0}, + {L::min(), 0}, + + {T{0.1f}, T{0}}, + + {std::nextafter(T{0.5}, T{0}), T{0}}, + {T{0.5}, T{0}}, + {std::nextafter(T{0.5}, T{1}), T{0}}, + + {std::nextafter(T{1}, T{0}), T{0}}, + {T{1}, T{1}}, + {std::nextafter(T{1}, T{2}), T{1}}, + + {std::nextafter(T{1.5}, T{1}), T{1}}, + {T{1.5}, T{1}}, + {std::nextafter(T{1.5}, T{2}), T{1}}, + + {std::nextafter(T{2}, T{1}), T{1}}, + {T{2}, T{2}}, + {std::nextafter(T{2}, T{3}), T{2}}, + + {int_only_begin - T{2}, int_only_begin - T{2}}, + {int_only_begin - T{1.5}, int_only_begin - T{2}}, + {int_only_begin - T{1}, int_only_begin - T{1}}, + {int_only_begin - T{0.5}, int_only_begin - T{1}}, + {int_only_begin, int_only_begin}, + {int_only_begin + T{1}, int_only_begin + T{1}}, + + {int_only_end - T{1}, int_only_end - T{1}}, + {int_only_end, int_only_end}, + {int_only_end + T{2}, int_only_end + T{2}}, + + {L::max(), L::max()}, + + // Checks the following rule common for all rounding instructions: + // f(+-inf) = +-inf + {L::infinity(), L::infinity()}, + }; + + /// Creates a wasm module with a single function for the given instructions opcode. + /// The opcode is converted to match the type, e.g. f32_add -> f64_add. + static bytes get_numeric_instruction_code( + bytes_view template_code, Instr template_opcode, Instr opcode) + { + constexpr auto f64_variant_offset = + static_cast(Instr::f64_add) - static_cast(Instr::f32_add); + + // Convert to f64 variant if needed. + const auto typed_opcode = + std::is_same_v ? + static_cast(static_cast(opcode) + f64_variant_offset) : + static_cast(opcode); + + bytes wasm{template_code}; + constexpr auto template_type = static_cast(ValType::f32); + const auto template_opcode_byte = static_cast(template_opcode); + const auto opcode_arity = get_instruction_type_table()[template_opcode_byte].inputs.size(); + + EXPECT_EQ(std::count(wasm.begin(), wasm.end(), template_type), opcode_arity + 1); + EXPECT_EQ(std::count(wasm.begin(), wasm.end(), template_opcode_byte), 1); + + std::replace(wasm.begin(), wasm.end(), template_type, + static_cast(WasmTypeTraits::valtype)); + std::replace(wasm.begin(), wasm.end(), template_opcode_byte, typed_opcode); + return wasm; + } + + static bytes get_unop_code(Instr opcode) + { + /* wat2wasm + (func (param f32) (result f32) + (f32.abs (local.get 0)) + ) + */ + auto wasm = from_hex("0061736d0100000001060160017d017d030201000a0701050020008b0b"); + return get_numeric_instruction_code(wasm, Instr::f32_abs, opcode); + } + + static bytes get_binop_code(Instr opcode) + { + /* wat2wasm + (func (param f32 f32) (result f32) + (f32.add (local.get 0) (local.get 1)) + ) + */ + auto wasm = from_hex("0061736d0100000001070160027d7d017d030201000a0901070020002001920b"); + return get_numeric_instruction_code(wasm, Instr::f32_add, opcode); + } +}; + +using FloatingPointTypes = testing::Types; +TYPED_TEST_SUITE(execute_floating_point_types, FloatingPointTypes, WasmTypeName); +} // namespace fizzy::test