usearch 2.25.1 → 2.25.3

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 (76) hide show
  1. package/README.md +2 -2
  2. package/c/lib.cpp +28 -13
  3. package/cpp/test.cpp +99 -0
  4. package/include/usearch/index.hpp +338 -57
  5. package/include/usearch/index_dense.hpp +155 -50
  6. package/include/usearch/index_plugins.hpp +94 -35
  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 +1 -1
  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
@@ -9,7 +9,7 @@
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
11
  #define USEARCH_VERSION_MINOR 25
12
- #define USEARCH_VERSION_PATCH 1
12
+ #define USEARCH_VERSION_PATCH 3
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)
@@ -1246,9 +1329,11 @@ class growing_hash_set_gt {
1246
1329
  count_ = 0;
1247
1330
  }
1248
1331
 
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) {
1332
+ growing_hash_set_gt(std::size_t capacity) noexcept : count_(0u) {
1333
+ checked_size_result_t slots_count = checked_ceil2(capacity);
1334
+ checked_size_result_t bytes = slots_count ? checked_mul(slots_count.value, sizeof(element_t)) : slots_count;
1335
+ slots_ = bytes ? (element_t*)allocator_t{}.allocate(bytes.value) : nullptr;
1336
+ capacity_ = slots_ ? slots_count.value : 0u;
1252
1337
  clear();
1253
1338
  }
1254
1339
 
@@ -1306,12 +1391,21 @@ class growing_hash_set_gt {
1306
1391
  * @return `true` if enough capacity is available, `false` if memory allocation failed.
1307
1392
  */
1308
1393
  bool reserve(std::size_t new_capacity) noexcept {
1309
- new_capacity = (new_capacity * 5u) / 3u;
1394
+ checked_size_result_t scaled_capacity = checked_mul(new_capacity, std::size_t{5});
1395
+ if (!scaled_capacity)
1396
+ return false;
1397
+ new_capacity = scaled_capacity.value / 3u;
1310
1398
  if (new_capacity <= capacity_)
1311
1399
  return true;
1312
1400
 
1313
- new_capacity = ceil2(new_capacity);
1314
- element_t* new_slots = (element_t*)allocator_t{}.allocate(new_capacity * sizeof(element_t));
1401
+ checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
1402
+ if (!rounded_capacity)
1403
+ return false;
1404
+ new_capacity = rounded_capacity.value;
1405
+ checked_size_result_t new_bytes = checked_mul(new_capacity, sizeof(element_t));
1406
+ if (!new_bytes)
1407
+ return false;
1408
+ element_t* new_slots = (element_t*)allocator_t{}.allocate(new_bytes.value);
1315
1409
  if (!new_slots)
1316
1410
  return false;
1317
1411
 
@@ -1414,7 +1508,10 @@ class ring_gt {
1414
1508
  return false; // prevent data loss
1415
1509
  if (n <= capacity())
1416
1510
  return true;
1417
- n = (std::max<std::size_t>)(ceil2(n), 64u);
1511
+ checked_size_result_t rounded_capacity = checked_ceil2(n);
1512
+ if (!rounded_capacity)
1513
+ return false;
1514
+ n = (std::max<std::size_t>)(rounded_capacity.value, 64u);
1418
1515
  element_t* elements = allocator_.allocate(n);
1419
1516
  if (!elements)
1420
1517
  return false;
@@ -1443,8 +1540,7 @@ class ring_gt {
1443
1540
  bool try_push(element_t const& value) noexcept {
1444
1541
  if (head_ == tail_ && !empty_)
1445
1542
  return false; // `elements_` is full
1446
-
1447
- return push(value);
1543
+ push(value);
1448
1544
  return true;
1449
1545
  }
1450
1546
 
@@ -1504,12 +1600,22 @@ struct index_config_t {
1504
1600
  inline error_t validate() noexcept {
1505
1601
  if (connectivity == 0)
1506
1602
  connectivity = default_connectivity();
1507
- if (connectivity_base == 0)
1508
- connectivity_base = connectivity * 2;
1603
+ if (connectivity_base == 0) {
1604
+ checked_size_result_t default_base = checked_mul(connectivity, std::size_t{2});
1605
+ if (!default_base)
1606
+ return "Connectivity is too large";
1607
+ connectivity_base = default_base.value;
1608
+ }
1509
1609
  if (connectivity < 2)
1510
1610
  return "Connectivity must be at least 2, otherwise the index degenerates into ropes";
1511
1611
  if (connectivity_base < connectivity)
1512
1612
  return "Base layer should be at least as connected as the rest of the graph";
1613
+ checked_size_result_t neighbors_bytes =
1614
+ checked_mul_add(connectivity, sizeof(std::uint64_t), sizeof(std::uint32_t));
1615
+ checked_size_result_t neighbors_base_bytes =
1616
+ checked_mul_add(connectivity_base, sizeof(std::uint64_t), sizeof(std::uint32_t));
1617
+ if (!neighbors_bytes || !neighbors_base_bytes)
1618
+ return "Connectivity is too large";
1513
1619
  return {};
1514
1620
  }
1515
1621
 
@@ -1520,6 +1626,15 @@ struct index_config_t {
1520
1626
  inline bool is_valid() const noexcept { return connectivity >= 2 && connectivity_base >= connectivity; }
1521
1627
  };
1522
1628
 
1629
+ /**
1630
+ * @brief Tag type selecting the "no upfront reservation" overload of
1631
+ * @ref index_limits_t. Modeled after @c std::defer_lock: the
1632
+ * resulting limits are all-zero and produce no allocations when
1633
+ * handed to @ref index_dense_gt::try_reserve.
1634
+ */
1635
+ struct unreserved_t {};
1636
+ constexpr unreserved_t unreserved{};
1637
+
1523
1638
  /**
1524
1639
  * @brief Growth settings for the index container.
1525
1640
  * Includes the upper bound for `::members` capacity,
@@ -1527,18 +1642,32 @@ struct index_config_t {
1527
1642
  */
1528
1643
  struct index_limits_t {
1529
1644
  /// @brief Maximum number of entries in the index.
1530
- std::size_t members = 0;
1645
+ std::size_t members;
1531
1646
  /// @brief Max number of threads simultaneously updating entries.
1532
- std::size_t threads_add = std::thread::hardware_concurrency();
1647
+ std::size_t threads_add;
1533
1648
  /// @brief Max number of threads simultaneously searching entries.
1534
- std::size_t threads_search = std::thread::hardware_concurrency();
1649
+ std::size_t threads_search;
1535
1650
 
1536
1651
  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()) {}
1652
+ inline index_limits_t(std::size_t n = 0) noexcept
1653
+ : index_limits_t(n, (std::max<std::size_t>)(1, std::thread::hardware_concurrency())) {}
1654
+ inline index_limits_t(unreserved_t) noexcept : members(0), threads_add(0), threads_search(0) {}
1538
1655
  /// @brief Returns the upper limit for the number of threads.
1539
1656
  inline std::size_t threads() const noexcept { return (std::max)(threads_add, threads_search); }
1540
1657
  /// @brief Returns the concurrency-level of the index - the minimum of thread counts.
1541
1658
  inline std::size_t concurrency() const noexcept { return (std::min)(threads_add, threads_search); }
1659
+ /// @brief Returns a copy with zero thread counts replaced by the library default.
1660
+ /// Use when carrying limits forward across operations that may have left
1661
+ /// @c threads_add / @c threads_search unset (e.g. @c unreserved construction).
1662
+ inline index_limits_t with_thread_defaults() const noexcept {
1663
+ index_limits_t result = *this;
1664
+ index_limits_t const defaults;
1665
+ if (!result.threads_add)
1666
+ result.threads_add = defaults.threads_add;
1667
+ if (!result.threads_search)
1668
+ result.threads_search = defaults.threads_search;
1669
+ return result;
1670
+ }
1542
1671
  };
1543
1672
 
1544
1673
  struct index_update_config_t {
@@ -2414,6 +2543,14 @@ class index_gt {
2414
2543
  /// @brief Array of thread-specific buffers for temporary data.
2415
2544
  mutable buffer_gt<context_t, contexts_allocator_t> contexts_{};
2416
2545
 
2546
+ context_t* context_or_null_(std::size_t thread) noexcept {
2547
+ return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
2548
+ }
2549
+
2550
+ context_t const* context_or_null_(std::size_t thread) const noexcept {
2551
+ return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
2552
+ }
2553
+
2417
2554
  public:
2418
2555
  std::size_t connectivity() const noexcept { return config_.connectivity; }
2419
2556
  std::size_t capacity() const noexcept { return nodes_capacity_; }
@@ -2547,6 +2684,114 @@ class index_gt {
2547
2684
  member_iterator_t iterator_at(compressed_slot_t slot) noexcept { return {this, slot}; }
2548
2685
  member_citerator_t citerator_at(compressed_slot_t slot) const noexcept { return {this, slot}; }
2549
2686
 
2687
+ /**
2688
+ * @brief A read-only random-access range over the neighbors of a single
2689
+ * node at a single graph level. Dereferencing yields a `member_cref_t`,
2690
+ * so callers can chain traversals without touching internal slots.
2691
+ *
2692
+ * @warning The range aliases the node's adjacency tape. It is only valid
2693
+ * while the index is not being mutated. Prefer immutable indexes
2694
+ * (see `is_immutable()`) or guarantee no concurrent `add`/`update`/
2695
+ * `remove` while the view is alive.
2696
+ */
2697
+ class neighbors_view_t {
2698
+ index_gt const* index_{};
2699
+ neighbors_ref_t neighbors_{nullptr};
2700
+
2701
+ public:
2702
+ class const_iterator {
2703
+ index_gt const* index_{};
2704
+ misaligned_ptr_gt<compressed_slot_t const> position_{nullptr};
2705
+
2706
+ public:
2707
+ using iterator_category = std::random_access_iterator_tag;
2708
+ using value_type = member_cref_t;
2709
+ using difference_type = std::ptrdiff_t;
2710
+ using pointer = void;
2711
+ using reference = member_cref_t;
2712
+
2713
+ const_iterator() noexcept = default;
2714
+ const_iterator(index_gt const* index, misaligned_ptr_gt<compressed_slot_t const> position) noexcept
2715
+ : index_(index), position_(position) {}
2716
+
2717
+ reference operator*() const noexcept {
2718
+ compressed_slot_t slot = static_cast<compressed_slot_t>(*position_);
2719
+ return {index_->node_at_(slot).ckey(), slot};
2720
+ }
2721
+ compressed_slot_t slot() const noexcept { return static_cast<compressed_slot_t>(*position_); }
2722
+
2723
+ // clang-format off
2724
+ const_iterator& operator++() noexcept { ++position_; return *this; }
2725
+ const_iterator operator++(int) noexcept { const_iterator old = *this; ++position_; return old; }
2726
+ const_iterator& operator--() noexcept { --position_; return *this; }
2727
+ const_iterator operator--(int) noexcept { const_iterator old = *this; --position_; return old; }
2728
+ const_iterator& operator+=(difference_type d) noexcept { position_ = position_ + d; return *this; }
2729
+ const_iterator& operator-=(difference_type d) noexcept { position_ = position_ - d; return *this; }
2730
+ const_iterator operator+(difference_type d) const noexcept { return {index_, position_ + d}; }
2731
+ const_iterator operator-(difference_type d) const noexcept { return {index_, position_ - d}; }
2732
+ difference_type operator-(const_iterator const& other) const noexcept { return position_ - other.position_; }
2733
+ bool operator==(const_iterator const& other) const noexcept { return position_ == other.position_; }
2734
+ bool operator!=(const_iterator const& other) const noexcept { return position_ != other.position_; }
2735
+ // clang-format on
2736
+ };
2737
+
2738
+ using iterator = const_iterator;
2739
+ using value_type = member_cref_t;
2740
+ using size_type = std::size_t;
2741
+
2742
+ neighbors_view_t() noexcept = default;
2743
+ neighbors_view_t(index_gt const* index, neighbors_ref_t neighbors) noexcept
2744
+ : index_(index), neighbors_(neighbors) {}
2745
+
2746
+ std::size_t size() const noexcept { return index_ ? neighbors_.size() : 0; }
2747
+ bool empty() const noexcept { return size() == 0; }
2748
+ member_cref_t operator[](std::size_t offset) const noexcept {
2749
+ compressed_slot_t slot = neighbors_[offset];
2750
+ return {index_->node_at_(slot).ckey(), slot};
2751
+ }
2752
+ const_iterator begin() const noexcept {
2753
+ return index_ ? const_iterator{index_, neighbors_.begin()} : const_iterator{};
2754
+ }
2755
+ const_iterator end() const noexcept {
2756
+ return index_ ? const_iterator{index_, neighbors_.end()} : const_iterator{};
2757
+ }
2758
+ const_iterator cbegin() const noexcept { return begin(); }
2759
+ const_iterator cend() const noexcept { return end(); }
2760
+ };
2761
+
2762
+ /**
2763
+ * @brief Returns a read-only range over the neighbors of the node at @p slot
2764
+ * in the graph @p level. Returned view is empty when @p level exceeds
2765
+ * the node's level.
2766
+ */
2767
+ neighbors_view_t neighbors(compressed_slot_t slot, std::size_t level) const noexcept {
2768
+ node_t node = node_at_(slot);
2769
+ if (static_cast<level_t>(level) > node.level())
2770
+ return {};
2771
+ return {this, neighbors_(node, static_cast<level_t>(level))};
2772
+ }
2773
+
2774
+ /**
2775
+ * @brief Returns a read-only range over the neighbors of the node referenced
2776
+ * by @p member at the graph @p level.
2777
+ */
2778
+ neighbors_view_t neighbors(member_citerator_t member, std::size_t level) const noexcept {
2779
+ return neighbors(get_slot(member), level);
2780
+ }
2781
+
2782
+ /**
2783
+ * @brief Returns the top graph level at which the node at @p slot is present.
2784
+ */
2785
+ std::size_t level_of(compressed_slot_t slot) const noexcept {
2786
+ return static_cast<std::size_t>(static_cast<level_t>(node_at_(slot).level()));
2787
+ }
2788
+
2789
+ /**
2790
+ * @brief Returns the top graph level at which the node referenced by @p member
2791
+ * is present.
2792
+ */
2793
+ std::size_t level_of(member_citerator_t member) const noexcept { return level_of(get_slot(member)); }
2794
+
2550
2795
  dynamic_allocator_t const& dynamic_allocator() const noexcept { return dynamic_allocator_; }
2551
2796
  tape_allocator_t const& tape_allocator() const noexcept { return tape_allocator_; }
2552
2797
 
@@ -2919,7 +3164,10 @@ class index_gt {
2919
3164
  return result.failed("Can't add to an immutable index");
2920
3165
 
2921
3166
  // Make sure we have enough local memory to perform this request
2922
- context_t& context = contexts_[config.thread];
3167
+ context_t* context_ptr = context_or_null_(config.thread);
3168
+ if (!context_ptr)
3169
+ return result.failed("Reserve capacity ahead of insertions!");
3170
+ context_t& context = *context_ptr;
2923
3171
  top_candidates_t& top = context.top_candidates;
2924
3172
  next_candidates_t& next = context.next_candidates;
2925
3173
  top.clear();
@@ -3057,7 +3305,10 @@ class index_gt {
3057
3305
  compressed_slot_t updated_slot = iterator.slot_;
3058
3306
 
3059
3307
  // Make sure we have enough local memory to perform this request
3060
- context_t& context = contexts_[config.thread];
3308
+ context_t* context_ptr = context_or_null_(config.thread);
3309
+ if (!context_ptr)
3310
+ return result.failed("Reserve capacity ahead of updates!");
3311
+ context_t& context = *context_ptr;
3061
3312
  top_candidates_t& top = context.top_candidates;
3062
3313
  next_candidates_t& next = context.next_candidates;
3063
3314
  top.clear();
@@ -3425,26 +3676,31 @@ class index_gt {
3425
3676
 
3426
3677
  // Progress status
3427
3678
  std::size_t processed = 0;
3428
- std::size_t const total = 2 * header.size;
3679
+ checked_size_result_t header_size = checked_size_from_u64(header.size);
3680
+ if (!header_size)
3681
+ return result.failed("Index is too large to serialize");
3682
+ checked_size_result_t total = checked_mul(std::size_t{2}, header_size.value);
3683
+ if (!total)
3684
+ return result.failed("Index is too large to serialize");
3429
3685
 
3430
3686
  // Export the number of levels per node
3431
3687
  // That is both enough to estimate the overall memory consumption,
3432
3688
  // 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) {
3689
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3434
3690
  node_t node = node_at_(i);
3435
3691
  level_t level = node.level();
3436
3692
  if (!output(&level, sizeof(level)))
3437
3693
  return result.failed("Failed to serialize into stream");
3438
- if (!progress(++processed, total))
3694
+ if (!progress(++processed, total.value))
3439
3695
  return result.failed("Terminated by user");
3440
3696
  }
3441
3697
 
3442
3698
  // After that dump the nodes themselves
3443
- for (std::size_t i = 0; i != header.size; ++i) {
3699
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3444
3700
  span_bytes_t node_bytes = node_bytes_(node_at_(i));
3445
3701
  if (!output(node_bytes.data(), node_bytes.size()))
3446
3702
  return result.failed("Failed to serialize into stream");
3447
- if (!progress(++processed, total))
3703
+ if (!progress(++processed, total.value))
3448
3704
  return result.failed("Terminated by user");
3449
3705
  }
3450
3706
 
@@ -3476,10 +3732,16 @@ class index_gt {
3476
3732
 
3477
3733
  // Allocate some dynamic memory to read all the levels
3478
3734
  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);
3735
+ checked_size_result_t header_size = checked_size_from_u64(header.size);
3736
+ if (!header_size)
3737
+ return result.failed("Index is too large");
3738
+ buffer_gt<level_t, levels_allocator_t> levels(header_size.value);
3480
3739
  if (!levels)
3481
3740
  return result.failed("Out of memory");
3482
- if (!input(levels, header.size * sizeof(level_t)))
3741
+ checked_size_result_t levels_bytes = checked_mul(header_size.value, sizeof(level_t));
3742
+ if (!levels_bytes)
3743
+ return result.failed("Index is too large");
3744
+ if (!input(levels, levels_bytes.value))
3483
3745
  return result.failed("Failed to pull nodes levels from the stream");
3484
3746
 
3485
3747
  // Submit metadata
@@ -3491,26 +3753,26 @@ class index_gt {
3491
3753
 
3492
3754
  pre_ = precompute_(config_);
3493
3755
  index_limits_t limits;
3494
- limits.members = header.size;
3756
+ limits.members = header_size.value;
3495
3757
  limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
3496
3758
  limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
3497
3759
  if (!reserve(limits)) {
3498
3760
  reset();
3499
3761
  return result.failed("Out of memory");
3500
3762
  }
3501
- nodes_count_ = header.size;
3763
+ nodes_count_ = header_size.value;
3502
3764
  max_level_ = static_cast<level_t>(header.max_level);
3503
3765
  entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
3504
3766
 
3505
3767
  // Load the nodes
3506
- for (std::size_t i = 0; i != header.size; ++i) {
3768
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3507
3769
  span_bytes_t node_bytes = node_malloc_(levels[i]);
3508
3770
  if (!input(node_bytes.data(), node_bytes.size())) {
3509
3771
  reset();
3510
3772
  return result.failed("Failed to pull nodes from the stream");
3511
3773
  }
3512
3774
  nodes_[i] = node_t{node_bytes.data()};
3513
- if (!progress(i + 1, header.size))
3775
+ if (!progress(i + 1, header_size.value))
3514
3776
  return result.failed("Terminated by user");
3515
3777
  }
3516
3778
  return {};
@@ -3657,11 +3919,14 @@ class index_gt {
3657
3919
  reset();
3658
3920
  return result;
3659
3921
  }
3922
+ checked_size_result_t header_size = checked_size_from_u64(header.size);
3923
+ if (!header_size)
3924
+ return result.failed("Index is too large");
3660
3925
 
3661
3926
  // Precompute offsets of every node, but before that we need to update the configs
3662
3927
  // This could have been done with `std::exclusive_scan`, but it's only available from C++17.
3663
3928
  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);
3929
+ buffer_gt<std::size_t, offsets_allocator_t> offsets(header_size.value);
3665
3930
  if (!offsets)
3666
3931
  return result.failed("Out of memory");
3667
3932
 
@@ -3673,33 +3938,47 @@ class index_gt {
3673
3938
 
3674
3939
  pre_ = precompute_(config_);
3675
3940
  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]);
3941
+ checked_size_result_t levels_bytes = checked_mul(sizeof(level_t), header_size.value);
3942
+ checked_size_result_t offset_after_header = checked_add(offset, sizeof(header));
3943
+ checked_size_result_t first_offset = levels_bytes && offset_after_header
3944
+ ? checked_add(offset_after_header.value, levels_bytes.value)
3945
+ : checked_size_overflow();
3946
+ if (!first_offset)
3947
+ return result.failed("Index is too large");
3948
+ offsets[0u] = first_offset.value;
3949
+ for (std::size_t i = 1; i < header_size.value; ++i) {
3950
+ checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
3951
+ if (!next_offset)
3952
+ return result.failed("Index is too large");
3953
+ offsets[i] = next_offset.value;
3954
+ }
3679
3955
 
3680
- std::size_t total_bytes = offsets[header.size - 1] + node_bytes_(levels[header.size - 1]);
3681
- if (file.size() < total_bytes) {
3956
+ checked_size_result_t total_bytes =
3957
+ checked_add(offsets[header_size.value - 1], node_bytes_(levels[header_size.value - 1]));
3958
+ if (!total_bytes)
3959
+ return result.failed("Index is too large");
3960
+ if (file.size() < total_bytes.value) {
3682
3961
  reset();
3683
3962
  return result.failed("File is corrupted and can't fit all the nodes");
3684
3963
  }
3685
3964
 
3686
3965
  // Submit metadata and reserve memory
3687
3966
  index_limits_t limits;
3688
- limits.members = header.size;
3967
+ limits.members = header_size.value;
3689
3968
  limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
3690
3969
  limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
3691
3970
  if (!reserve(limits)) {
3692
3971
  reset();
3693
3972
  return result.failed("Out of memory");
3694
3973
  }
3695
- nodes_count_ = header.size;
3974
+ nodes_count_ = header_size.value;
3696
3975
  max_level_ = static_cast<level_t>(header.max_level);
3697
3976
  entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
3698
3977
 
3699
3978
  // Rapidly address all the nodes
3700
- for (std::size_t i = 0; i != header.size; ++i) {
3979
+ for (std::size_t i = 0; i != header_size.value; ++i) {
3701
3980
  nodes_[i] = node_t{(byte_t*)file.data() + offsets[i]};
3702
- if (!progress(i + 1, header.size))
3981
+ if (!progress(i + 1, header_size.value))
3703
3982
  return result.failed("Terminated by user");
3704
3983
  }
3705
3984
  viewed_file_ = std::move(file);
@@ -3751,7 +4030,9 @@ class index_gt {
3751
4030
  // Progress status
3752
4031
  std::atomic<bool> do_tasks{true};
3753
4032
  std::atomic<std::size_t> processed{0};
3754
- std::size_t const total = 3 * slots_and_levels.size();
4033
+ checked_size_result_t total = checked_mul(std::size_t{3}, slots_and_levels.size());
4034
+ if (!total)
4035
+ return;
3755
4036
 
3756
4037
  // For every bottom level node, determine its parent cluster
3757
4038
  executor.dynamic(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot_as_uint) {
@@ -3764,7 +4045,7 @@ class index_gt {
3764
4045
  slots_and_levels[old_slot] = {old_slot, cluster, node_at_(old_slot).level()};
3765
4046
  ++processed;
3766
4047
  if (thread_idx == 0)
3767
- do_tasks = progress(processed.load(), total);
4048
+ do_tasks = progress(processed.load(), total.value);
3768
4049
  return do_tasks.load();
3769
4050
  });
3770
4051
  if (!do_tasks.load())
@@ -3798,7 +4079,7 @@ class index_gt {
3798
4079
  neighbor = static_cast<compressed_slot_t>(old_slot_to_new[compressed_slot_t(neighbor)]);
3799
4080
 
3800
4081
  reordered_nodes[new_slot] = new_node;
3801
- if (!progress(++processed, total))
4082
+ if (!progress(++processed, total.value))
3802
4083
  return;
3803
4084
  }
3804
4085
 
@@ -3807,7 +4088,7 @@ class index_gt {
3807
4088
  slot_transition(node_at_(old_slot).ckey(), //
3808
4089
  static_cast<compressed_slot_t>(old_slot), //
3809
4090
  static_cast<compressed_slot_t>(new_slot));
3810
- if (!progress(++processed, total))
4091
+ if (!progress(++processed, total.value))
3811
4092
  return;
3812
4093
  }
3813
4094