usearch 2.26.1 → 2.26.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 (146) hide show
  1. package/README.md +1 -1
  2. package/include/usearch/index.hpp +5 -1
  3. package/numkong/CITATION.cff +13 -9
  4. package/numkong/CONTRIBUTING.md +29 -24
  5. package/numkong/Cargo.toml +1 -1
  6. package/numkong/README.md +21 -7
  7. package/numkong/binding.gyp +47 -31
  8. package/numkong/build.rs +15 -5
  9. package/numkong/c/dispatch.h +0 -1
  10. package/numkong/c/parallel.c +139 -0
  11. package/numkong/c/parallel.h +47 -0
  12. package/numkong/cmake/toolchain-wasi.cmake +39 -5
  13. package/numkong/include/README.md +24 -17
  14. package/numkong/include/numkong/attention/sapphireamx.h +6 -6
  15. package/numkong/include/numkong/attention/sme.h +2 -2
  16. package/numkong/include/numkong/capabilities.h +2 -2
  17. package/numkong/include/numkong/cast/README.md +9 -7
  18. package/numkong/include/numkong/cast/loongsonasx.h +48 -40
  19. package/numkong/include/numkong/cast/serial.h +89 -11
  20. package/numkong/include/numkong/cast.h +3 -3
  21. package/numkong/include/numkong/curved/README.md +10 -10
  22. package/numkong/include/numkong/curved/neon.h +1 -0
  23. package/numkong/include/numkong/curved/neonbfdot.h +1 -0
  24. package/numkong/include/numkong/curved/smef64.h +1 -0
  25. package/numkong/include/numkong/dot/README.md +18 -18
  26. package/numkong/include/numkong/dot/alder.h +8 -8
  27. package/numkong/include/numkong/dot/haswell.h +8 -8
  28. package/numkong/include/numkong/dot/icelake.h +12 -12
  29. package/numkong/include/numkong/dot/loongsonasx.h +6 -6
  30. package/numkong/include/numkong/dot/neon.h +2 -1
  31. package/numkong/include/numkong/dot/neonbfdot.h +1 -0
  32. package/numkong/include/numkong/dot/neonfhm.h +1 -0
  33. package/numkong/include/numkong/dot/neonsdot.h +1 -0
  34. package/numkong/include/numkong/dot/powervsx.h +3 -3
  35. package/numkong/include/numkong/dot/serial.h +19 -6
  36. package/numkong/include/numkong/dot/skylake.h +4 -4
  37. package/numkong/include/numkong/dot/svehalf.h +1 -1
  38. package/numkong/include/numkong/dot/v128relaxed.h +24 -23
  39. package/numkong/include/numkong/dots/README.md +26 -26
  40. package/numkong/include/numkong/dots/genoa.h +3 -1
  41. package/numkong/include/numkong/dots/icelake.h +1 -0
  42. package/numkong/include/numkong/dots/powervsx.h +1 -1
  43. package/numkong/include/numkong/dots/serial.h +32 -33
  44. package/numkong/include/numkong/dots/sierra.h +4 -3
  45. package/numkong/include/numkong/each/README.md +5 -3
  46. package/numkong/include/numkong/each/sapphire.h +2 -0
  47. package/numkong/include/numkong/each/skylake.h +1 -0
  48. package/numkong/include/numkong/geospatial/README.md +6 -6
  49. package/numkong/include/numkong/geospatial/v128relaxed.h +1 -0
  50. package/numkong/include/numkong/maxsim/README.md +5 -5
  51. package/numkong/include/numkong/maxsim/genoa.h +4 -0
  52. package/numkong/include/numkong/maxsim/icelake.h +1 -0
  53. package/numkong/include/numkong/maxsim/serial.h +22 -9
  54. package/numkong/include/numkong/maxsim/sme.h +3 -2
  55. package/numkong/include/numkong/mesh/README.md +7 -4
  56. package/numkong/include/numkong/mesh/genoa.h +2 -2
  57. package/numkong/include/numkong/mesh/haswell.h +8 -8
  58. package/numkong/include/numkong/mesh/neon.h +6 -6
  59. package/numkong/include/numkong/mesh/neonbfdot.h +3 -2
  60. package/numkong/include/numkong/mesh/neonfhm.h +3 -2
  61. package/numkong/include/numkong/mesh/rvv.h +4 -4
  62. package/numkong/include/numkong/mesh/serial.h +17 -17
  63. package/numkong/include/numkong/mesh/skylake.h +4 -4
  64. package/numkong/include/numkong/mesh/v128relaxed.h +4 -4
  65. package/numkong/include/numkong/mesh.hpp +2 -2
  66. package/numkong/include/numkong/numkong.h +1 -1
  67. package/numkong/include/numkong/numkong.hpp +1 -1
  68. package/numkong/include/numkong/probability/README.md +17 -15
  69. package/numkong/include/numkong/probability/skylake.h +1 -1
  70. package/numkong/include/numkong/reduce/README.md +27 -21
  71. package/numkong/include/numkong/reduce/genoa.h +2 -1
  72. package/numkong/include/numkong/reduce/icelake.h +1 -0
  73. package/numkong/include/numkong/reduce/neon.h +1 -1
  74. package/numkong/include/numkong/reduce/serial.h +9 -9
  75. package/numkong/include/numkong/reduce/skylake.h +4 -4
  76. package/numkong/include/numkong/reduce.hpp +38 -0
  77. package/numkong/include/numkong/scalar/README.md +29 -28
  78. package/numkong/include/numkong/set/README.md +17 -14
  79. package/numkong/include/numkong/set/haswell.h +15 -16
  80. package/numkong/include/numkong/set/icelake.h +15 -16
  81. package/numkong/include/numkong/set/loongsonasx.h +12 -10
  82. package/numkong/include/numkong/set/neon.h +15 -19
  83. package/numkong/include/numkong/set/powervsx.h +15 -21
  84. package/numkong/include/numkong/set/serial.h +19 -20
  85. package/numkong/include/numkong/set/v128relaxed.h +10 -8
  86. package/numkong/include/numkong/sets/icelake.h +1 -0
  87. package/numkong/include/numkong/sparse/README.md +4 -4
  88. package/numkong/include/numkong/sparse/icelake.h +1 -0
  89. package/numkong/include/numkong/sparse/neon.h +1 -0
  90. package/numkong/include/numkong/sparse/serial.h +10 -3
  91. package/numkong/include/numkong/sparse/turin.h +1 -0
  92. package/numkong/include/numkong/spatial/README.md +25 -24
  93. package/numkong/include/numkong/spatial/alder.h +2 -1
  94. package/numkong/include/numkong/spatial/haswell.h +48 -40
  95. package/numkong/include/numkong/spatial/icelake.h +2 -1
  96. package/numkong/include/numkong/spatial/neon.h +54 -46
  97. package/numkong/include/numkong/spatial/neonbfdot.h +1 -1
  98. package/numkong/include/numkong/spatial/powervsx.h +54 -46
  99. package/numkong/include/numkong/spatial/serial.h +56 -46
  100. package/numkong/include/numkong/spatial/sierra.h +4 -3
  101. package/numkong/include/numkong/spatial/skylake.h +27 -22
  102. package/numkong/include/numkong/spatial/sve.h +6 -6
  103. package/numkong/include/numkong/spatial/svebfdot.h +1 -1
  104. package/numkong/include/numkong/spatial/svehalf.h +1 -1
  105. package/numkong/include/numkong/spatial/v128relaxed.h +36 -30
  106. package/numkong/include/numkong/spatials/README.md +15 -15
  107. package/numkong/include/numkong/spatials/genoa.h +3 -0
  108. package/numkong/include/numkong/spatials/graniteamx.h +2 -0
  109. package/numkong/include/numkong/spatials/icelake.h +1 -0
  110. package/numkong/include/numkong/spatials/sierra.h +1 -0
  111. package/numkong/include/numkong/tensor.hpp +236 -123
  112. package/numkong/include/numkong/trigonometry/README.md +4 -4
  113. package/numkong/include/numkong/trigonometry/serial.h +6 -6
  114. package/numkong/include/numkong/trigonometry/v128relaxed.h +1 -0
  115. package/numkong/include/numkong/types.hpp +46 -16
  116. package/numkong/include/numkong/vector.hpp +116 -23
  117. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  118. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  119. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  120. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  121. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  122. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  123. package/numkong/javascript/README.md +18 -16
  124. package/numkong/javascript/numkong.c +70 -40
  125. package/numkong/package.json +8 -8
  126. package/numkong/rust/README.md +19 -9
  127. package/numkong/rust/cast.rs +52 -1
  128. package/numkong/rust/each.rs +480 -7
  129. package/numkong/rust/matrix.rs +877 -216
  130. package/numkong/rust/numkong.rs +12 -8
  131. package/numkong/rust/reduce.rs +433 -5
  132. package/numkong/rust/tensor.rs +450 -842
  133. package/numkong/rust/vector.rs +520 -101
  134. package/numkong/swift/README.md +5 -3
  135. package/numkong/test/README.md +4 -4
  136. package/numkong/test/test-wasi.mjs +17 -0
  137. package/numkong/test/test_each.cpp +30 -1
  138. package/numkong/test/test_each.py +74 -1
  139. package/numkong/test/test_reduce.cpp +51 -0
  140. package/numkong/test/test_tensor.cpp +2 -63
  141. package/numkong/test/test_tensor.py +93 -0
  142. package/package.json +1 -1
  143. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  144. package/prebuilds/linux-arm64/usearch.node +0 -0
  145. package/prebuilds/linux-x64/usearch.node +0 -0
  146. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -31,7 +31,7 @@
