From 96bc00a1c3e859212dde09ac19cb36cc5e5525de Mon Sep 17 00:00:00 2001 From: Robert Shi Date: Sun, 20 Sep 2026 22:53:21 -0700 Subject: [PATCH 1/5] Add test and benchmarks for Int64HashMap --- .../core/data_structures/CMakeLists.txt | 1 + .../data_structures/int64_hash_map_bench.cpp | 112 +++ .../unit/core/data_structures/CMakeLists.txt | 1 + .../data_structures/int64_hash_map_test.cpp | 691 ++++++++++++++++++ 4 files changed, 805 insertions(+) create mode 100644 benchmarks/core/data_structures/int64_hash_map_bench.cpp create mode 100644 tests/unit/core/data_structures/int64_hash_map_test.cpp diff --git a/benchmarks/core/data_structures/CMakeLists.txt b/benchmarks/core/data_structures/CMakeLists.txt index 364087f4..f7b0debb 100644 --- a/benchmarks/core/data_structures/CMakeLists.txt +++ b/benchmarks/core/data_structures/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(data_structures_bench bitset_bench.cpp int64_array_bench.cpp + int64_hash_map_bench.cpp int64_hash_set_bench.cpp u64x2_static_hash_set_bench.cpp ) diff --git a/benchmarks/core/data_structures/int64_hash_map_bench.cpp b/benchmarks/core/data_structures/int64_hash_map_bench.cpp new file mode 100644 index 00000000..434a2bd9 --- /dev/null +++ b/benchmarks/core/data_structures/int64_hash_map_bench.cpp @@ -0,0 +1,112 @@ +/** + * @file int64_hash_map_bench.cpp + * @author Generated by Claude Opus 4.6 + * @author Robert Shi (robertyishi@berkeley.edu): prompts and code review + * @brief Benchmarks for Int64HashMap. + * + * @copyright GamesCrafters research and development group + * SPDX-License-Identifier: GPL-3.0-or-later + * (see root COPYING file or accompanying source file) + */ + +#include + +#include + +extern "C" { +#include "core/data_structures/int64_hash_map.h" +} + +static void BM_Int64HashMapSet(benchmark::State& state) { + int64_t num_elements = state.range(0); + double max_load_factor = state.range(1) / 100.0; + + for (auto _ : state) { + Int64HashMap map; + Int64HashMapInit(&map, max_load_factor); + + for (int64_t i = 0; i < num_elements; ++i) { + benchmark::DoNotOptimize(Int64HashMapSet(&map, i, i * 7)); + } + + Int64HashMapDestroy(&map); + } + + state.SetItemsProcessed(state.iterations() * num_elements); +} + +BENCHMARK(BM_Int64HashMapSet) + ->ArgsProduct({benchmark::CreateRange(1, 4096, /*multi=*/2), {50}}); + +static void BM_Int64HashMapGetHit(benchmark::State& state) { + int64_t num_elements = state.range(0); + double max_load_factor = state.range(1) / 100.0; + + Int64HashMap map; + Int64HashMapInit(&map, max_load_factor); + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i * 7); + } + + for (auto _ : state) { + for (int64_t i = 0; i < num_elements; ++i) { + benchmark::DoNotOptimize(Int64HashMapGet(&map, i)); + } + } + + state.SetItemsProcessed(state.iterations() * num_elements); + Int64HashMapDestroy(&map); +} + +BENCHMARK(BM_Int64HashMapGetHit) + ->ArgsProduct({benchmark::CreateRange(1, 4096, /*multi=*/2), {50}}); + +static void BM_Int64HashMapGetMiss(benchmark::State& state) { + int64_t num_elements = state.range(0); + double max_load_factor = state.range(1) / 100.0; + + Int64HashMap map; + Int64HashMapInit(&map, max_load_factor); + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i * 7); + } + + for (auto _ : state) { + // Query keys that were never inserted. + for (int64_t i = num_elements; i < 2 * num_elements; ++i) { + benchmark::DoNotOptimize(Int64HashMapGet(&map, i)); + } + } + + state.SetItemsProcessed(state.iterations() * num_elements); + Int64HashMapDestroy(&map); +} + +BENCHMARK(BM_Int64HashMapGetMiss) + ->ArgsProduct({benchmark::CreateRange(1, 4096, /*multi=*/2), {50}}); + +static void BM_Int64HashMapIterate(benchmark::State& state) { + int64_t num_elements = state.range(0); + double max_load_factor = state.range(1) / 100.0; + + Int64HashMap map; + Int64HashMapInit(&map, max_load_factor); + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i * 7); + } + + for (auto _ : state) { + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key, value; + while (Int64HashMapIteratorNext(&it, &key, &value)) { + benchmark::DoNotOptimize(key); + benchmark::DoNotOptimize(value); + } + } + + state.SetItemsProcessed(state.iterations() * num_elements); + Int64HashMapDestroy(&map); +} + +BENCHMARK(BM_Int64HashMapIterate) + ->ArgsProduct({benchmark::CreateRange(1, 4096, /*multi=*/2), {50}}); diff --git a/tests/unit/core/data_structures/CMakeLists.txt b/tests/unit/core/data_structures/CMakeLists.txt index efc0fd2e..04ffc191 100644 --- a/tests/unit/core/data_structures/CMakeLists.txt +++ b/tests/unit/core/data_structures/CMakeLists.txt @@ -5,6 +5,7 @@ add_library_test(data_structures_test data_structures cstring_test.cpp hash_test.cpp int64_array_test.cpp + int64_hash_map_test.cpp int64_hash_set_test.cpp int64_queue_test.cpp int64_static_hash_set_test.cpp diff --git a/tests/unit/core/data_structures/int64_hash_map_test.cpp b/tests/unit/core/data_structures/int64_hash_map_test.cpp new file mode 100644 index 00000000..1af82a25 --- /dev/null +++ b/tests/unit/core/data_structures/int64_hash_map_test.cpp @@ -0,0 +1,691 @@ +/** + * @file int64_hash_map_test.cpp + * @author Robert Shi (robertyishi@berkeley.edu) + * @brief Unit tests for Int64HashMap. + * + * @copyright GamesCrafters research and development group + * SPDX-License-Identifier: GPL-3.0-or-later + * (see root COPYING file or accompanying source file) + */ + +#include + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "core/data_structures/int64_hash_map.h" +} + +// ============================= Int64HashMapInit ============================= + +TEST(Int64HashMapTest, InitializesWithStandardLoadFactor) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + EXPECT_TRUE(Int64HashMapSet(&map, 42, 100)) + << "The map should initialize correctly and accept a basic insertion " + "with a standard 0.5 load factor."; + + Int64HashMapIterator it = Int64HashMapGet(&map, 42); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should return a valid iterator for a key that was just " + "inserted."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 100) + << "The map should return the correct value for the inserted key."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, ClampsLoadFactorBelowMinimum) { + Int64HashMap map; + Int64HashMapInit(&map, 0.01); + + EXPECT_TRUE(Int64HashMapSet(&map, 1, 10)) + << "The map should be functional despite a below-minimum load factor " + "input."; + + EXPECT_TRUE(Int64HashMapSet(&map, 2, 20)) + << "The map should allow multiple unique insertions after clamping."; + + Int64HashMapIterator it = Int64HashMapGet(&map, 1); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should find a previously inserted key after clamping."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 10) + << "The map should return the correct value for the clamped case."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, ClampsLoadFactorAboveMaximum) { + Int64HashMap map; + Int64HashMapInit(&map, 0.99); + + EXPECT_TRUE(Int64HashMapSet(&map, -999, 111)) + << "The map should be functional despite an above-maximum load factor " + "input."; + + EXPECT_TRUE(Int64HashMapSet(&map, 999, 222)) + << "The map should allow multiple unique insertions after clamping."; + + EXPECT_TRUE(Int64HashMapContains(&map, -999)) + << "The map should find a previously inserted key after clamping."; + + EXPECT_TRUE(Int64HashMapContains(&map, 999)) + << "The map should find all previously inserted keys."; + + Int64HashMapDestroy(&map); +} + +// ============================ Int64HashMapDestroy ============================ + +TEST(Int64HashMapTest, DestroysEmptyMap) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + // Should not crash or leak memory. + SUCCEED(); + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, DestroysPopulatedMap) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 1, 10); + Int64HashMapSet(&map, 2, 20); + Int64HashMapSet(&map, 3, 30); + + // Should not crash or leak memory. + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, AllowsReuseAfterDestruction) { + Int64HashMap map; + + Int64HashMapInit(&map, 0.5); + EXPECT_TRUE(Int64HashMapSet(&map, 50, 500)) + << "The map should accept an insertion during its first lifecycle."; + Int64HashMapDestroy(&map); + + Int64HashMapInit(&map, 0.5); + EXPECT_TRUE(Int64HashMapSet(&map, 50, 999)) + << "The map should allow inserting the same key after being destroyed " + "and re-initialized."; + + Int64HashMapIterator it = Int64HashMapGet(&map, 50); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should find the key inserted during the second lifecycle."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 999) + << "The map should return the value from the second lifecycle, not the " + "first."; + + Int64HashMapDestroy(&map); +} + +// ============================== Int64HashMapSet ============================== + +TEST(Int64HashMapTest, SetsSingleEntry) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + EXPECT_TRUE(Int64HashMapSet(&map, 42, 100)) + << "Setting a single key-value pair should succeed."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, SetsMultipleDistinctEntries) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + EXPECT_TRUE(Int64HashMapSet(&map, 10, 100)) + << "Setting the first key-value pair should succeed."; + EXPECT_TRUE(Int64HashMapSet(&map, 20, 200)) + << "Setting the second key-value pair should succeed."; + EXPECT_TRUE(Int64HashMapSet(&map, 30, 300)) + << "Setting the third key-value pair should succeed."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, OverwritesExistingKeyWithNewValue) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + EXPECT_TRUE(Int64HashMapSet(&map, 42, 100)) + << "Initial insertion should succeed."; + + EXPECT_TRUE(Int64HashMapSet(&map, 42, 999)) + << "Overwriting an existing key with a new value should succeed."; + + Int64HashMapIterator it = Int64HashMapGet(&map, 42); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The key should still be found after overwriting."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 999) + << "The value should be updated to the new value after overwriting."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, OverwriteMultipleTimesRetainsLatestValue) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + for (int64_t v = 0; v < 10; ++v) { + EXPECT_TRUE(Int64HashMapSet(&map, 42, v)) + << "Overwrite #" << v << " should succeed."; + } + + Int64HashMapIterator it = Int64HashMapGet(&map, 42); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The key should still be found after repeated overwrites."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 9) + << "The value should reflect the last overwrite."; + + Int64HashMapDestroy(&map); +} + +// ============================== Int64HashMapGet ============================== + +TEST(Int64HashMapTest, GetReturnsValidIteratorForExistingKey) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 42, 100); + + Int64HashMapIterator it = Int64HashMapGet(&map, 42); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "Get should return a valid iterator for an existing key."; + + EXPECT_EQ(Int64HashMapIteratorKey(&it), 42) + << "The iterator should point to the correct key."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 100) + << "The iterator should point to the correct value."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, GetReturnsInvalidIteratorForMissingKey) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 42, 100); + + Int64HashMapIterator it = Int64HashMapGet(&map, 99); + EXPECT_FALSE(Int64HashMapIteratorIsValid(&it)) + << "Get should return an invalid iterator for a key that was never " + "inserted."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, GetReturnsInvalidIteratorForEmptyMap) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapIterator it = Int64HashMapGet(&map, 42); + EXPECT_FALSE(Int64HashMapIteratorIsValid(&it)) + << "Get should return an invalid iterator when the map is empty."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, GetReturnsCorrectValuesForMultipleKeys) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 10, 100); + Int64HashMapSet(&map, 20, 200); + Int64HashMapSet(&map, 30, 300); + + for (int64_t key = 10; key <= 30; key += 10) { + Int64HashMapIterator it = Int64HashMapGet(&map, key); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "Get should return a valid iterator for key " << key << "."; + + EXPECT_EQ(Int64HashMapIteratorKey(&it), key) + << "The iterator should point to the correct key."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), key * 10) + << "The iterator should point to the correct value for key " << key + << "."; + } + + Int64HashMapDestroy(&map); +} + +// =========================== Int64HashMapContains =========================== + +TEST(Int64HashMapTest, ContainsReturnsTrueForExistingKeys) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 10, 100); + Int64HashMapSet(&map, 20, 200); + + EXPECT_TRUE(Int64HashMapContains(&map, 10)) + << "Contains should return true for the first inserted key."; + + EXPECT_TRUE(Int64HashMapContains(&map, 20)) + << "Contains should return true for the second inserted key."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, ContainsReturnsFalseForMissingKeys) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 10, 100); + + EXPECT_FALSE(Int64HashMapContains(&map, 20)) + << "Contains should return false for a key that was never inserted."; + + EXPECT_FALSE(Int64HashMapContains(&map, 0)) + << "Contains should return false for zero when it was never inserted."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, ContainsReturnsFalseForEmptyMap) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + EXPECT_FALSE(Int64HashMapContains(&map, 42)) + << "Contains should return false when the map is completely empty."; + + Int64HashMapDestroy(&map); +} + +// ============================= Edge Case Values ============================= + +class Int64HashMapEdgeCaseTest + : public ::testing::TestWithParam> {}; + +TEST_P(Int64HashMapEdgeCaseTest, HandlesEdgeCaseKeyValuePairs) { + auto [key, value] = GetParam(); + + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + EXPECT_FALSE(Int64HashMapContains(&map, key)) + << "The map should not contain key " << key << " when empty."; + + EXPECT_TRUE(Int64HashMapSet(&map, key, value)) + << "The map should successfully set key " << key << " to value " + << value << "."; + + EXPECT_TRUE(Int64HashMapContains(&map, key)) + << "The map should contain key " << key << " after insertion."; + + Int64HashMapIterator it = Int64HashMapGet(&map, key); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "Get should return a valid iterator for key " << key << "."; + + EXPECT_EQ(Int64HashMapIteratorKey(&it), key) + << "The iterator key should match."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), value) + << "The iterator value should match."; + + Int64HashMapDestroy(&map); +} + +INSTANTIATE_TEST_SUITE_P( + EdgeCases, Int64HashMapEdgeCaseTest, + ::testing::Values( + // {key, value} + std::make_pair(int64_t{0}, int64_t{0}), + std::make_pair(int64_t{-1}, int64_t{-1}), + std::make_pair(std::numeric_limits::max(), + std::numeric_limits::max()), + std::make_pair(std::numeric_limits::max(), + std::numeric_limits::min()), + std::make_pair(int64_t{0}, std::numeric_limits::max()), + std::make_pair(int64_t{0}, std::numeric_limits::min()), + std::make_pair(int64_t{1}, int64_t{0}), + std::make_pair(int64_t{-1}, int64_t{0}))); + +// ================================= Iteration ================================= + +TEST(Int64HashMapTest, IteratesOverEmptyMap) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + // Force allocation so Begin/Next have something to iterate over. + Int64HashMapSet(&map, 1, 1); + Int64HashMapDestroy(&map); + Int64HashMapInit(&map, 0.5); + + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key, value; + + EXPECT_FALSE(Int64HashMapIteratorNext(&it, &key, &value)) + << "Iterating over an empty map should immediately return false."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IteratesOverSingleEntry) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 42, 100); + + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key, value; + + EXPECT_TRUE(Int64HashMapIteratorNext(&it, &key, &value)) + << "The first call to IteratorNext should find the single entry."; + + EXPECT_EQ(key, 42) << "The iterated key should match the inserted key."; + + EXPECT_EQ(value, 100) + << "The iterated value should match the inserted value."; + + EXPECT_FALSE(Int64HashMapIteratorNext(&it, &key, &value)) + << "The second call to IteratorNext should return false for a " + "single-entry map."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IteratesOverMultipleEntries) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + const std::vector> expected = { + {10, 100}, {20, 200}, {30, 300}, {40, 400}, {50, 500}, + }; + + for (auto &[k, v] : expected) { + Int64HashMapSet(&map, k, v); + } + + // Collect all iterated entries. + std::vector> actual; + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key, value; + while (Int64HashMapIteratorNext(&it, &key, &value)) { + actual.emplace_back(key, value); + } + + // Sort both for comparison since iteration order is unspecified. + auto sorted_expected = expected; + std::sort(sorted_expected.begin(), sorted_expected.end()); + std::sort(actual.begin(), actual.end()); + + EXPECT_EQ(actual.size(), expected.size()) + << "Iteration should visit exactly all inserted entries."; + + EXPECT_EQ(actual, sorted_expected) + << "Iteration should return all inserted key-value pairs."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IterationVisitsEveryEntryExactlyOnce) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + constexpr int64_t num_elements = 100; + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i * 10); + } + + std::set visited_keys; + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key, value; + while (Int64HashMapIteratorNext(&it, &key, &value)) { + EXPECT_EQ(visited_keys.count(key), 0u) + << "Key " << key << " was visited more than once."; + visited_keys.insert(key); + + EXPECT_EQ(value, key * 10) + << "Value for key " << key << " should be " << (key * 10) << "."; + } + + EXPECT_EQ(static_cast(visited_keys.size()), num_elements) + << "Iteration should visit exactly " << num_elements << " entries."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IterationWithNullKeyPointer) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 42, 100); + + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t value; + + EXPECT_TRUE(Int64HashMapIteratorNext(&it, nullptr, &value)) + << "IteratorNext should succeed with a NULL key pointer."; + + EXPECT_EQ(value, 100) + << "The value should still be correctly written when key is NULL."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IterationWithNullValuePointer) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 42, 100); + + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key; + + EXPECT_TRUE(Int64HashMapIteratorNext(&it, &key, nullptr)) + << "IteratorNext should succeed with a NULL value pointer."; + + EXPECT_EQ(key, 42) + << "The key should still be correctly written when value is NULL."; + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IterationWithBothPointersNull) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + Int64HashMapSet(&map, 42, 100); + + Int64HashMapIterator it = Int64HashMapBegin(&map); + + EXPECT_TRUE(Int64HashMapIteratorNext(&it, nullptr, nullptr)) + << "IteratorNext should succeed with both pointers NULL."; + + EXPECT_FALSE(Int64HashMapIteratorNext(&it, nullptr, nullptr)) + << "The second call should return false for a single-entry map."; + + Int64HashMapDestroy(&map); +} + +// ======================= Stress Testing and Rehashing ======================= + +TEST(Int64HashMapTest, TriggersNaturalRehashing) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + constexpr int64_t num_elements = 10000; + + for (int64_t i = 0; i < num_elements; ++i) { + EXPECT_TRUE(Int64HashMapSet(&map, i, i * 7)) + << "The map should dynamically grow and successfully set element " + << i << " during natural rehashing."; + } + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, PreservesDataAfterRehashing) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + constexpr int64_t num_elements = 10000; + + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i * 7); + } + + // Verify all values are correct after rehashing. + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapIterator it = Int64HashMapGet(&map, i); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should still contain key " << i << " after rehashing."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), i * 7) + << "The value for key " << i + << " should be preserved after " + "rehashing."; + } + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, ContainsHandlesMissingKeysInDenseMap) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + // Insert even keys only. + for (int64_t i = 0; i < 2000; i += 2) { + Int64HashMapSet(&map, i, i); + } + + // Odd keys should not be found. + for (int64_t i = 1; i < 2000; i += 2) { + EXPECT_FALSE(Int64HashMapContains(&map, i)) + << "The map should not contain the unadded odd key " << i << "."; + } + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, OverwritesDuringRehashing) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + constexpr int64_t num_elements = 10000; + + // First pass: insert all. + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i); + } + + // Second pass: overwrite all with different values. + for (int64_t i = 0; i < num_elements; ++i) { + EXPECT_TRUE(Int64HashMapSet(&map, i, i + 1000000)) + << "Overwriting key " << i << " should succeed."; + } + + // Verify all values are the overwritten ones. + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapIterator it = Int64HashMapGet(&map, i); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should still contain key " << i << "."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), i + 1000000) + << "The value for key " << i << " should reflect the overwrite."; + } + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, HandlesDenseClusters) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + // Dense block of contiguous integers across the zero boundary. + for (int64_t i = -2500; i <= 2500; ++i) { + EXPECT_TRUE(Int64HashMapSet(&map, i, -i)) + << "The map should successfully set contiguous key " << i << "."; + } + + for (int64_t i = -2500; i <= 2500; ++i) { + Int64HashMapIterator it = Int64HashMapGet(&map, i); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should contain key " << i + << " after dense cluster insertion."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), -i) + << "The value for key " << i << " should be " << (-i) << "."; + } + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, HandlesSparseClusters) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + const std::vector> sparse_entries = { + {0, 1}, + {1LL << 20, 2}, + {-(1LL << 30), 3}, + {1LL << 40, 4}, + {-(1LL << 50), 5}, + {std::numeric_limits::max(), 6}, + }; + + for (auto &[k, v] : sparse_entries) { + EXPECT_TRUE(Int64HashMapSet(&map, k, v)) + << "The map should successfully set sparse key " << k << "."; + } + + for (auto &[k, v] : sparse_entries) { + Int64HashMapIterator it = Int64HashMapGet(&map, k); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)) + << "The map should find sparse key " << k << "."; + + EXPECT_EQ(Int64HashMapIteratorValue(&it), v) + << "The value for sparse key " << k << " should be " << v << "."; + } + + Int64HashMapDestroy(&map); +} + +TEST(Int64HashMapTest, IterationAfterRehashing) { + Int64HashMap map; + Int64HashMapInit(&map, 0.5); + + constexpr int64_t num_elements = 10000; + for (int64_t i = 0; i < num_elements; ++i) { + Int64HashMapSet(&map, i, i * 3); + } + + // Collect all entries via iteration. + std::set visited_keys; + Int64HashMapIterator it = Int64HashMapBegin(&map); + int64_t key, value; + while (Int64HashMapIteratorNext(&it, &key, &value)) { + EXPECT_EQ(visited_keys.count(key), 0u) + << "Key " << key + << " was visited more than once during " + "post-rehash iteration."; + visited_keys.insert(key); + + EXPECT_EQ(value, key * 3) + << "Value for key " << key << " should be " << (key * 3) + << " during post-rehash iteration."; + } + + EXPECT_EQ(static_cast(visited_keys.size()), num_elements) + << "Iteration after rehashing should visit all " << num_elements + << " entries."; + + Int64HashMapDestroy(&map); +} From edaf5c92fbae52a31139455fa834a8ff0afae35d Mon Sep 17 00:00:00 2001 From: Robert Shi Date: Fri, 25 Sep 2026 00:04:21 -0700 Subject: [PATCH 2/5] Rewrite Int64HashMap for a similar optimization as Int64HashSet --- src/core/data_structures/int64_hash_map.c | 179 ++----- src/core/data_structures/int64_hash_map.h | 436 ++++++++++++------ .../data_structures/int64_hash_map_test.cpp | 6 - 3 files changed, 349 insertions(+), 272 deletions(-) diff --git a/src/core/data_structures/int64_hash_map.c b/src/core/data_structures/int64_hash_map.c index 41194da3..613e24ac 100644 --- a/src/core/data_structures/int64_hash_map.c +++ b/src/core/data_structures/int64_hash_map.c @@ -3,9 +3,7 @@ * @author Robert Shi (robertyishi@berkeley.edu) * @author GamesCrafters Research Group, UC Berkeley * Supervised by Dan Garcia - * @brief Linear-probing (open addressing) int64_t to int64_t hash map. - * @version 1.0.4 - * @date 2025-04-26 + * @brief `Int64HashMap` implementation (cold-path functions). * * @copyright This file is part of GAMESMAN, The Finite, Two-person * Perfect-Information Game Generator released under the GPL: @@ -34,147 +32,64 @@ #include "core/data_structures/hash.h" #include "core/gamesman_memory.h" -static int64_t Hash(int64_t key, int64_t capacity_mask) { - return (int64_t)Splitmix64((uint64_t)key) & capacity_mask; -} - -static int64_t NextIndex(int64_t index, int64_t capacity_mask) { - return (index + 1) & capacity_mask; -} - -void Int64HashMapInit(Int64HashMap *map, double max_load_factor) { - Int64HashMapInitAllocator(map, max_load_factor, NULL); -} +static bool ExpandTo(Int64HashMap *map, uint64_t new_mask) { + uint64_t new_capacity = new_mask + 1; -void Int64HashMapInitAllocator(Int64HashMap *map, double max_load_factor, - GamesmanAllocator *allocator) { - map->allocator = GamesmanAllocatorAddRef(allocator); - map->entries = NULL; - map->capacity_mask = -1; - map->size = 0; - if (max_load_factor > 0.75) max_load_factor = 0.75; - if (max_load_factor < 0.25) max_load_factor = 0.25; - map->max_load_factor = max_load_factor; -} - -void Int64HashMapDestroy(Int64HashMap *map) { - GamesmanAllocatorDeallocate(map->allocator, map->entries); - GamesmanAllocatorRelease(map->allocator); - map->allocator = NULL; - map->entries = NULL; - map->capacity_mask = -1; - map->size = 0; -} - -static Int64HashMapIterator NewIterator(const Int64HashMap *map, - int64_t index) { - Int64HashMapIterator iterator; - iterator.map = map; - iterator.index = index; - return iterator; -} - -static Int64HashMapIterator NewInvalidIterator(const Int64HashMap *map) { - return NewIterator(map, map->capacity_mask + 1); -} - -Int64HashMapIterator Int64HashMapGet(const Int64HashMap *map, int64_t key) { - // Edge case: return invalid iterator if map is empty. - if (map->capacity_mask < 0) return NewIterator(map, -1); - - int64_t index = Hash(key, map->capacity_mask); - while (map->entries[index].used) { - if (map->entries[index].key == key) { - return NewIterator(map, index); - } - index = NextIndex(index, map->capacity_mask); + // Allocate new key array. + int64_t *__restrict new_keys = (int64_t *)GamesmanAllocatorAllocate( + map->allocator, new_capacity * sizeof(int64_t)); + if (new_keys == NULL) { + return false; } - return NewInvalidIterator(map); -} - -static bool Expand(Int64HashMap *map, int64_t new_mask) { - size_t alloc_size = (new_mask + 1) * sizeof(Int64HashMapEntry); - Int64HashMapEntry *new_entries = - (Int64HashMapEntry *)GamesmanAllocatorAllocate(map->allocator, - alloc_size); - if (new_entries == NULL) return false; - memset(new_entries, 0, alloc_size); - - for (int64_t i = 0; i <= map->capacity_mask; ++i) { - if (map->entries[i].used) { - int64_t new_index = Hash(map->entries[i].key, new_mask); - while (new_entries[new_index].used) { - new_index = NextIndex(new_index, new_mask); - } - new_entries[new_index] = map->entries[i]; - } + // Allocate new value array. + int64_t *__restrict new_values = (int64_t *)GamesmanAllocatorAllocate( + map->allocator, new_capacity * sizeof(int64_t)); + if (new_values == NULL) { + GamesmanAllocatorDeallocate(map->allocator, new_keys); + return false; } - GamesmanAllocatorDeallocate(map->allocator, map->entries); - map->entries = new_entries; - map->capacity_mask = new_mask; - return true; -} - -bool Int64HashMapSet(Int64HashMap *map, int64_t key, int64_t value) { - // Check if resizing is needed. - if (map->capacity_mask < 0) { - if (!Expand(map, 1)) return false; - } else if ((double)(map->size + 1) > - (double)(map->capacity_mask + 1) * map->max_load_factor) { - int64_t new_capacity_mask = (map->capacity_mask << 1) | 1; - if (!Expand(map, new_capacity_mask)) return false; + // Initialize all key slots to the sentinel. + for (uint64_t i = 0; i < new_capacity; ++i) { + new_keys[i] = INT64_HASH_MAP_EMPTY_KEY; } - // Set value at key. - int64_t index = Hash(key, map->capacity_mask); - while (map->entries[index].used) { - if (map->entries[index].key == key) { - map->entries[index].value = value; - return true; + // Hoist old pointers to locals. + int64_t *__restrict old_keys = map->keys; + int64_t *__restrict old_values = map->values; + + // Rehash existing entries if any. + if (old_keys != NULL) { + uint64_t old_mask = map->mask; + for (uint64_t i = 0; i <= old_mask; ++i) { + int64_t key = old_keys[i]; + if (key != INT64_HASH_MAP_EMPTY_KEY) { + uint64_t new_index = Splitmix64(key) & new_mask; + while (new_keys[new_index] != INT64_HASH_MAP_EMPTY_KEY) { + new_index = (new_index + 1) & new_mask; + } + new_keys[new_index] = key; + new_values[new_index] = old_values[i]; + } } - index = NextIndex(index, map->capacity_mask); } - map->entries[index].key = key; - map->entries[index].value = value; - map->entries[index].used = true; - ++map->size; - - return true; -} - -bool Int64HashMapContains(const Int64HashMap *map, int64_t key) { - Int64HashMapIterator it = Int64HashMapGet(map, key); - return Int64HashMapIteratorIsValid(&it); -} -Int64HashMapIterator Int64HashMapBegin(Int64HashMap *map) { - return (Int64HashMapIterator){.map = map, .index = -1}; -} + GamesmanAllocatorDeallocate(map->allocator, old_keys); + GamesmanAllocatorDeallocate(map->allocator, old_values); + map->keys = new_keys; + map->mask = new_mask; + map->max_size = (int64_t)((new_mask + 1) / map->inv_max_load_factor); + map->values = new_values; -int64_t Int64HashMapIteratorKey(const Int64HashMapIterator *it) { - return it->map->entries[it->index].key; -} - -int64_t Int64HashMapIteratorValue(const Int64HashMapIterator *it) { - return it->map->entries[it->index].value; -} - -bool Int64HashMapIteratorIsValid(const Int64HashMapIterator *it) { - return it->index >= 0 && it->index <= it->map->capacity_mask; + return true; } -bool Int64HashMapIteratorNext(Int64HashMapIterator *it, int64_t *key, - int64_t *value) { - const Int64HashMap *map = it->map; - while (++it->index <= map->capacity_mask) { - if (map->entries[it->index].used) { - if (key) *key = map->entries[it->index].key; - if (value) *value = map->entries[it->index].value; - return true; - } - } +bool Int64HashMapInternalExpand(Int64HashMap *map) { + // If keys is non-NULL, this is a normal expansion step; + // if keys is NULL, this is the lazy initialization step. + // Initial capacity is 128, so the mask is 127 (0x7F). + uint64_t new_mask = map->keys ? ((map->mask << 1) | 1ULL) : 0x7F; - return false; + return ExpandTo(map, new_mask); } diff --git a/src/core/data_structures/int64_hash_map.h b/src/core/data_structures/int64_hash_map.h index b2d3efec..fb6f8ba2 100644 --- a/src/core/data_structures/int64_hash_map.h +++ b/src/core/data_structures/int64_hash_map.h @@ -3,9 +3,8 @@ * @author Robert Shi (robertyishi@berkeley.edu) * @author GamesCrafters Research Group, UC Berkeley * Supervised by Dan Garcia - * @brief Linear-probing (open addressing) int64_t to int64_t hash map. - * @version 1.0.4 - * @date 2025-04-26 + * @brief Dynamically-sized linear probing `int64_t` to `int64_t` hash map with + * sentinel value optimization. * * @copyright This file is part of GAMESMAN, The Finite, Two-person * Perfect-Information Game Generator released under the GPL: @@ -28,195 +27,364 @@ #define GAMESMANONE_CORE_DATA_STRUCTURES_INT64_HASH_MAP_H_ #include +#include #include +#include "core/data_structures/hash.h" #include "core/gamesman_memory.h" /** - * @brief Entry object of an \c Int64HashMap. This struct is not meant to be - * used by the user of this library. Always use accessor and mutator functions - * instead. + * @brief Sentinel value used to represent an empty slot in the hash map. + * + * @details The user of `Int64HashMap` is responsible for making sure that + * `INT64_MIN` is never inserted as a key. The library is aggressively + * optimized and will not check for such insertions. */ -typedef struct Int64HashMapEntry { - int64_t key; /**< Key to the entry. */ - int64_t value; /**< Value of the entry. */ - bool used; /**< True iff this bucket contains an actual record. */ -} Int64HashMapEntry; +#define INT64_HASH_MAP_EMPTY_KEY INT64_MIN /** - * @brief Linear-probing int64_t to int64_t hash map. + * @brief Dynamically-sized linear probing `int64_t` to `int64_t` hash map. + * + * @details Uses a parallel arrays layout where keys and values are stored + * separate parallel `int64_t` arrays. This design keeps the probing loop + * operating on keys only, maximizing cache line utilization during lookups. * - * @example - * Int64HashMap mymap; - * Int64HashMapInit(&mymap, 0.5); // Sets max_load_factor to 0.5. - * Int64HashMapSet(&mymap, 0, 1); - * Int64HashMapSet(&mymap, -4, 7); - * Int64HashMapSet(&mymap, 2, 4); - * Int64HashMapIterator it = Int64HashMapGet(&mymap, -4); - * if (Int64HashMapIteratorIsValid(&it)) { - * int64_t key = Int64HashMapIteratorKey(&it); - * int64_t value = Int64HashMapIteratorValue(&it); + * @note `INT64_MIN` (`INT64_HASH_MAP_EMPTY_KEY`) is reserved as a sentinel + * value for the hash table to represent an empty slot for optimization + * purposes. Because of this, `Int64HashMap` cannot be used to store + * `INT64_MIN` as a key. The user must make sure that `INT64_MIN` is never + * inserted as a key. * - * // key: -4, value: 7 - * printf("key: %" PRId64 ", value: %" PRId64 "\n", key, value); - * } - * Int64HashMapDestroy(&mymap); + * @warning This struct should be treated as opaque. Use accessor and mutator + * functions to interact with the hash map. */ typedef struct Int64HashMap { - GamesmanAllocator *allocator; /**< Allocator in use. */ - Int64HashMapEntry *entries; /**< Dynamic array of buckets. */ - int64_t capacity_mask; /**< Number of buckets - 1. */ - int64_t size; /**< Number of entries in the hash map. */ - double max_load_factor; /**< Hash map will automatically expand if - (double)size/capacity is greater than this value. */ + /** Key array. */ + int64_t *keys; + + /** Parallel value array, indexed identically to `keys`. */ + int64_t *values; + + /** Bitmask used for indexing into the keys. */ + uint64_t mask; + + /** Current number of key-value pairs in the map. */ + int64_t size; + + /** Maximum number of keys before expansion is needed. */ + int64_t max_size; + + /** Equals to `1.0 / max_load_factor`. */ + double inv_max_load_factor; + + /** Custom allocator, or `NULL` for default allocation. */ + GamesmanAllocator *allocator; } Int64HashMap; /** - * @brief Int64HashMap iterator. This is usually returned by an Int64HashMap - * accessor function. + * @brief Iterator for `Int64HashMap`. * - * @example - * Int64HashMapIterator it; - * Int64HashMapIteratorInit(&it); - * while (Int64HashMapIteratorNext) { - * // Do stuff. - * } + * @warning This struct should be treated as opaque. Use accessor and mutator + * functions to interact with the iterator. */ typedef struct Int64HashMapIterator { - const Int64HashMap - *map; /**< The Int64HashMap this iterator is being used on. */ - int64_t index; /**< Internal index into the Int64HashMap.entries array. */ + /** The `Int64HashMap` this iterator is being used on. */ + const Int64HashMap *map; + + /** Internal index into the hash table arrays. */ + int64_t index; } Int64HashMapIterator; +// ================================= Lifecycle ================================= + /** - * @brief Initializes the given MAP. + * @brief Initializes the given `map` using the given memory `allocator`. + * + * @details No memory is allocated until the first insertion or reservation. + * The `max_load_factor` is clamped to [0.5, 0.8]. If `allocator` is `NULL`, + * the effect is equivalent to calling `Int64HashMapInit`. * - * @param map Map to initialize. - * @param max_load_factor Set maximum load factor of MAP to this value. The hash - * map will automatically expand its capacity if (double)size/capacity is - * greater than the max_load_factor. A small max_load_factor trades memory for - * speed whereas a large max_load_factor trades speed for memory. This value is - * restricted to be in the range [0.25, 0.75]. If the user passes a - * max_load_factor that is smaller than 0.25 or greater than 0.75, the internal - * value will be set to 0.25 and 0.75, respectively, regardless of the - * user-specified value. + * @param[out] map The `Int64HashMap` to initialize. + * @param[in] max_load_factor The maximum load factor, clamped to [0.5, 0.8]. + * @param[in] allocator Memory allocator to use, or `NULL` for default. */ -void Int64HashMapInit(Int64HashMap *map, double max_load_factor); +static inline void Int64HashMapInitAllocator(Int64HashMap *map, + double max_load_factor, + GamesmanAllocator *allocator) { + // Clamp max_load_factor to [0.5, 0.8] + if (max_load_factor < 0.5) { + max_load_factor = 0.5; + } else if (max_load_factor > 0.8) { + max_load_factor = 0.8; + } + + map->keys = NULL; + map->values = NULL; + map->mask = 0x0ULL; + map->size = 0L; + map->max_size = 0L; + map->inv_max_load_factor = 1.0 / max_load_factor; + map->allocator = GamesmanAllocatorAddRef(allocator); +} /** - * @brief Initializes the given \p map using the given memory \p allocator . + * @brief Initializes the given `map`. + * + * @details No memory is allocated until the first insertion or reservation. + * The `max_load_factor` is clamped to [0.5, 0.8]. * - * @param map Map to initialize. - * @param max_load_factor Set maximum load factor of MAP to this value. The hash - * map will automatically expand its capacity if (double)size/capacity is - * greater than the max_load_factor. A small max_load_factor trades memory for - * speed whereas a large max_load_factor trades speed for memory. This value is - * restricted to be in the range [0.25, 0.75]. If the user passes a - * max_load_factor that is smaller than 0.25 or greater than 0.75, the internal - * value will be set to 0.25 and 0.75, respectively, regardless of the - * user-specified value. - * @param allocator Memory allocator to use. If \c NULL is passed, the effect is - * equivalent to calling Int64HashMapInit. + * @param[out] map The `Int64HashMap` to initialize. + * @param[in] max_load_factor The maximum load factor, clamped to [0.5, 0.8]. */ -void Int64HashMapInitAllocator(Int64HashMap *map, double max_load_factor, - GamesmanAllocator *allocator); +static inline void Int64HashMapInit(Int64HashMap *map, double max_load_factor) { + Int64HashMapInitAllocator(map, max_load_factor, NULL); +} -/** @brief Deallocates the given MAP. */ -void Int64HashMapDestroy(Int64HashMap *map); +/** + * @brief Frees the memory associated with the hash map and resets its state. + * + * @param[in,out] map The `Int64HashMap` to destroy. + */ +static inline void Int64HashMapDestroy(Int64HashMap *map) { + GamesmanAllocatorDeallocate(map->allocator, map->keys); + GamesmanAllocatorDeallocate(map->allocator, map->values); + GamesmanAllocatorRelease(map->allocator); + map->keys = NULL; + map->values = NULL; + map->mask = 0x0ULL; + map->size = 0L; + map->max_size = 0L; + map->inv_max_load_factor = 0.0; + map->allocator = NULL; +} + +// ================================== Getters ================================== /** - * @brief Returns an iterator to the MAP entry containing the given KEY. - * Returns an invalid iterator if KEY is not found in MAP. The user should - * test if the iterator is valid using Int64HashMapIteratorIsValid. + * @brief Returns the number of key-value pairs stored in `map`. + * + * @param[in] map The `Int64HashMap` to query. * - * @param map Hash map to get the entry from. - * @param key Key to the desired entry. - * @return Int64HashMapIterator pointing to the entry with KEY, or invalid - * if KEY is not found in MAP. + * @return The number of key-value pairs currently in the map. */ -Int64HashMapIterator Int64HashMapGet(const Int64HashMap *map, int64_t key); +static inline int64_t Int64HashMapSize(const Int64HashMap *map) { + return map->size; +} + +// =========================== Insertion and Lookup =========================== /** - * @brief Sets the entry with KEY in MAP to the given VALUE and returns true. - * Creates a new entry if KEY does not exist in MAP. If the operation fails - * for any reason, MAP remains unchanged and the function returns false. + * @brief Sets the value associated with `key` in `map` to `value`. + * + * @details Creates a new entry if `key` does not exist in `map`. If `key` + * already exists, its value is updated to `value`. If the operation fails for + * any reason, `map` remains unchanged. + * + * @param[in,out] map The `Int64HashMap` to modify. + * @param[in] key The key of the entry. Must not be equal to `INT64_MIN` + * (`INT64_HASH_MAP_EMPTY_KEY`). + * @param[in] value The value to associate with `key`. * - * @param map Destination hash map. - * @param key Key of the entry. - * @param value Value of the entry. - * @return true on success, - * @return false otherwise. + * @retval true The entry was successfully set. + * @retval false Memory allocation failed during expansion. */ -bool Int64HashMapSet(Int64HashMap *map, int64_t key, int64_t value); +static inline bool Int64HashMapSet(Int64HashMap *map, int64_t key, + int64_t value) { + if (map->size >= map->max_size) { + // Declared in int64_hash_map.c; handles parallel values rehashing. + bool Int64HashMapInternalExpand(Int64HashMap * map); + if (!Int64HashMapInternalExpand(map)) { + return false; + } + } + + // Hoist pointers and values to locals so the compiler + // doesn't worry about memory aliasing during the loop. + int64_t *__restrict keys = map->keys; + int64_t *__restrict values = map->values; + uint64_t mask = map->mask; + + uint64_t index = Splitmix64(key) & mask; + while (keys[index] != INT64_HASH_MAP_EMPTY_KEY) { + if (keys[index] == key) { + values[index] = value; + return true; + } + index = (index + 1) & mask; + } + keys[index] = key; + values[index] = value; + ++map->size; + + return true; +} /** - * @brief Returns true if the given MAP contains an entry with the given KEY, - * or false otherwise. + * @brief Returns an iterator pointing to the entry with the given `key` in + * `map`, or an invalid iterator if `key` is not found. + * + * @details Use `Int64HashMapIteratorIsValid` to check whether `key` was found. + * + * @param[in] map The `Int64HashMap` to search. + * @param[in] key The key to search for. + * + * @return An `Int64HashMapIterator` pointing to the entry with `key`, or an + * invalid iterator if `key` is not found. */ -bool Int64HashMapContains(const Int64HashMap *map, int64_t key); +static inline Int64HashMapIterator Int64HashMapGet(const Int64HashMap *map, + int64_t key) { + Int64HashMapIterator result; + result.map = map; + + const int64_t *__restrict keys = map->keys; + + // Return invalid iterator if map has not been lazily initialized. + if (!keys) { + result.index = -1; + return result; + } + + const uint64_t mask = map->mask; + uint64_t index = Splitmix64(key) & mask; + while (keys[index] != INT64_HASH_MAP_EMPTY_KEY) { + if (keys[index] == key) { + result.index = (int64_t)index; + return result; + } + index = (index + 1) & mask; + } + + result.index = -1; + return result; +} + +/** + * @brief Returns whether `map` contains an entry with the given `key`. + * + * @param[in] map The `Int64HashMap` to search. + * @param[in] key The key to search for. + * + * @retval true The `key` is present in the map. + * @retval false The `key` is not present, or the map is not initialized. + */ +static inline bool Int64HashMapContains(const Int64HashMap *map, int64_t key) { + const int64_t *__restrict keys = map->keys; + + if (!keys) { + return false; + } + + const uint64_t mask = map->mask; + uint64_t index = Splitmix64(key) & mask; + while (keys[index] != INT64_HASH_MAP_EMPTY_KEY) { + if (keys[index] == key) { + return true; + } + index = (index + 1) & mask; + } + + return false; +} + +// ================================= Iteration ================================= /** - * @brief Returns an invalid iterator to the entry before the first entry of - * MAP. + * @brief Returns an iterator positioned before the first entry of `map`. * - * @note This function is designed to be used in conjunction with - * Int64HashMapIteratorNext to iterate through all the entries in the hash map. + * @details This function is designed to be used with `Int64HashMapIteratorNext` + * to iterate through all entries in the hash map. * - * @example - * Int64HashMapIterator it = Int64HashMapBegin(map); - * int64_t key, value; + * @param[in] map The `Int64HashMap` to iterate over. * - * // While the next entry exists... - * while (Int64HashMapIteratorNext(&it, &key, &value)) { - * // Do stuff with key and value... - * } + * @return An `Int64HashMapIterator` positioned before the first entry. */ -Int64HashMapIterator Int64HashMapBegin(Int64HashMap *map); +static inline Int64HashMapIterator Int64HashMapBegin(const Int64HashMap *map) { + Int64HashMapIterator result; + result.map = map; + result.index = -1; + return result; +} /** - * @brief Returns the key of the entry IT is pointing to. The user should - * validate the iterator using Int64HashMapIteratorIsValid before calling this - * function. Calling this function on an invalid iterator results in undefined - * behavior. + * @brief Returns the key of the entry that `it` is pointing to. * - * @param it Int64HashMapIterator. - * @return Key to the entry. + * @warning Calling this function on an invalid iterator results in undefined + * behavior. Use `Int64HashMapIteratorIsValid` to validate the iterator first. + * + * @param[in] it The `Int64HashMapIterator` to read. + * + * @return The key of the entry. */ -int64_t Int64HashMapIteratorKey(const Int64HashMapIterator *it); +static inline int64_t Int64HashMapIteratorKey(const Int64HashMapIterator *it) { + return it->map->keys[it->index]; +} /** - * @brief Returns the value of the entry IT is pointing to. The user should - * validate the iterator using Int64HashMapIteratorIsValid before calling this - * function. Calling this function on an invalid iterator results in undefined - * behavior. + * @brief Returns the value of the entry that `it` is pointing to. + * + * @warning Calling this function on an invalid iterator results in undefined + * behavior. Use `Int64HashMapIteratorIsValid` to validate the iterator first. + * + * @param[in] it The `Int64HashMapIterator` to read. * - * @param it Int64HashMapIterator. - * @return Value of the entry. + * @return The value of the entry. */ -int64_t Int64HashMapIteratorValue(const Int64HashMapIterator *it); +static inline int64_t Int64HashMapIteratorValue( + const Int64HashMapIterator *it) { + return it->map->values[it->index]; +} -/** @brief Returns true if the given IT-erator is valid, or false otherwise. */ -bool Int64HashMapIteratorIsValid(const Int64HashMapIterator *it); +/** + * @brief Returns whether the given iterator `it` is valid. + * + * @param[in] it The `Int64HashMapIterator` to validate. + * + * @retval true The iterator points to a valid entry. + * @retval false The iterator is invalid. + */ +static inline bool Int64HashMapIteratorIsValid(const Int64HashMapIterator *it) { + return it->index >= 0 && it->index <= (int64_t)it->map->mask; +} /** - * @brief Moves iterator IT to the next valid entry inside the hash map IT was - * initialized with, sets KEY and VALUE to the key and value of that next entry, - * and returns true. Sets IT to an invalid state and returns false if no valid - * next entry exists. + * @brief Advances `it` to the next valid entry and returns `true`, or returns + * `false` if no more entries exist. + * + * @details This function is designed to be used with `Int64HashMapBegin` to + * iterate through all entries in the hash map. * - * @note This function is designed to be used in conjunction with - * Int64HashMapBegin to iterate through all entries in the hash map. Calling - * this function on an uninitialized iterator results in undefined behavior. + * @param[in,out] it The `Int64HashMapIterator` to advance. + * @param[out] key Pointer to receive the key, or `NULL` if not needed. + * @param[out] value Pointer to receive the value, or `NULL` if not needed. * - * @param iterator Initialized iterator. - * @param key Pointer to enough space to hold an int64_t object, or NULL if the - * key to the next entry is not needed. - * @param value Pointer to enough space to hold an int64_t object, or NULL if - * the value to the next entry is not needed. - * @return true if next entry exists, - * @return false otherwise. + * @retval true The iterator was advanced to a valid entry. + * @retval false No more entries exist. */ -bool Int64HashMapIteratorNext(Int64HashMapIterator *it, int64_t *key, - int64_t *value); +static inline bool Int64HashMapIteratorNext(Int64HashMapIterator *it, + int64_t *key, int64_t *value) { + const Int64HashMap *map = it->map; + const int64_t *__restrict keys = map->keys; + + // Return false if map has not been lazily initialized. + if (!keys) { + return false; + } + + const int64_t capacity = (int64_t)(map->mask + 1); + + while (++it->index < capacity) { + if (keys[it->index] != INT64_HASH_MAP_EMPTY_KEY) { + if (key) { + *key = keys[it->index]; + } + if (value) { + *value = map->values[it->index]; + } + return true; + } + } + + return false; +} #endif // GAMESMANONE_CORE_DATA_STRUCTURES_INT64_HASH_MAP_H_ diff --git a/tests/unit/core/data_structures/int64_hash_map_test.cpp b/tests/unit/core/data_structures/int64_hash_map_test.cpp index 1af82a25..dec23786 100644 --- a/tests/unit/core/data_structures/int64_hash_map_test.cpp +++ b/tests/unit/core/data_structures/int64_hash_map_test.cpp @@ -362,12 +362,6 @@ INSTANTIATE_TEST_SUITE_P( TEST(Int64HashMapTest, IteratesOverEmptyMap) { Int64HashMap map; Int64HashMapInit(&map, 0.5); - - // Force allocation so Begin/Next have something to iterate over. - Int64HashMapSet(&map, 1, 1); - Int64HashMapDestroy(&map); - Int64HashMapInit(&map, 0.5); - Int64HashMapIterator it = Int64HashMapBegin(&map); int64_t key, value; From d7d8c2fe8da80ffe5e0bd0a3a15403dcf904f385 Mon Sep 17 00:00:00 2001 From: Robert Shi Date: Fri, 25 Sep 2026 22:08:50 -0700 Subject: [PATCH 3/5] Move Int64HashMapInternalExpand out of block scope --- src/core/data_structures/int64_hash_map.h | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/core/data_structures/int64_hash_map.h b/src/core/data_structures/int64_hash_map.h index fb6f8ba2..7c7eae57 100644 --- a/src/core/data_structures/int64_hash_map.h +++ b/src/core/data_structures/int64_hash_map.h @@ -173,6 +173,18 @@ static inline int64_t Int64HashMapSize(const Int64HashMap *map) { // =========================== Insertion and Lookup =========================== +/** + * @brief [INTERNAL] Expands `map` to a strictly larger capacity. + * + * @warning This is an internal function exposed for optimization purposes. + * Users of this library should never call this function directly. + * + * @param map Map to expand. + * @retval true on success. + * @retval false otherwise. + */ +bool Int64HashMapInternalExpand(Int64HashMap *map); + /** * @brief Sets the value associated with `key` in `map` to `value`. * @@ -191,8 +203,6 @@ static inline int64_t Int64HashMapSize(const Int64HashMap *map) { static inline bool Int64HashMapSet(Int64HashMap *map, int64_t key, int64_t value) { if (map->size >= map->max_size) { - // Declared in int64_hash_map.c; handles parallel values rehashing. - bool Int64HashMapInternalExpand(Int64HashMap * map); if (!Int64HashMapInternalExpand(map)) { return false; } From e2542adede6c4c3f3893118fa04a2dd2d15cefb9 Mon Sep 17 00:00:00 2001 From: Robert Shi Date: Fri, 25 Sep 2026 22:49:42 -0700 Subject: [PATCH 4/5] Update docstrings of tier_hash_map.h --- src/core/types/tier_hash_map.h | 188 ++++++++++++++++++--------------- 1 file changed, 104 insertions(+), 84 deletions(-) diff --git a/src/core/types/tier_hash_map.h b/src/core/types/tier_hash_map.h index 998013ee..b19b768f 100644 --- a/src/core/types/tier_hash_map.h +++ b/src/core/types/tier_hash_map.h @@ -3,7 +3,7 @@ * @author Robert Shi (robertyishi@berkeley.edu) * @author GamesCrafters Research Group, UC Berkeley * Supervised by Dan Garcia - * @brief Linear-probing Tier hash map that maps Tiers to 64-bit signed + * @brief Linear-probing `Tier` hash map that maps `Tier`s to 64-bit signed * integers. * * @copyright This file is part of GAMESMAN, The Finite, Two-person @@ -33,45 +33,45 @@ #include "core/gamesman_memory.h" #include "core/types/base.h" -/** @brief Linear-probing Tier to int64_t hash map using Int64HashMap. */ +/** + * @brief Linear-probing `Tier` to `int64_t` hash map using `Int64HashMap`. + * + * @warning This typedef should be treated as opaque. Use accessor and mutator + * functions to interact with the hash map. + */ typedef Int64HashMap TierHashMap; -/** @brief Iterator for TierHashMap. */ +/** + * @brief Iterator for `TierHashMap`. + * + * @warning This typedef should be treated as opaque. Use accessor and mutator + * functions to interact with the iterator. + */ typedef Int64HashMapIterator TierHashMapIterator; /** - * @brief Initializes Tier hash map MAP to an empty map with maximum load - * factor MAX_LOAD_FACTOR. - * - * @param map Map to initialize. - * @param max_load_factor Set maximum load factor of MAP to this value. The hash - * map will automatically expand its capacity if (double)size/capacity is - * greater than this value. A small max_load_factor trades memory for - * speed whereas a large max_load_factor trades speed for memory. This value is - * restricted to be in the range [0.25, 0.75]. If the user passes a - * max_load_factor that is smaller than 0.25 or greater than 0.75, the internal - * value will be set to 0.25 and 0.75, respectively, regardless of the - * user-specified value. + * @brief Initializes the given `map`. + * + * @details No memory is allocated until the first insertion or reservation. + * The `max_load_factor` is clamped to [0.5, 0.8]. + * + * @param[out] map The `TierHashMap` to initialize. + * @param[in] max_load_factor The maximum load factor, clamped to [0.5, 0.8]. */ static inline void TierHashMapInit(TierHashMap *map, double max_load_factor) { Int64HashMapInit(map, max_load_factor); } /** - * @brief Initializes the given \p map to an empty map with maximum load - * factor \p max_load_factor using the given memory \p allocator . - * - * @param map Map to initialize. - * @param max_load_factor Set maximum load factor of \p map to this value. The - * hash map will automatically expand its capacity if (double)size/capacity is - * greater than this value. A small max_load_factor trades memory for - * speed whereas a large max_load_factor trades speed for memory. This value is - * restricted to be in the range [0.25, 0.75]. If the user passes a - * max_load_factor that is smaller than 0.25 or greater than 0.75, the internal - * value will be set to 0.25 and 0.75, respectively, regardless of the - * user-specified value. - * @param allocator Memory allocator to use. If \c NULL is passed, the effect is - * equivalent to calling TierHashMapInit. + * @brief Initializes the given `map` using the given memory `allocator`. + * + * @details No memory is allocated until the first insertion or reservation. + * The `max_load_factor` is clamped to [0.5, 0.8]. If `allocator` is `NULL`, + * the effect is equivalent to calling `TierHashMapInit`. + * + * @param[out] map The `TierHashMap` to initialize. + * @param[in] max_load_factor The maximum load factor, clamped to [0.5, 0.8]. + * @param[in] allocator Memory allocator to use, or `NULL` for default. */ static inline void TierHashMapInitAllocator(TierHashMap *map, double max_load_factor, @@ -79,109 +79,129 @@ static inline void TierHashMapInitAllocator(TierHashMap *map, Int64HashMapInitAllocator(map, max_load_factor, allocator); } -/** @brief Destroys the Tier hash map MAP. */ +/** + * @brief Frees the memory associated with the hash map and resets its state. + * + * @param[in,out] map The `TierHashMap` to destroy. + */ static inline void TierHashMapDestroy(TierHashMap *map) { Int64HashMapDestroy(map); } /** - * @brief Returns an iterator to the MAP entry containing the given KEY. - * Returns an invalid iterator if KEY is not found in MAP. The user should - * test if the iterator is valid using TierHashMapIteratorIsValid. - * - * @param map Tier hash map to get the entry from. - * @param key Tier value as key to the desired entry. - * @return TierHashMapIterator pointing to the entry with KEY, or invalid - * if KEY is not found in MAP. + * @brief Returns an iterator pointing to the entry with the given `key` in + * `map`, or an invalid iterator if `key` is not found. + * + * @details Use `TierHashMapIteratorIsValid` to check whether `key` was found. + * + * @param[in] map The `TierHashMap` to search. + * @param[in] key The key to search for. + * + * @return A `TierHashMapIterator` pointing to the entry with `key`, or an + * invalid iterator if `key` is not found. */ static inline TierHashMapIterator TierHashMapGet(TierHashMap *map, Tier key) { return Int64HashMapGet(map, key); } /** - * @brief Sets the entry with key equal to TIER in MAP to the given VALUE and - * returns true. Creates a new entry if TIER does not exist in MAP. If the - * operation fails for any reason, MAP remains unchanged and the function - * returns false. - * - * @param map Destination hash map. - * @param tier Tier as the key to the entry. - * @param value Value of the entry. - * @return true on success, - * @return false otherwise. + * @brief Sets the value associated with `tier` in `map` to `value`. + * + * @details Creates a new entry if `tier` does not exist in `map`. If `tier` + * already exists, its value is updated to `value`. If the operation fails for + * any reason, `map` remains unchanged. + * + * @param[in,out] map The `TierHashMap` to modify. + * @param[in] tier The key of the entry. + * @param[in] value The value to associate with `tier`. + * + * @retval true The entry was successfully set. + * @retval false Memory allocation failed during expansion. */ static inline bool TierHashMapSet(TierHashMap *map, Tier tier, int64_t value) { return Int64HashMapSet(map, tier, value); } /** - * @brief Returns true if the given MAP contains an entry with key equal to - * TIER, or false otherwise. + * @brief Returns whether `map` contains an entry with the given `tier`. + * + * @param[in] map The `TierHashMap` to search. + * @param[in] tier The key to search for. + * + * @retval true The `tier` is present in the map. + * @retval false The `tier` is not present, or the map is not initialized. */ static inline bool TierHashMapContains(const TierHashMap *map, Tier tier) { return Int64HashMapContains(map, tier); } /** - * @brief Returns an invalid iterator to the entry before the first entry of - * MAP. + * @brief Returns an iterator positioned before the first entry of `map`. + * + * @details This function is designed to be used with `TierHashMapIteratorNext` + * to iterate through all entries in the hash map. * - * @note This function is designed to be used in conjunction with - * TierHashMapIteratorNext to iterate through all the entries in the Tier hash - * map. + * @param[in] map The `TierHashMap` to iterate over. + * + * @return A `TierHashMapIterator` positioned before the first entry. */ static inline TierHashMapIterator TierHashMapBegin(TierHashMap *map) { return Int64HashMapBegin(map); } /** - * @brief Returns the Tier that was used as the key to the entry IT is pointing - * to. The user should validate the iterator using TierHashMapIteratorIsValid - * before calling this function. Calling this function on an invalid iterator - * results in undefined behavior. + * @brief Returns the key of the entry that `it` is pointing to. + * + * @warning Calling this function on an invalid iterator results in undefined + * behavior. Use `TierHashMapIteratorIsValid` to validate the iterator first. * - * @param it Tier hash map iterator. - * @return Key to the entry. + * @param[in] it The `TierHashMapIterator` to read. + * + * @return The key of the entry. */ static inline Tier TierHashMapIteratorKey(const TierHashMapIterator *it) { return Int64HashMapIteratorKey(it); } /** - * @brief Returns the value of the entry IT is pointing to. The user should - * validate the iterator using TierHashMapIteratorIsValid before calling this - * function. Calling this function on an invalid iterator results in undefined - * behavior. + * @brief Returns the value of the entry that `it` is pointing to. + * + * @warning Calling this function on an invalid iterator results in undefined + * behavior. Use `TierHashMapIteratorIsValid` to validate the iterator first. * - * @param it Tier hash map iterator. - * @return Value of the entry. + * @param[in] it The `TierHashMapIterator` to read. + * + * @return The value of the entry. */ static inline int64_t TierHashMapIteratorValue(const TierHashMapIterator *it) { return Int64HashMapIteratorValue(it); } -/** @brief Returns true if the given IT-erator is valid, or false otherwise. */ +/** + * @brief Returns whether the given iterator `it` is valid. + * + * @param[in] it The `TierHashMapIterator` to validate. + * + * @retval true The iterator points to a valid entry. + * @retval false The iterator is invalid. + */ static inline bool TierHashMapIteratorIsValid(const TierHashMapIterator *it) { return Int64HashMapIteratorIsValid(it); } /** - * @brief Moves iterator IT to the next valid entry inside the Tier hash map - * that IT was initialized with, sets TIER to the key and VALUE to the value of - * that next entry, and returns true. Sets IT to an invalid state and returns - * false if no valid next entry exists. - * - * @note This function is designed to be used in conjunction with - * TierHashMapBegin to iterate through all entries in the Tier hash map. Calling - * this function on an uninitialized iterator results in undefined behavior. - * - * @param iterator Initialized iterator. - * @param tier Pointer to enough space to hold a Tier object, or NULL if the - * key to the next entry is not needed. - * @param value Pointer to enough space to hold an int64_t object, or NULL if - * the value to the next entry is not needed. - * @return true if next entry exists, - * @return false otherwise. + * @brief Advances `iterator` to the next valid entry and returns `true`, or + * returns `false` if no more entries exist. + * + * @details This function is designed to be used with `TierHashMapBegin` to + * iterate through all entries in the hash map. + * + * @param[in,out] iterator The `TierHashMapIterator` to advance. + * @param[out] tier Pointer to receive the key, or `NULL` if not needed. + * @param[out] value Pointer to receive the value, or `NULL` if not needed. + * + * @retval true The iterator was advanced to a valid entry. + * @retval false No more entries exist. */ static inline bool TierHashMapIteratorNext(TierHashMapIterator *iterator, Tier *tier, int64_t *value) { From 4f05a5b22e8aabc2792030a9b9794c37a6ed8b78 Mon Sep 17 00:00:00 2001 From: Robert Shi Date: Fri, 25 Sep 2026 23:07:21 -0700 Subject: [PATCH 5/5] Update tests --- .../data_structures/int64_hash_map_test.cpp | 53 ++++- tests/unit/core/types/CMakeLists.txt | 1 + tests/unit/core/types/tier_hash_map_test.cpp | 225 ++++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 tests/unit/core/types/tier_hash_map_test.cpp diff --git a/tests/unit/core/data_structures/int64_hash_map_test.cpp b/tests/unit/core/data_structures/int64_hash_map_test.cpp index dec23786..3a5b7985 100644 --- a/tests/unit/core/data_structures/int64_hash_map_test.cpp +++ b/tests/unit/core/data_structures/int64_hash_map_test.cpp @@ -1,6 +1,7 @@ /** * @file int64_hash_map_test.cpp - * @author Robert Shi (robertyishi@berkeley.edu) + * @author Generated by Claude Opus 4.6 + Gemini 3.1 Pro + * @author Robert Shi (robertyishi@berkeley.edu): prompts and code review * @brief Unit tests for Int64HashMap. * * @copyright GamesCrafters research and development group @@ -83,6 +84,56 @@ TEST(Int64HashMapTest, ClampsLoadFactorAboveMaximum) { Int64HashMapDestroy(&map); } +// ======================== Int64HashMapInitAllocator ======================== + +TEST(Int64HashMapTest, InitializesWithAllocator) { + GamesmanAllocatorOptions options; + GamesmanAllocatorOptionsSetDefaults(&options); + GamesmanAllocator *allocator = GamesmanAllocatorCreate(&options); + + Int64HashMap map; + Int64HashMapInitAllocator(&map, 0.5, allocator); + + EXPECT_TRUE(Int64HashMapSet(&map, 42, 100)) + << "The map should initialize correctly and accept a basic insertion " + "when provided with an allocator."; + + Int64HashMapIterator it = Int64HashMapGet(&map, 42); + EXPECT_TRUE(Int64HashMapIteratorIsValid(&it)); + + EXPECT_EQ(Int64HashMapIteratorValue(&it), 100); + + Int64HashMapDestroy(&map); + GamesmanAllocatorRelease(allocator); +} + +TEST(Int64HashMapTest, ExpansionFailsWhenAllocatorMemoryExhausted) { + GamesmanAllocatorOptions options; + GamesmanAllocatorOptionsSetDefaults(&options); + options.pool_size = 200; // Small pool size to trigger expansion failure + GamesmanAllocator *allocator = GamesmanAllocatorCreate(&options); + + Int64HashMap map; + Int64HashMapInitAllocator(&map, 0.5, allocator); + + // Initial insertions should succeed (depending on the initial capacity) + // Eventually it should fail to expand and return false. + bool expansion_failed = false; + for (int64_t i = 0; i < 50; ++i) { + if (!Int64HashMapSet(&map, i, i * 10)) { + expansion_failed = true; + break; + } + } + + EXPECT_TRUE(expansion_failed) + << "The map should fail to expand and Set should return false when " + "allocator memory is exhausted."; + + Int64HashMapDestroy(&map); + GamesmanAllocatorRelease(allocator); +} + // ============================ Int64HashMapDestroy ============================ TEST(Int64HashMapTest, DestroysEmptyMap) { diff --git a/tests/unit/core/types/CMakeLists.txt b/tests/unit/core/types/CMakeLists.txt index fffe2dec..14e5b175 100644 --- a/tests/unit/core/types/CMakeLists.txt +++ b/tests/unit/core/types/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources(gamesman_test PRIVATE position_hash_set_test.cpp position_static_hash_set_test.cpp + tier_hash_map_test.cpp tier_hash_set_test.cpp tier_position_hash_set_test.cpp tier_position_static_hash_set_test.cpp diff --git a/tests/unit/core/types/tier_hash_map_test.cpp b/tests/unit/core/types/tier_hash_map_test.cpp new file mode 100644 index 00000000..a2d12a73 --- /dev/null +++ b/tests/unit/core/types/tier_hash_map_test.cpp @@ -0,0 +1,225 @@ +/** + * @file tier_hash_map_test.cpp + * @author Generated by Gemini 3.1 Pro + * @author Robert Shi (robertyishi@berkeley.edu): prompts and code review + * @brief Unit tests for TierHashMap. + * + * @copyright GamesCrafters research and development group + * SPDX-License-Identifier: GPL-3.0-or-later + * (see root COPYING file or accompanying source file) + */ + +#include + +#include +#include +#include +#include + +extern "C" { +#include "core/types/base.h" +#include "core/types/tier_hash_map.h" +} + +// ============================= TierHashMapInit ============================= + +TEST(TierHashMapTest, InitializesWithStandardLoadFactor) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + EXPECT_TRUE(TierHashMapSet(&map, 42, 100)) + << "The map should initialize correctly and accept a basic insertion."; + + TierHashMapIterator it = TierHashMapGet(&map, 42); + EXPECT_TRUE(TierHashMapIteratorIsValid(&it)) + << "The map should return a valid iterator for a key that was just " + "inserted."; + + EXPECT_EQ(TierHashMapIteratorValue(&it), 100) + << "The map should return the correct value for the inserted key."; + + TierHashMapDestroy(&map); +} + +// ======================== TierHashMapInitAllocator ======================== + +TEST(TierHashMapTest, InitializesWithAllocator) { + GamesmanAllocatorOptions options; + GamesmanAllocatorOptionsSetDefaults(&options); + GamesmanAllocator *allocator = GamesmanAllocatorCreate(&options); + + TierHashMap map; + TierHashMapInitAllocator(&map, 0.5, allocator); + + EXPECT_TRUE(TierHashMapSet(&map, 42, 100)) + << "The map should initialize correctly and accept a basic insertion " + "when provided with an allocator."; + + TierHashMapIterator it = TierHashMapGet(&map, 42); + EXPECT_TRUE(TierHashMapIteratorIsValid(&it)); + + EXPECT_EQ(TierHashMapIteratorValue(&it), 100); + + TierHashMapDestroy(&map); + GamesmanAllocatorRelease(allocator); +} + +TEST(TierHashMapTest, ExpansionFailsWhenAllocatorMemoryExhausted) { + GamesmanAllocatorOptions options; + GamesmanAllocatorOptionsSetDefaults(&options); + options.pool_size = 200; // Small pool size to trigger expansion failure + GamesmanAllocator *allocator = GamesmanAllocatorCreate(&options); + + TierHashMap map; + TierHashMapInitAllocator(&map, 0.5, allocator); + + bool expansion_failed = false; + for (int64_t i = 0; i < 50; ++i) { + if (!TierHashMapSet(&map, (Tier)i, i * 10)) { + expansion_failed = true; + break; + } + } + + EXPECT_TRUE(expansion_failed) + << "The map should fail to expand and Set should return false when " + "allocator memory is exhausted."; + + TierHashMapDestroy(&map); + GamesmanAllocatorRelease(allocator); +} + +// ============================ TierHashMapDestroy ============================ + +TEST(TierHashMapTest, DestroysEmptyMap) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + // Should not crash or leak memory. + SUCCEED(); + TierHashMapDestroy(&map); +} + +TEST(TierHashMapTest, DestroysPopulatedMap) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + TierHashMapSet(&map, 1, 10); + TierHashMapSet(&map, 2, 20); + + // Should not crash or leak memory. + TierHashMapDestroy(&map); +} + +// ============================== TierHashMapSet ============================== + +TEST(TierHashMapTest, OverwritesExistingKeyWithNewValue) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + EXPECT_TRUE(TierHashMapSet(&map, 42, 100)) + << "Initial insertion should succeed."; + + EXPECT_TRUE(TierHashMapSet(&map, 42, 999)) + << "Overwriting an existing key with a new value should succeed."; + + TierHashMapIterator it = TierHashMapGet(&map, 42); + EXPECT_TRUE(TierHashMapIteratorIsValid(&it)) + << "The key should still be found after overwriting."; + + EXPECT_EQ(TierHashMapIteratorValue(&it), 999) + << "The value should be updated to the new value after overwriting."; + + TierHashMapDestroy(&map); +} + +// ============================== TierHashMapGet ============================== + +TEST(TierHashMapTest, GetReturnsValidIteratorForExistingKey) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + TierHashMapSet(&map, 42, 100); + + TierHashMapIterator it = TierHashMapGet(&map, 42); + EXPECT_TRUE(TierHashMapIteratorIsValid(&it)) + << "Get should return a valid iterator for an existing key."; + + EXPECT_EQ(TierHashMapIteratorKey(&it), 42) + << "The iterator should point to the correct key."; + + EXPECT_EQ(TierHashMapIteratorValue(&it), 100) + << "The iterator should point to the correct value."; + + TierHashMapDestroy(&map); +} + +TEST(TierHashMapTest, GetReturnsInvalidIteratorForMissingKey) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + TierHashMapSet(&map, 42, 100); + + TierHashMapIterator it = TierHashMapGet(&map, 99); + EXPECT_FALSE(TierHashMapIteratorIsValid(&it)) + << "Get should return an invalid iterator for a key that was never " + "inserted."; + + TierHashMapDestroy(&map); +} + +// =========================== TierHashMapContains =========================== + +TEST(TierHashMapTest, ContainsReturnsCorrectResults) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + TierHashMapSet(&map, 10, 100); + TierHashMapSet(&map, 20, 200); + + EXPECT_TRUE(TierHashMapContains(&map, 10)) + << "Contains should return true for the first inserted key."; + + EXPECT_TRUE(TierHashMapContains(&map, 20)) + << "Contains should return true for the second inserted key."; + + EXPECT_FALSE(TierHashMapContains(&map, 30)) + << "Contains should return false for a key that was never inserted."; + + TierHashMapDestroy(&map); +} + +// ================================= Iteration ================================= + +TEST(TierHashMapTest, IteratesOverMultipleEntries) { + TierHashMap map; + TierHashMapInit(&map, 0.5); + + const std::vector> expected = { + {10, 100}, {20, 200}, {30, 300}}; + + for (auto &[k, v] : expected) { + TierHashMapSet(&map, k, v); + } + + std::vector> actual; + TierHashMapIterator it = TierHashMapBegin(&map); + Tier key; + int64_t value; + + while (TierHashMapIteratorNext(&it, &key, &value)) { + actual.emplace_back(key, value); + } + + auto sorted_expected = expected; + std::sort(sorted_expected.begin(), sorted_expected.end()); + std::sort(actual.begin(), actual.end()); + + EXPECT_EQ(actual.size(), expected.size()) + << "Iteration should visit exactly all inserted entries."; + + EXPECT_EQ(actual, sorted_expected) + << "Iteration should return all inserted key-value pairs."; + + TierHashMapDestroy(&map); +}