blob: 7e071cf1fb330cfba0053bbcef2cbd3582c18de8 [file] [log] [blame]
// 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.
#ifndef SRC_TINT_LANG_CORE_CONSTANT_EVAL_TEST_H_
#define SRC_TINT_LANG_CORE_CONSTANT_EVAL_TEST_H_
#include <limits>
#include <optional>
#include <string>
#include <utility>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "src/tint/lang/core/type/helper_test.h"
#include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
#include "src/tint/utils/rtti/switch.h"
#include "src/tint/utils/text/string_stream.h"
namespace tint::constant::test {
// TODO(bclayton): Don't depend on resolver
namespace builder = tint::resolver::builder;
// TODO(bclayton): Don't depend on resolver
using ConstEvalTest = resolver::ResolverTest;
// TODO(bclayton): Don't depend on resolver
template <typename T>
using ConstEvalTestWithParam = resolver::ResolverTestWithParam<T>;
template <typename T>
inline const auto kPiOver2 = T(UnwrapNumber<T>(1.57079632679489661923));
template <typename T>
inline const auto kPiOver4 = T(UnwrapNumber<T>(0.785398163397448309616));
template <typename T>
inline const auto k3PiOver4 = T(UnwrapNumber<T>(2.356194490192344928846));
/// Walks the constant::Value @p c, accumulating all the inner-most scalar values into @p args
template <size_t N>
inline void CollectScalars(const constant::Value* c, Vector<builder::Scalar, N>& scalars) {
Switch(
c->Type(), //
[&](const type::AbstractInt*) { scalars.Push(c->ValueAs<AInt>()); },
[&](const type::AbstractFloat*) { scalars.Push(c->ValueAs<AFloat>()); },
[&](const type::Bool*) { scalars.Push(c->ValueAs<bool>()); },
[&](const type::I32*) { scalars.Push(c->ValueAs<i32>()); },
[&](const type::U32*) { scalars.Push(c->ValueAs<u32>()); },
[&](const type::F32*) { scalars.Push(c->ValueAs<f32>()); },
[&](const type::F16*) { scalars.Push(c->ValueAs<f16>()); },
[&](Default) {
size_t i = 0;
while (auto* child = c->Index(i++)) {
CollectScalars(child, scalars);
}
});
}
/// Walks the constant::Value @p c, returning all the inner-most scalar values.
inline Vector<builder::Scalar, 16> ScalarsFrom(const constant::Value* c) {
Vector<builder::Scalar, 16> out;
CollectScalars(c, out);
return out;
}
template <typename T>
inline auto Abs(const Number<T>& v) {
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>) {
return v;
} else {
return Number<T>(std::abs(v));
}
}
/// Flags that can be passed to CheckConstant()
struct CheckConstantFlags {
/// Expected value may be positive or negative
bool pos_or_neg = false;
/// Expected value should be compared using EXPECT_FLOAT_EQ instead of EQ, or EXPECT_NEAR if
/// float_compare_epsilon is set.
bool float_compare = false;
/// Expected value should be compared using EXPECT_NEAR if float_compare is set.
std::optional<double> float_compare_epsilon;
};
/// CheckConstant checks that @p got_constant, the result value of
/// constant-evaluation is equal to @p expected_value.
/// @param got_constant the constant value evaluated by the resolver
/// @param expected_value the expected value for the test
/// @param flags optional flags for controlling the comparisons
inline void CheckConstant(const constant::Value* got_constant,
const builder::Value& expected_value,
CheckConstantFlags flags = {}) {
auto values_flat = ScalarsFrom(got_constant);
auto expected_values_flat = expected_value.args;
ASSERT_EQ(values_flat.Length(), expected_values_flat.Length());
for (size_t i = 0; i < values_flat.Length(); ++i) {
auto& got_scalar = values_flat[i];
auto& expected_scalar = expected_values_flat[i];
std::visit(
[&](const auto& expected) {
using T = std::decay_t<decltype(expected)>;
ASSERT_TRUE(std::holds_alternative<T>(got_scalar))
<< "Scalar variant index: " << got_scalar.index();
auto got = std::get<T>(got_scalar);
if constexpr (std::is_same_v<bool, T>) {
EXPECT_EQ(got, expected) << "index: " << i;
} else if constexpr (IsFloatingPoint<T>) {
if (std::isnan(expected)) {
EXPECT_TRUE(std::isnan(got)) << "index: " << i;
} else {
if (flags.pos_or_neg) {
got = Abs(got);
}
if (flags.float_compare) {
if (flags.float_compare_epsilon) {
EXPECT_NEAR(got, expected, *flags.float_compare_epsilon)
<< "index: " << i;
} else {
EXPECT_FLOAT_EQ(got, expected) << "index: " << i;
}
} else {
EXPECT_EQ(got, expected) << "index: " << i;
}
}
} else {
if (flags.pos_or_neg) {
got = Abs(got);
}
EXPECT_EQ(got, expected) << "index: " << i;
// Check that the constant's integer doesn't contain unexpected
// data in the MSBs that are outside of the bit-width of T.
EXPECT_EQ(AInt(got), AInt(expected)) << "index: " << i;
}
},
expected_scalar);
}
}
template <typename T>
inline constexpr auto Negate(const Number<T>& v) {
if constexpr (std::is_integral_v<T>) {
if constexpr (std::is_signed_v<T>) {
// For signed integrals, avoid C++ UB by not negating the smallest negative number. In
// WGSL, this operation is well defined to return the same value, see:
// https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr.
if (v == std::numeric_limits<T>::min()) {
return v;
}
return -v;
} else {
// Allow negating unsigned values
using ST = std::make_signed_t<T>;
auto as_signed = Number<ST>{static_cast<ST>(v)};
return Number<T>{static_cast<T>(Negate(as_signed))};
}
} else {
// float case
return -v;
}
}
TINT_BEGIN_DISABLE_WARNING(CONSTANT_OVERFLOW);
template <typename T>
inline constexpr Number<T> Mul(Number<T> v1, Number<T> v2) {
if constexpr (std::is_integral_v<T> && std::is_signed_v<T>) {
// For signed integrals, avoid C++ UB by multiplying as unsigned
using UT = std::make_unsigned_t<T>;
return static_cast<Number<T>>(static_cast<UT>(v1) * static_cast<UT>(v2));
} else {
return static_cast<Number<T>>(v1 * v2);
}
}
TINT_END_DISABLE_WARNING(CONSTANT_OVERFLOW);
TINT_BEGIN_DISABLE_WARNING(CONSTANT_OVERFLOW);
template <typename T>
inline constexpr Number<T> Add(Number<T> v1, Number<T> v2) {
if constexpr (std::is_integral_v<T> && std::is_signed_v<T>) {
// For signed integrals, avoid C++ UB by adding as unsigned
using UT = std::make_unsigned_t<T>;
return static_cast<Number<T>>(static_cast<UT>(v1) + static_cast<UT>(v2));
} else {
return static_cast<Number<T>>(v1 + v2);
}
}
TINT_END_DISABLE_WARNING(CONSTANT_OVERFLOW);
// Concats any number of std::vectors
template <typename Vec, typename... Vecs>
[[nodiscard]] inline auto Concat(Vec&& v1, Vecs&&... vs) {
auto total_size = v1.size() + (vs.size() + ...);
v1.reserve(total_size);
(std::move(vs.begin(), vs.end(), std::back_inserter(v1)), ...);
return std::move(v1);
}
// Concats vectors `vs` into `v1`
template <typename Vec, typename... Vecs>
inline void ConcatInto(Vec& v1, Vecs&&... vs) {
auto total_size = v1.size() + (vs.size() + ...);
v1.reserve(total_size);
(std::move(vs.begin(), vs.end(), std::back_inserter(v1)), ...);
}
// Concats vectors `vs` into `v1` iff `condition` is true
template <bool condition, typename Vec, typename... Vecs>
inline void ConcatIntoIf([[maybe_unused]] Vec& v1, [[maybe_unused]] Vecs&&... vs) {
if constexpr (condition) {
ConcatInto(v1, std::forward<Vecs>(vs)...);
}
}
/// Returns the overflow error message for binary ops
template <typename NumberT>
inline std::string OverflowErrorMessage(NumberT lhs, const char* op, NumberT rhs) {
StringStream ss;
ss << "'" << lhs.value << " " << op << " " << rhs.value << "' cannot be represented as '"
<< FriendlyName<NumberT>() << "'";
return ss.str();
}
/// Returns the overflow error message for conversions
template <typename VALUE_TY>
std::string OverflowErrorMessage(VALUE_TY value, std::string_view target_ty) {
StringStream ss;
ss << "value " << value << " cannot be represented as "
<< "'" << target_ty << "'";
return ss.str();
}
/// Returns the overflow error message for exponentiation
template <typename NumberT>
std::string OverflowExpErrorMessage(std::string_view base, NumberT exp) {
StringStream ss;
ss << base << "^" << exp << " cannot be represented as "
<< "'" << FriendlyName<NumberT>() << "'";
return ss.str();
}
using builder::Array;
using builder::IsValue;
using builder::Mat;
using builder::Val;
using builder::Value;
using builder::Vec;
// Calls `f` on deepest elements of both `a` and `b`. If function returns Action::kStop, it stops
// traversing, and return Action::kStop; if the function returns Action::kContinue, it continues and
// returns Action::kContinue when done.
// TODO(amaiorano): Move to Constant.h?
enum class Action { kStop, kContinue };
template <typename Func>
inline Action ForEachElemPair(const constant::Value* a, const constant::Value* b, Func&& f) {
EXPECT_EQ(a->Type(), b->Type());
size_t i = 0;
while (true) {
auto* a_elem = a->Index(i);
if (!a_elem) {
break;
}
auto* b_elem = b->Index(i);
if (ForEachElemPair(a_elem, b_elem, f) == Action::kStop) {
return Action::kStop;
}
i++;
}
if (i == 0) {
return f(a, b);
}
return Action::kContinue;
}
/// Defines common bit value patterns for the input `NumberT` type used for testing.
template <typename NumberT>
struct BitValues {
/// The unwrapped number type
using T = UnwrapNumber<NumberT>;
/// The unsigned unwrapped number type
using UT = std::make_unsigned_t<T>;
/// Details
struct detail {
/// Unsigned type of `T`
using UT = std::make_unsigned_t<T>;
/// Size in bits of type T
static constexpr size_t NumBits = sizeof(T) * 8;
/// All bits set 1
static constexpr T All = T{~T{0}};
/// Only left-most bits set to 1, rest set to 0
static constexpr T LeftMost = static_cast<T>(UT{1} << (NumBits - 1u));
/// Only left-most bits set to 0, rest set to 1
static constexpr T AllButLeftMost = T{~LeftMost};
/// Only two left-most bits set to 1, rest set to 0
static constexpr T TwoLeftMost = static_cast<T>(UT{0b11} << (NumBits - 2u));
/// Only two left-most bits set to 0, rest set to 1
static constexpr T AllButTwoLeftMost = T{~TwoLeftMost};
/// Only right-most bit set to 1, rest set to 0
static constexpr T RightMost = T{1};
/// Only right-most bit set to 0, rest set to 1
static constexpr T AllButRightMost = T{~RightMost};
};
/// Size in bits of type NumberT
static inline const size_t NumBits = detail::NumBits;
/// All bits set 1
static inline const NumberT All = NumberT{detail::All};
/// Only left-most bits set to 1, rest set to 0
static inline const NumberT LeftMost = NumberT{detail::LeftMost};
/// Only left-most bits set to 0, rest set to 1
static inline const NumberT AllButLeftMost = NumberT{detail::AllButLeftMost};
/// Only two left-most bits set to 1, rest set to 0
static inline const NumberT TwoLeftMost = NumberT{detail::TwoLeftMost};
/// Only two left-most bits set to 0, rest set to 1
static inline const NumberT AllButTwoLeftMost = NumberT{detail::AllButTwoLeftMost};
/// Only right-most bit set to 1, rest set to 0
static inline const NumberT RightMost = NumberT{detail::RightMost};
/// Only right-most bit set to 0, rest set to 1
static inline const NumberT AllButRightMost = NumberT{detail::AllButRightMost};
/// Performs a left-shift of `val` by `shiftBy`, both of varying type cast to `T`.
/// @param val value to shift left
/// @param shiftBy number of bits to shift left by
/// @returns the shifted value
template <typename U, typename V>
static constexpr NumberT Lsh(U val, V shiftBy) {
return NumberT{static_cast<T>(static_cast<UT>(val) << static_cast<UT>(shiftBy))};
}
};
} // namespace tint::constant::test
#endif // SRC_TINT_LANG_CORE_CONSTANT_EVAL_TEST_H_