From f7282d995e5bf4bea92c68cca0872923e041c681 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 30 Jun 2025 19:46:41 -0700 Subject: [PATCH 1/2] best table --- .bazelrc | 3 +- .clangd | 2 + best/base/BUILD | 5 + best/base/arch.h | 109 ++++ best/base/fwd.h | 4 + best/base/hint.h | 27 + best/container/BUILD | 36 ++ best/container/box.h | 8 + best/container/choice.h | 10 + best/container/internal/table.h | 493 +++++++++++++++ best/container/object.h | 8 + best/container/option.h | 85 ++- best/container/result.h | 13 +- best/container/row.h | 42 ++ best/container/table.h | 973 ++++++++++++++++++++++++++++++ best/container/table_test.cc | 229 +++++++ best/hash/BUILD | 39 ++ best/hash/fxhash.h | 128 ++++ best/hash/hash.h | 178 ++++++ best/hash/hash_test.cc | 0 best/hash/impls.h | 106 ++++ best/hash/internal/hash.h | 36 ++ best/log/wtf.h | 29 + best/math/bit.h | 9 + best/math/overflow.h | 42 ++ best/memory/BUILD | 3 + best/memory/internal/layout.h | 39 +- best/memory/layout.h | 49 ++ best/memory/ptr.h | 26 + best/memory/span.h | 18 +- best/meta/tlist.h | 2 + best/meta/traits/BUILD | 8 + best/meta/traits/arrays.h | 21 + best/meta/traits/empty.h | 2 + best/meta/traits/internal/types.h | 45 +- best/meta/traits/pairs.h | 57 ++ best/meta/traits/types.h | 4 +- best/test/test.h | 11 +- best/text/BUILD | 1 + best/text/format.h | 14 +- best/text/internal/format_impls.h | 8 +- best/text/rune.h | 5 + 42 files changed, 2864 insertions(+), 63 deletions(-) create mode 100644 .clangd create mode 100644 best/base/arch.h create mode 100644 best/container/internal/table.h create mode 100644 best/container/table.h create mode 100644 best/container/table_test.cc create mode 100644 best/hash/BUILD create mode 100644 best/hash/fxhash.h create mode 100644 best/hash/hash.h create mode 100644 best/hash/hash_test.cc create mode 100644 best/hash/impls.h create mode 100644 best/hash/internal/hash.h create mode 100644 best/meta/traits/pairs.h diff --git a/.bazelrc b/.bazelrc index c34a1e0..fc671b2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,2 +1,3 @@ # Makes bazel test always print errors. -test --test_output=errors \ No newline at end of file +test --test_output=errors +build --copt=-ftemplate-backtrace-limit=0 \ No newline at end of file diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..345f70d --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: ['-march=native'] \ No newline at end of file diff --git a/best/base/BUILD b/best/base/BUILD index 23d74ac..6f415bf 100644 --- a/best/base/BUILD +++ b/best/base/BUILD @@ -25,6 +25,11 @@ cc_library( deps = [":macro"], ) +cc_library( + name = "arch", + hdrs = ["arch.h"], +) + cc_library( name = "macro", hdrs = [ diff --git a/best/base/arch.h b/best/base/arch.h new file mode 100644 index 0000000..59b8858 --- /dev/null +++ b/best/base/arch.h @@ -0,0 +1,109 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_BASE_INTRINSICS_H_ +#define BEST_BASE_INTRINSICS_H_ + +//! Helpers for working with vendor intrinsics. This file will include all +//! the relevant intrinsics headers *and* will define macros for detecting what +//! features are statically available. +//! +//! Every macro is always defined; they should be used with `#if`, not `#ifdef`. + +/// # `BEST_X86` +/// +/// True if we're targeting `x86_64`. +#ifdef __x86_64__ +#define BEST_X86 1 +#else +#define BEST_X86 0 +#endif + +/// # `BEST_AARCH64` +/// +/// True if we're targeting `aarch64`. +#ifdef __aarch64__ +#define BEST_AARCH64 1 +#else +#define BEST_AARCH64 0 +#endif + +#if !(BEST_X86 || BEST_AARCH64) +#error "unsupported architecture :(" +#endif + +/// # `BEST_SSE`, `BEST_SSE2`, `BEST_SSE3`, `BEST_SSSE3` +/// # `BEST_SSE41`, `BEST_SSE42`, `BEST_SSE4A` +/// +/// True if the named x86 extension is statically available. +#ifdef __SSE__ +#define BEST_SSE 1 +#else +#define BEST_SSE 0 +#endif +#ifdef __SSE2__ +#define BEST_SSE2 1 +#else +#define BEST_SSE2 0 +#endif +#ifdef __SSE3__ +#define BEST_SSE3 1 +#else +#define BEST_SSE3 0 +#endif +#ifdef __SSSE3__ +#define BEST_SSSE3 1 +#else +#define BEST_SSSE3 0 +#endif +#ifdef __SSE4_1__ +#define BEST_SSE41 1 +#else +#define BEST_SSE41 0 +#endif +#ifdef __SSE4_2__ +#define BEST_SSE42 1 +#else +#define BEST_SSE42 0 +#endif +#ifdef __SSE4A__ +#define BEST_SSE4A 1 +#else +#define BEST_SSE4A 0 +#endif + +/// # `BEST_AVX`, `BEST_AVX2` +/// +/// True if the named x86 extension is statically available. +#ifdef __AVX__ +#define BEST_AVX 1 +#else +#define BEST_AVX 0 +#endif +#ifdef __AVX2__ +#define BEST_AVX2 1 +#else +#define BEST_AVX2 0 +#endif + +#if BEST_X86 +#include +#endif // BEST_X86 + +#endif // BEST_BASE_INTRINSICS_H_ diff --git a/best/base/fwd.h b/best/base/fwd.h index 678a434..61d3cc9 100644 --- a/best/base/fwd.h +++ b/best/base/fwd.h @@ -190,6 +190,10 @@ struct access; namespace dyn_internal { struct access; } +namespace format_internal { +template +class templ; +} } // namespace best #endif // BEST_BASE_FWD_H_ diff --git a/best/base/hint.h b/best/base/hint.h index b97076b..7cc2a79 100644 --- a/best/base/hint.h +++ b/best/base/hint.h @@ -106,6 +106,33 @@ BEST_INLINE_ALWAYS constexpr void assume(bool truth) { if (!std::is_constant_evaluated()) { asm volatile("" ::"m,r"(value)); } return decltype(value)(value); } + +/// # `best::prefetch_locality` +/// +/// A locality value for a prefetch operation. Specifies which cache the +/// prefetched memory should stick around in. +enum class prefetch_locality { + Nontemporal = 0, + L3 = 1, + L2 = 2, + L1 = 3, +}; + +/// # `best::prefetch_for_read()`, `best::prefetch_for_write()` +/// +/// Prefetches the given memory. +template +BEST_INLINE_SYNTHETIC constexpr void prefetch_for_read(const void* addr) { + if (!std::is_constant_evaluated()) { + __builtin_prefetch(addr, 0, int(locality)); + } +} +template +BEST_INLINE_SYNTHETIC constexpr void prefetch_for_write(const void* addr) { + if (!std::is_constant_evaluated()) { + __builtin_prefetch(addr, 1, int(locality)); + } +} } // namespace best #endif // BEST_BASE_HINT_H_ diff --git a/best/container/BUILD b/best/container/BUILD index f008675..f4eb411 100644 --- a/best/container/BUILD +++ b/best/container/BUILD @@ -10,6 +10,7 @@ cc_library( ":pun", "//best/base:port", "//best/base:tags", + "//best/hash", "//best/log/internal:crash", ], ) @@ -31,6 +32,7 @@ cc_library( deps = [ "//best/base:ord", "//best/memory:ptr", + "//best/hash", ] ) @@ -49,6 +51,7 @@ cc_library( hdrs = ["option.h"], deps = [ ":choice", + "//best/hash", "//best/log/internal:crash", ], ) @@ -97,6 +100,7 @@ cc_library( deps = [ ":choice", ":row", + "//best/hash", "//best/log/internal:crash", ], ) @@ -122,6 +126,7 @@ cc_library( ], deps = [ ":object", + "//best/hash", "//best/base:port", "//best/base:tags", ], @@ -143,6 +148,7 @@ cc_library( hdrs = ["box.h"], deps = [ "//best/func:dyn", + "//best/hash", "//best/memory:allocator", "//best/memory:ptr", ] @@ -187,6 +193,36 @@ cc_test( "//best/test:fodder", ], ) +hash +cc_library( + name = "table", + hdrs = [ + "table.h", + "internal/table.h", + ], + deps = [ + ":option", + "//best/base:arch", + "//best/hash", + "//best/hash:fxhash", + "//best/hash:impls", + "//best/memory:allocator", + "//best/memory:span", + "//best/memory:layout", + "//best/meta/traits:pairs", + ], +) + +cc_test( + name = "table_test", + srcs = ["table_test.cc"], + linkopts = ["-rdynamic"], + deps = [ + ":table", + "//best/test", + "//best/test:fodder", + ], +) cc_library( name = "simple_option", diff --git a/best/container/box.h b/best/container/box.h index 7327f45..c6f367f 100644 --- a/best/container/box.h +++ b/best/container/box.h @@ -26,6 +26,7 @@ #include "best/container/object.h" #include "best/container/option.h" #include "best/func/dyn.h" +#include "best/hash/hash.h" #include "best/memory/allocator.h" #include "best/memory/layout.h" #include "best/memory/ptr.h" @@ -260,6 +261,13 @@ class BEST_RELOCATABLE box final { }; } + template + constexpr friend void BestHash(best::hasher& h, const box& value) + requires requires { h.write(*value); } + { + h.write(*value); + } + template constexpr bool operator==(const best::box& that) const requires best::equatable, best::view> diff --git a/best/container/choice.h b/best/container/choice.h index e095b1d..bf6f178 100644 --- a/best/container/choice.h +++ b/best/container/choice.h @@ -25,9 +25,11 @@ #include "best/base/ord.h" #include "best/base/tags.h" #include "best/container/internal/choice.h" +#include "best/hash/hash.h" #include "best/log/internal/crash.h" #include "best/log/location.h" #include "best/meta/init.h" +#include "best/meta/traits/empty.h" //! A sum type, like `std::variant`. //! @@ -329,6 +331,14 @@ class choice final { }; } + template + constexpr friend void BestHash(best::hasher& h, const choice& value) + requires (best::hashable && ...) + { + value.index_match( + [&](auto idx, const auto& value) { h.write(idx.value, value); }); + } + // Comparisons. template BEST_INLINE_ALWAYS constexpr bool operator==(const choice& that) const diff --git a/best/container/internal/table.h b/best/container/internal/table.h new file mode 100644 index 0000000..e72066d --- /dev/null +++ b/best/container/internal/table.h @@ -0,0 +1,493 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_CONTAINER_INTERNAL_TABLE_H_ +#define BEST_CONTAINER_INTERNAL_TABLE_H_ + +#include +#include + +#include + +#include "best/base/arch.h" +#include "best/base/hint.h" +#include "best/container/option.h" +#include "best/hash/fxhash.h" +#include "best/hash/impls.h" +#include "best/log/wtf.h" +#include "best/math/bit.h" +#include "best/math/int.h" +#include "best/memory/layout.h" +#include "best/meta/traits/empty.h" + +namespace best::table_internal { +template +class iter_impl; + +struct ctrl final { + static const ctrl Empty, Tombstone, Sentinel; + + constexpr ctrl() = default; + constexpr ctrl(int8_t bits) : bits(bits) {} + + constexpr bool is_occupied() const { return bits >= 0; } + constexpr bool is_vacant() const { return bits < -1; } + constexpr bool is_empty() const { return bits == Empty; } + constexpr bool is_tombstone() const { return bits == Tombstone; } + + constexpr bool operator==(const ctrl&) const = default; + + friend void BestFmt(auto& fmt, ctrl c) { + if (c.is_empty()) { + fmt.write("--"); + } else if (c.is_tombstone()) { + fmt.write("πŸͺ¦"); + } else { + fmt.format("{:02x}", best::to_unsigned(c.bits)); + } + } + + int8_t bits; +}; + +inline constexpr ctrl ctrl::Empty = 0b1000'0000; +inline constexpr ctrl ctrl::Tombstone = 0b1111'1110; +inline constexpr ctrl ctrl::Sentinel = 0b1111'1111; + +inline uint64_t seed(const void* array) { return (uintptr_t)array >> 12; } + +// A processed hash from a table. +struct hash final { + template + static hash compute(const auto& query, const auto& id, const void* salt) { + best::hasher hasher; + id.template hash(hasher, query); + uint64_t h = hasher.finish(); + + return { + .h1 = (h >> 7) ^ (reinterpret_cast(salt) >> 12), + .h2 = h & 0x7f, + }; + } + + uint64_t h1; + ctrl h2; +}; + +template +struct policy final { + using identity = I; + using hash_state = H; + using allocator = A; +}; + +/// A general bitset, stored in an integer. +/// +/// `width` is the number of bits in the integer dedicated to each. The nth bit +/// is set when the nth run of `width` bits is not all zeros. `len` is the total +/// length of the bitset. +/// +/// For example, when `width` is 1, this is an actual bitset. When `width` is +/// 8, each byte corresponds to one bit. +template +struct bitset final { + static_assert(width * len <= best::bits_of); + Int bits; + + bool empty() const { return bits == 0; } + + /// Lowest and highest set bit index in the set. + uint32_t lowest() const { return best::trailing_zeros(bits) / width; } + uint32_t highest() const { return best::bits_for(bits) / width; } + + /// The number of leading and trailing zero bits. + uint32_t trailing_zeros() const { return lowest(); } + uint32_t leading_zeros() const { + auto total_bits = width * len; + auto extra_bits = best::bits_of - total_bits; + return best::leading_zeros(bits) / width - extra_bits; + } + + /// Returns the next bit index set. + BEST_INLINE_ALWAYS + best::option next() { + if (empty()) { return best::none; } + + uint32_t idx = lowest(); + bits &= bits - 1; + return idx; + } +}; + +#if BEST_SSE2 +// An implementation of the group operations using an SSE2 (xmm) vector. +class group_sse2 final { + public: + using mask = bitset; + + group_sse2() = default; + group_sse2(__m128i xmm) : xmm_(xmm){}; + + static group_sse2 load(const void* array, size_t offset) { + auto* ptr = (const char*)array + offset; + + return _mm_loadu_si128((const __m128i*)ptr); + } + + void store(void* array, size_t offset) { + auto* ptr = (char*)array + offset; + _mm_storeu_si128((__m128i*)ptr, xmm_); + } + + mask match(ctrl h2) const { + auto broadcast = _mm_set1_epi8(h2.bits); + auto eq = _mm_cmpeq_epi8(broadcast, xmm_); + return {.bits = uint64_t(_mm_movemask_epi8(eq))}; + } + + mask match_empty() const { +#if BEST_SSSE3 + // This only works because Empty is 0b1000'0000, the only value whose + // two's complement is itself. All other values produce non-negative bytes + // when pushed through `vpsignb`. + auto sign_bits = _mm_sign_epi8(xmm_, xmm_); + return {.bits = uint64_t(_mm_movemask_epi8(sign_bits))}; +#else + return match(ctrl::Empty); +#endif + } + + mask match_vacant() const { + auto broadcast = _mm_set1_epi8(ctrl::Sentinel.bits); + auto gt = _mm_cmpgt_epi8(broadcast, xmm_); + return {.bits = uint64_t(_mm_movemask_epi8(gt))}; + } + + uint32_t count_leading_vacant() const { + auto broadcast = _mm_set1_epi8(ctrl::Sentinel.bits); + auto gt = _mm_cmpgt_epi8(broadcast, xmm_); + return best::trailing_zeros(_mm_movemask_epi8(gt) + 1); + } + + group_sse2 prepare_for_rehash() const { + auto msbs = _mm_set1_epi8(0b1000'0000); + auto x126 = _mm_set1_epi8(0b0111'1110); +#if BEST_SSSE3 + return _mm_or_si128(_mm_shuffle_epi8(x126, xmm_), msbs); +#else + auto zero = _mm_setzero_si128(); + auto special_mask = _mm_cmpgt_epi8(zero, xmm_); + return _mm_or_si128(msbs, _mm_andnot_si128(special_mask, x126)); +#endif + } + + friend void BestFmt(auto& fmt, group_sse2 g) { + ctrl bytes[sizeof(group_sse2)]; + best::span(bytes).copy_from(best::span(&g, 1).as_bytes()); + + bool first = true; + for (auto byte : bytes) { + if (!std::exchange(first, false)) { fmt.write(" "); } + fmt.format("{:?}", byte); + } + } + + private: + __m128i xmm_; +}; +#endif + +// An implementation of the group operations using plain uint64 operations. +class group_scalar final { + public: + using mask = bitset; + + group_scalar() = default; + group_scalar(uint64_t reg) : reg_(reg){}; + + static group_scalar load(const void* array, size_t offset) { + auto* ptr = (const char*)array + offset; + group_scalar g; + std::memcpy(&g, ptr, sizeof(g)); + return g; + } + + void store(void* array, size_t offset) { + auto* ptr = (char*)array + offset; + std::memcpy(ptr, this, sizeof(*this)); + } + + mask match(ctrl h2) const { + // For the technique, see: + // http://graphics.stanford.edu/~seander/bithacks.html##ValueInWord + // (Determine if a word has a byte equal to n). + // + // Caveat: there are false positives but: + // - they only occur if there is a real match + // - they never occur on ctrl_t::kEmpty, ctrl_t::kDeleted, ctrl_t::kSentinel + // - they will be handled gracefully by subsequent checks in code + // + // Example: + // v = 0x1716151413121110 + // hash = 0x12 + // retval = (v - lsbs) & ~v & msbs = 0x0000000080800000 + + auto x = reg_ ^ (Lsbs * h2.bits); + return {.bits = (x - Lsbs) & ~x & Msbs}; + } + + mask match_empty() const { return {.bits = (reg_ & (~reg_ << 6)) & Msbs}; } + + mask match_vacant() const { return {.bits = (reg_ & (~reg_ << 7)) & Msbs}; } + + uint32_t count_leading_vacant() const { + return best::trailing_zeros((((~reg_ & (reg_ >> 7)) | Gaps) + 1) + 7) / 8; + } + + group_scalar prepare_for_rehash() const { + auto x = reg_ & Msbs; + return (~x + (x >> 7)) & ~Lsbs; + } + + friend void BestFmt(auto& fmt, group_scalar g) { + ctrl bytes[sizeof(group_sse2)]; + best::span(bytes).copy_from(best::span(&g, 1).as_bytes()); + + bool first = true; + for (auto byte : bytes) { + if (!std::exchange(first, false)) { fmt.write(" "); } + fmt.format("{}", byte); + } + } + + private: + static constexpr uint64_t Msbs = 0x8080808080808080; + static constexpr uint64_t Lsbs = 0x0101010101010101; + static constexpr uint64_t Gaps = ~Lsbs >> 8; + + uint64_t reg_; +}; + +#if BEST_SSE2 +using group = group_sse2; +#else +using group = group_scalar; +#endif + +constexpr size_t mirror(size_t idx, size_t cloned, size_t hard) { + // This is intentionally branchless. If `i < kWidth`, it will write to the + // cloned bytes as well as the "real" byte; otherwise, it will store `h` + // twice. + // + // We want to map all values n < sizeof(group) to mask - n. Subtracting + // off sizeof(group) produces negative values only for the values of interest; + // for these values, `(idx - cloned) & mask` is `mask - (cloned - idx)`. We + // can then add (cloned & mask) to shift everything up. + size_t mask = hard - 1; + return ((idx - cloned) & mask) + (cloned); +} + +// A quadratic probe sequence. +// +// Currently, the sequence is a triangular progression of the form +// ``` +// p(i) := kWidth/2 * (i^2 - i) + hash (mod mask + 1) +// ``` +BEST_INLINE_ALWAYS auto probe(auto& table, const void* ctrl, hash h, + size_t hard, auto cb) { + size_t mask = hard - 1; + size_t offset = h.h1 & mask; + size_t index = 0; + + best::prefetch_for_read(ctrl); + while (true) { + auto g = group::load(ctrl, offset); + auto got = cb(offset, g); + if (best::likely(got.has_value())) { return *got; } + + index += sizeof(group); + offset += index; + offset &= mask; + + best::debug_must(index < hard, "full table:\n{}", table.debug(false)); + } +} + +// The capacity of a table. +class capacity final { + public: + capacity() = default; + + // Constructs a new layout for the given minimum number of elements. + explicit capacity(size_t at_least) { + hard = at_least + at_least / 7; + hard = + best::checked_next_pow2(hard).expect("map size too large: {}", at_least); + hard = best::max(hard, sizeof(group)); + reset_soft(); + } + + // The number of groups this in this table's ctrl array. + size_t group_count() const { return hard / size_of + 1; } + + // Returns the layout for the backing array. + template + best::layout layout() const { + if constexpr (best::is_empty) { + return best::layout::of_struct({ + best::layout::of().repeat(group_count()), + best::layout::of().repeat(hard), + best::layout::of(), + }); + } else { + return best::layout::of_struct({ + best::layout::of().repeat(group_count()), + best::layout::of().repeat(hard), + best::layout::of().repeat(hard), + }); + } + } + + template + size_t keys_offset() const { + auto ctrl = best::layout::of().repeat(group_count()); + return best::round_up_to_pow2(ctrl, align_of); + } + + template + size_t values_offset() const { + auto keys = best::layout::of_struct({ + best::layout::of().repeat(group_count()), + best::layout::of().repeat(hard), + }); + return best::round_up_to_pow2(keys, align_of); + } + + void reset_soft() { + soft = hard - hard / 8; // soft = hard * 7/8 + } + + // hard is always a power of 2; soft is the number of slots left before a + // rehash needs to happen. + size_t soft{}, hard{}; +}; + +// The heap-backed array for a table. This is essentially the whole table +// except for the size and the policy. +template +class array final { + public: + array() = default; + explicit array(best::ptr base, capacity cap) : cap_(cap) { + size_t ctrl_count = cap.group_count() * sizeof(group); + + ctrl_ = base.template cast().raw(); + keys_ = ctrl_.template skip(ctrl_count); + reset_ctrl(); + } + + table_internal::ctrl ctrl(size_t n) const { return *ctrl_.offset(n); } + void set_ctrl(size_t n, table_internal::ctrl c) { + if (ctrl(n) == table_internal::ctrl::Empty) { --cap_.soft; } + + *ctrl_.offset(n) = c; + *ctrl_.offset(mirror(n, sizeof(group), cap_.hard)) = c; + } + + best::ptr key(size_t idx) const { return keys_ + idx; } + best::ptr value(size_t idx) const { + auto ptr = keys_.template skip(cap_.hard); + if constexpr (!best::is_empty) { ptr += idx; } + return ptr; + } + + const capacity& cap() const { return cap_; } + capacity& cap() { return cap_; } + + auto* ptr() const { return ctrl_.raw(); } + + void reset_ctrl() { + ctrl_.fill(table_internal::ctrl::Empty.bits, + cap_.group_count() * sizeof(group)); + } + + void destroy() { + if (ctrl_ == nullptr) { return; } + + auto keys = keys_; + auto values = keys.template skip(cap_.hard); + + auto cur = ctrl_, end = ctrl_ + cap_.hard; + do { + if (cur->is_occupied()) { + keys.destroy(); + values.destroy(); + } + + ++cur; + ++keys; + if constexpr (!best::is_empty) { ++values; } + } while (cur != end); + } + + BEST_INLINE_ALWAYS size_t vacant_unchecked(hash hash) const { + return best::table_internal::probe( // + *this, ptr(), hash, cap().hard, + [&](size_t base, group g) -> best::option { + return g.match_vacant().next().map( + [&](auto i) { return (base + i) & (cap().hard - 1); }); + }); + } + + BEST_INLINE_ALWAYS best::option vacant(hash hash) const { + if (ptr() == nullptr) { return best::none; } + + size_t idx = vacant_unchecked(hash); + if (cap().soft == 0 && !ctrl(idx).is_tombstone()) { return best::none; } + + return idx; + } + + best::strbuf debug(bool) const { + best::strbuf out = + best::format("type: {}\narray: {:p}, {}/{}\n", + best::type_names::of.path_with_params(), ptr(), + cap().soft, cap().hard); + + if (ptr() == nullptr) { return out; } + + out.push("ctrl:"); + for (auto i : best::bounds{.count = cap().group_count()}) { + i *= sizeof(group); + best::format(out, "\n {:p}: {:?}", ptr() + i, group::load(ptr(), i)); + } + out.push(" (mirrored)\n"); + return out; + } + + private: + best::ptr ctrl_; + best::ptr keys_; + capacity cap_; +}; + +} // namespace best::table_internal + +#endif // BEST_CONTAINER_INTERNAL_TABLE_H_ \ No newline at end of file diff --git a/best/container/object.h b/best/container/object.h index f1cd2d5..405b292 100644 --- a/best/container/object.h +++ b/best/container/object.h @@ -26,6 +26,7 @@ #include "best/base/ord.h" #include "best/base/tags.h" +#include "best/hash/hash.h" #include "best/memory/ptr.h" #include "best/meta/init.h" #include "best/meta/traits/empty.h" @@ -205,6 +206,13 @@ class object final { query = query.template of; } + template + constexpr friend void BestHash(best::hasher& h, const object& value) + requires requires { h.write(value.or_empty()); } + { + h.write(value.or_empty()); + } + public: [[no_unique_address]] wrapped_type BEST_OBJECT_VALUE_; }; diff --git a/best/container/option.h b/best/container/option.h index 57075e2..dbc9275 100644 --- a/best/container/option.h +++ b/best/container/option.h @@ -26,6 +26,7 @@ #include "best/base/fwd.h" #include "best/base/tags.h" #include "best/container/choice.h" +#include "best/hash/hash.h" #include "best/log/internal/crash.h" #include "best/log/location.h" #include "best/meta/init.h" @@ -78,6 +79,12 @@ concept is_option = requires { template using option_type = typename best::as_auto::type; +namespace option_internal { +// A helper for threading formatting into option::check_ok. The corresponding +// definition lives in format.h. +struct fmt; +}; // namespace option_internal + /// # `best::option` /// /// An optional value. @@ -129,8 +136,8 @@ class option final { template static constexpr bool not_forbidden_conversion = - (!best::same>)&& // - (!best::same>)&& // + (!best::same>) && // + (!best::same>) && // (!best::same>); public: @@ -267,14 +274,15 @@ class option final { /// This constructor is intended for constructing options that contain /// values with troublesome constructors. template - constexpr explicit option(best::in_place_t, Args&&... args) - requires best::constructible + constexpr explicit(sizeof...(Args) != 1) + option(best::in_place_t, Args&&... args) + requires best::constructible : BEST_OPTION_IMPL_(best::index<1>, BEST_FWD(args)...) {} template - constexpr explicit option(best::in_place_t, std::initializer_list il, - Args&&... args) - requires best::constructible, Args&&...> + constexpr explicit(sizeof...(Args) != 1) + option(best::in_place_t, std::initializer_list il, Args&&... args) + requires best::constructible, Args&&...> : BEST_OPTION_IMPL_(best::index<1>, il, BEST_FWD(args)...) {} /// # `option::is_empty()` @@ -351,6 +359,27 @@ class option final { constexpr crref value(best::location loc = best::here) const&&; constexpr rref value(best::location loc = best::here) &&; + private: + template + using format_template = best::format_internal::templ< // + format_spec, best::dependent...>; + + public: + /// # `option::expect()`. + /// + /// Extracts the value of this option. Crashes with the given message. + template + constexpr cref expect(format_template templ, + const Args&... args) const&; + template + constexpr ref expect(format_template templ, const Args&... args) &; + template + constexpr crref expect(format_template templ, + const Args&... args) const&&; + template + constexpr crref expect(format_template templ, + const Args&... args) &&; + /// # `option::value_or(...)` /// /// Extracts the value of this option by copy/move, or constructs a default @@ -680,6 +709,13 @@ class option final { query.requires_debug = true; } + template + constexpr friend void BestHash(best::hasher& h, const option& value) + requires best::hashable + { + h.write(value.BEST_OPTION_IMPL_); + } + // Conversions w/ simple_option. private: using objT = best::select, T, best::empty>; @@ -759,6 +795,12 @@ class option final { } } + template + constexpr void check_ok(format_template templ, + const Args&... args) const { + if (best::unlikely(is_empty())) { F::wtf(templ, args...); } + } + constexpr const auto& impl() const& { return BEST_OPTION_IMPL_; } constexpr auto& impl() & { return BEST_OPTION_IMPL_; } constexpr const auto&& impl() const&& { return BEST_MOVE(BEST_OPTION_IMPL_); } @@ -871,6 +913,35 @@ constexpr option::rref option::value(best::location loc) && { BEST_MOVE(*this).value(unsafe("check_ok() called before this")); } +template +template +constexpr option::cref option::expect(format_template templ, + const Args&... args) const& { + return check_ok(templ, args...), + value(unsafe("check_ok() called before this")); +} +template +template +constexpr option::ref option::expect(format_template templ, + const Args&... args) & { + return check_ok(templ, args...), + value(unsafe("check_ok() called before this")); +} +template +template +constexpr option::crref option::expect(format_template templ, + const Args&... args) const&& { + return check_ok(templ, args...), + BEST_MOVE(*this).value(unsafe("check_ok() called before this")); +} +template +template +constexpr option::crref option::expect(format_template templ, + const Args&... args) && { + return check_ok(templ, args...), + BEST_MOVE(*this).value(unsafe("check_ok() called before this")); +} + template constexpr auto option::map(auto&& f) const& { using U = best::call_result; diff --git a/best/container/result.h b/best/container/result.h index c329ba2..4fb6aa4 100644 --- a/best/container/result.h +++ b/best/container/result.h @@ -121,9 +121,9 @@ class [[nodiscard( template static constexpr bool cannot_init_from = ((!best::constructible && !best::constructible) || - best::is_void)&&((!best::constructible && - !best::constructible) || - best::is_void); + best::is_void) && + ((!best::constructible && !best::constructible) || + best::is_void); public: /// Helper type aliases. @@ -392,6 +392,13 @@ class [[nodiscard( }; } + template + constexpr friend void BestHash(best::hasher& h, const result& value) + requires best::hashable && best::hashable + { + h.write(value.BEST_RESULT_IMPL_); + } + private: template friend class result; diff --git a/best/container/row.h b/best/container/row.h index 922c1b7..50ce969 100644 --- a/best/container/row.h +++ b/best/container/row.h @@ -25,8 +25,10 @@ #include "best/base/tags.h" #include "best/container/internal/row.h" #include "best/container/object.h" +#include "best/hash/hash.h" #include "best/meta/init.h" #include "best/meta/tlist.h" +#include "best/meta/traits/refs.h" //! A product type, like `std::tuple`. //! @@ -215,6 +217,14 @@ class row final constexpr best::row...> as_ref() const&&; constexpr best::row...> as_ref() &&; + /// # `row::copied()` + /// + /// Copies a row of values copied from this row. + constexpr best::row...> copied() const&; + constexpr best::row...> copied() &; + constexpr best::row...> copied() const&&; + constexpr best::row...> copied() &&; + /// # `row[index]`, `row[best::values]` /// /// Returns a reference to the `n`th element, or a subrange as a row (values @@ -511,6 +521,13 @@ class row final }; } + template + constexpr friend void BestHash(best::hasher& h, const row& value) + requires (best::hashable && ...) + { + value.each([&](const auto& x) { h.write(x); }); + } + // Comparisons. template constexpr bool operator==(const row& that) const @@ -600,6 +617,31 @@ constexpr best::row...> row::as_ref() && { }); } +template +constexpr best::row...> best::row::copied() const& { + return apply([](auto&&... args) { + return best::row...>(BEST_FWD(args)...); + }); +} +template +constexpr best::row...> best::row::copied() & { + return apply([](auto&&... args) { + return best::row...>(BEST_FWD(args)...); + }); +} +template +constexpr best::row...> best::row::copied() const&& { + return BEST_MOVE(*this).apply([](auto&&... args) { + return best::row...>(BEST_FWD(args)...); + }); +} +template +constexpr best::row...> best::row::copied() && { + return BEST_MOVE(*this).apply([](auto&&... args) { + return best::row...>(BEST_FWD(args)...); + }); +} + template template constexpr row::cref row::operator[]( diff --git a/best/container/table.h b/best/container/table.h new file mode 100644 index 0000000..01f6e54 --- /dev/null +++ b/best/container/table.h @@ -0,0 +1,973 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_CONTAINER_TABLE_H_ +#define BEST_CONTAINER_TABLE_H_ + +#include "best/base/fwd.h" +#include "best/base/hint.h" +#include "best/base/tags.h" +#include "best/base/unsafe.h" +#include "best/container/internal/table.h" +#include "best/container/option.h" +#include "best/hash/hash.h" +#include "best/iter/bounds.h" +#include "best/log/location.h" +#include "best/log/wtf.h" +#include "best/memory/allocator.h" +#include "best/memory/layout.h" +#include "best/meta/init.h" +#include "best/meta/traits/empty.h" +#include "best/meta/traits/pairs.h" +#include "best/meta/traits/refs.h" +#include "best/meta/traits/types.h" +#include "best/text/format.h" + +namespace best { +/// # `best::no_insert` +/// +/// Type tag for marking a `best::table::entry` as non-inserting. +struct no_insert final {}; + +template +class table_entry; + +/// # `best::table` +/// +/// `best::table` is a Swisstable implementation, a generic hash table type that +/// can be used as either a map or a set. +/// +/// The API is radically different from that of `std::unordered_map`, which it +/// replaces. Instead of having a variety of lookup operations, `best::table` +/// has exactly one: `operator[]`. Depending on which overload is picked, this +/// will return an accessor for the corresponding entry (sort of like an +/// enhanced `best::option>`); the mutable version is +/// `best::table::inserter`, which can be used to create an entry in the table. +/// +/// For example, inserting a value into the table looks like this: +/// +/// ``` +/// best::table ints; +/// ints[42].insert("hello!"); +/// ``` +/// +/// Removal is similar: `ints[42].remove()`, which move the associated entry +/// out of the map. Accessing members of a value is as simple as +/// `my_table[k]->foo()`, assuming `k` is present. Operating on possibly absent +/// members has a clean idiom: +/// +/// ``` +/// if (auto v = my_table[k]) { +/// v->do_something(); +/// } +/// ``` +/// +/// When `V` is `void`, this type takes on a set-like interface. Almost +/// everything is the same, except that `operator*` and `operator->` on an entry +/// will operate on the key rather than the value. +template >> +class BEST_RELOCATABLE table final { + private: + using policy = best::unabridge; + using group = best::table_internal::group; + using ctrl = best::table_internal::ctrl; + + public: + using key_type = K; + using value_type = V; + using identity = policy::identity; + using hash_state = policy::hash_state; + using allocator = policy::allocator; + + static_assert(best::hash_identity); + + using key_cref = best::as_ref; + using key_ref = best::as_ref; + using key_crref = best::as_rref; + using key_rref = best::as_rref; + using key_cptr = best::as_raw_ptr; + using key_ptr = best::as_raw_ptr; + + using value_cref = best::as_ref; + using value_ref = best::as_ref; + using value_crref = best::as_rref; + using value_rref = best::as_rref; + using value_cptr = best::as_raw_ptr; + using value_ptr = best::as_raw_ptr; + + // clang-format off + /// # `table::with_identity`, `table::with_hash`, `table::with_allocator` + /// + /// Aliases for creating a new table type with the given configuration + /// changes. + // clang-format on + template I> + using with_identity = best::table< + K, V, + best::abridge>>; + template + using with_hash = best::table< + K, V, best::abridge>>; + template + using with_allocator = best::table< + K, V, best::abridge>>; + + /// # `table::const_entry`, `table::entry` + /// + /// This tables's entry types. + using const_entry = best::table_entry; + using entry = best::table_entry; + + /// # `table::inserter` + /// + /// This table's inserter type, used for performing mutation operations. + template + using inserter = best::table_entry; + + /// # `table::table()` + /// + /// Constructs a new empty table. + table() = default; + explicit table(identity identity, allocator allocator = {}) + : identity_(BEST_MOVE(identity)), alloc_(BEST_MOVE(allocator)) {} + + /// # `table::table { ... }` + /// + /// Constructs a new empty table with the given elements. + table(std::initializer_list> il) requires (!best::is_void) + { + reserve(il.size()); + for (const auto& [k, v] : il) { query(k).insert(v); } + } + template + table(std::initializer_list il) requires best::is_void + { + reserve(il.size()); + for (const auto& k : il) { query(k).insert(); } + } + + /// # `vec::vec(iterator)` + /// + /// Constructs a vector by emptying an iterator. + template + explicit table(Iter&& iter) + requires (!best::is_void) && + best::constructible>> && + best::constructible>> + { + reserve(iter.size_hint().lower); + for (auto&& [k, v] : iter) { query(BEST_FWD(k)).insert(BEST_FWD(v)); } + } + template + explicit table(Iter&& iter) + requires best::is_void && best::constructible> + { + reserve(iter.size_hint().lower); + for (auto&& k : iter) { query(BEST_FWD(k)).insert(); } + } + + /// # `table::table(table)` + /// + /// Copy and move constructors. + table(const table&) requires best::copyable && best::copyable; + table& operator=(const table&) // + requires best::copyable && best::copyable; + table(table&&); + table& operator=(table&&); + + /// # `table::~table()` + /// + /// Destroys this table. + ~table() { + raw_.destroy(); + free(); + } + + /// # `table::size()` + /// + /// Returns the number of entries in the table. + size_t size() const { return size_; } + + /// # `table::capacity()` + /// + /// Returns the table's capacity, i.e. the total number of entries it can + /// contain before a rehash happens. + size_t capacity() const { return size_ + raw_.cap().soft; } + + /// # `table::operator[]` + /// + /// Extracts an entry from the table using the given query. + /// + /// This will not trigger an insertion if the key is not present; to do so, + /// use the member functions on the returned `table::inserter`. + template + const_entry operator[](Q&& query) const + requires best::hash_identity; + template + inserter operator[](Q&& query) requires best::hash_identity + ; + + /// # `table::query()` + /// + /// Identical to `operator[]`; exists only to be used when accessing via `->`. + /// Prefer `operator[]` in general. + template + const_entry query(Q&& query) const + requires best::hash_identity + { + return operator[](BEST_FWD(query)); + } + template + inserter query(Q&& query) requires best::hash_identity + { + return operator[](BEST_FWD(query)); + } + + /// # `table::contains()` + /// + /// Returns whether an entry exists in this table matching the given query. + template + bool contains(Q&& query) const { + return this->query(BEST_FWD(query)).is_occupied(); + } + + /// # `table::const_iterator`, `table::iterator` + /// + /// This tables's iterator types. + using const_iterator = best::iter>; + using iterator = best::iter>; + + /// # `table::iter()`, `table::begin()`, `table::end()`. + /// + /// Tables are iterable; the yielded values are entries, which support + /// structured bindings, but, in the case of the non-const iterator, can also + /// be used for removal. + const_iterator iter() const { + return const_iterator(table_internal::iter_impl(this)); + } + iterator iter() { return iterator(table_internal::iter_impl
(this)); } + auto begin() const { return iter().into_range(); } + auto begin() { return iter().into_range(); } + auto end() const { return best::iter_range_end{}; } + + /// # `table::clear()` + /// + /// Destroys all entries in the table without freeing capacity. This will + /// always be faster than calling `e.remove()` in a loop. + void clear() { + raw_.destroy(); + raw_.reset_ctrl(); + raw_.cap().reset_soft(); + size_ = 0; + } + + /// # `table::reserve()` + /// + /// Ensures that inserting another `capacity` elements will not trigger a + /// rehash of the table. + void reserve(size_t capacity); + + /// # `table::rehash()` + /// + /// Shuffles around the internal hash table data structure to improve future + /// lookups. May cause reallocation. + void rehash(); + + /// # `table::hash_value` + /// + /// A hash calculated for some query. See `table::hash()`. + class hash_value final { + public: + constexpr hash_value() = default; + + private: + friend table; + template + friend class best::table_entry; + + table_internal::hash h_; + }; + + /// # `table::hash()` + /// + /// Calculates the hash for some query. + /// The returned hash becomes invalid if the table is resized or rehashed. + template + requires best::hash_identity + hash_value hash(const Q& query) const { + return hash(query, raw_.ptr()); + } + + /// # `table::debug()` + /// + /// Dumps debugging information about this table. + best::strbuf debug(bool include_entries = true) const; + + private: + template + friend class best::table_entry; + template + friend class best::table_internal::iter_impl; + + template + requires best::hash_identity + hash_value hash(const Q& query, const void* ctrl) const { + hash_value h; + h.h_ = best::table_internal::hash::compute(query, identity_, + ctrl); + return h; + } + + best::option search(hash_value hash, const auto& query) const; + best::row maybe_preinsert(hash_value hash, const auto& query); + size_t preinsert(hash_value hash, const auto& query); + + template + table_internal::array rehash_to(table_internal::capacity new_cap, + const table& target) const; + + void free() { + if (raw_.ptr() != nullptr) { + alloc_.dealloc(best::ptr(raw_.ptr()), raw_.cap().template layout()); + } + } + + table_internal::array raw_{}; + size_t size_{}; + [[no_unique_address]] identity identity_; + [[no_unique_address]] allocator alloc_; +}; + +/// # `best::table_entry` +/// +/// An entry in a table (a bucket). All operations on the table use entries +/// in some way. Entries are ephemeral; whenever a rehash occurs, all entries +/// returned by a table are invalidated. +/// +/// To obtain a value of this type, use `best::table::operator[]`. +template +class table_entry final { + private: + using group = best::table_internal::group; + using ctrl = best::table_internal::ctrl; + + public: + using table = Table; + static constexpr bool is_const = best::is_const
; + + using key_type = table::key_type; + using value_type = table::value_type; + using query_type = Query; + + using kref = table::key_cref; + using vref = best::select; + + static constexpr bool CanInsert = !best::same; + + /// # `table_entry::table_entry()` + /// + /// Creates a new vacant entry which cannot be inserted with. Attempting to + /// insert + constexpr table_entry() requires best::constructible + : idx_(-1) {} + + /// # `table_entry::is_vacant()`, `table_entry::is_occupied()` + /// + /// Whether this entry is currently occupied by a pair or not. + bool is_vacant() const { return best::to_signed(idx_) < 0; } + bool is_occupied() const { return !is_vacant(); } + + /// # `table_entry::pair()`, `table_entry::key()`, `table_entry::value()` + /// + /// If the entry is occupied, returns the key/value for the entry. + best::option key() const; + best::option value() const; + best::option> pair() const; + + /// # `table_entry::value_or()` + /// + /// Forwards to `value().value_or()`, which comes off as very silly otherwise. + template + decltype(auto) value_or(Args&&... args) const { + return value().value_or(BEST_FWD(args)...); + } + + /// # `table_entry::operator*`, `table_entry::operator->` + /// + /// Allow access to the value. Panic if this entry is vacant. + decltype(auto) operator*() const { + check_occupied(); + if constexpr (best::is_void) { + return *key(); + } else { + return *value(); + } + } + auto* operator->() const { + check_occupied(); + if constexpr (best::is_void) { + return key().operator->(); + } else { + return value().operator->(); + } + } + explicit operator bool() const { return is_occupied(); } + + /// # `table_entry::insert()`, `table_entry::or_insert()` + /// + /// Inserts a new value, constructing it in-place using the given arguments. + /// `insert()` does so unconditionally, destroying an existing value in the + /// process. `or_insert()` only does so if this entry is vacant. + /// + /// Returns a reference to the inserted value. + template + vref insert(Args&&... args) requires CanInsert; + template + vref or_insert(Args&&... args) requires CanInsert; + + /// # `option::or_insert([] { ... })` + /// + /// Like `best::option::value_or([] { ... })`. + template + vref or_insert(best::callable auto&& or_else) requires CanInsert + ; + + operator best::table_entry
() const { + return best::table_entry
(table_, idx_); + } + + /// # `table_entry::remove()`, `table_entry::erase()` + /// + /// Removes this entry, if it was occupied. `erase` is like `remove`, but it + /// does not return a value. + best::option> remove() &&; + void erase() &&; + + private: + friend table; + template + friend class best::table_entry; + template + friend class best::table_internal::iter_impl; + + explicit table_entry(table* table, size_t idx, table::hash_value hash, + best::object query) requires CanInsert + : table_(table), idx_(idx), insert_info_{hash, BEST_MOVE(query)} {} + + explicit table_entry(table* table, size_t idx) requires (!CanInsert) + : table_(table), idx_(idx) {} + + template + requires (i < 2) + friend decltype(auto) get(const table_entry& entry, + best::location loc = best::here) { + entry.check_occupied(loc); + if constexpr (i == 0) { + return entry.key().value(best::unsafe{"checked above"}); + } else { + return entry.value().value(best::unsafe{"checked above"}); + } + } + + void check_occupied(best::location loc = best::here) const { + if (best::unlikely(is_vacant())) { + if constexpr (CanInsert) { + best::wtf({"accessed a vacant table::entry with query ({:?})", loc}, + best::make_formattable(insert_info_.query.or_empty())); + } + best::wtf({"accessed a vacant table::entry", loc}); + } + } + + best::row move_pair(); + void destroy_pair(); + + static constexpr size_t Full = ~(best::max_of >> 1); + + table* table_; + size_t idx_; // Negative if not present in the table. If an insertion + // position is known, it is ~ that index. If not, because the + // table is at-capacity, this will be Full. + + // Fields necessary to execute an insertion. + struct insert_info { + table::hash_value hash; + best::object query; + }; + [[no_unique_address]] best::select + insert_info_; +}; +} // namespace best + +/* ////////////////////////////////////////////////////////////////////////// *\ + * ////////////////// !!! IMPLEMENTATION DETAILS BELOW !!! ////////////////// * +\* ////////////////////////////////////////////////////////////////////////// */ + +// Enable structured bindings. +namespace std { +template +struct tuple_size<::best::table_entry> { + static constexpr size_t value = 2; +}; +template +struct tuple_element<0, ::best::table_entry> { + using type = ::best::table_entry::kref; +}; +template +struct tuple_element<1, ::best::table_entry> { + using type = ::best::table_entry::vref; +}; +} // namespace std + +namespace best { +template +template +auto table::operator[](Q&& query) const -> const_entry + requires best::hash_identity +{ + auto hash = this->hash(query); + return const_entry(this, search(hash, query).value_or(-1)); +} + +template +template +auto table::operator[](Q&& query) -> inserter + requires best::hash_identity +{ + auto hash = this->hash(query); + auto found = search(hash, query); + if (!found) { + found = raw_.vacant(hash.h_).map([&](auto i) { return ~i; }); + } + + return inserter(this, found.value_or(inserter::Full), hash, + best::object(best::in_place, BEST_FWD(query))); +} + +template +auto table_entry::key() const -> best::option { + if (is_vacant()) { return best::none; } + + return table_->raw_.key(idx_).deref(); +} + +template +auto table_entry::value() const -> best::option { + if (is_vacant()) { return best::none; } + + if constexpr (best::is_void) { + return best::option(best::in_place); + } else { + return table_->raw_.value(idx_).deref(); + } +} + +template +auto table_entry::pair() const -> best::option> { + if (is_vacant()) { return best::none; } + + using row = best::row; + if constexpr (best::is_void) { + return best::option(row{table_->raw_.key(idx_).deref(), {}}); + } else { + return best::option(row{ + table_->raw_.key(idx_).deref(), + table_->raw_.value(idx_).deref(), + }); + } +} + +template +auto table_entry::move_pair() -> best::row { + using row = best::row; + if constexpr (best::is_void) { + return row{BEST_MOVE(table_->raw_.key(idx_).deref()), {}}; + } else { + return row{ + BEST_MOVE(table_->raw_.key(idx_).deref()), + BEST_MOVE(table_->raw_.value(idx_).deref()), + }; + } +} + +template +void table_entry::destroy_pair() { + table_->raw_.key(idx_).destroy(); + if constexpr (!best::is_void) { + table_->raw_.value(idx_).destroy(); + } +} + +template +template +auto table_entry::insert(Args&&... args) -> vref requires CanInsert +{ + best::debug_must(table_ != nullptr, + "called insert() on default-constructed best::table_entry"); + + if (is_vacant()) { return BEST_MOVE(*this).or_insert(BEST_FWD(args)...); } + + table_->raw_.key(idx_).assign(BEST_MOVE(*insert_info_.query)); + + auto value = table_->raw_.value(idx_); + value.assign(BEST_FWD(args)...); + return *value; +} + +template +template +auto table_entry::or_insert(Args&&... args) -> vref requires CanInsert +{ + best::debug_must(table_ != nullptr, + "called insert() on default-constructed best::table_entry"); + + bool vacant = is_vacant(); + if (vacant) { + if (idx_ == Full) { + table_->rehash(); + insert_info_.hash = table_->hash(*insert_info_.query); + idx_ = table_->raw_.vacant_unchecked(insert_info_.hash.h_); + } else { + idx_ = ~idx_; + } + + table_->raw_.set_ctrl(idx_, insert_info_.hash.h_.h2); + table_->raw_.key(idx_).construct(*BEST_MOVE(insert_info_.query)); + ++table_->size_; + } + + auto ptr = table_->raw_.value(idx_); + if (vacant) { ptr.construct(BEST_FWD(args)...); } + + return *ptr; +} + +template +auto table_entry::remove() && -> best::option< + best::row> { + best::debug_must(table_ != nullptr, + "called insert() on default-constructed best::table_entry"); + if (is_vacant()) { return best::none; } + + auto removed = move_pair(); + BEST_MOVE(*this).erase(); + return removed; +} + +template +void table_entry::erase() && { + if (is_vacant()) { return; }; + destroy_pair(); + + table_->size_--; + auto next = idx_; + auto prev = (idx_ - size_of)&(table_->raw_.cap().hard - 1); + + auto next_empty = group::load(table_->raw_.ptr(), next).match_empty(); + auto prev_empty = group::load(table_->raw_.ptr(), prev).match_empty(); + + bool was_never_full = + !next_empty.empty() && !prev_empty.empty() && + next_empty.trailing_zeros() + prev_empty.leading_zeros() < sizeof(group); + + table_->raw_.set_ctrl(idx_, was_never_full ? ctrl::Empty : ctrl::Tombstone); + table_->raw_.cap().soft += was_never_full; + + table_ = nullptr; + idx_ = -1; +} + +namespace table_internal { +template +class iter_impl final { + using table = Table; + + public: + best::option> next() { + while (cur_ != end_) { + auto cur = *cur_++; + if (cur.is_occupied()) { + --count_; + return best::table_entry
(table_, cur_ - start_ - 1); + } + } + + return best::none; + } + + best::size_hint size_hint() const { + return {.lower = count_, .upper = count_}; + } + size_t count() && { return count_; } + + private: + friend table; + template + friend class table_entry; + + explicit iter_impl(table* table) : table_(table), count_(table->size_) { + start_ = table_->raw_.ptr(); + cur_ = start_; + end_ = start_ + table_->raw_.cap().hard; + } + + table* table_; + const ctrl *start_, *cur_, *end_; + size_t count_; +}; +} // namespace table_internal + +template +table::table(const table& that) + requires best::copyable && best::copyable + : table(0, that.identity_, that.alloc_) { + *this = that; +} + +template +auto table::operator=(const table& that) + -> table& requires best::copyable && best::copyable +{ + raw_.destroy(); + raw_ = that.rehash_to(that.cap_, *this); + size_ = that.size_; + + return *this; +} + +template +table::table(table&& that) + : raw_(that.raw_), + size_(that.size_), + identity_(BEST_MOVE(that.identity_)), + alloc_(BEST_MOVE(that.alloc_)) { + that.raw_ = {}; + that.size_ = 0; +} + +template +auto table::operator=(table&& that) -> table& { + raw_.destroy(); + + size_ = that.size_; + raw_ = that.raw_; + identity_ = BEST_MOVE(that.identity_); + alloc_ = BEST_MOVE(that.alloc_); + + that.raw_ = {}; + that.size_ = 0; + return *this; +} + +template +void table::reserve(size_t n) { + auto new_size = size() + n; + if (new_size <= capacity()) { return; } + + table_internal::capacity new_cap(new_size); + auto new_array = rehash_to(new_cap, *this); + + free(); + raw_ = new_array; +} + +template +void table::rehash() { + if ( + raw_.cap().hard < sizeof(group) || + // See + // https://github.com/google/cwisstable/blob/main/cwisstable/internal/raw_table.h#L445. + size_ * uint64_t(32) > raw_.cap().hard * uint64_t(25)) { + reserve(1); + return; + } + + for (auto i : best::bounds{.count = raw_.cap().group_count()}) { + i *= sizeof(group); + group::load(raw_.ptr(), i).prepare_for_rehash().store(raw_.ptr(), i); + } + + for (size_t i = 0; i < raw_.cap().hard; ++i) { + if (!raw_.ctrl(i).is_tombstone()) { continue; } + + auto old_key = raw_.key(i); + auto old_value = raw_.value(i); + auto h = hash(*old_key); + + size_t new_i = raw_.vacant_unchecked(h.h_); + auto new_key = raw_.key(i); + auto new_value = raw_.value(i); + + // Verify if the old and new i fall within the same group wrt the hash. + // If they do, we don't need to move the object as it falls already in the + // best probe we can. + auto mask = raw_.cap().hard - 1; + size_t probe_offset = h.h_.h1 & mask; + auto probe_index = [probe_offset, mask](size_t i) { + return ((i - probe_offset) & mask) / sizeof(group); + }; + + if (best::likely(probe_index(i) == probe_index(new_i))) { + // Don't move. + raw_.set_ctrl(i, h.h_.h2); + continue; + } + + if (raw_.ctrl(new_i).is_empty()) { + // Relocate to new slot. + raw_.set_ctrl(new_i, h.h_.h2); + new_key.relo(old_key); + if constexpr (!best::is_void) { new_value.relo(old_value); } + raw_.set_ctrl(i, ctrl::Empty); + + continue; + } + + best::debug_must(raw_.ctrl(new_i).is_tombstone(), + "corrupt control byte: {:?}", raw_.ctrl(new_i)); + raw_.set_ctrl(new_i, h.h_.h2); + + // TODO: Swap. + best::object tmp{best::in_place, BEST_MOVE(*old_key)}; + old_key.move_assign(new_key); + new_key.move_assign(tmp.as_ptr()); + if constexpr (!best::is_void) { + best::object tmp{best::in_place, BEST_MOVE(*old_value)}; + old_value.move_assign(new_value); + new_value.move_assign(tmp.as_ptr()); + } + + --i; // Try again. + } + + raw_.cap().reset_soft(); + raw_.cap().soft -= size_; +} + +template +template +table_internal::array table::rehash_to( + table_internal::capacity new_cap, const table& target) const { + table_internal::array new_raw{ + target.alloc_.alloc(new_cap.layout()), + new_cap, + }; + + for (auto entry : iter()) { + size_t idx_rhs = entry.idx_; + auto key_rhs = raw_.key(idx_rhs); + auto value_rhs = raw_.value(idx_rhs); + + auto hash = target.hash(*key_rhs, new_raw.ptr()); + size_t idx_lhs = new_raw.vacant_unchecked(hash.h_); + + auto key_lhs = new_raw.key(idx_lhs); + auto value_lhs = new_raw.value(idx_lhs); + + if constexpr (relo) { + key_lhs.relo(key_rhs); + } else { + key_lhs.copy(key_rhs); + } + if constexpr (!best::is_void) { + if constexpr (relo) { + value_lhs.relo(value_rhs); + } else { + value_lhs.copy(value_rhs); + } + } + + new_raw.set_ctrl(idx_lhs, hash.h_.h2); + } + return new_raw; +} + +template +BEST_INLINE_ALWAYS best::option table::search( + hash_value hash, const auto& query) const { + if (raw_.ptr() == nullptr) { return best::none; } + + return best::table_internal::probe( // + *this, raw_.ptr(), hash.h_, raw_.cap().hard, + [&](size_t base, group g) -> best::option> { + auto match = g.match(hash.h_.h2); + while (auto next = match.next()) { + size_t idx = (base + *next) & (raw_.cap().hard - 1); + if (best::likely(identity_.equal(*raw_.key(idx), query))) { + return {best::in_place, best::option(idx)}; + } + } + + if (best::likely(!g.match_empty().empty())) { + return {best::in_place, best::none}; + } + + return best::none; + }); +} + +template +best::strbuf table::debug(bool include_entries) const { + best::strbuf out = + best::format("type: {}\narray: {:p}, {}/{}/{}\n", + best::type_names::of
.path_with_params(), raw_.ptr(), + size_, raw_.cap().soft, raw_.cap().hard); + + if (raw_.ptr() == nullptr) { return out; } + + out.push("ctrl:"); + for (auto i : best::bounds{.count = raw_.cap().group_count()}) { + i *= sizeof(group); + best::format(out, "\n {:p}: {:?}", raw_.ptr() + i, + group::load(raw_.ptr(), i)); + } + out.push(" (mirrored)\n"); + + if (!include_entries) { return out; } + + out.push("keys:\n"); + for (auto i : best::bounds{.count = raw_.cap().hard}) { + auto key = raw_.key(i); + if (raw_.ctrl(i).is_vacant()) { + best::format(out, " {:p}: ---\n", key); + continue; + } + + best::format(out, " {:p}: {:?}\n", key, best::make_formattable(*key)); + } + + if constexpr (best::is_empty) { return out; } + out.push("values:\n"); + for (auto i : best::bounds{.count = raw_.cap().hard}) { + auto value = raw_.value(i); + if (raw_.ctrl(i).is_vacant()) { + best::format(out, " {:p}: ---\n", value); + continue; + } + + best::format(out, " {:p}: {:?}\n", value, best::make_formattable(*value)); + } + + return out; +} +} // namespace best + +#endif // BEST_CONTAINER_TABLE_H_ \ No newline at end of file diff --git a/best/container/table_test.cc b/best/container/table_test.cc new file mode 100644 index 0000000..e18dec7 --- /dev/null +++ b/best/container/table_test.cc @@ -0,0 +1,229 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://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. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#include "best/container/table.h" + +#include "best/container/internal/table.h" +#include "best/test/fodder.h" +#include "best/test/test.h" + +namespace best::table_test { +using ::best::table_internal::mirror; +using ::best_fodder::MoveOnly; + +// A maximally swisstable-hostile hash implementation. +struct TerribleHash final { + using output = uint64_t; + constexpr void write(uint64_t x) { h += x; } + constexpr void write(best::span bytes) {} + constexpr output finish() const { return h << 7; } + + uint64_t h = 0; +}; + +best::test Mirror = [](auto& t) { + t.expect_eq(mirror(0, 4, 4), 4); + t.expect_eq(mirror(1, 4, 4), 5); + t.expect_eq(mirror(2, 4, 4), 6); + t.expect_eq(mirror(3, 4, 4), 7); + + t.expect_eq(mirror(0, 4, 8), 8); + t.expect_eq(mirror(1, 4, 8), 9); + t.expect_eq(mirror(2, 4, 8), 10); + t.expect_eq(mirror(3, 4, 8), 11); + + t.expect_eq(mirror(4, 4, 8), 4); + t.expect_eq(mirror(5, 4, 8), 5); + t.expect_eq(mirror(6, 4, 8), 6); + t.expect_eq(mirror(7, 4, 8), 7); +}; + +best::test SmallMap = [](auto& t) { + best::table m{ + {1, 2}, + {3, 4}, + {5, 6}, + {5, 7}, + }; + best::eprintln("{}", m.debug()); + + t.expect_eq(m[1].pair(), best::row(1, 2)); + t.expect_eq(m[3].pair(), best::row(3, 4)); + t.expect_eq(m[5].pair(), best::row(5, 7)); + + auto entries = + m.iter().map([](auto e) { return e.pair()->copied(); }).to_vec(); + entries->sort([](const auto& e) { return e.first(); }); + + t.expect_eq(entries, best::vec>{{1, 2}, {3, 4}, {5, 7}}); +}; + +best::test SmallSet = [](auto& t) { + best::table m{1, 2, 3, 4, 5, 6}; + best::eprintln("{}", m.debug()); + + t.expect(m[1]); + t.expect(m[2]); + t.expect(m[3]); + t.expect(m[4]); + t.expect(m[5]); + t.expect(m[6]); + t.expect(!m[7]); + + auto entries = m.iter().map([](auto e) { return *e; }).to_vec(); + entries->sort(); + + t.expect_eq(entries, best::vec{1, 2, 3, 4, 5, 6}); +}; + +best::test HammerInts = [](auto& t) { + best::table m; + + for (int i : best::bounds{.count = 1000}) { m[i].insert(-i); } + best::eprintln("{}", m.debug(false)); + for (int i : best::bounds{.count = 1000}) { + t.expect_eq(m[i].pair(), best::row(i, -i)); + } + for (auto i : best::bounds{.count = 1000 / 2}) { + t.expect_eq(m[2 * i].remove(), best::row(2 * i, -2 * i)); + } + best::eprintln("{}", m.debug(false)); + for (auto i : best::bounds{.count = 1000}) { + switch (i % 2) { + case 0: t.expect_eq(m[i].pair(), best::none); break; + case 1: t.expect_eq(m[i].pair(), best::row(i, -i)); break; + } + } + for (auto i : best::bounds{.count = 1000}) { + int v = m[i].or_insert(1 - i); + t.expect_eq(v, i % 2 == 0 ? 1 - i : -i); + } + best::eprintln("{}", m.debug(false)); + for (auto e : m) { + auto k = *e.key(); + switch (k % 2) { + case 0: t.expect_eq(*e, 1 - k); break; + case 1: + t.expect_eq(*e, -k); + t.expect_eq(BEST_MOVE(e).remove(), best::row(k, -k)); + break; + } + } + best::eprintln("{}", m.debug(false)); + for (auto e : m) { + auto k = *e.key(); + t.expect_eq(k % 2, 0); + t.expect_eq(*e, 1 - k); + } +}; + +best::test Terrible = [](auto& t) { + best::table::with_hash m; + + for (int i : best::bounds{.count = 100}) { m[i * 2].insert(); } + best::eprintln("{}", m.debug()); + + for (int i : best::bounds{.count = 200}) { + t.expect_eq(m[i].is_occupied(), i % 2 == 0); + } + + for (int i : best::bounds{.count = 50}) { m[i * 4 + 2].remove(); } + best::eprintln("{}", m.debug()); + + for (int i : best::bounds{.count = 200}) { + t.expect_eq(m[i].is_occupied(), i % 4 == 0); + } +}; + +best::test Drain = [](auto& t) { + best::table::with_hash set( + best::bounds{.count = 100}.iter()); + best::eprintln("{}", set.debug()); + for (auto e : set) { BEST_MOVE(e).remove(); } + best::eprintln("{}", set.debug()); + + for (int i : best::bounds{.start = 100, .count = 100}) { set[i].insert(); } + best::eprintln("{}", set.debug()); + + auto entries = set.iter().map([](auto e) { return *e; }).to_vec(); + entries->sort(); + t.expect_eq(entries, + best::vec(best::bounds{.start = 100, .count = 100}.iter())); +}; + +best::test StringSet = [](auto& t) { + best::table movies = { + best::str("The Phonecian Scheme"), + best::str("Beau Is Afraid"), + best::str("Twelve Angry Men"), + best::str("Final Destination"), + }; + + t.expect(movies["The Phonecian Scheme"]); + t.expect(movies["Beau Is Afraid"]); + t.expect(movies["Twelve Angry Men"]); + t.expect(movies["Final Destination"]); + + t.expect(!movies["Spaceballs"]); + movies[best::str("Spaceballs")].insert(); + t.expect(movies["Spaceballs"]); +}; + +best::test HammerStrings = [](auto& t) { + best::table strs; + + best::strbuf k; + for (auto _ : best::bounds{.count = 100}) { + strs[k].insert(); + k.push('#'); + } + best::eprintln("{}", strs.debug()); + for (auto i : best::bounds{.count = 100 / 2}) { + strs[k[{.count = 2 * i}]].remove(); + } + best::eprintln("{}", strs.debug()); + for (auto i : best::bounds{.count = 100}) { + t.expect_eq(strs[k[{.count = i}]].is_occupied(), i % 2 != 0); + } +}; + +best::test HammerIntVec = [](auto& t) { + best::table> vecs; + + best::vec k; + for (auto i : best::bounds{.count = 100}) { + vecs[k].insert(); + k.push(i); + } + best::eprintln("{}", vecs.debug()); + for (auto i : best::bounds{.count = 100 / 2}) { + vecs[k[{.count = 2 * i}]].remove(); + } + best::eprintln("{}", vecs.debug()); + for (auto i : best::bounds{.count = 100}) { + t.expect_eq(vecs[k[{.count = i}]].is_occupied(), i % 2 != 0); + } +}; + +best::test IntOption = [](auto& t) { + best::table, int> m = { + + }; +}; +} // namespace best::table_test diff --git a/best/hash/BUILD b/best/hash/BUILD new file mode 100644 index 0000000..1047c25 --- /dev/null +++ b/best/hash/BUILD @@ -0,0 +1,39 @@ +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "hash", + hdrs = [ + "hash.h", + "internal/hash.h", + ], + deps = [ + "//best/base:fwd", + "//best/base:ord", + "//best/meta:init", + "//best/meta/traits:types", + ], +) + +cc_library( + name = "impls", + hdrs = ["impls.h"], + deps = [ + ":hash", + "//best/memory:span", + "//best/meta:reflect", + "//best/text:str", + ], +) + + +cc_library( + name = "fxhash", + hdrs = ["fxhash.h"], + deps = [ + ":hash", + "//best/math:bit", + "//best/math:int", + "//best/math:overflow", + "//best/memory:span", + ], +) \ No newline at end of file diff --git a/best/hash/fxhash.h b/best/hash/fxhash.h new file mode 100644 index 0000000..aa85309 --- /dev/null +++ b/best/hash/fxhash.h @@ -0,0 +1,128 @@ + +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_HASH_FXHASH_H_ +#define BEST_HASH_FXHASH_H_ + +#include + +#include "best/base/fwd.h" +#include "best/math/bit.h" +#include "best/math/int.h" +#include "best/math/overflow.h" +#include "best/memory/span.h" + +namespace best { +/// # `best::fxhash` +/// +/// An implementation of the "rustc hash", a variant of fxhash, a multiplicative +/// hash originally developed for FireFox. +/// +/// See https://github.com/rust-lang/rustc-hash. +class fxhash final { + public: + using output = uint64_t; + + constexpr fxhash() : state_(Seed) {} + explicit constexpr fxhash(uint64_t seed) : state_(seed) {} + + template + constexpr void write(Int value) { + write(static_cast(value)); + } + + constexpr void write(uint64_t n) { state_ = (state_ + n) * K; } + + constexpr void write(best::span bytes) { + auto n = bytes.size(); + uint64_t x0 = Y0, x1 = Y1; // Digits of pi. + + auto p = bytes.data().raw(); + if (n <= 16) { + if (n >= 8) { + x0 ^= load64(p); + x1 ^= load64(p + (n - 8)); + } else if (n >= 4) { + x0 ^= load32(p); + x1 ^= load32(p + (n - 4)); + } else if (n > 0) { + x0 ^= p[0]; + x1 ^= p[n / 2]; + x1 ^= uint64_t(p[n - 1]) << 8; + } + } else { + auto end = p + (n - 16); + while (p < end) { + auto x = load64(p); + auto y = load64(p + 8); + auto t = best::mul(x0 ^ x, Y2 ^ y).mix(); + x0 = x1; + x1 = t; + p += 16; + } + + x0 ^= load64(end); + x1 ^= load64(end + 8); + } + + write(best::mul(x0, x1).mix() ^ n); + } + + constexpr output finish() const { return best::rotate_left(state_, 26); } + + private: + // A non-deterministic seed, which will vary per-process due to ASLR, but will + // at best vary per-build otherwise. + static const uint64_t Seed; + + static constexpr uint64_t K = 0xf1357aea2e62a9c5; + + // Digits of pi. + static constexpr uint64_t Y0 = 0x243f6a8885a308d3; + static constexpr uint64_t Y1 = 0x13198a2e03707344; + static constexpr uint64_t Y2 = 0xa4093822299f31d0; + + constexpr uint32_t load32(const char* p) { + uint32_t value = 0; + if (std::is_constant_evaluated()) { + for (auto i = 0; i < 4; i++) { value |= uint32_t(p[i]) << (i * 8); } + } else { + memcpy(&value, p, 4); + } + return value; + } + + constexpr uint64_t load64(const char* p) { + uint64_t value = 0; + if (std::is_constant_evaluated()) { + for (auto i = 0; i < 8; i++) { value |= uint64_t(p[i]) << (i * 8); } + } else { + memcpy(&value, p, 8); + } + return value; + } + + uint64_t state_ = 0; +}; + +inline const uint64_t fxhash::Seed = reinterpret_cast(&Seed); +} // namespace best + +#endif // BEST_HASH_FXHASH_H_ \ No newline at end of file diff --git a/best/hash/hash.h b/best/hash/hash.h new file mode 100644 index 0000000..864ee13 --- /dev/null +++ b/best/hash/hash.h @@ -0,0 +1,178 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_HASH_HASH_H_ +#define BEST_HASH_HASH_H_ + +#include "best/base/fwd.h" +#include "best/hash/internal/hash.h" +#include "best/meta/init.h" +#include "best/meta/traits/types.h" + +//! General hashing utilities for use with hash tables. +//! +//! A `best::hasher` represents an in-process hash operation, which is passed +//! to hashing functions for writing data to. These "hashing functions" are +//! FTADLEs of the form +//! +//! template +//! friend void BestHash(best::hasher h, const T& value); +//! +//! where `T` is the type being made hashable. This function is automatically +//! provided for all equality-comparable types in best. Reflectable types are +//! automatically made hashable by hashing their fields. +//! +//! Implementing `BestHash()` only requires including this header; other headers +//! provide the rest of the machinery. + +namespace best { +/// # `best::hash_state` +/// +/// An implementation of a hasher. Such types should not be used directly; +/// instead, use `best::hasher`. +/// +/// A hash state is a type that provides a member alias `output`, +/// a member function `write(best::span)`, and a member function +/// `finish()`. +/// +/// A hash state may provide further overloads of `write()` for optimizing +/// writing values of certain types, such as integers. +template +concept hash_state = + requires(H& state, best::span> data) { + typename H::output; + + { state.write(data) }; + { state.finish() } -> best::same; + }; + +/// # `best::hash` +/// +/// The output of some `best::hasher`. +template +using hash = State::output; + +/// # `best::transient` +/// +/// A field tag which marks a field as "transient" and thus not relevant for +/// hashing. Can be used to exclude fields from being hashed when using +/// reflection-generated hash functions. +struct transient_t final {}; +inline constexpr best::transient_t transient{}; + +/// # `best::hasher` +/// +/// A hasher calculates hashes of hashable values. Values should be written +/// using `write()`, and the final state +template +class hasher final { + public: + /// # `hasher::hasher(...)` + /// + /// Constructs a new hasher with the given arguments, which are forwarded + /// to `H`. + template + explicit constexpr hasher(Args&&... args) + requires best::constructible + : state_(BEST_FWD(args)...) {} + + constexpr hasher() = default; + constexpr hasher(const hasher&) = default; + constexpr hasher& operator=(const hasher&) = default; + constexpr hasher(const hasher&&) = default; + constexpr hasher& operator=(const hasher&&) = default; + + /// # `hasher::write(...)` + /// + /// Advances the state of the hasher by writing hashable values to it. + template + hasher& write(const T& value) requires ( + requires { best::lie.write(value); } || + requires { BestHash(*this, value); }) + { + if constexpr (requires { state_.write(value); }) { + state_.write(value); + } else { + BestHash(*this, value); + } + + return *this; + } + template + hasher& write(const T&... value) + requires (sizeof...(T) != 1 && requires { (write(value), ...); }) + { + (write(value), ...); + return *this; + } + + /// # `hasher::finish()` + /// + /// Completes a hashing operation, and returns the result. + best::hash finish() { return state_.finish(); } + + private: + H state_; +}; + +/// # `best::hashable` +/// +/// A type which can be hashed, i.e, which provides BestHash. +template +concept hashable = + best::is_void || + requires( + const T& v, + best::hasher>>& + h) { h.write(v); }; + +/// # `best::hash_of` +/// +/// Returns the hash of the given elements as if they were passed to +/// a `best::hasher` in that order. The hash state used must be +/// default-constructible. +template +best::hash hash_of(const Args&... args) { + return best::hasher().write(args...).finish(); +} + +/// # `best::hash_identity` +/// +/// A hash identity is a generalization of equality suitable for use with a +/// hash table. It specifies an equality function between a key type and a +/// query type, and a way to hash them, such that if two values are equal, then +/// their hashes are also equal. +/// +/// A specific type may be the hash identity for many possible combinations of +/// K and Q. The only requirement is that satisfaction of this concept must be +/// an equivalence relation on types, and the equality function must also be an +/// equivalence relation in the expected way. +template +concept hash_identity = requires( + const Id& id, const K& key, const Q& query, + best::hasher>>& + h) { + { id.equal(key, query) } -> best::same; + { id.hash(h, key) }; + { id.hash(h, query) }; +}; + +} // namespace best + +#endif // BEST_HASH_HASH_H_ \ No newline at end of file diff --git a/best/hash/hash_test.cc b/best/hash/hash_test.cc new file mode 100644 index 0000000..e69de29 diff --git a/best/hash/impls.h b/best/hash/impls.h new file mode 100644 index 0000000..c8744a0 --- /dev/null +++ b/best/hash/impls.h @@ -0,0 +1,106 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_HASH_IMPLS_H_ +#define BEST_HASH_IMPLS_H_ + +#include "best/hash/hash.h" +#include "best/math/int.h" +#include "best/memory/span.h" +#include "best/meta/reflect.h" +#include "best/meta/traits/arrays.h" +#include "best/meta/traits/enums.h" + +//! Implementations of `BestHash` for built-in types. + +namespace best { +/// # `best::default_identity` +/// +/// The default identity, which uses `operator==` and ordinary hashing. +struct default_identity final { + template Q> + constexpr bool equal(const K& key, const Q& query) const { + return key == query; + } + + template + constexpr void hash(best::hasher& h, const Q& query) const { + if constexpr (best::is_string) { + // Need to deal with nul-terminated strings here. + + using code = best::code>; + using S = best::as_auto; + if constexpr (best::same) { + h.write(best::span::from_nul(query)); + return; + } else if constexpr (best::is_sized_array_of) { + constexpr auto n = *best::static_size - 1; + h.write(best::span(&query[0], n)); + return; + } + } + + h.write(query); + } +}; + +template +constexpr void BestHash(best::hasher& h, bool value) { + h.write((char)value); +} + +template +constexpr void BestHash(best::hasher& h, Int value) { + h.write(best::span(&value, 1).as_bytes()); +} + +template +constexpr void BestHash(best::hasher& h, + const best::contiguous auto& value) + requires requires { h.write(best::span(value)[0]); } +{ + best::span sp = value; + if constexpr (best::bytes_internal::byte_comparable< + best::data_type>) { + h.write(sp.size(), sp.as_bytes()); + return; + } + + h.write(sp.size()); + for (const auto& v : sp) { h.write(v); } +} + +template +constexpr void BestHash(best::hasher& h, + const best::is_reflected_struct auto& value) { + best::reflect.each([&](auto field) { + if (!field.template tags().is_empty()) { return; } + + h.write(value->*field); + }); +} + +template +constexpr void BestHash(best::hasher& h, + const best::is_enum auto& value) { + h.write(best::to_underlying(value)); +} +} // namespace best + +#endif // BEST_HASH_IMPLS_H_ \ No newline at end of file diff --git a/best/hash/internal/hash.h b/best/hash/internal/hash.h new file mode 100644 index 0000000..b0eeecd --- /dev/null +++ b/best/hash/internal/hash.h @@ -0,0 +1,36 @@ +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_HASH_INTERNAL_HASH_H_ +#define BEST_HASH_INTERNAL_HASH_H_ + +#include "best/base/fwd.h" + +namespace best::hash_internal { +// Dummy hash state for best::hashable. +template +struct dummy final { + using output = uint64_t; + + void write(best::span); + output finish(); +}; +} // namespace best::hash_internal + +#endif // BEST_HASH_INTERNAL_HASH_H_ \ No newline at end of file diff --git a/best/log/wtf.h b/best/log/wtf.h index 89e3c76..6b61f43 100644 --- a/best/log/wtf.h +++ b/best/log/wtf.h @@ -20,6 +20,7 @@ #ifndef BEST_LOG_WTF_H_ #define BEST_LOG_WTF_H_ +#include "best/base/port.h" #include "best/text/format.h" #include "best/text/strbuf.h" @@ -47,6 +48,34 @@ template crash_internal::die(templ.where(), write); } + +/// # `best::must()` +/// +/// Crashes if `cond` is false. `cond` may be any type convertible to `bool`, or +/// a callback that, when called with no arguments, produces a type convertible +/// to `bool`. +template +void must(Truthy&& cond, best::format_template templ, + const Args&... args) { + if constexpr (requires { static_cast(BEST_FWD(cond)()); }) { + if (static_cast(BEST_FWD(cond)())) { return; } + } else if (static_cast(BEST_FWD(cond))) { + return; + } + + best::wtf(templ, args...); +} + +/// # `best::debug_must()` +/// +/// Identical in all ways to `best::must()` except that it only checks the +/// condition if `best::is_debug()` returns true. +template +void debug_must(Truthy&& cond, best::format_template templ, + const Args&... args) { + if (best::is_debug()) { best::must(BEST_FWD(cond), templ, args...); } +} + } // namespace best #endif // BEST_LOG_WTF_H_ diff --git a/best/math/bit.h b/best/math/bit.h index 0265da6..c2dd7fb 100644 --- a/best/math/bit.h +++ b/best/math/bit.h @@ -181,6 +181,15 @@ BEST_INLINE_ALWAYS constexpr Int next_pow2(Int x) { return (best::next_pow2_minus1(x) + best::overflow(1)).strict(); } +/// # `best::round_up_to_pow2()` +/// +/// Rounds `x` to the next multiple of `pow2`. `pow2` must be a power of 2. +template +BEST_INLINE_ALWAYS constexpr Int round_up_to_pow2(Int x, Int pow2) { + auto mask = pow2 - 1; + return (x + mask) & ~mask; +} + /// # `best::bits_for()` /// /// Computes the number of bits needed to store every value from `0` to `x`, diff --git a/best/math/overflow.h b/best/math/overflow.h index d7633ac..ddcb996 100644 --- a/best/math/overflow.h +++ b/best/math/overflow.h @@ -34,6 +34,48 @@ namespace best { template struct overflow; +/// # `best::mul` +/// +/// Constructing a value of this type performs a full-width multiplication. +/// +/// Multiplication without overflow produces a double-width value, a high and +/// a low part. If the high part is zero (or, for signed integers, all ones), +/// the product did not overflow. +template +struct mul final { + constexpr mul(Int a, Int b) { + // There isn't an especially good intrinsic for this. For now, we just + // perform the operation in i128 and chop it back down. + if constexpr (best::is_signed) { + auto c = __int128(a) * __int128(b); + lo = (Int)(c); + hi = (Int)(c >> best::bits_of); + } else { + auto c = (unsigned __int128)(a) * (unsigned __int128)(b); + lo = (Int)(c); + hi = (Int)(c >> best::bits_of); + } + } + + Int lo, hi; + + // # `mul::overflowed()` + // + // Returns whether this operation overflowed. + constexpr bool overflowed() const { + return hi == 0 || (hi == -1 && best::is_signed); + } + + // # `mul::mix()` + // + // Mixes the high and low results of this multiplication. Useful for writing + // non-cryptographic hashes. + constexpr Int mix() const { return lo ^ hi; } +}; + +template +mul(A, B) -> mul>; + /// # `best::div_t` /// /// The result of calling `best::div`: a quotient and a remainder. diff --git a/best/memory/BUILD b/best/memory/BUILD index 6773142..adb3bbc 100644 --- a/best/memory/BUILD +++ b/best/memory/BUILD @@ -36,6 +36,7 @@ cc_library( "internal/bytes.h", ], deps = [ + "//best/hash", "//best/container:option", "//best/container:result", "//best/iter", @@ -59,7 +60,9 @@ cc_library( "internal/layout.h" ], deps = [ + "//best/math:bit", "//best/base:unsafe", + "//best/hash", "//best/math:overflow", "//best/meta:ops", "//best/meta:tlist", diff --git a/best/memory/internal/layout.h b/best/memory/internal/layout.h index 74d54f9..af50ee9 100644 --- a/best/memory/internal/layout.h +++ b/best/memory/internal/layout.h @@ -23,6 +23,7 @@ #include #include +#include "best/math/bit.h" #include "best/meta/tlist.h" #include "best/meta/traits/ptrs.h" @@ -42,12 +43,7 @@ using to_object = best::devoid< /// /// This is guaranteed to be a power of 2. template -inline constexpr size_t align_of = [] { - size_t align = 1; - best::types...>.each( - [&] { align = (align > alignof(T) ? align : alignof(T)); }); - return align; -}(); +inline constexpr size_t align_of = best::max(size_t{1}, alignof(to_object)...); /// Computes the size of a struct with the given member types. /// @@ -57,21 +53,13 @@ template inline constexpr size_t size_of = [] { if (sizeof...(Types) == 0) { return size_t{1}; } - size_t size = 0, align = 1; - - auto align_to = [&size](size_t align) { - auto remainder = size % align; - if (remainder != 0) { size += align - remainder; } - }; - + size_t size = 0; best::types...>.each([&] { - align_to(alignof(T)); - align = (align > alignof(T) ? align : alignof(T)); + size = best::round_up_to_pow2(size, alignof(T)); size += sizeof(T); }); - align_to(align); - return size; + return best::round_up_to_pow2(size, align_of); }(); /// Computes the size of a union with the given member types. @@ -81,21 +69,10 @@ inline constexpr size_t size_of = [] { /// a size of 1. template inline constexpr size_t size_of_union = [] { - if (sizeof...(Types) == 0) { return size_t{1}; } - - size_t size = 0, align = 1; - auto align_to = [&](size_t align) { - auto remainder = size % align; - if (remainder != 0) { size += align - remainder; } - }; - - best::types...>.each([&] { - align = (align > alignof(T) ? align : alignof(T)); - size = (size > sizeof(T) ? size : sizeof(T)); - }); + if constexpr (sizeof...(Types) == 0) { return size_t{1}; } - align_to(align); - return size; + return best::round_up_to_pow2(best::max(0, sizeof(to_object)...), + align_of); }(); } // namespace best::layout_internal diff --git a/best/memory/layout.h b/best/memory/layout.h index 7f21d2a..e8ea30a 100644 --- a/best/memory/layout.h +++ b/best/memory/layout.h @@ -24,6 +24,8 @@ #include #include "best/base/unsafe.h" +#include "best/hash/hash.h" +#include "best/math/bit.h" #include "best/math/int.h" #include "best/math/overflow.h" #include "best/memory/internal/layout.h" @@ -126,6 +128,48 @@ class layout final { layout_internal::align_of); } + /// # `layout::of_struct()` + /// + /// Appends several layouts together, as if by `layout::of_struct()`. + static constexpr layout of_struct(std::initializer_list layouts) { + size_t size = 0, align = 1; + for (auto layout : layouts) { + size = best::round_up_to_pow2(size, layout.align()); + size += layout.size(); + align = best::max(align, layout.align()); + } + size = best::round_up_to_pow2(size, align); + return layout(unsafe("same impl as of_struct()"), size, align); + } + + /// # `layout::of_struct()` + /// + /// Overlays several layouts together, as if by `layout::of_union()`. + static constexpr layout of_union(std::initializer_list layouts) { + size_t size = 0, align = 1; + for (auto layout : layouts) { + size = best::max(size, layout.size()); + align = best::max(align, layout.align()); + } + size = best::round_up_to_pow2(size, align); + return layout(unsafe("same impl as of_struct()"), size, align); + } + + /// # `layout::repeat()` + /// + /// Appends several copies of this layout together, as if by + /// `layout::array()`. + constexpr layout repeat(size_t n) const { + auto [sz, of] = best::overflow(size()) * n; + if (of || sz > best::max_of) { + best::crash_internal::crash( + "attempted to allocate more than max_of/2 bytes"); + } + + return layout(unsafe("manifest from the bounds check above and align()"), + sz, align()); + } + /// # `layout::size()`. /// /// The size, in bytes. This is always divisible by `align`. @@ -152,6 +196,11 @@ class layout final { rec.field("align", ly.align()); } + template + constexpr friend void BestHash(best::hasher& h, const layout& value) { + h.write(value.size(), value.align()); + } + public: size_t BEST_LAYOUT_SIZE_ = 1, BEST_LAYOUT_ALIGN_ = 1; // Public for structural-ness. diff --git a/best/memory/ptr.h b/best/memory/ptr.h index 5a43185..709b33e 100644 --- a/best/memory/ptr.h +++ b/best/memory/ptr.h @@ -366,6 +366,21 @@ class ptr final { return (best::pointee*)raw(); } + /// # `ptr::skip()` + /// + /// Advances to a `U` stored after `count` `T`s. In other words, this + /// takes an array of type `T` with `count` elements, and skips past it to + /// an array of `U`s. + template + constexpr best::ptr skip(size_t count = 1, best::tlist = {}) const + requires thin && best::ptr::thin + { + auto end = offset(count); + return end.template cast() + .offset(end.misalignment(best::align_of)) + .template cast(); + } + /// # `ptr::to_addr()`, `ptr::from_addr()` /// /// Converts this pointer to/from a raw address. @@ -375,6 +390,17 @@ class ptr final { return {reinterpret_cast(addr), BEST_MOVE(meta)}; } + /// # `ptr::misalignment()` + /// + /// Returns the byte offset from this pointer's address to the next address + /// aligned to `align`, which must be a power of two. + ptrdiff_t misalignment(size_t align) const { + auto addr = to_addr(); + auto mask = align - 1; + auto next = (addr + mask) & ~mask; + return next - addr; + } + /// # `ptr::raw()`, `ptr::meta()` /// /// Returns the raw underlying pointer and its metadata. diff --git a/best/memory/span.h b/best/memory/span.h index fdfea2f..2478755 100644 --- a/best/memory/span.h +++ b/best/memory/span.h @@ -36,6 +36,7 @@ #include "best/memory/internal/bytes.h" #include "best/meta/init.h" #include "best/meta/tlist.h" +#include "best/meta/traits/quals.h" //! Data spans. //! @@ -309,6 +310,20 @@ class span final { return size_; } + /// # `span::as_bytes()` + /// + /// Returns a `char`-typed span of the same size as this span (adjusted for + /// scaling by the size of `T`). + constexpr best::span, + is_static ? *n * sizeof(T) : n> + as_bytes() const { + if constexpr (is_static) { + return {data().cast(best::types)}; + } else { + return {data().cast(best::types), size() * sizeof(T)}; + } + } + /// # `span::is_empty()` /// /// Returns whether this span is empty. @@ -927,8 +942,7 @@ constexpr span span::from_nul(T* data) { } auto ptr = data; - while (*ptr++ != T{0}) - ; + while (*ptr++ != T{0}); return best::span(data, ptr - data - 1); } diff --git a/best/meta/tlist.h b/best/meta/tlist.h index 3d8d93c..24b78db 100644 --- a/best/meta/tlist.h +++ b/best/meta/tlist.h @@ -423,6 +423,8 @@ class tlist final { constexpr bool operator==(best::is_tlist auto) const; constexpr best::partial_ord operator<=>(best::is_tlist auto that) const; + constexpr friend void BestHash(auto& h, const tlist& value) {} + private: template friend class tlist; diff --git a/best/meta/traits/BUILD b/best/meta/traits/BUILD index 6dcf19d..e58f3b2 100644 --- a/best/meta/traits/BUILD +++ b/best/meta/traits/BUILD @@ -39,6 +39,14 @@ cc_library( hdrs = ["objects.h"], ) +cc_library( + name = "pairs", + hdrs = ["pairs.h"], + deps = [ + ":types", + ] +) + cc_library( name = "ptrs", hdrs = ["ptrs.h"], diff --git a/best/meta/traits/arrays.h b/best/meta/traits/arrays.h index ba20be5..d3f64c8 100644 --- a/best/meta/traits/arrays.h +++ b/best/meta/traits/arrays.h @@ -23,6 +23,7 @@ #include #include "best/base/fwd.h" +#include "best/meta/traits/ptrs.h" //! Type traits for array types. //! @@ -61,6 +62,26 @@ concept is_sized_array = std::is_bounded_array_v; template concept is_unsized_array = std::is_unbounded_array_v; +/// # `best::is_array` +/// +/// Identifies an array type with a given element type. +template +concept is_array_of = best::is_array && best::same>; + +/// # `best::is_sized_array_of` +/// +/// Identifies a sized array type, i.e. `T[n]`, for some specific type T. +template +concept is_sized_array_of = + best::is_sized_array && best::same>; + +/// # `best::is_unsized_array_of` +/// +/// Identifies a unsized array type, i.e. `T[]`, for some specific type T. +template +concept is_unsized_array_of = + best::is_unsized_array && best::same>; + /// # `best::shape_of` /// /// Static information about the shape of the multi-dimensional array type `T`. diff --git a/best/meta/traits/empty.h b/best/meta/traits/empty.h index 31477e0..9466627 100644 --- a/best/meta/traits/empty.h +++ b/best/meta/traits/empty.h @@ -51,6 +51,8 @@ concept is_empty = best::is_void || std::is_empty_v; struct empty final { constexpr bool operator==(const empty& that) const = default; constexpr std::strong_ordering operator<=>(const empty& that) const = default; + + constexpr friend void BestHash(auto& h, const auto& value) {} }; /// # `best::devoid` diff --git a/best/meta/traits/internal/types.h b/best/meta/traits/internal/types.h index e3f2088..aa09935 100644 --- a/best/meta/traits/internal/types.h +++ b/best/meta/traits/internal/types.h @@ -47,18 +47,16 @@ T&& lie = [] { "attempted to tell a best::lie: this value cannot be materialized"); }(); -struct wax {}; -template -concept sealed = requires(Sealed sealed) { - sealed(wax{}); - +sealed; // Ensure that the user isn't passing a generic lambda. +template +struct wax final { + using type = T; }; -template -inline constexpr auto seal = sealed; +template +concept sealed = requires(Sealed sealed) { sealed(wax{}); }; template -using unseal = decltype(S{}(wax{})); +using unseal = decltype(S{}(wax{}))::type; template struct same { @@ -83,4 +81,35 @@ struct same { } // namespace best::traits_internal +// This symbol is carefully crafted to be very small when mangled but also +// readable in gdb's demangler. +// It looks something like this: `sealed::{lambda(auto:1)#3}`. +// +// Asking for the name of this type using `best::type_name` will produce +// something like `(lambda at :1:1)`, which is what the `#line` directive below +// is for. +namespace sealed { +// clang-format off +template + >::value + { + return best::traits_internal::wax{}; + } +> +inline constexpr auto BEST_MAKE_SEAL_ = sealed; +// clang-format on +} // namespace sealed + +namespace best::traits_internal { +template +inline constexpr auto seal = sealed::BEST_MAKE_SEAL_; +} + +#define BEST_MAKE_SEAL_ _priv + #endif // BEST_META_TRAITS_INTERNAL_TYPES_H_ diff --git a/best/meta/traits/pairs.h b/best/meta/traits/pairs.h new file mode 100644 index 0000000..b3453bf --- /dev/null +++ b/best/meta/traits/pairs.h @@ -0,0 +1,57 @@ + +/* //-*- C++ -*-///////////////////////////////////////////////////////////// *\ + + Copyright 2024 + Miguel Young de la Sota and the Best Contributors πŸ§ΆπŸˆβ€β¬› + + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +\* ////////////////////////////////////////////////////////////////////////// */ + +#ifndef BEST_META_TRAITS_PAIRS_H_ +#define BEST_META_TRAITS_PAIRS_H_ + +#include "best/meta/traits/types.h" + +//! Type traits for working with pair-like types. + +namespace best { +/// # `best::is_pair` +/// +/// Returns whether a type is pair-like, in that it can be destructured into +/// exactly two elements using structured bindings. +template +concept is_pair = requires { + { + [](P pair) { auto&& [a, b] = BEST_FWD(pair); } + }; +}; + +/// # `best::first_in