31
31
  #include <array> // `std::array`
32
32
  #include <cstdio> // `std::fprintf`, `stderr`
33
33
  #include <cstdlib> // `std::abort`
34
- #include <cstring> // `std::memset`
34
+ #include <cstring> // `std::memcpy`, `std::memcmp`, `std::memset`
35
35
  #include <span> // `std::span`
36
36
  #include <tuple> // `std::tuple_element_t`
37
37
  #include <type_traits>
@@ -117,6 +117,22 @@ struct shape_storage_ {
117
117
  return offset;
118
118
  }
119
119
 
120
+ /**
121
+ * @brief Reset the spare slots [rank, max_rank_) to a harmless 1x0 layout.
122
+ *
123
+ * `tensor_flat_lookup_` iterates a runtime `rank` bound (`flat % extents[d]; flat /= extents[d]`),
124
+ * but under `-flto` the compiler unrolls/vectorises that loop to `max_rank_` iterations and
125
+ * speculatively evaluates `flat / extents[d]` for every `d` - including `d >= rank`. Leaving
126
+ * those extents at the in-class `= {}` zero turns the unused speculative quotient into a
127
+ * hardware divide-by-zero trap. Any builder that produces a reduced-rank shape must call this.
128
+ */
129
+ constexpr void normalize_spare_() noexcept {
130
+ for (std::size_t d = rank; d < max_rank_; ++d) {
131
+ extents[d] = 1;
132
+ strides[d] = 0;
133
+ }
134
+ }
135
+
120
136
  /** @brief Create contiguous (row-major) shape storage. */
121
137
  static constexpr shape_storage_ contiguous(std::size_t const *exts, std::size_t rank_val,
122
138
  std::size_t elem_bytes) noexcept {
@@ -128,6 +144,7 @@ struct shape_storage_ {
128
144
  s.strides[i - 1] = stride;
129
145
  stride *= static_cast<std::ptrdiff_t>(exts[i - 1]);
130
146
  }
147
+ s.normalize_spare_();
131
148
  return s;
132
149
  }
133
150
  };
@@ -160,6 +177,7 @@ constexpr shape_storage_<max_rank_> make_contiguous_shape_(std::size_t const *ex
160
177
  : static_cast<std::size_t>(exts[i - 1]);
161
178
  stride *= static_cast<std::ptrdiff_t>(extent_factor);
162
179
  }
180
+ s.normalize_spare_();
163
181
  return s;
164
182
  }
165
183
 
@@ -294,6 +312,7 @@ struct tensor_view {
294
312
  sub.extents[d] = shape_.extents[d + 1];
295
313
  sub.strides[d] = shape_.strides[d + 1];
296
314
  }
