usearch 2.25.1 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +2 -2
  2. package/c/lib.cpp +28 -13
  3. package/cpp/test.cpp +169 -0
  4. package/include/usearch/index.hpp +364 -58
  5. package/include/usearch/index_dense.hpp +155 -50
  6. package/include/usearch/index_plugins.hpp +156 -79
  7. package/javascript/lib.cpp +4 -2
  8. package/numkong/CITATION.cff +1 -1
  9. package/numkong/Cargo.toml +1 -1
  10. package/numkong/bench/bench_cross_amx.cpp +12 -0
  11. package/numkong/bench/bench_mesh.cpp +12 -0
  12. package/numkong/bench/bench_spatial.cpp +0 -3
  13. package/numkong/binding.gyp +18 -0
  14. package/numkong/build.rs +1 -0
  15. package/numkong/c/dispatch_e5m2.c +23 -3
  16. package/numkong/include/numkong/capabilities.h +1 -1
  17. package/numkong/include/numkong/cast/README.md +3 -0
  18. package/numkong/include/numkong/cast/haswell.h +28 -64
  19. package/numkong/include/numkong/cast/serial.h +17 -0
  20. package/numkong/include/numkong/cast/skylake.h +67 -52
  21. package/numkong/include/numkong/cast.h +1 -0
  22. package/numkong/include/numkong/dot/README.md +1 -0
  23. package/numkong/include/numkong/dot/haswell.h +92 -13
  24. package/numkong/include/numkong/dot/serial.h +15 -0
  25. package/numkong/include/numkong/dot/skylake.h +61 -14
  26. package/numkong/include/numkong/dots/README.md +2 -0
  27. package/numkong/include/numkong/dots/graniteamx.h +434 -0
  28. package/numkong/include/numkong/dots/haswell.h +28 -28
  29. package/numkong/include/numkong/dots/sapphireamx.h +1 -1
  30. package/numkong/include/numkong/dots/serial.h +23 -8
  31. package/numkong/include/numkong/dots/skylake.h +28 -23
  32. package/numkong/include/numkong/dots.h +12 -0
  33. package/numkong/include/numkong/each/serial.h +18 -1
  34. package/numkong/include/numkong/geospatial/serial.h +14 -3
  35. package/numkong/include/numkong/maxsim/serial.h +15 -0
  36. package/numkong/include/numkong/mesh/README.md +50 -44
  37. package/numkong/include/numkong/mesh/genoa.h +462 -0
  38. package/numkong/include/numkong/mesh/haswell.h +806 -933
  39. package/numkong/include/numkong/mesh/neon.h +871 -943
  40. package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
  41. package/numkong/include/numkong/mesh/neonfhm.h +676 -0
  42. package/numkong/include/numkong/mesh/rvv.h +404 -319
  43. package/numkong/include/numkong/mesh/serial.h +204 -162
  44. package/numkong/include/numkong/mesh/skylake.h +1029 -1585
  45. package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
  46. package/numkong/include/numkong/mesh.h +38 -0
  47. package/numkong/include/numkong/reduce/serial.h +15 -1
  48. package/numkong/include/numkong/sparse/serial.h +17 -2
  49. package/numkong/include/numkong/spatial/genoa.h +0 -68
  50. package/numkong/include/numkong/spatial/haswell.h +98 -56
  51. package/numkong/include/numkong/spatial/serial.h +15 -0
  52. package/numkong/include/numkong/spatial/skylake.h +114 -54
  53. package/numkong/include/numkong/spatial.h +0 -12
  54. package/numkong/include/numkong/spatials/graniteamx.h +128 -0
  55. package/numkong/include/numkong/spatials/serial.h +18 -1
  56. package/numkong/include/numkong/spatials/skylake.h +2 -2
  57. package/numkong/include/numkong/spatials.h +17 -0
  58. package/numkong/include/numkong/tensor.hpp +107 -23
  59. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  60. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  61. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  62. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  63. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  64. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  65. package/numkong/javascript/numkong.c +3 -2
  66. package/numkong/package.json +7 -7
  67. package/numkong/test/test.cu +841 -0
  68. package/numkong/test/test_cross_amx.cpp +13 -0
  69. package/numkong/test/test_dlpack.py +492 -0
  70. package/numkong/test/test_mesh.cpp +12 -0
  71. package/numkong/test/test_spatial.cpp +0 -3
  72. package/numkong/test/test_tensor.cpp +87 -27
  73. package/package.json +2 -2
  74. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  75. package/prebuilds/linux-arm64/usearch.node +0 -0
  76. package/prebuilds/linux-x64/usearch.node +0 -0
  77. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -8,8 +8,8 @@