`, `best::second_in

` +/// +/// Returns the type of the first or second value in a pair, as defined by +/// `best::is_pair`. +template +using first_in = decltype([](auto&& pair) -> decltype(auto) { + auto&& [a, b] = BEST_FWD(pair); + return BEST_FWD(a); +}(best::lie

)); +template +using second_in = decltype([](auto&& pair) -> decltype(auto) { + auto&& [a, b] = BEST_FWD(pair); + return BEST_FWD(b); +}(best::lie

)); + +} // namespace best + +#endif // BEST_META_TRAITS_PAIRS_H_ \ No newline at end of file diff --git a/best/meta/traits/types.h b/best/meta/traits/types.h index f6b43f2..f4f193d 100644 --- a/best/meta/traits/types.h +++ b/best/meta/traits/types.h @@ -127,9 +127,9 @@ using select_trait = traits_internal::select::type::type; /// This produces a new type with a very small opaque name that can be /// `best::unabridge`ed to produce the original type. template -using abridge = decltype(best::traits_internal::seal>); +using abridge = std::remove_const_t)>; template -using unabridge = best::traits_internal::unseal::type; +using unabridge = best::traits_internal::unseal; /// # `best::abridged` /// diff --git a/best/test/test.h b/best/test/test.h index 507c327..63fb438 100644 --- a/best/test/test.h +++ b/best/test/test.h @@ -22,6 +22,7 @@ #include "best/cli/cli.h" #include "best/log/location.h" +#include "best/meta/init.h" #include "best/text/format.h" #include "best/text/str.h" @@ -99,10 +100,12 @@ class test final { /// ``` /// if (!t.expect(...)) { return; } /// ``` - template - bool expect(bool cond, best::format_template message = "", + template + requires best::constructible + bool expect(Cond&& cond, best::format_template message = "", const Args&... args) { - if (!cond) { + bool c = static_cast(BEST_FWD(cond)); + if (!c) { best::eprintln("failed expect() at {:?}", message.where()); if (!message.as_str().is_empty()) { best::eprint("=> "); @@ -110,7 +113,7 @@ class test final { } failed_ = true; } - return cond; + return c; } /// # `test::expect_eq()` et. al. diff --git a/best/text/BUILD b/best/text/BUILD index 01a4911..dcb3652 100644 --- a/best/text/BUILD +++ b/best/text/BUILD @@ -15,6 +15,7 @@ cc_library( hdrs = ["rune.h"], deps = [ ":encoding", + "//best/hash", "//best/log/internal:crash", ] ) diff --git a/best/text/format.h b/best/text/format.h index 155d79f..6f425ea 100644 --- a/best/text/format.h +++ b/best/text/format.h @@ -193,8 +193,7 @@ struct format_spec final { BestFmtQuery(query, best::as_raw_ptr()); } return query; - } - (); + }(); }; constexpr bool operator==(const format_spec&) const = default; @@ -586,6 +585,17 @@ void eprintln(best::format_template templ, const Args&... args) { ::fwrite(result.data(), 1, result.size(), stderr); } +namespace option_internal { +// See the matching decl in result.h. +struct fmt final { + template + BEST_INLINE_SYNTHETIC static void wtf(Args&&... args) { + auto message = best::format(BEST_FWD(args)...); + best::crash_internal::crash("%.*s", message.size(), message.data()); + } +}; +} // namespace option_internal + namespace result_internal { // See the matching decl in result.h. struct fmt final { diff --git a/best/text/internal/format_impls.h b/best/text/internal/format_impls.h index 2caee1f..f174a7a 100644 --- a/best/text/internal/format_impls.h +++ b/best/text/internal/format_impls.h @@ -23,6 +23,7 @@ #include #include +#include "best/math/int.h" #include "best/meta/reflect.h" #include "best/text/rune.h" #include "best/text/str.h" @@ -145,15 +146,16 @@ void BestFmt(auto& fmt, best::is_int auto value) { if (negative) { value = -value; } // Construct the actual digits. + auto uvalue = best::to_unsigned(value); char buf[128]; size_t count = 0; do { - rune r = *rune::from_digit(value % base, base); + rune r = *rune::from_digit(uvalue % base, base); if (uppercase) { r = r.to_ascii_upper(); } buf[128 - count++ - 1] = r; - value /= base; - } while (value != 0); + uvalue /= base; + } while (uvalue != 0); best::str digits(unsafe("all characters are ascii"), best::span(buf + 128 - count, count)); diff --git a/best/text/rune.h b/best/text/rune.h index 2c2760d..11a8b7a 100644 --- a/best/text/rune.h +++ b/best/text/rune.h @@ -327,6 +327,11 @@ class rune final { query.uses_method = [](rune r) { return r == 'q'; }; } + template + constexpr friend void BestHash(best::hasher& h, const rune& value) { + h.write(value.value_); + } + // best::rune has a niche representation. constexpr explicit rune(niche) : value_(-1) {} constexpr bool operator==(niche) const { return value_ == -1; } From 38f23ed1706101d18d7b6da8982c7c8341605319 Mon Sep 17 00:00:00 2001 From: Miguel Young de la Sota Date: Mon, 30 Jun 2025 20:03:42 -0700 Subject: [PATCH 2/2] fix --- best/container/table.h | 20 +++++++++++--------- best/container/table_test.cc | 4 +++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/best/container/table.h b/best/container/table.h index 01f6e54..abc5438 100644 --- a/best/container/table.h +++ b/best/container/table.h @@ -954,16 +954,18 @@ best::strbuf table::debug(bool include_entries) const { best::format(out, " {:p}: {:?}\n", key, best::make_formattable(*key)); } - if constexpr (best::is_empty) { return out; } - out.push("values:\n"); - for (auto i : best::bounds{.count = raw_.cap().hard}) { - auto value = raw_.value(i); - if (raw_.ctrl(i).is_vacant()) { - best::format(out, " {:p}: ---\n", value); - continue; - } + if constexpr (!best::is_empty) { + out.push("values:\n"); + for (auto i : best::bounds{.count = raw_.cap().hard}) { + auto value = raw_.value(i); + if (raw_.ctrl(i).is_vacant()) { + best::format(out, " {:p}: ---\n", value); + continue; + } - best::format(out, " {:p}: {:?}\n", value, best::make_formattable(*value)); + best::format(out, " {:p}: {:?}\n", value, + best::make_formattable(*value)); + } } return out; diff --git a/best/container/table_test.cc b/best/container/table_test.cc index e18dec7..276a3cf 100644 --- a/best/container/table_test.cc +++ b/best/container/table_test.cc @@ -189,7 +189,9 @@ best::test HammerStrings = [](auto& t) { best::table strs; best::strbuf k; - for (auto _ : best::bounds{.count = 100}) { + for (auto ignore : best::bounds{.count = 100}) { + (void)ignore; + strs[k].insert(); k.push('#'); }