Skip to content

Dependencies cleanup#659

Open
Tydik42 wants to merge 3 commits intoDesbordante:mainfrom
Tydik42:dependencies_cleanup
Open

Dependencies cleanup#659
Tydik42 wants to merge 3 commits intoDesbordante:mainfrom
Tydik42:dependencies_cleanup

Conversation

@Tydik42
Copy link
Collaborator

@Tydik42 Tydik42 commented Dec 24, 2025

Replace three unmaintained external libraries with internal solutions:

  • frozen -> util::StaticMap (constexpr std::array)
  • better_enums -> magic_enum + util::EnumToStr
  • atomicbitvector -> util::AtomicBitVector

The enum naming conversion (kCamelCase <-> snake_case) is a workaround that may need rework. We need to agree on whether the current solution is acceptable, or whether we need to do something else, for example, native Python enums.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

clang-tidy made some suggestions

@Tydik42 Tydik42 force-pushed the dependencies_cleanup branch 5 times, most recently from 6186a55 to a072fa0 Compare December 24, 2025 23:51
@Tydik42 Tydik42 changed the title [DRAFT] Dependencies cleanup Dependencies cleanup Dec 24, 2025
@Tydik42 Tydik42 marked this pull request as ready for review December 24, 2025 23:51
@Tydik42 Tydik42 force-pushed the dependencies_cleanup branch from a072fa0 to 7520e2b Compare December 25, 2025 11:10
Replace with constexpr std::array-based
StaticMap in util/static_map.h using C++20 features.
Add util::EnumToStr/EnumFromStr for
kCamelCase <-> snake_case conversion
to keep Python API unchanged.
Inline simplified version in util/atomic_bit_vector.h
without iterator support (unused).
@Tydik42 Tydik42 force-pushed the dependencies_cleanup branch from 7520e2b to 4579220 Compare January 6, 2026 13:21
Comment on lines +8 to +18
/**
* @brief Provides a constexpr immutable map-like lookup structure based on std::array.

* @tparam Key The key type. Must be comparable with `operator==`.
* @tparam Value The value type.
* @tparam N The number of key-value pairs in the map.
*/
template <typename Key, typename Value, std::size_t N>
struct StaticMap {
std::array<std::pair<Key, Value>, N> data;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Provides a constexpr immutable map-like lookup structure

But data is public field so anyone can modify it, like

util::StaticMap<int, int, 2> m{{{ {1, 10}, {2, 20} }}};
m.data[0].second = 123;

So let's move data to a private section

/**
* @brief Provides a constexpr immutable map-like lookup structure based on std::array.

* @tparam Key The key type. Must be comparable with `operator==`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can enforce it with concepts:

template <std::equality_comparable Key, typename Value, std::size_t N>
class StaticMap {

Comment on lines +17 to +18
std::array<std::pair<Key, Value>, N> data;

Copy link
Collaborator

Choose a reason for hiding this comment

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

we want our class to be map-like, but nobody stops us from providing duplicate keys:

constexpr util::StaticMap<int, int, 3> m{{{ {1, 10}, {1, 999}, {2, 20} }}};
// m.At(1) is 10

This is bad because the code looks like 1 maps to 999, but it doesn’t. The first match wins because you linearly scan and return on first hit.

Of course in perfect world nobody will initialize StaticMap with such data, but someone could make a typo and enforcing no duplicates could save hours of debugging.

I suggest to add a compile-time duplicate check and static_assert if duplicates exist, smth like this:

template <std::equality_comparable Key, typename Value, std::size_t N>
class StaticMap {
public:
    using Pair = std::pair<Key, Value>;
    using Storage = std::array<Pair, N>;

    constexpr explicit StaticMap(Storage data) : data_(std::move(data)) {
        static_assert(!HasDuplicateKeys(data_), "StaticMap contains duplicate keys");
    }
    //...
private:
    Storage data_;

    static consteval bool HasDuplicateKeys(Storage const& data) {
        for (std::size_t i = 0; i < N; ++i) {
            for (std::size_t j = i + 1; j < N; ++j) {
                if (data[i].first == data[j].first) return true;
            }
        }
        return false;
    }

Copy link
Collaborator

Choose a reason for hiding this comment

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

note: consteval there forces that check to happen at compile time so things like this:

std::array<std::pair<int,int>, 2> runtime_data = ...;
util::StaticMap<int,int,2> maybe_good(runtime_data);

won't compile at all. But I guess it makes sense here since StaticMap is constexpr, a replacement for frozen::unordered_map so basically if it’s not known at compile time, it shouldn't exist.

Comment on lines +37 to +51
[[nodiscard]] constexpr auto begin() const {
return data.begin();
}

[[nodiscard]] constexpr auto end() const {
return data.end();
}

[[nodiscard]] constexpr auto Size() const {
return N;
}

[[nodiscard]] constexpr bool Empty() const {
return N == 0;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

hm, why is size and empty are not starting with lowercase letter? We have that rule it in our clang-format so that we could use things like range-based for without extra hassle?

}

[[nodiscard]] constexpr Value const* Find(Key const& key) const {
for (auto const& pair : data) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use auto const& [k, v] : data_ to avoid doing pair.first and pair.second

Comment on lines +20 to +25
for (auto const& pair : data) {
if (pair.first == key) {
return pair.second;
}
}
throw std::out_of_range("StaticMap::at: key not found");
Copy link
Collaborator

Choose a reason for hiding this comment

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

We already have Find so we can just do this:

        if (auto const* p = Find(key))
            return *p;
        throw std::out_of_range("StaticMap::At: key not found");

Comment on lines +15 to +17
template <typename Key, typename Value, std::size_t N>
struct StaticMap {
std::array<std::pair<Key, Value>, N> data;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:
if you want, you can add some tests. Honestly this is quite simple class so they are probably not necessary, but small things like

TEST(StaticMapTest, FindAndAtWork) {
    constexpr StaticMap<int, int, 3> map{{{{{1, 10}, {2, 20}, {3, 30}}}}};

    auto const* p = map.Find(2);
    ASSERT_NE(p, nullptr);
    EXPECT_EQ(*p, 20);

    EXPECT_EQ(map.At(3), 30);
}

TEST(StaticMapTest, MissingKey) {
    constexpr StaticMap<int, int, 1> map{{{{{42, 100}}}}};

    EXPECT_EQ(map.Find(0), nullptr);
    EXPECT_THROW(map.At(0), std::out_of_range);
}

won't hurt

Comment on lines +18 to +82
class AtomicBitVector {
public:
AtomicBitVector(size_t size)
: size_(size),
num_blocks_((size + kBitsPerBlock - 1) / kBitsPerBlock),
data_(std::make_unique<std::atomic<BlockType>[]>(num_blocks_)) {}

// Set bit to true. Returns previous value.
bool Set(size_t idx, std::memory_order order = std::memory_order_seq_cst) {
assert(idx < size_);
BlockType mask = kOne << BitOffset(idx);
return data_[BlockIndex(idx)].fetch_or(mask, order) & mask;
}

// Set bit to false. Returns previous value.
bool Reset(size_t idx, std::memory_order order = std::memory_order_seq_cst) {
assert(idx < size_);
BlockType mask = kOne << BitOffset(idx);
return data_[BlockIndex(idx)].fetch_and(~mask, order) & mask;
}

// Set bit to given value. Returns previous value.
bool Set(size_t idx, bool value, std::memory_order order = std::memory_order_seq_cst) {
return value ? Set(idx, order) : Reset(idx, order);
}

// Read bit value.
bool Test(size_t idx, std::memory_order order = std::memory_order_seq_cst) const {
assert(idx < size_);
BlockType mask = kOne << BitOffset(idx);
return data_[BlockIndex(idx)].load(order) & mask;
}

bool operator[](size_t idx) const {
return Test(idx);
}

constexpr size_t Size() const noexcept {
return size_;
}

private:
#if (ATOMIC_LLONG_LOCK_FREE == 2)
using BlockType = unsigned long long;
#elif (ATOMIC_LONG_LOCK_FREE == 2)
using BlockType = unsigned long;
#else
using BlockType = unsigned int;
#endif

static constexpr size_t kBitsPerBlock = std::numeric_limits<BlockType>::digits;
static constexpr BlockType kOne = 1;

static constexpr size_t BlockIndex(size_t bit) {
return bit / kBitsPerBlock;
}

static constexpr size_t BitOffset(size_t bit) {
return bit % kBitsPerBlock;
}

size_t size_;
size_t num_blocks_;
std::unique_ptr<std::atomic<BlockType>[]> data_;
};
Copy link
Collaborator

@ol-imorozko ol-imorozko Jan 21, 2026

Choose a reason for hiding this comment

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

Well done!

Only one question: original code relied on value-initialization of std::atomic inside std::vector:

struct atomwrapper {
    std::atomic<T> _a;
    atomwrapper() : _a() { }
};
//...
std::vector<AtomicBlockType> data_;
//...
    atomic_bv_t(size_t N) : _size(N),
                            kNumBlocks((N + kBitsPerBlock - 1) / kBitsPerBlock) {
        data_.resize(kNumBlocks);
    }

That _a() is value-initialization, so initially we will have an array full of zeroes which is expected.

But here we have std::make_unique<std::atomic<BlockType>[]>(num_blocks_), which only default constructs std::atomic, so no value-initialization takes place

Copy link
Collaborator

Choose a reason for hiding this comment

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

image Oh, nevermind, I just found out that this behavior got changed in C++20 and now default initialization of `std::atomic` actually value-initializes

Comment on lines +1 to +6
/*
* Based on atomic_bitvector by Erik Garrison (Apache 2.0)
* https://github.com/ekg/atomicbitvector
*
* Simplified version without iterator support.
*/
Copy link
Collaborator

@ol-imorozko ol-imorozko Jan 21, 2026

Choose a reason for hiding this comment

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

I'm not an expert, but chatgpt tells me that in order to use apache 2.0 we need to:

  • include copyright
  • include license
  • state changes
  • include notice

It suggests this:

/*
 * Based on atomic_bitvector by Erik Garrison
 * https://github.com/ekg/atomicbitvector
 *
 * Copyright 2019 Erik Garrison
 * Copyright 2019 Facebook, Inc. and its affiliates
 *
 * Modifications:
 *  - Removed iterator support
 *  - Replaced std::vector with std::unique_ptr<std::atomic[]>
 *  - Simplified API
 *
 * 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.
 */

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants