blob: 409cc577ff692f8121e5ec6da5ff9457594652ad [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
// Host-only unit tests for normalizing_hasher<Key>::_canonicalize.
//
// These run without a GPU and verify that each normalization branch produces
// the canonical uint64_t bit pattern matching datasketches-cpp's host-side
// update(...) overloads. The tests are intentionally white-box: they compare
// hash outputs between types that datasketches-cpp maps to the same canonical
// representation (e.g. int32_t(-1) == uint32_t(0xFFFFFFFF) after sign-extension).
#include <cstdint>
#include <cstring>
#include <limits>
#include <catch2/catch_test_macros.hpp>
#include <datasketches/cuda/detail/hll/normalizing_hasher.cuh>
using datasketches::cuda::detail::hll::normalizing_hasher;
// Helper: invoke normalizing_hasher with seed 9001 (same as all tests).
template <class T>
static __uint128_t h(T v)
{
return normalizing_hasher<T>{}(v);
}
// ============================================================
// Integral sign-extension parity
// ============================================================
TEST_CASE("int32/16/8 sign-extend to same int64 representation", "[normalizing_hasher]")
{
// int32_t(-1) sign-extends to int64_t(-1); all three signed narrow types do.
REQUIRE(h<int32_t>(-1) == h<int64_t>(-1));
REQUIRE(h<int16_t>(-1) == h<int64_t>(-1));
REQUIRE(h<int8_t>(-1) == h<int64_t>(-1));
REQUIRE(h<int32_t>(1) == h<int64_t>(1));
REQUIRE(h<int16_t>(1) == h<int64_t>(1));
REQUIRE(h<int8_t>(1) == h<int64_t>(1));
// INT32_MIN sign-extends to INT64_MIN.
REQUIRE(h<int32_t>(std::numeric_limits<int32_t>::min()) ==
h<int64_t>(static_cast<int64_t>(std::numeric_limits<int32_t>::min())));
}
TEST_CASE("uint narrow types reinterpret-as-signed then sign-extend", "[normalizing_hasher]")
{
// update(uint32_t) → update(int32_t) → sign-extend to int64_t
// So uint32_t(0xFFFFFFFF) == uint32_t bit-pattern of int32_t(-1) → int64_t(-1)
REQUIRE(h<uint32_t>(0xFFFFFFFFu) == h<int64_t>(-1));
REQUIRE(h<uint16_t>(0xFFFFu) == h<int64_t>(-1));
REQUIRE(h<uint8_t>(0xFFu) == h<int64_t>(-1));
// uint32_t(0) == int64_t(0)
REQUIRE(h<uint32_t>(0u) == h<int64_t>(0));
REQUIRE(h<uint16_t>(0u) == h<int64_t>(0));
REQUIRE(h<uint8_t>(0u) == h<int64_t>(0));
// uint32_t(1) == int64_t(1) (no reinterpretation needed, same bits)
REQUIRE(h<uint32_t>(1u) == h<int64_t>(1));
// uint32_t(0x80000000) reinterprets as int32_t(-2147483648) then sign-extends
REQUIRE(h<uint32_t>(0x80000000u) ==
h<int64_t>(static_cast<int64_t>(static_cast<int32_t>(0x80000000u))));
}
TEST_CASE("uint64_t and int64_t with same bit pattern produce same hash", "[normalizing_hasher]")
{
// Both hash 8 raw bytes -- no normalization for 64-bit types.
REQUIRE(h<uint64_t>(0) == h<int64_t>(0));
REQUIRE(h<uint64_t>(1) == h<int64_t>(1));
// 0xFFFFFFFFFFFFFFFF == int64_t(-1) as bit pattern
REQUIRE(h<uint64_t>(UINT64_MAX) == h<int64_t>(-1));
}
// ============================================================
// double / float canonicalization
// ============================================================
TEST_CASE("double: -0.0 and +0.0 produce the same hash", "[normalizing_hasher]")
{
const double pos_zero = 0.0;
double neg_zero = 0.0;
neg_zero = -neg_zero;
REQUIRE(h<double>(pos_zero) == h<double>(neg_zero));
}
TEST_CASE("double: all NaN payloads produce the same hash", "[normalizing_hasher]")
{
// Construct distinct NaN bit patterns (positive quiet, negative quiet,
// signaling-like with different payloads).
auto bits_to_double = [](uint64_t b) {
double d;
std::memcpy(&d, &b, 8);
return d;
};
const double nan_a = bits_to_double(0x7ff8000000000001ULL); // positive quiet NaN
const double nan_b = bits_to_double(0xfff8000000000001ULL); // negative quiet NaN
const double nan_c = bits_to_double(0x7ff0000000000001ULL); // signaling-ish NaN
REQUIRE(h<double>(nan_a) == h<double>(nan_b));
REQUIRE(h<double>(nan_b) == h<double>(nan_c));
}
TEST_CASE("double: NaN hash equals the Java canonical 0x7ff8000000000000", "[normalizing_hasher]")
{
auto bits_to_double = [](uint64_t b) {
double d;
std::memcpy(&d, &b, 8);
return d;
};
const double canonical_nan = bits_to_double(0x7ff8000000000000ULL);
const double other_nan = bits_to_double(0x7ff8000000000042ULL);
// Both NaNs must hash the same as the canonical form itself.
REQUIRE(h<double>(other_nan) == h<double>(canonical_nan));
}
TEST_CASE("float: -0.0f and +0.0f produce the same hash", "[normalizing_hasher]")
{
const float pos_zero = 0.0f;
float neg_zero = 0.0f;
neg_zero = -neg_zero;
REQUIRE(h<float>(pos_zero) == h<float>(neg_zero));
}
TEST_CASE("float: all NaN payloads produce the same hash", "[normalizing_hasher]")
{
auto bits_to_float = [](uint32_t b) {
float f;
std::memcpy(&f, &b, 4);
return f;
};
const float nan_a = bits_to_float(0x7fc00001u); // positive quiet NaN
const float nan_b = bits_to_float(0xffc00001u); // negative quiet NaN
REQUIRE(h<float>(nan_a) == h<float>(nan_b));
}
TEST_CASE("float NaN hashes same as double NaN (both canonicalize to 0x7ff8000000000000)",
"[normalizing_hasher]")
{
auto bits_to_float = [](uint32_t b) {
float f;
std::memcpy(&f, &b, 4);
return f;
};
auto bits_to_double = [](uint64_t b) {
double d;
std::memcpy(&d, &b, 8);
return d;
};
const float fnan = bits_to_float(0x7fc00001u);
const double dnan = bits_to_double(0x7ff8000000000000ULL); // the canonical form
// float NaN → double NaN → canonical 0x7ff8000000000000 → same hash as dnan
REQUIRE(h<float>(fnan) == h<double>(dnan));
}
TEST_CASE("float -0.0f hashes same as double -0.0", "[normalizing_hasher]")
{
float neg_zero_f = -0.0f;
double neg_zero_d = -0.0;
// Both canonicalize to +0.0 before hashing.
REQUIRE(h<float>(neg_zero_f) == h<double>(neg_zero_d));
}