8
8
  #define UNUM_USEARCH_HPP
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
- #define USEARCH_VERSION_MINOR 25
12
- #define USEARCH_VERSION_PATCH 1
11
+ #define USEARCH_VERSION_MINOR 26
12
+ #define USEARCH_VERSION_PATCH 0
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -176,6 +176,40 @@ namespace usearch {
176
176
 
177
177
  using byte_t = char;
178
178
 
179
+ struct checked_size_result_t {
180
+ std::size_t value;
181
+ bool overflow;
182
+ constexpr checked_size_result_t(std::size_t value = 0, bool overflow = false) noexcept
183
+ : value(value), overflow(overflow) {}
184
+ constexpr explicit operator bool() const noexcept { return !overflow; }
185
+ };
186
+
187
+ constexpr checked_size_result_t checked_size_overflow() noexcept { return {0, true}; }
188
+
189
+ constexpr checked_size_result_t checked_size_from_u64(std::uint64_t value) noexcept {
190
+ return value > static_cast<std::uint64_t>((std::numeric_limits<std::size_t>::max)())
191
+ ? checked_size_overflow()
192
+ : checked_size_result_t{static_cast<std::size_t>(value), false};
193
+ }
194
+
195
+ constexpr checked_size_result_t checked_add(std::size_t a, std::size_t b) noexcept {
196
+ return (std::numeric_limits<std::size_t>::max)() - a < b ? checked_size_overflow()
197
+ : checked_size_result_t{a + b, false};
198
+ }
199
+
200
+ constexpr checked_size_result_t checked_mul(std::size_t a, std::size_t b) noexcept {
201
+ return a && b > (std::numeric_limits<std::size_t>::max)() / a ? checked_size_overflow()
202
+ : checked_size_result_t{a * b, false};
203
+ }
204
+
205
+ constexpr checked_size_result_t checked_mul_add_(checked_size_result_t product, std::size_t c) noexcept {
206
+ return product ? checked_add(product.value, c) : product;
207
+ }
208
+
209
+ constexpr checked_size_result_t checked_mul_add(std::size_t a, std::size_t b, std::size_t c) noexcept {
210
+ return checked_mul_add_(checked_mul(a, b), c);
211
+ }
212
+
179
213
  template <std::size_t multiple_ak> std::size_t divide_round_up(std::size_t num) noexcept {
180
214
  return (num + multiple_ak - 1) / multiple_ak;
181
215
  }
@@ -184,6 +218,21 @@ inline std::size_t divide_round_up(std::size_t num, std::size_t denominator) noe
184
218
  return (num + denominator - 1) / denominator;
185
219
  }
186
220
 