315
+ sub.normalize_spare_(); // keep [rank, max_rank_) at 1x0 so -flto's speculative divides are harmless
297
316
  return {data_ + offset, sub};
298
317
  }
299
318
 
@@ -361,10 +380,11 @@ struct tensor_view {
361
380
  return {data_, shape_.extents[0], shape_.strides[0]};
362
381
  }
363
382
 
364
- /** @brief Reinterpret as a 2D matrix view. Requires rank >= 2. */
383
+ /** @brief Narrow an already-2D handle to a rank-2 type. Requires rank == 2; use reshape<2>({...}) to collapse a
384
+ * higher-rank tensor. */
365
385
  tensor_view<value_type_, 2> as_matrix() const noexcept {
366
- nk_assert_(shape_.rank >= 2);
367
- if (shape_.rank < 2) return {};
386
+ nk_assert_(shape_.rank == 2);
387
+ if (shape_.rank != 2) return {};
368
388
  shape_storage_<2> matrix_shape;
369
389
  matrix_shape.rank = 2;
370
390
  matrix_shape.extents[0] = shape_.extents[0];
@@ -391,12 +411,14 @@ struct tensor_view {
391
411
  return {data_, transposed};
392
412
  }
393
413
 
394
- /** @brief Reshape to new extents (requires contiguous layout and matching element count).
414
+ /** @brief Reshape to new extents (requires contiguous layout and matching element count). The output rank may
415
+ * differ from the source; pass it as the template argument when narrowing or widening.
395
416
  * Returns an empty view if the tensor is not contiguous or element counts don't match. */
396
- tensor_view reshape(std::initializer_list<size_type> new_extents) const noexcept {
417
+ template <std::size_t out_rank_ = max_rank_>
418
+ tensor_view<value_type_, out_rank_> reshape(std::initializer_list<size_type> new_extents) const noexcept {
397
419
  auto new_rank = new_extents.size();
398
- if (!is_contiguous() || new_rank > max_rank_ || new_rank == 0) return {};
399
- auto new_shape = make_contiguous_shape_<value_type, max_rank_>(new_extents.begin(), new_rank);
420
+ if (!is_contiguous() || new_rank > out_rank_ || new_rank == 0) return {};
421
+ auto new_shape = make_contiguous_shape_<value_type, out_rank_>(new_extents.begin(), new_rank);
400
422
  if (storage_values_for_shape_<value_type>(new_shape) != storage_values_for_shape_<value_type>(shape_))
401
423
  return {};
402
424
  return {data_, new_shape};
@@ -444,6 +466,7 @@ struct tensor_view {
444
466
  result.strides[0] = static_cast<difference_type>(sizeof(value_type));
445
467
  }
446
468
  result.rank = new_rank;
469
+ result.normalize_spare_(); // the copy of `shape_` may carry zeros in [new_rank, max_rank_)
447
470
  return {data_, result};
448
471
  }
449
472
  };
@@ -459,6 +482,9 @@ struct tensor_view {
459
482
  */
460
483
  template <typename value_type_, std::size_t max_rank_ = 8>
461
484
  struct tensor_span {
485
+ static_assert(!std::is_const_v<value_type_>,
486
+ "tensor_span requires a non-const value_type_; use tensor_view<value_type_> for read-only access");
487
+
462
488
  using value_type = value_type_;
463
489
  using size_type = std::size_t;
464
490
  using difference_type = std::ptrdiff_t;
@@ -492,11 +518,9 @@ struct tensor_span {
492
518
  constexpr bool empty() const noexcept { return data_ == nullptr || shape_.numel() == 0; }
493
519
 
494
520
  /** @brief Raw byte pointer. */
495
- constexpr char *byte_data() noexcept { return data_; }
496
- constexpr char const *byte_data() const noexcept { return data_; }
521
+ constexpr char *byte_data() const noexcept { return data_; }
497
522
  /** @brief Typed pointer (assumes data is contiguous from this pointer). */
498
- constexpr value_type *data() noexcept { return reinterpret_cast<value_type *>(data_); }
499
- constexpr value_type const *data() const noexcept { return reinterpret_cast<value_type const *>(data_); }
523
+ constexpr value_type *data() const noexcept { return reinterpret_cast<value_type *>(data_); }
500
524
  /** @brief Access the shape storage. */
501
525
  constexpr shape_storage_<max_rank_> const &shape() const noexcept { return shape_; }
502
526
 
@@ -518,6 +542,7 @@ struct tensor_span {
518
542
  sub.extents[d] = shape_.extents[d + 1];
519
543
  sub.strides[d] = shape_.strides[d + 1];
520
544
  }
545
+ sub.normalize_spare_(); // keep [rank, max_rank_) at 1x0 so -flto's speculative divides are harmless
521
546
  return {data_ + offset, sub};
522
547
  }
523
548
 
@@ -529,40 +554,22 @@ struct tensor_span {
529
554
 
530
555
  /** @brief Flat logical scalar access. */
531
556
  template <std::integral index_type_>
532
- decltype(auto) operator[](index_type_ idx) noexcept {
533
- return tensor_flat_lookup_(*this, idx);
534
- }
535
-
536
- /** @brief Const flat logical scalar access. */
537
- template <std::integral index_type_>
538
557
  decltype(auto) operator[](index_type_ idx) const noexcept {
539
- return tensor_flat_lookup_(static_cast<tensor_view<value_type_, max_rank_>>(*this), idx);
558
+ return tensor_flat_lookup_(*this, idx);
540
559
  }
541
560
 
542
561
  /** @brief Exact multi-dimensional scalar lookup via call syntax (C++20-portable). */
543
562
  template <std::integral... index_types_>
544
563
  requires(sizeof...(index_types_) >= 2)
545
- decltype(auto) operator()(index_types_... idxs) noexcept {
564
+ decltype(auto) operator()(index_types_... idxs) const noexcept {
546
565
  nk_assert_(shape_.rank == sizeof...(index_types_));
547
566
  auto coords = resolve_tensor_indices_<value_type_>(shape_, std::index_sequence_for<index_types_...> {},
548
567
  idxs...);
549
568
  return tensor_lookup_resolved_(*this, std::span<std::size_t const, sizeof...(index_types_)>(coords));
550
569
  }
551
570
 
552
- /** @brief Const full-coordinate lookup via call syntax. */
553
- template <std::integral... index_types_>
554
- requires(sizeof...(index_types_) >= 2)
555
- decltype(auto) operator()(index_types_... idxs) const noexcept {
556
- return static_cast<tensor_view<value_type_, max_rank_>>(*this)(idxs...);
557
- }
558
-
559
571
  #if NK_HAS_MULTIDIMENSIONAL_SUBSCRIPT_
560
572
  /** @brief C++23 sugar: multi-arg `[]` scalar lookup, delegates to `operator()`. */
561
- template <std::integral... index_types_>
562
- requires(sizeof...(index_types_) >= 2)
563
- decltype(auto) operator[](index_types_... idxs) noexcept {
564
- return (*this)(idxs...);
565
- }
566
573
  template <std::integral... index_types_>
567
574
  requires(sizeof...(index_types_) >= 2)
568
575
  decltype(auto) operator[](index_types_... idxs) const noexcept {
@@ -571,43 +578,26 @@ struct tensor_span {
571
578
  #endif
572
579
 
573
580
  /** @brief Trailing `slice` returns the same span. */
574
- constexpr tensor_span operator[](tensor_slice_t) noexcept { return *this; }
575
- constexpr tensor_view<value_type_, max_rank_> operator[](tensor_slice_t) const noexcept {
576
- return static_cast<tensor_view<value_type_, max_rank_>>(*this);
577
- }
581
+ constexpr tensor_span operator[](tensor_slice_t) const noexcept { return *this; }
578
582
 
579
583
  /** @brief Prefix leading-axis slicing via call syntax (C++20-portable). */
580
584
  template <typename first_type_, typename second_type_, typename... rest_types_>
581
585
  requires(trailing_tensor_slice_args_v<first_type_, second_type_, rest_types_...>)
582
- tensor_span operator()(first_type_ first, second_type_ second, rest_types_... rest) noexcept {
586
+ tensor_span operator()(first_type_ first, second_type_ second, rest_types_... rest) const noexcept {
583
587
  return tensor_slice_suffix_(*this, first, second, rest...);
584
588
  }
585
589
 
586
- /** @brief Const prefix leading-axis slicing via call syntax. */
587
- template <typename first_type_, typename second_type_, typename... rest_types_>
588
- requires(trailing_tensor_slice_args_v<first_type_, second_type_, rest_types_...>)
589
- tensor_view<value_type_, max_rank_> operator()(first_type_ first, second_type_ second,
590
- rest_types_... rest) const noexcept {
591
- return tensor_slice_suffix_(static_cast<tensor_view<value_type_, max_rank_>>(*this), first, second, rest...);
592
- }
593
-
594
590
  #if NK_HAS_MULTIDIMENSIONAL_SUBSCRIPT_
595
591
  /** @brief C++23 sugar: multi-arg `[]` slicing, delegates to `operator()`. */
596
592
  template <typename first_type_, typename second_type_, typename... rest_types_>
597
593
  requires(trailing_tensor_slice_args_v<first_type_, second_type_, rest_types_...>)
598
- tensor_span operator[](first_type_ first, second_type_ second, rest_types_... rest) noexcept {
599
- return (*this)(first, second, rest...);
600
- }
601
- template <typename first_type_, typename second_type_, typename... rest_types_>
602
- requires(trailing_tensor_slice_args_v<first_type_, second_type_, rest_types_...>)
603
- tensor_view<value_type_, max_rank_> operator[](first_type_ first, second_type_ second,
604
- rest_types_... rest) const noexcept {
594
+ tensor_span operator[](first_type_ first, second_type_ second, rest_types_... rest) const noexcept {
605
595
  return (*this)(first, second, rest...);
606
596
  }
607
597
  #endif
608
598
 
609
599
  /** @brief Rank-0 mutable scalar access. */
610
- decltype(auto) scalar_ref() noexcept {
600
+ decltype(auto) scalar_ref() const noexcept {
611
601
  nk_assert_(shape_.rank == 0);
612
602
  nk_assert_(data_ != nullptr);
613
603
  return *reinterpret_cast<value_type_ *>(data_);
@@ -617,23 +607,17 @@ struct tensor_span {
617
607
  decltype(auto) scalar() const noexcept { return static_cast<tensor_view<value_type_, max_rank_>>(*this).scalar(); }
618
608
 
619
609
  /** @brief Convert to vector_span (requires rank == 1). */
620
- vector_span<value_type> as_vector() noexcept {
610
+ vector_span<value_type> as_vector() const noexcept {
621
611
  nk_assert_(shape_.rank == 1);
622
612
  if (shape_.rank != 1) return {};
623
613
  return {data_, shape_.extents[0], shape_.strides[0]};
624
614
  }
625
615
 
626
- /** @brief Convert to vector_view (requires rank == 1). */
627
- vector_view<value_type> as_vector() const noexcept {
628
- nk_assert_(shape_.rank == 1);
629
- if (shape_.rank != 1) return {};
630
- return {static_cast<char const *>(data_), shape_.extents[0], shape_.strides[0]};
631
- }
632
-
633
- /** @brief Reinterpret as a 2D matrix span. Requires rank >= 2. */
634
- tensor_span<value_type_, 2> as_matrix() noexcept {
635
- nk_assert_(shape_.rank >= 2);
636
- if (shape_.rank < 2) return {};
616
+ /** @brief Narrow an already-2D handle to a rank-2 type. Requires rank == 2; use reshape<2>({...}) to collapse a
617
+ * higher-rank tensor. */
618
+ tensor_span<value_type_, 2> as_matrix() const noexcept {
619
+ nk_assert_(shape_.rank == 2);
620
+ if (shape_.rank != 2) return {};
637
621
  shape_storage_<2> matrix_shape;
638
622
  matrix_shape.rank = 2;
639
623
  matrix_shape.extents[0] = shape_.extents[0];
@@ -643,24 +627,11 @@ struct tensor_span {
643
627
  return {data_, matrix_shape};
644
628
  }
645
629
 
646
- /** @brief Reinterpret as a 2D const matrix view. Requires rank >= 2. */
647
- tensor_view<value_type_, 2> as_matrix() const noexcept {
648
- nk_assert_(shape_.rank >= 2);
649
- if (shape_.rank < 2) return {};
650
- shape_storage_<2> matrix_shape;
651
- matrix_shape.rank = 2;
652
- matrix_shape.extents[0] = shape_.extents[0];
653
- matrix_shape.extents[1] = shape_.extents[1];
654
- matrix_shape.strides[0] = shape_.strides[0];
655
- matrix_shape.strides[1] = shape_.strides[1];
656
- return {static_cast<char const *>(data_), matrix_shape};
657
- }
658
-
659
630
  /** @brief Check if contiguous in memory. */
660
631
  constexpr bool is_contiguous() const noexcept { return is_tensor_contiguous_<value_type>(shape_); }
661
632
 
662
633
  /** @brief Transpose: reverse the order of all dimensions (swap extents and strides). */
663
- constexpr tensor_span transpose() noexcept {
634
+ constexpr tensor_span transpose() const noexcept {
664
635
  if constexpr (dimensions_per_value<value_type>() > 1) {
665
636
  if (shape_.rank >= 2) return {};
666
637
  }
@@ -673,12 +644,14 @@ struct tensor_span {
673
644
  return {data_, transposed};
674
645
  }
675
646
 
676
- /** @brief Reshape to new extents (requires contiguous layout and matching element count).
647
+ /** @brief Reshape to new extents (requires contiguous layout and matching element count). The output rank may
648
+ * differ from the source; pass it as the template argument when narrowing or widening.
677
649
  * Returns an empty span if not contiguous or element counts don't match. */
678
- tensor_span reshape(std::initializer_list<size_type> new_extents) noexcept {
650
+ template <std::size_t out_rank_ = max_rank_>
651
+ tensor_span<value_type_, out_rank_> reshape(std::initializer_list<size_type> new_extents) const noexcept {
679
652
  auto new_rank = new_extents.size();
680
- if (!is_contiguous() || new_rank > max_rank_ || new_rank == 0) return {};
681
- auto new_shape = make_contiguous_shape_<value_type, max_rank_>(new_extents.begin(), new_rank);
653
+ if (!is_contiguous() || new_rank > out_rank_ || new_rank == 0) return {};
654
+ auto new_shape = make_contiguous_shape_<value_type, out_rank_>(new_extents.begin(), new_rank);
682
655
  if (storage_values_for_shape_<value_type>(new_shape) != storage_values_for_shape_<value_type>(shape_))
683
656
  return {};
684
657
  return {data_, new_shape};
@@ -687,56 +660,37 @@ struct tensor_span {
687
660
  /** @brief Range of mutable sub-spans along the leading dimension. */
688
661
  struct rows_spans_t {
689
662
  tensor_span parent;
690
- axis_iterator<tensor_span> begin() noexcept { return {parent, 0}; }
691
- axis_iterator<tensor_span> end() noexcept { return {parent, parent.extent(0)}; }
663
+ axis_iterator<tensor_span> begin() const noexcept { return {parent, 0}; }
664
+ axis_iterator<tensor_span> end() const noexcept { return {parent, parent.extent(0)}; }
692
665
  };
693
666
 
694
- rows_spans_t rows() noexcept { return {*this}; }
695
-
696
- /** @brief Range of immutable sub-views along the leading dimension. */
697
- struct rows_views_t {
698
- tensor_view<value_type_, max_rank_> parent;
699
- axis_iterator<tensor_view<value_type_, max_rank_>> begin() const noexcept { return {parent, 0}; }
700
- axis_iterator<tensor_view<value_type_, max_rank_>> end() const noexcept { return {parent, parent.extent(0)}; }
701
- };
702
-
703
- rows_views_t rows() const noexcept {
704
- tensor_view<value_type_, max_rank_> v = *this;
705
- return {v};
706
- }
667
+ rows_spans_t rows() const noexcept { return {*this}; }
707
668
 
708
669
  static constexpr std::size_t max_rank = max_rank_;
709
670
 
710
671
  /** @brief Mutable element iterator (begin): yields `(position, ref_or_proxy)` pairs. */
711
- tensor_span_iterator_<tensor_span> begin() noexcept { return {*this}; }
672
+ tensor_span_iterator_<tensor_span> begin() const noexcept { return {const_cast<tensor_span &>(*this)}; }
712
673
  /** @brief Mutable element iterator (end). */
713
- tensor_span_iterator_<tensor_span> end() noexcept { return {*this, true}; }
714
- /** @brief Const element iterator (begin): yields `(position, scalar)` pairs. */
715
- tensor_view_iterator_<tensor_view<value_type_, max_rank_>> begin() const noexcept {
716
- return {static_cast<tensor_view<value_type_, max_rank_>>(*this)};
717
- }
718
- /** @brief Const element iterator (end). */
719
- tensor_view_iterator_<tensor_view<value_type_, max_rank_>> end() const noexcept {
720
- return {static_cast<tensor_view<value_type_, max_rank_>>(*this), true};
721
- }
674
+ tensor_span_iterator_<tensor_span> end() const noexcept { return {const_cast<tensor_span &>(*this), true}; }
722
675
  /** @brief Number of logical scalar elements. */
723
676
  constexpr size_type size() const noexcept { return numel(); }
724
677
  /** @brief Mutable dimension-only view. */
725
- tensor_dims_view_<tensor_span_iterator_<tensor_span>> dims() noexcept {
726
- return {tensor_span_iterator_<tensor_span> {*this}, numel()};
727
- }
728
- /** @brief Const dimension-only view. */
729
- tensor_dims_view_<tensor_view_iterator_<tensor_view<value_type_, max_rank_>>> dims() const noexcept {
730
- return {tensor_view_iterator_<tensor_view<value_type_, max_rank_>> {
731
- static_cast<tensor_view<value_type_, max_rank_>>(*this)},
732
- numel()};
678
+ tensor_dims_view_<tensor_span_iterator_<tensor_span>> dims() const noexcept {
679
+ return {tensor_span_iterator_<tensor_span> {const_cast<tensor_span &>(*this)}, numel()};
733
680
  }
734
681
 
735
682
  /** @brief Flatten to 1D span (requires contiguous layout). Returns empty span if not contiguous. */
736
- tensor_span flatten() noexcept { return reshape({numel()}); }
683
+ tensor_span flatten() const noexcept { return reshape({numel()}); }
684
+
685
+ /** @brief Zero-fill every element (declared here, defined after the free `fill_zeros`). */
686
+ bool fill_zeros() noexcept;
687
+ /** @brief Fill every element with `value` (declared here, defined after the free `fill`). */
688
+ bool fill(value_type value) noexcept;
689
+ /** @brief Copy from a same-shape view (declared here, defined after the free `copy`). */
690
+ bool copy_from(tensor_view<value_type_, max_rank_> input) noexcept;
737
691
 
738
692
  /** @brief Remove dimensions of size 1. */
739
- tensor_span squeeze() noexcept {
693
+ tensor_span squeeze() const noexcept {
740
694
  auto result = shape_;
741
695
  size_type new_rank = 0;
742
696
  for (size_type i = 0; i < shape_.rank; ++i) {
@@ -752,6 +706,7 @@ struct tensor_span {
752
706
  result.strides[0] = static_cast<difference_type>(sizeof(value_type));
753
707
  }
754
708
  result.rank = new_rank;
709
+ result.normalize_spare_(); // the copy of `shape_` may carry zeros in [new_rank, max_rank_)
755
710
  return {data_, result};
756
711
  }
757
712
  };
@@ -887,6 +842,7 @@ tensor_type_ tensor_slice_suffix_(tensor_type_ input, all_t, rest_types_... rest
887
842
  result_shape.extents[1 + d] = inner.extent(d);
888
843
  result_shape.strides[1 + d] = inner.stride_bytes(d);
889
844
  }
845
+ result_shape.normalize_spare_(); // see shape_storage_::normalize_spare_ — -flto speculative-divide guard
890
846
 
891
847
  // The data pointer is the inner slice's offset relative to the first row,
892
848
  // applied to the original data pointer.
@@ -926,6 +882,7 @@ tensor_type_ tensor_slice_suffix_(tensor_type_ input, range r, rest_types_... re
926
882
  result_shape.extents[d] = input.extent(d);
927
883
  result_shape.strides[d] = input.stride_bytes(d);
928
884
  }
885
+ result_shape.normalize_spare_();
929
886
  using byte_ptr = decltype(input.byte_data());
930
887
  return {const_cast<byte_ptr>(input.byte_data() + data_offset), result_shape};
931
888
  }
@@ -942,6 +899,7 @@ tensor_type_ tensor_slice_suffix_(tensor_type_ input, range r, rest_types_... re
942
899
  result_shape.extents[1 + d] = inner.extent(d);
943
900
  result_shape.strides[1 + d] = inner.stride_bytes(d);
944
901
  }
902
+ result_shape.normalize_spare_();
945
903
 
946
904
  using byte_ptr = decltype(input.byte_data());
947
905
  auto inner_byte_offset = inner.byte_data() - first_row.byte_data();
@@ -1555,13 +1513,19 @@ struct tensor {
1555
1513
  /** @brief Transpose: reverse dimension order (mutable span). */
1556
1514
  span_type transpose() noexcept { return span().transpose(); }
1557
1515
 
1558
- /** @brief Reshape (immutable view). Requires contiguous layout and matching element count. */
1559
- view_type reshape(std::initializer_list<size_type> new_extents) const noexcept {
1560
- return view().reshape(new_extents);
1516
+ /** @brief Reshape (immutable view). The output rank may differ from the source; pass it as the template
1517
+ * argument when narrowing or widening. Requires contiguous layout and matching element count. */
1518
+ template <std::size_t out_rank_ = max_rank_>
1519
+ tensor_view<value_type_, out_rank_> reshape(std::initializer_list<size_type> new_extents) const noexcept {
1520
+ return view().template reshape<out_rank_>(new_extents);
1561
1521
  }
1562
1522
 
1563
- /** @brief Reshape (mutable span). Requires contiguous layout and matching element count. */
1564
- span_type reshape(std::initializer_list<size_type> new_extents) noexcept { return span().reshape(new_extents); }
1523
+ /** @brief Reshape (mutable span). The output rank may differ from the source; pass it as the template
1524
+ * argument when narrowing or widening. Requires contiguous layout and matching element count. */
1525
+ template <std::size_t out_rank_ = max_rank_>
1526
+ tensor_span<value_type_, out_rank_> reshape(std::initializer_list<size_type> new_extents) noexcept {
1527
+ return span().template reshape<out_rank_>(new_extents);
1528
+ }
1565
1529
 
1566
1530
  /** @brief Check if contiguous in memory. Always true for freshly-constructed tensors. */
1567
1531
  constexpr bool is_contiguous() const noexcept { return view().is_contiguous(); }
@@ -1685,6 +1649,15 @@ struct tensor {
1685
1649
 
1686
1650
  /** @brief Squeeze (mutable span). Removes size-1 dimensions. */
1687
1651
  span_type squeeze() noexcept { return span().squeeze(); }
1652
+
1653
+ /** @brief Zero-fill every element. */
1654
+ bool fill_zeros() noexcept { return span().fill_zeros(); }
1655
+
1656
+ /** @brief Fill every element with `value`. */
1657
+ bool fill(value_type value) noexcept { return span().fill(value); }
1658
+
1659
+ /** @brief Copy from a same-shape view. */
1660
+ bool copy_from(view_type input) noexcept { return span().copy_from(input); }
1688
1661
  };
1689
1662
 
1690
1663
  /** @brief Non-member swap. */
@@ -1856,6 +1829,7 @@ bool for_each_axis_lane_(tensor_view<value_type_, max_rank_> input, std::size_t
1856
1829
  lane_shape.rank = 1;
1857
1830
  lane_shape.extents[0] = input.extent(axis);
1858
1831
  lane_shape.strides[0] = input.stride_bytes(axis);
1832
+ lane_shape.normalize_spare_();
1859
1833
 
1860
1834
  std::size_t remaining_dims[max_rank_] = {};
1861
1835
  std::size_t remaining_count = 0;
@@ -1949,6 +1923,7 @@ tensor_view<value_type_, max_rank_> collapse_contiguous_tail_(tensor_view<value_
1949
1923
  for (std::size_t i = input.rank() - tail_dims; i < input.rank(); ++i) product *= input.extent(i);
1950
1924
  s.extents[s.rank - 1] = product;
1951
1925
  s.strides[s.rank - 1] = static_cast<std::ptrdiff_t>(sizeof(value_type_));
1926
+ s.normalize_spare_();
1952
1927
  return {input.byte_data(), s};
1953
1928
  }
1954
1929
 
@@ -1966,6 +1941,7 @@ tensor_span<value_type_, max_rank_> collapse_contiguous_tail_(tensor_span<value_
1966
1941
  for (std::size_t i = input.rank() - tail_dims; i < input.rank(); ++i) product *= input.extent(i);
1967
1942
  s.extents[s.rank - 1] = product;
1968
1943
  s.strides[s.rank - 1] = static_cast<std::ptrdiff_t>(sizeof(value_type_));
1944
+ s.normalize_spare_();
1969
1945
  return {input.byte_data(), s};
1970
1946
  }
1971
1947
 
@@ -2058,8 +2034,145 @@ bool elementwise_into_(tensor_view<value_type_, max_rank_> a, tensor_view<value_
2058
2034
  return true;
2059
2035
  }
2060
2036
 
2037
+ /** @brief Output-only traversal: walks a span, collapses contiguous tail dims, and invokes
2038
+ * `leaf(byte_data, byte_count)` on each maximal contiguous byte run. Strided rank-1 leaves
2039
+ * invoke `leaf` once per element. Sub-byte dtypes require a fully contiguous span. */
2040
+ template <typename value_type_, std::size_t max_rank_, typename leaf_function_>
2041
+ bool span_for_each_contiguous_run_(tensor_span<value_type_, max_rank_> output, leaf_function_ &&leaf) noexcept {
2042
+ if (!tensor_layout_supported_(output)) return false;
2043
+ if (output.empty()) return true;
2044
+ if (output.rank() >= 2) {
2045
+ auto tail_dims = shared_contiguous_tail_dims_<value_type_, max_rank_>(output.rank(), output.shape().extents,
2046
+ {output.shape().strides});
2047
+ if (tail_dims >= 2)
2048
+ return span_for_each_contiguous_run_<value_type_, max_rank_>(collapse_contiguous_tail_(output, tail_dims),
2049
+ std::forward<leaf_function_>(leaf));
2050
+ for (std::size_t row_index = 0; row_index < output.extent(0); ++row_index) {
2051
+ auto signed_index = static_cast<std::ptrdiff_t>(row_index);
2052
+ if (!span_for_each_contiguous_run_<value_type_, max_rank_>(output.slice_leading(signed_index), leaf))
2053
+ return false;
2054
+ }
2055
+ return true;
2056
+ }
2057
+ if (output.byte_data() == nullptr) return false;
2058
+ auto element_count = output.extent(0);
2059
+ if (element_count == 0) return true;
2060
+ if constexpr (dimensions_per_value<value_type_>() > 1) {
2061
+ if (!output.is_contiguous()) return false;
2062
+ auto byte_count = dims_to_values_<value_type_>(element_count) * sizeof(value_type_);
2063
+ leaf(output.byte_data(), byte_count);
2064
+ return true;
2065
+ }
2066
+ else {
2067
+ auto element_stride_bytes = output.stride_bytes(0);
2068
+ if (element_stride_bytes == static_cast<std::ptrdiff_t>(sizeof(value_type_))) {
2069
+ leaf(output.byte_data(), element_count * sizeof(value_type_));
2070
+ return true;
2071
+ }
2072
+ auto *base_byte_data = output.byte_data();
2073
+ for (std::size_t element_index = 0; element_index < element_count; ++element_index)
2074
+ leaf(base_byte_data + static_cast<std::ptrdiff_t>(element_index) * element_stride_bytes,
2075
+ sizeof(value_type_));
2076
+ return true;
2077
+ }
2078
+ }
2079
+
2061
2080
  #pragma endregion Helpers
2062
2081
 
2082
+ #pragma region Tensor Fill and Copy
2083
+
2084
+ /** @brief Zero-fill every element of `output` via memset on each maximal contiguous run.
2085
+ * Works for every dtype since `is_memset_zero_safe_v` is true across all numeric_dtypes.
2086
+ * Returns false on unsupported layout. */
2087
+ template <typename value_type_, std::size_t max_rank_>
2088
+ bool fill_zeros(tensor_span<value_type_, max_rank_> output) noexcept {
2089
+ static_assert(is_memset_zero_safe_v<value_type_>,
2090
+ "fill_zeros requires a dtype whose binary-zero is the value-zero");
2091
+ return span_for_each_contiguous_run_<value_type_, max_rank_>(
2092
+ output, [](char *byte_data, std::size_t byte_count) noexcept {
2093
+ std::memset(static_cast<void *>(byte_data), 0, byte_count);
2094
+ });
2095
+ }
2096
+
2097
+ /** @brief Fill every element of `output` with `value`. Two-step strategy: first memsets the
2098
+ * buffer to binary zero (avoiding NaN/inf propagation from uninitialised memory), then
2099
+ * overlays the value via memset (1-byte storage) or a typed scalar broadcast loop
2100
+ * (multi-byte storage). Returns false on unsupported layout. */
2101
+ template <typename value_type_, std::size_t max_rank_>
2102
+ bool fill(tensor_span<value_type_, max_rank_> output, value_type_ value) noexcept {
2103
+ if (!fill_zeros<value_type_, max_rank_>(output)) return false;
2104
+ // Skip the overlay when `value` is bitwise-equal to a default-constructed `value_type_`,
2105
+ // since the memset above has already produced exactly that pattern.
2106
+ value_type_ const default_value {};
2107
+ if (std::memcmp(&value, &default_value, sizeof(value_type_)) == 0) return true;
2108
+ if constexpr (sizeof(value_type_) == 1) {
2109
+ unsigned char byte_pattern;
2110
+ std::memcpy(&byte_pattern, &value, 1);
2111
+ return span_for_each_contiguous_run_<value_type_, max_rank_>(
2112
+ output, [byte_pattern](char *byte_data, std::size_t byte_count) noexcept {
2113
+ std::memset(static_cast<void *>(byte_data), byte_pattern, byte_count);
2114
+ });
2115
+ }
2116
+ else {
2117
+ return span_for_each_contiguous_run_<value_type_, max_rank_>(
2118
+ output, [&value](char *byte_data, std::size_t byte_count) noexcept {
2119
+ auto *typed_data = reinterpret_cast<value_type_ *>(byte_data);
2120
+ auto element_count = byte_count / sizeof(value_type_);
2121
+ for (std::size_t element_index = 0; element_index < element_count; ++element_index)
2122
+ typed_data[element_index] = value;
2123
+ });
2124
+ }
2125
+ }
2126
+
2127
+ /** @brief Copy `input` element-by-element into `output`. Recursive traversal collapses
2128
+ * contiguous tail dims into one memcpy per leaf; strided outer dims still recurse.
2129
+ * Returns false on shape mismatch or unsupported layout. */
2130
+ template <typename value_type_, std::size_t max_rank_>
2131
+ bool copy(tensor_view<value_type_, max_rank_> input, tensor_span<value_type_, max_rank_> output) noexcept {
2132
+ return elementwise_into_<value_type_, max_rank_>(
2133
+ input, output,
2134
+ [](tensor_view<value_type_, max_rank_> input_leaf, tensor_span<value_type_, max_rank_> output_leaf) {
2135
+ auto byte_count = dims_to_values_<value_type_>(input_leaf.extent(0)) * sizeof(value_type_);
2136
+ std::memcpy(static_cast<void *>(output_leaf.byte_data()), static_cast<void const *>(input_leaf.byte_data()),
2137
+ byte_count);
2138
+ });
2139
+ }
2140
+
2141
+ /** @brief Allocating copy: returns a fresh contiguous tensor with the same shape and contents
2142
+ * as `input`. Returns an empty tensor on allocation failure or empty input. */
2143
+ template <typename value_type_, std::size_t max_rank_ = 8, typename allocator_type_ = aligned_allocator<value_type_>>
2144
+ [[nodiscard]] tensor<value_type_, allocator_type_, max_rank_> try_copy(
2145
+ tensor_view<value_type_, max_rank_> input) noexcept {
2146
+ using out_tensor_t = tensor<value_type_, allocator_type_, max_rank_>;
2147
+ if (input.empty()) return out_tensor_t {};
2148
+ auto const &input_shape = input.shape();
2149
+ auto result = out_tensor_t::try_empty(input_shape.extents, input_shape.rank);
2150
+ if (result.empty()) return result;
2151
+ if (!copy<value_type_, max_rank_>(input, result.span())) return out_tensor_t {};
2152
+ return result;
2153
+ }
2154
+
2155
+ #pragma endregion Tensor Fill and Copy
2156
+
2157
+ #pragma region Tensor Span Member Sugar
2158
+
2159
+ template <typename value_type_, std::size_t max_rank_>
2160
+ bool tensor_span<value_type_, max_rank_>::fill_zeros() noexcept {
2161
+ return ashvardanian::numkong::fill_zeros<value_type_, max_rank_>(*this);
2162
+ }
2163
+
2164
+ template <typename value_type_, std::size_t max_rank_>
2165
+ bool tensor_span<value_type_, max_rank_>::fill(value_type value) noexcept {
2166
+ return ashvardanian::numkong::fill<value_type_, max_rank_>(*this, value);
2167
+ }
2168
+
2169
+ template <typename value_type_, std::size_t max_rank_>
2170
+ bool tensor_span<value_type_, max_rank_>::copy_from(tensor_view<value_type_, max_rank_> input) noexcept {
2171
+ return ashvardanian::numkong::copy<value_type_, max_rank_>(input, *this);
2172
+ }
2173
+
2174
+ #pragma endregion Tensor Span Member Sugar
2175
+
2063
2176
  } // namespace ashvardanian::numkong
2064
2177
 
2065
2178
  #endif // NK_TENSOR_HPP
@@ -49,16 +49,16 @@ def atan(a: np.ndarray) -> np.ndarray:
49
49
 
50
50
  ### Cody-Waite Range Reduction
51
51
 
52
- All trigonometric kernels reduce the input angle to $[-\pi/4, \pi/4]$ before polynomial evaluation using Cody-Waite argument reduction.
52
+ All trigonometric kernels reduce the input angle to $[-\pi/2, \pi/2]$ before polynomial evaluation using Cody-Waite argument reduction.
53
53
  The constant $\pi$ is split into high and low parts ($\pi_{\text{hi}} + \pi_{\text{lo}}$) to maintain precision during the subtraction $x - n\pi$: `reduced = (x - n * pi_hi) - n * pi_lo`.
54
54
  Single-part subtraction would lose ~3 bits of precision for large multiples of $\pi$; the two-part split preserves the full mantissa.
55
- The quadrant index $n = \text{round}(x / \pi)$ selects which trigonometric identity to apply (sin-cos swap, sign flip) via a 2-bit branch.
55
+ The multiple $n = \text{round}(x / \pi)$ decides the sign of the result through its parity alone; cosine reuses the same sine polynomial by folding an extra $\pi/2$ into the reduction offset.
56
56
 
57
57
  ### Minimax Polynomial Approximation
58
58
 
59
59
  `nk_each_sin_f32_serial`, `nk_each_cos_f32_serial` evaluate degree-9 minimax polynomials via Horner's method after range reduction.
60
- The polynomial coefficients are precomputed to minimize maximum error over $[-\pi/4, \pi/4]$ — Chebyshev-optimal, not Taylor truncation.
61
- Horner evaluation: `p = c9*x^2 + c7; p = p*x^2 + c5; p = p*x^2 + c3; p = p*x^2 + c1; p = p*x` — 4 FMA operations plus 1 multiply for the final odd-power term.
60
+ The polynomial coefficients are precomputed to minimize maximum error over $[-\pi/2, \pi/2]$ — a minimax fit rather than a plain Taylor truncation.
61
+ Horner evaluation: `p = c9; p = p*x^2 + c7; p = p*x^2 + c5; p = p*x^2 + c3; result = p*x^3 + x` — 3 FMA operations for the polynomial plus one more against the cubed angle.
62
62
  `nk_each_sin_f64_serial` uses degree-19 polynomials for 52-bit mantissa coverage.
63
63
 
64
64
  ### Vectorized Polynomial Evaluation