221
+ constexpr checked_size_result_t checked_divide_round_up(std::size_t num, std::size_t denominator) noexcept {
222
+ return !denominator ? checked_size_overflow()
223
+ : (std::numeric_limits<std::size_t>::max)() - num < denominator - 1
224
+ ? checked_size_overflow()
225
+ : checked_size_result_t{(num + denominator - 1) / denominator, false};
226
+ }
227
+
228
+ constexpr checked_size_result_t checked_round_up_(checked_size_result_t quotient, std::size_t multiple) noexcept {
229
+ return quotient ? checked_mul(quotient.value, multiple) : quotient;
230
+ }
231
+
232
+ constexpr checked_size_result_t checked_round_up(std::size_t num, std::size_t multiple) noexcept {
233
+ return checked_round_up_(checked_divide_round_up(num, multiple), multiple);
234
+ }
235
+
187
236
  inline std::size_t ceil2(std::size_t v) noexcept {
188
237
  v--;
189
238
  v |= v >> 1;
@@ -198,6 +247,14 @@ inline std::size_t ceil2(std::size_t v) noexcept {
198
247
  return v;
199
248
  }
200
249
 
250
+ inline checked_size_result_t checked_ceil2(std::size_t v) noexcept {
251
+ if (!v)
252
+ return checked_size_result_t{0, false};
253
+ if (v > (std::size_t{1} << ((sizeof(std::size_t) * CHAR_BIT) - 1)))
254
+ return checked_size_overflow();
255
+ return checked_size_result_t{ceil2(v), false};
256
+ }
257
+
201
258
  /// @brief Simply dereferencing misaligned pointers can be dangerous.
202
259
  template <typename at> void misaligned_store(void* ptr, at v) noexcept {
203
260
  static_assert(!std::is_reference<at>::value, "Can't store a reference");
@@ -525,9 +582,12 @@ template <typename allocator_at = std::allocator<byte_t>> class bitset_gt {
525
582
  count_ = 0;
526
583
  }
527
584
 
528
- bitset_gt(std::size_t capacity) noexcept
529
- : slots_((compressed_slot_t*)allocator_t{}.allocate(bits_slots(capacity) * sizeof(compressed_slot_t))),
530
- count_(slots_ ? bits_slots(capacity) : 0u) {
585
+ bitset_gt(std::size_t capacity) noexcept {
586
+ checked_size_result_t slots_count = checked_divide_round_up(capacity, bits_per_slot());
587
+ checked_size_result_t bytes =
588
+ slots_count ? checked_mul(slots_count.value, sizeof(compressed_slot_t)) : slots_count;
589
+ slots_ = bytes ? (compressed_slot_t*)allocator_t{}.allocate(bytes.value) : nullptr;
590
+ count_ = slots_ ? slots_count.value : 0u;
531
591
  clear();
532
592
  }
533
593
 
@@ -654,10 +714,19 @@ class striped_locks_gt {
654
714
  }
655
715
 
656
716
  striped_locks_gt(std::size_t threads, std::size_t connectivity) noexcept {
657
- std::size_t desired = threads * connectivity * 4;
658
- if (desired < 256)
659
- desired = 256;
660
- count_ = ceil2(desired);
717
+ checked_size_result_t desired = checked_mul(threads, connectivity);
718
+ desired = desired ? checked_mul(desired.value, std::size_t{4}) : desired;
719
+ if (!desired) {
720
+ shift_ = 64;
721
+ return;
722
+ }
723
+
724
+ checked_size_result_t count = checked_ceil2((std::max<std::size_t>)(desired.value, 256));
725
+ if (!count) {
726
+ shift_ = 64;
727
+ return;
728
+ }
729
+ count_ = count.value;
661
730
  shift_ = 64;
662
731
  for (std::size_t n = count_; n > 1; n >>= 1)
663
732
  shift_--;
@@ -665,7 +734,13 @@ class striped_locks_gt {
665
734
  // `cache_line_ak`-aligned address inside the allocation, regardless of
666
735
  // what the underlying allocator returns.
667
736
  constexpr std::size_t alignment_k = alignof(padded_lock_t);
668
- raw_bytes_ = count_ * sizeof(padded_lock_t) + alignment_k;
737
+ checked_size_result_t raw_bytes = checked_mul_add(count_, sizeof(padded_lock_t), alignment_k);
738
+ if (!raw_bytes) {
739
+ count_ = 0;
740
+ shift_ = 64;
741
+ return;
742
+ }
743
+ raw_bytes_ = raw_bytes.value;
669
744
  raw_ = allocator_t{}.allocate(raw_bytes_);
670
745
  if (!raw_) {
671
746
  raw_bytes_ = 0;
@@ -850,10 +925,14 @@ class max_heap_gt {
850
925
  if (new_capacity <= capacity_)
851
926
  return true;
852
927
 
853
- new_capacity = ceil2(new_capacity);
854
- if (new_capacity == 0)
928
+ checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
929
+ if (!rounded_capacity)
930
+ return false;
931
+ checked_size_result_t doubled_capacity = checked_mul(capacity_, std::size_t{2});
932
+ if (!doubled_capacity)
855
933
  return false;
856
- new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
934
+ new_capacity =
935
+ (std::max<std::size_t>)(rounded_capacity.value, (std::max<std::size_t>)(doubled_capacity.value, 16u));
857
936
  auto allocator = allocator_t{};
858
937
  auto new_elements = allocator.allocate(new_capacity);
859
938
  if (!new_elements)
@@ -1018,10 +1097,14 @@ class sorted_buffer_gt {
1018
1097
  if (new_capacity <= capacity_)
1019
1098
  return true;
1020
1099
 
1021
- new_capacity = ceil2(new_capacity);
1022
- if (new_capacity == 0)
1100
+ checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
1101
+ if (!rounded_capacity)
1023
1102
  return false;
1024
- new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
1103
+ checked_size_result_t doubled_capacity = checked_mul(capacity_, std::size_t{2});
1104
+ if (!doubled_capacity)
1105
+ return false;
1106
+ new_capacity =
1107
+ (std::max<std::size_t>)(rounded_capacity.value, (std::max<std::size_t>)(doubled_capacity.value, 16u));
1025
1108
  auto allocator = allocator_t{};
1026
1109
  auto new_elements = allocator.allocate(new_capacity);
1027
1110
  if (!new_elements)
@@ -1194,6 +1277,31 @@ template <typename element_at> struct hash_gt {
1194
1277
  std::size_t operator()(element_at const& element) const noexcept { return std::hash<element_at>{}(element); }
1195
1278
  };
1196
1279
 
1280
+ /**
1281
+ * @brief SplitMix64 finalizer, used to scatter integer keys before masking.
1282
+ *
1283
+ * On libstdc++ and libc++ `std::hash` is the identity for integers. Our open-addressing
1284
+ * tables mask that hash into a power-of-2 slot count and probe linearly until an @b empty
1285
+ * slot, so consecutive keys land in adjacent slots and merge into one contiguous run.
1286
+ * Both insertions and lookups then scan that run end-to-end, which is quadratic overall:
1287
+ * a dense ascending key range collapses insertion throughput by three orders of magnitude.
1288
+ * Mixing costs ~20ns per key and is dwarfed by the graph traversal in `add`.
1289
+ */
1290
+ template <> struct hash_gt<std::uint64_t> {
1291
+ std::size_t operator()(std::uint64_t const& element) const noexcept {
1292
+ std::uint64_t x = element;
1293
+ x = (x ^ (x >> 30u)) * 0xBF58476D1CE4E5B9ULL;
1294
+ x = (x ^ (x >> 27u)) * 0x94D049BB133111EBULL;
1295
+ return static_cast<std::size_t>(x ^ (x >> 31u));
1296
+ }
1297
+ };
1298
+
1299
+ template <> struct hash_gt<std::int64_t> {
1300
+ std::size_t operator()(std::int64_t const& element) const noexcept {
1301
+ return hash_gt<std::uint64_t>{}(static_cast<std::uint64_t>(element));
1302
+ }
1303
+ };
1304
+
1197
1305
  template <> struct hash_gt<uint40_t> {
1198
1306
  std::size_t operator()(uint40_t const& element) const noexcept { return std::hash<std::size_t>{}(element); }
1199
1307
  };
@@ -1246,9 +1354,11 @@ class growing_hash_set_gt {
1246
1354
  count_ = 0;
1247
1355
  }
1248
1356
 
1249
- growing_hash_set_gt(std::size_t capacity) noexcept
1250
- : slots_((element_t*)allocator_t{}.allocate(ceil2(capacity) * sizeof(element_t))),
1251
- capacity_(slots_ ? ceil2(capacity) : 0u), count_(0u) {
1357
+ growing_hash_set_gt(std::size_t capacity) noexcept : count_(0u) {
1358
+ checked_size_result_t slots_count = checked_ceil2(capacity);
1359
+ checked_size_result_t bytes = slots_count ? checked_mul(slots_count.value, sizeof(element_t)) : slots_count;
1360
+ slots_ = bytes ? (element_t*)allocator_t{}.allocate(bytes.value) : nullptr;
1361
+ capacity_ = slots_ ? slots_count.value : 0u;
1252
1362
  clear();
1253
1363
  }
1254
1364
 
@@ -1306,12 +1416,21 @@ class growing_hash_set_gt {
1306
1416
  * @return `true` if enough capacity is available, `false` if memory allocation failed.
1307
1417
  */
1308
1418
  bool reserve(std::size_t new_capacity) noexcept {
1309
- new_capacity = (new_capacity * 5u) / 3u;
1419
+ checked_size_result_t scaled_capacity = checked_mul(new_capacity, std::size_t{5});
1420
+ if (!scaled_capacity)
1421
+ return false;
1422
+ new_capacity = scaled_capacity.value / 3u;
1310
1423
  if (new_capacity <= capacity_)
1311
1424
  return true;
1312
1425
 
1313
- new_capacity = ceil2(new_capacity);
1314
- element_t* new_slots = (element_t*)allocator_t{}.allocate(new_capacity * sizeof(element_t));
1426
+ checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
1427
+ if (!rounded_capacity)
1428
+ return false;
1429
+ new_capacity = rounded_capacity.value;
1430
+ checked_size_result_t new_bytes = checked_mul(new_capacity, sizeof(element_t));
1431
+ if (!new_bytes)
1432
+ return false;
1433
+ element_t* new_slots = (element_t*)allocator_t{}.allocate(new_bytes.value);
1315
1434
  if (!new_slots)
1316
1435
  return false;
1317
1436
 
@@ -1414,7 +1533,10 @@ class ring_gt {
1414
1533
  return false; // prevent data loss
1415
1534
  if (n <= capacity())
1416
1535
  return true;
1417
- n = (std::max<std::size_t>)(ceil2(n), 64u);
1536
+ checked_size_result_t rounded_capacity = checked_ceil2(n);
1537
+ if (!rounded_capacity)
1538
+ return false;
1539
+ n = (std::max<std::size_t>)(rounded_capacity.value, 64u);
1418
1540
  element_t* elements = allocator_.allocate(n);
1419
1541
  if (!elements)
1420
1542
  return false;
@@ -1443,8 +1565,7 @@ class ring_gt {
1443
1565
  bool try_push(element_t const& value) noexcept {
1444
1566
  if (head_ == tail_ && !empty_)
1445
1567
  return false; // `elements_` is full
1446
-
1447
- return push(value);
1568
+ push(value);
1448
1569
  return true;
1449
1570
  }
1450
1571
 
@@ -1504,12 +1625,22 @@ struct index_config_t {
1504
1625
  inline error_t validate() noexcept {
1505
1626
  if (connectivity == 0)
1506
1627
  connectivity = default_connectivity();
1507
- if (connectivity_base == 0)
1508
- connectivity_base = connectivity * 2;
1628
+ if (connectivity_base == 0) {
1629
+ checked_size_result_t default_base = checked_mul(connectivity, std::size_t{2});
1630
+ if (!default_base)
1631
+ return "Connectivity is too large";
1632
+ connectivity_base = default_base.value;
1633
+ }
1509
1634
  if (connectivity < 2)
1510
1635
  return "Connectivity must be at least 2, otherwise the index degenerates into ropes";
1511
1636
  if (connectivity_base < connectivity)
1512
1637
  return "Base layer should be at least as connected as the rest of the graph";
1638
+ checked_size_result_t neighbors_bytes =
1639
+ checked_mul_add(connectivity, sizeof(std::uint64_t), sizeof(std::uint32_t));
1640
+ checked_size_result_t neighbors_base_bytes =
1641
+ checked_mul_add(connectivity_base, sizeof(std::uint64_t), sizeof(std::uint32_t));
1642
+ if (!neighbors_bytes || !neighbors_base_bytes)
1643
+ return "Connectivity is too large";
1513
1644
  return {};
1514
1645
  }
1515
1646
 
@@ -1520,6 +1651,15 @@ struct index_config_t {
1520
1651
  inline bool is_valid() const noexcept { return connectivity >= 2 && connectivity_base >= connectivity; }
1521
1652
  };
1522
1653
 
1654
+ /**
1655
+ * @brief Tag type selecting the "no upfront reservation" overload of
1656
+ * @ref index_limits_t. Modeled after @c std::defer_lock: the
1657
+ * resulting limits are all-zero and produce no allocations when
1658
+ * handed to @ref index_dense_gt::try_reserve.
1659
+ */
1660
+ struct unreserved_t {};
1661
+ constexpr unreserved_t unreserved{};
1662
+
1523
1663
  /**
1524
1664
  * @brief Growth settings for the index container.
1525
1665
  * Includes the upper bound for `::members` capacity,
@@ -1527,18 +1667,32 @@ struct index_config_t {
1527
1667
  */
1528
1668
  struct index_limits_t {
1529
1669
  /// @brief Maximum number of entries in the index.
1530
- std::size_t members = 0;
1670
+ std::size_t members;
1531
1671
  /// @brief Max number of threads simultaneously updating entries.
1532
- std::size_t threads_add = std::thread::hardware_concurrency();
1672
+ std::size_t threads_add;
1533
1673
  /// @brief Max number of threads simultaneously searching entries.
1534
- std::size_t threads_search = std::thread::hardware_concurrency();
1674
+ std::size_t threads_search;
1535
1675
 
1536
1676
  inline index_limits_t(std::size_t n, std::size_t t) noexcept : members(n), threads_add(t), threads_search(t) {}
1537
- inline index_limits_t(std::size_t n = 0) noexcept : index_limits_t(n, std::thread::hardware_concurrency()) {}
1677
+ inline index_limits_t(std::size_t n = 0) noexcept
1678
+ : index_limits_t(n, (std::max<std::size_t>)(1, std::thread::hardware_concurrency())) {}
1679
+ inline index_limits_t(unreserved_t) noexcept : members(0), threads_add(0), threads_search(0) {}
1538
1680
  /// @brief Returns the upper limit for the number of threads.
1539
1681
  inline std::size_t threads() const noexcept { return (std::max)(threads_add, threads_search); }
1540
1682
  /// @brief Returns the concurrency-level of the index - the minimum of thread counts.
1541
1683
  inline std::size_t concurrency() const noexcept { return (std::min)(threads_add, threads_search); }
1684
+ /// @brief Returns a copy with zero thread counts replaced by the library default.
1685
+ /// Use when carrying limits forward across operations that may have left
1686
+ /// @c threads_add / @c threads_search unset (e.g. @c unreserved construction).
1687
+ inline index_limits_t with_thread_defaults() const noexcept {
1688
+ index_limits_t result = *this;
1689
+ index_limits_t const defaults;
1690
+ if (!result.threads_add)
1691
+ result.threads_add = defaults.threads_add;
1692
+ if (!result.threads_search)
1693
+ result.threads_search = defaults.threads_search;
1694
+ return result;
1695
+ }
1542
1696
  };
1543
1697
 
1544
1698
  struct index_update_config_t {
@@ -2414,6 +2568,14 @@ class index_gt {
2414
2568
  /// @brief Array of thread-specific buffers for temporary data.
2415
2569
  mutable buffer_gt<context_t, contexts_allocator_t> contexts_{};
2416
2570
 
2571
+ context_t* context_or_null_(std::size_t thread) noexcept {
2572
+ return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
2573
+ }
2574
+
2575
+ context_t const* context_or_null_(std::size_t thread) const noexcept {
2576
+ return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
2577
+ }
2578
+
2417
2579
  public:
2418
2580
  std::size_t connectivity() const noexcept { return config_.connectivity; }
2419
2581
  std::size_t capacity() const noexcept { return nodes_capacity_; }
@@ -2547,6 +2709,114 @@ class index_gt {
2547
2709
  member_iterator_t iterator_at(compressed_slot_t slot) noexcept { return {this, slot}; }
2548
2710
  member_citerator_t citerator_at(compressed_slot_t slot) const noexcept { return {this, slot}; }
2549
2711
 
2712
+ /**
2713
+ * @brief A read-only random-access range over the neighbors of a single
2714
+ * node at a single graph level. Dereferencing yields a `member_cref_t`,
2715
+ * so callers can chain traversals without touching internal slots.
2716
+ *
2717
+ * @warning The range aliases the node's adjacency tape. It is only valid
2718
+ * while the index is not being mutated. Prefer immutable indexes
2719
+ * (see `is_immutable()`) or guarantee no concurrent `add`/`update`/
2720
+ * `remove` while the view is alive.
2721
+ */
2722
+ class neighbors_view_t {
2723
+ index_gt const* index_{};
2724
+ neighbors_ref_t neighbors_{nullptr};
2725
+
2726
+ public:
2727
+ class const_iterator {
2728
+ index_gt const* index_{};
2729
+ misaligned_ptr_gt<compressed_slot_t const> position_{nullptr};
2730
+
2731
+ public:
2732
+ using iterator_category = std::random_access_iterator_tag;
2733
+ using value_type = member_cref_t;
2734
+ using difference_type = std::ptrdiff_t;
2735
+ using pointer = void;
2736
+ using reference = member_cref_t;
2737
+
2738
+ const_iterator() noexcept = default;
2739
+ const_iterator(index_gt const* index, misaligned_ptr_gt<compressed_slot_t const> position) noexcept
2740
+ : index_(index), position_(position) {}
2741
+
2742
+ reference operator*() const noexcept {
2743
+ compressed_slot_t slot = static_cast<compressed_slot_t>(*position_);
2744
+ return {index_->node_at_(slot).ckey(), slot};
2745
+ }
2746
+ compressed_slot_t slot() const noexcept { return static_cast<compressed_slot_t>(*position_); }
2747
+
2748
+ // clang-format off
2749
+ const_iterator& operator++() noexcept { ++position_; return *this; }
2750
+ const_iterator operator++(int) noexcept { const_iterator old = *this; ++position_; return old; }
2751
+ const_iterator& operator--() noexcept { --position_; return *this; }
2752
+ const_iterator operator--(int) noexcept { const_iterator old = *this; --position_; return old; }
2753
+ const_iterator& operator+=(difference_type d) noexcept { position_ = position_ + d; return *this; }
2754
+ const_iterator& operator-=(difference_type d) noexcept { position_ = position_ - d; return *this; }
2755
+ const_iterator operator+(difference_type d) const noexcept { return {index_, position_ + d}; }
2756
+ const_iterator operator-(difference_type d) const noexcept { return {index_, position_ - d}; }
2757
+ difference_type operator-(const_iterator const& other) const noexcept { return position_ - other.position_; }
2758
+ bool operator==(const_iterator const& other) const noexcept { return position_ == other.position_; }
2759
+ bool operator!=(const_iterator const& other) const noexcept { return position_ != other.position_; }
2760
+ // clang-format on
2761
+ };
2762
+
2763
+ using iterator = const_iterator;
2764
+ using value_type = member_cref_t;
2765
+ using size_type = std::size_t;
2766
+
2767
+ neighbors_view_t() noexcept = default;
2768
+ neighbors_view_t(index_gt const* index, neighbors_ref_t neighbors) noexcept
2769
+ : index_(index), neighbors_(neighbors) {}
2770
+
2771
+ std::size_t size() const noexcept { return index_ ? neighbors_.size() : 0; }
2772
+ bool empty() const noexcept { return size() == 0; }
2773
+ member_cref_t operator[](std::size_t offset) const noexcept {
2774
+ compressed_slot_t slot = neighbors_[offset];
2775
+ return {index_->node_at_(slot).ckey(), slot};
2776
+ }
2777
+ const_iterator begin() const noexcept {
2778
+ return index_ ? const_iterator{index_, neighbors_.begin()} : const_iterator{};
2779
+ }
2780
+ const_iterator end() const noexcept {
2781
+ return index_ ? const_iterator{index_, neighbors_.end()} : const_iterator{};
2782
+ }
2783
+ const_iterator cbegin() const noexcept { return begin(); }
2784
+ const_iterator cend() const noexcept { return end(); }
2785
+ };
2786
+
2787
+ /**
2788
+ * @brief Returns a read-only range over the neighbors of the node at @p slot
2789
+ * in the graph @p level. Returned view is empty when @p level exceeds
2790
+ * the node's level.
2791
+ */
2792
+ neighbors_view_t neighbors(compressed_slot_t slot, std::size_t level) const noexcept {
2793
+ node_t node = node_at_(slot);
2794
+ if (static_cast<level_t>(level) > node.level())
2795
+ return {};
2796
+ return {this, neighbors_(node, static_cast<level_t>(level))};
2797
+ }
2798
+
2799
+ /**
2800
+ * @brief Returns a read-only range over the neighbors of the node referenced
2801
+ * by @p member at the graph @p level.
2802
+ */
2803
+ neighbors_view_t neighbors(member_citerator_t member, std::size_t level) const noexcept {
2804
+ return neighbors(get_slot(member), level);
2805
+ }
2806
+
2807
+ /**
2808
+ * @brief Returns the top graph level at which the node at @p slot is present.
2809
+ */
2810
+ std::size_t level_of(compressed_slot_t slot) const noexcept {
2811
+ return static_cast<std::size_t>(static_cast<level_t>(node_at_(slot).level()));
2812
+ }
2813
+
2814
+ /**
2815
+ * @brief Returns the top graph level at which the node referenced by @p member
2816
+ * is present.
2817
+ */
2818
+ std::size_t level_of(member_citerator_t member) const noexcept { return level_of(get_slot(member)); }
2819
+
2550
2820
  dynamic_allocator_t const& dynamic_allocator() const noexcept { return dynamic_allocator_; }
2551
2821
  tape_allocator_t const& tape_allocator() const noexcept { return tape_allocator_; }
2552
2822
 
@@ -2919,7 +3189,10 @@ class index_gt {
2919
3189
  return result.failed("Can't add to an immutable index");
2920
3190
 
2921
3191
  // Make sure we have enough local memory to perform this request
2922
- context_t& context = contexts_[config.thread];
3192
+ context_t* context_ptr = context_or_null_(config.thread);
3193
+ if (!context_ptr)
3194
+ return result.failed("Reserve capacity ahead of insertions!");
3195
+ context_t& context = *context_ptr;
2923
3196
  top_candidates_t& top = context.top_candidates;
2924
3197
  next_candidates_t& next = context.next_candidates;
2925
3198
  top.clear();
@@ -3057,7 +3330,10 @@ class index_gt {
3057
3330
  compressed_slot_t updated_slot = iterator.slot_;
3058
3331
 
3059
3332
  // Make sure we have enough local memory to perform this request
3060
- context_t& context = contexts_[config.thread];
3333
+ context_t* context_ptr = context_or_null_(config.thread);
3334
+ if (!context_ptr)
3335
+ return result.failed("Reserve capacity ahead of updates!");
3336
+ context_t& context = *context_ptr;
3061
3337
  top_candidates_t& top = context.top_candidates;
3062
3338
  next_candidates_t& next = context.next_candidates;
3063
3339
  top.clear();
@@ -3425,26 +3701,31 @@ class index_gt {
3425
3701
 
3426
3702
  // Progress status
3427
3703
  std::size_t processed = 0;
3428
- std::size_t const total = 2 * header.size;
3704
+ checked_size_result_t header_size = checked_size_from_u64(header.size);
3705
+ if (!header_size)
3706
+ return result.failed("Index is too large to serialize");
3707
+ checked_size_result_t total = checked_mul(std::size_t{2}, header_size.value);
3708
+ if (!total)
3709
+ return result.failed("Index is too large to serialize");
3429
3710
 
3430
3711
  // Export the number of levels per node
3431
3712
  // That is both enough to estimate the overall memory consumption,
3432
3713
  // and to be able to estimate the offsets of every entry in the file.
3433
- for (std::size_t i = 0; i != header.size; ++i) {
3714
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3434
3715
  node_t node = node_at_(i);
3435
3716
  level_t level = node.level();
3436
3717
  if (!output(&level, sizeof(level)))
3437
3718
  return result.failed("Failed to serialize into stream");
3438
- if (!progress(++processed, total))
3719
+ if (!progress(++processed, total.value))
3439
3720
  return result.failed("Terminated by user");
3440
3721
  }
3441
3722
 
3442
3723
  // After that dump the nodes themselves
3443
- for (std::size_t i = 0; i != header.size; ++i) {
3724
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3444
3725
  span_bytes_t node_bytes = node_bytes_(node_at_(i));
3445
3726
  if (!output(node_bytes.data(), node_bytes.size()))
3446
3727
  return result.failed("Failed to serialize into stream");
3447
- if (!progress(++processed, total))
3728
+ if (!progress(++processed, total.value))
3448
3729
  return result.failed("Terminated by user");
3449
3730
  }
3450
3731
 
@@ -3476,10 +3757,16 @@ class index_gt {
3476
3757
 
3477
3758
  // Allocate some dynamic memory to read all the levels
3478
3759
  using levels_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<level_t>;
3479
- buffer_gt<level_t, levels_allocator_t> levels(header.size);
3760
+ checked_size_result_t header_size = checked_size_from_u64(header.size);
3761
+ if (!header_size)
3762
+ return result.failed("Index is too large");
3763
+ buffer_gt<level_t, levels_allocator_t> levels(header_size.value);
3480
3764
  if (!levels)
3481
3765
  return result.failed("Out of memory");
3482
- if (!input(levels, header.size * sizeof(level_t)))
3766
+ checked_size_result_t levels_bytes = checked_mul(header_size.value, sizeof(level_t));
3767
+ if (!levels_bytes)
3768
+ return result.failed("Index is too large");
3769
+ if (!input(levels, levels_bytes.value))
3483
3770
  return result.failed("Failed to pull nodes levels from the stream");
3484
3771
 
3485
3772
  // Submit metadata
@@ -3491,26 +3778,26 @@ class index_gt {
3491
3778
 
3492
3779
  pre_ = precompute_(config_);
3493
3780
  index_limits_t limits;
3494
- limits.members = header.size;
3781
+ limits.members = header_size.value;
3495
3782
  limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
3496
3783
  limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
3497
3784
  if (!reserve(limits)) {
3498
3785
  reset();
3499
3786
  return result.failed("Out of memory");
3500
3787
  }
3501
- nodes_count_ = header.size;
3788
+ nodes_count_ = header_size.value;
3502
3789
  max_level_ = static_cast<level_t>(header.max_level);
3503
3790
  entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
3504
3791
 
3505
3792
  // Load the nodes
3506
- for (std::size_t i = 0; i != header.size; ++i) {
3793
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3507
3794
  span_bytes_t node_bytes = node_malloc_(levels[i]);
3508
3795
  if (!input(node_bytes.data(), node_bytes.size())) {
3509
3796
  reset();
3510
3797
  return result.failed("Failed to pull nodes from the stream");
3511
3798
  }
3512
3799
  nodes_[i] = node_t{node_bytes.data()};
3513
- if (!progress(i + 1, header.size))
3800
+ if (!progress(i + 1, header_size.value))
3514
3801
  return result.failed("Terminated by user");
3515
3802
  }
3516
3803
  return {};
@@ -3657,11 +3944,14 @@ class index_gt {
3657
3944
  reset();
3658
3945
  return result;
3659
3946
  }
3947
+ checked_size_result_t header_size = checked_size_from_u64(header.size);
3948
+ if (!header_size)
3949
+ return result.failed("Index is too large");
3660
3950
 
3661
3951
  // Precompute offsets of every node, but before that we need to update the configs
3662
3952
  // This could have been done with `std::exclusive_scan`, but it's only available from C++17.
3663
3953
  using offsets_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<std::size_t>;
3664
- buffer_gt<std::size_t, offsets_allocator_t> offsets(header.size);
3954
+ buffer_gt<std::size_t, offsets_allocator_t> offsets(header_size.value);
3665
3955
  if (!offsets)
3666
3956
  return result.failed("Out of memory");
3667
3957
 
@@ -3673,33 +3963,47 @@ class index_gt {
3673
3963
 
3674
3964
  pre_ = precompute_(config_);
3675
3965
  misaligned_ptr_gt<level_t> levels{(byte_t*)file.data() + offset + sizeof(header)};
3676
- offsets[0u] = offset + sizeof(header) + sizeof(level_t) * header.size;
3677
- for (std::size_t i = 1; i < header.size; ++i)
3678
- offsets[i] = offsets[i - 1] + node_bytes_(levels[i - 1]);
3966
+ checked_size_result_t levels_bytes = checked_mul(sizeof(level_t), header_size.value);
3967
+ checked_size_result_t offset_after_header = checked_add(offset, sizeof(header));
3968
+ checked_size_result_t first_offset = levels_bytes && offset_after_header
3969
+ ? checked_add(offset_after_header.value, levels_bytes.value)
3970
+ : checked_size_overflow();
3971
+ if (!first_offset)
3972
+ return result.failed("Index is too large");
3973
+ offsets[0u] = first_offset.value;
3974
+ for (std::size_t i = 1; i < header_size.value; ++i) {
3975
+ checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
3976
+ if (!next_offset)
3977
+ return result.failed("Index is too large");
3978
+ offsets[i] = next_offset.value;
3979
+ }
3679
3980
 
3680
- std::size_t total_bytes = offsets[header.size - 1] + node_bytes_(levels[header.size - 1]);
3681
- if (file.size() < total_bytes) {
3981
+ checked_size_result_t total_bytes =
3982
+ checked_add(offsets[header_size.value - 1], node_bytes_(levels[header_size.value - 1]));
3983
+ if (!total_bytes)
3984
+ return result.failed("Index is too large");
3985
+ if (file.size() < total_bytes.value) {
3682
3986
  reset();
3683
3987
  return result.failed("File is corrupted and can't fit all the nodes");
3684
3988
  }
3685
3989
 
3686
3990
  // Submit metadata and reserve memory
3687
3991
  index_limits_t limits;
3688
- limits.members = header.size;
3992
+ limits.members = header_size.value;
3689
3993
  limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
3690
3994
  limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
3691
3995
  if (!reserve(limits)) {
3692
3996
  reset();
3693
3997
  return result.failed("Out of memory");
3694
3998
  }
3695
- nodes_count_ = header.size;
3999
+ nodes_count_ = header_size.value;
3696
4000
  max_level_ = static_cast<level_t>(header.max_level);
3697
4001
  entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
3698
4002
 
3699
4003
  // Rapidly address all the nodes
3700
- for (std::size_t i = 0; i != header.size; ++i) {
4004
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3701
4005
  nodes_[i] = node_t{(byte_t*)file.data() + offsets[i]};
3702
- if (!progress(i + 1, header.size))
4006
+ if (!progress(i + 1, header_size.value))
3703
4007
  return result.failed("Terminated by user");
3704
4008
  }
3705
4009
  viewed_file_ = std::move(file);
@@ -3751,7 +4055,9 @@ class index_gt {
3751
4055
  // Progress status
3752
4056
  std::atomic<bool> do_tasks{true};
3753
4057
  std::atomic<std::size_t> processed{0};
3754
- std::size_t const total = 3 * slots_and_levels.size();
4058
+ checked_size_result_t total = checked_mul(std::size_t{3}, slots_and_levels.size());
4059
+ if (!total)
4060
+ return;
3755
4061
 
3756
4062
  // For every bottom level node, determine its parent cluster
3757
4063
  executor.dynamic(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot_as_uint) {
@@ -3764,7 +4070,7 @@ class index_gt {
3764
4070
  slots_and_levels[old_slot] = {old_slot, cluster, node_at_(old_slot).level()};
3765
4071
  ++processed;
3766
4072
  if (thread_idx == 0)
3767
- do_tasks = progress(processed.load(), total);
4073
+ do_tasks = progress(processed.load(), total.value);
3768
4074
  return do_tasks.load();
3769
4075
  });
3770
4076
  if (!do_tasks.load())
@@ -3798,7 +4104,7 @@ class index_gt {
3798
4104
  neighbor = static_cast<compressed_slot_t>(old_slot_to_new[compressed_slot_t(neighbor)]);
3799
4105
 
3800
4106
  reordered_nodes[new_slot] = new_node;
3801
- if (!progress(++processed, total))
4107
+ if (!progress(++processed, total.value))
3802
4108
  return;
3803
4109
  }
3804
4110
 
@@ -3807,7 +4113,7 @@ class index_gt {
3807
4113
  slot_transition(node_at_(old_slot).ckey(), //
3808
4114
  static_cast<compressed_slot_t>(old_slot), //
3809
4115
  static_cast<compressed_slot_t>(new_slot));
3810
- if (!progress(++processed, total))
4116
+ if (!progress(++processed, total.value))
3811
4117
  return;
3812
4118
  }
3813
4119