Pixie
Loading...
Searching...
No Matches
sequence_tree.h
1#pragma once
2
3#include <pixie/permutations/detail/sequence_tree_kernels.h>
4#include <pixie/permutations/detail/tree_policy.h>
6#include <pixie/storage/aligned.h>
7
8#include <algorithm>
9#include <array>
10#include <bit>
11#include <cassert>
12#include <concepts>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <memory>
17#include <ranges>
18#include <stdexcept>
19#include <type_traits>
20#include <utility>
21
22#ifdef PIXIE_SEQUENCE_TREE_TESTING
23#include <unordered_set>
24#include <vector>
25#endif
26
28namespace pixie {
29
30template <class Index,
31 std::size_t StorageBits,
32 std::size_t Fanout,
33 LengthLayout Layout>
34class Permutation;
35} // namespace pixie
36
37// Unsupported implementation shared by the two public sequence families.
38namespace pixie::permutations::detail {
39
53template <class B>
54concept SequenceBlock =
55 std::is_nothrow_default_constructible_v<B> &&
56 std::is_nothrow_move_constructible_v<B> &&
57 std::is_nothrow_move_assignable_v<B> && std::is_nothrow_destructible_v<B> &&
58 requires(B& a, B& b, const B& c, std::size_t n) {
59 typename B::value_type;
60 requires B::capacity >= 1 &&
61 B::capacity <=
62 std::numeric_limits<std::size_t>::max() / 2;
63 { c.size() } noexcept -> std::same_as<std::size_t>;
64 { c[n] } -> std::same_as<typename B::value_type>;
65 { a.rotate_left(n, n, n) } noexcept -> std::same_as<void>;
66 { a.redistribute(b, n) } noexcept -> std::same_as<void>;
67 };
68
76template <class B>
77concept InsertableSequenceBlock =
78 SequenceBlock<B> &&
79 requires(B& a, std::size_t n, typename B::value_type v) {
80 { a.insert_at(n, v) } noexcept -> std::same_as<void>;
81 };
82
117template <SequenceBlock Block,
118 std::size_t Fanout = 8,
119 LengthLayout Layout = LengthLayout::cumulative,
120 bool IndexBias = false,
121 class Policy = SequenceTreePolicy>
122class SequenceTree {
123 static_assert(Fanout >= 4 && Fanout % 2 == 0);
124 using ChildOrder = typename Policy::template Order<Fanout>;
125 static_assert(!IndexBias || ChildOrder::supports_bias);
126 static_assert(!IndexBias ||
127 (std::is_unsigned_v<typename Block::value_type> &&
128 !std::is_same_v<typename Block::value_type, bool>));
129 static_assert(!IndexBias || requires(Block& block, std::uint64_t bias) {
130 { block.add_bias(bias) } noexcept -> std::same_as<void>;
131 });
132 struct NoBias {};
133 struct PendingBias {
134 std::uint64_t value = 0;
135 };
136 using Bias = std::conditional_t<IndexBias, PendingBias, NoBias>;
137 struct alignas(std::max(alignof(CacheLine), alignof(Block))) Leaf {
138 Block block;
139 [[no_unique_address]] Bias bias;
140 };
141 struct alignas(IndexBias ? alignof(CacheLine)
143 : alignof(std::max_align_t)) Node
144 : ChildOrder::Header {
145 // pack initializes every occupied child and measure. A spare's only
146 // readable slot is its free-list link, initialized by Pool::recycle.
147 // Avoid clearing unused capacity during allocation preflight.
148 std::array<void*, Fanout> children;
149 std::array<std::size_t, Fanout - 1> measures;
150 [[no_unique_address]] Bias bias;
151 [[no_unique_address]] ChildOrder order;
152
153 void*& child(std::size_t i, std::size_t height) noexcept {
154 return children[order.slot(*this, i, height)];
155 }
156 void* child(std::size_t i, std::size_t height) const noexcept {
157 return children[order.slot(*this, i, height)];
158 }
159 };
160 template <class, std::size_t, std::size_t, LengthLayout>
161 friend class ::pixie::Permutation;
162 static constexpr std::size_t max_height =
163 std::numeric_limits<std::size_t>::digits;
164 static constexpr std::size_t partial_prefix_min_children = 10;
165 static constexpr std::size_t minimum_leaf_size =
166 (static_cast<std::size_t>(Block::capacity) + 1) / 2;
167
168 public:
170 using block_type = Block;
172 using value_type = typename Block::value_type;
174 static constexpr std::size_t block_capacity = Block::capacity;
176 static constexpr std::size_t block_storage_bytes = sizeof(Leaf);
178 static constexpr std::size_t node_storage_bytes = sizeof(Node);
180 static constexpr std::size_t block_alignment = alignof(Leaf);
182 static constexpr std::size_t node_alignment = alignof(Node);
183
184#ifdef PIXIE_SEQUENCE_TREE_TESTING
192 struct TestCounters : Policy::TestCounters {
193 std::size_t allocations = 0;
194 std::size_t node_visits = 0;
195 std::size_t child_transfers = 0;
196 std::size_t payload_mutations = 0;
197 std::size_t live_allocations = 0;
198 std::size_t peak_allocations = 0;
199 std::size_t live_bytes = 0;
200 std::size_t peak_bytes = 0;
201 };
203 inline static thread_local TestCounters test_counters{};
208 static void test_fail_after(std::ptrdiff_t n) noexcept { failure_ = n; }
211 static void test_reset_counters() noexcept {
212 const auto live = test_counters.live_allocations;
213 const auto bytes = test_counters.live_bytes;
214 test_counters = {};
215 test_counters.live_allocations = test_counters.peak_allocations = live;
216 test_counters.live_bytes = test_counters.peak_bytes = bytes;
217 }
218#endif
220 SequenceTree() noexcept = default;
222 SequenceTree(const SequenceTree&) = delete;
224 SequenceTree& operator=(const SequenceTree&) = delete;
226 SequenceTree(SequenceTree&&) noexcept = default;
228 SequenceTree& operator=(SequenceTree&&) noexcept = default;
230 std::size_t size() const noexcept { return root_.total; }
233 bool empty() const noexcept { return !root_.p; }
236 std::size_t height() const noexcept { return root_.height; }
243 std::size_t regular_prefix_size() const noexcept
244 requires(!IndexBias && std::has_single_bit(Fanout) &&
245 Layout == LengthLayout::cumulative)
246 {
247 if (height() == 0) {
248 return size();
249 }
250 const auto prefix = static_cast<const Node*>(root_.p)->regular_prefix;
251 if (prefix == 0) {
252 return 0;
253 }
254 // pack records a nonzero prefix only when the child capacity fits.
255 const auto shift = (height() - 1) * std::countr_zero(Fanout);
256 return prefix * (block_capacity << shift);
257 }
265 value_type read_regular_unchecked(std::size_t index) const noexcept
266 requires(!IndexBias && std::has_single_bit(Fanout) &&
267 Layout == LengthLayout::cumulative &&
268 requires(const Block& block) {
269 {
270 block.read_unchecked(index)
271 } noexcept -> std::same_as<value_type>;
272 })
273 {
274 assert(index < regular_prefix_size());
275 const auto block_index = index / block_capacity;
276 auto* p = root_.p;
277 for (auto h = height(); h != 0; --h) {
278 visit();
279 const auto child =
280 (block_index >> ((h - 1) * std::countr_zero(Fanout))) & (Fanout - 1);
281 p = static_cast<const Node*>(p)->child(child, h);
282 }
283 return static_cast<const Leaf*>(p)->block.read_unchecked(index %
284 block_capacity);
285 }
292 value_type operator[](std::size_t i) const {
293 if (i >= size()) {
294 throw std::out_of_range("SequenceTree: index");
295 }
296 const auto position = locate<IndexBias>(root_, i);
297 const auto& block = std::as_const(position.leaf->block);
298 const auto value = [&] {
299 if constexpr (requires {
300 {
301 block.read_unchecked(position.offset)
302 } noexcept -> std::same_as<value_type>;
303 }) {
304 return block.read_unchecked(position.offset);
305 } else {
306 return block[position.offset];
307 }
308 }();
309 if constexpr (IndexBias) {
310 return static_cast<value_type>(value + position.bias.value);
311 } else {
312 return value;
313 }
314 }
331 template <class Pred>
332 std::size_t lower_bound(Pred pred) const {
333 if (empty()) {
334 return 0;
335 }
336 std::size_t base = 0;
337 std::size_t total = root_.total;
338 void* p = root_.p;
339 Bias bias{};
340 for (auto h = root_.height; h != 0; --h) {
341 auto& node = *static_cast<Node*>(p);
342 if constexpr (IndexBias) {
343 bias.value += node.bias.value;
344 }
345 std::size_t lo = 0;
346 std::size_t hi = node.count;
347 while (lo < hi) {
348 const auto mid = lo + (hi - lo) / 2;
349 if (pred(edge_value(node.child(mid, h), h - 1, bias, true))) {
350 hi = mid;
351 } else {
352 lo = mid + 1;
353 }
354 }
355 if (lo == node.count) {
356 return base + total;
357 }
358 std::size_t prefix;
359 std::size_t child_total;
360 if constexpr (Layout == LengthLayout::cumulative) {
361 prefix = lo == 0 ? 0 : node.measures[lo - 1];
362 const auto end = lo + 1 == node.count ? total : node.measures[lo];
363 child_total = end - prefix;
364 } else {
365 prefix = 0;
366 for (std::size_t i = 0; i < lo; ++i) {
367 prefix += length(node, i, total, prefix);
368 }
369 child_total = length(node, lo, total, prefix);
370 }
371 base += prefix;
372 total = child_total;
373 p = node.child(lo, h);
374 }
375 auto& leaf = *static_cast<Leaf*>(p);
376 if constexpr (IndexBias) {
377 bias.value += leaf.bias.value;
378 }
379 const auto& block = std::as_const(leaf.block);
380 std::size_t lo = 0;
381 std::size_t hi = block.size();
382 while (lo < hi) {
383 const auto mid = lo + (hi - lo) / 2;
384 const auto raw = block[mid];
385 const auto v = [&] {
386 if constexpr (IndexBias) {
387 return static_cast<value_type>(raw + bias.value);
388 } else {
389 return raw;
390 }
391 }();
392 if (pred(v)) {
393 hi = mid;
394 } else {
395 lo = mid + 1;
396 }
397 }
398 return base + lo;
399 }
414 template <std::ranges::input_range Range>
415 requires std::
416 constructible_from<Block, std::ranges::range_rvalue_reference_t<Range>>
417 static SequenceTree from_blocks(Range&& blocks) {
418 auto it = std::ranges::begin(blocks);
419 const auto end = std::ranges::end(blocks);
420 if (it == end) {
421 return {};
422 }
423 Block pending(std::ranges::iter_move(it));
424 std::size_t total = pending.size();
425 if (total > Block::capacity) {
426 throw std::length_error("SequenceTree: construction size");
427 }
428 ++it;
429 if (it == end) {
430 // Avoid initializing O(F*max_height) owning slots for a single leaf.
431 SequenceTree result;
432 if (total != 0) {
433 result.root_ = Owner(allocate<Leaf>(), total, 0);
434 static_cast<Leaf*>(result.root_.p)->block = std::move(pending);
435 }
436 return result;
437 }
438 return from_blocks_many(std::move(it), end, pending, total);
439 }
449 SequenceTree split_off(std::size_t p) {
450 if (p > size()) {
451 throw std::out_of_range("SequenceTree: split position");
452 }
453 SequenceTree result;
454 if (p == size()) {
455 return result;
456 }
457 if (p == 0) {
458 return std::move(*this);
459 }
460 Pool pool;
461 pool.reserve(3 * root_.height, locate(root_, p).offset != 0);
462 auto pair = split(std::move(root_), p, pool);
463 root_ = std::move(pair.left);
464 result.root_ = std::move(pair.right);
465 reserve_.trim(size(), sizeof(value_type));
466 return result;
467 }
477 void merge(SequenceTree& donor) { merge_impl<false>(donor); }
494 void rotate_left(std::size_t left, std::size_t right, std::size_t distance) {
495 if (left > right || right > size()) {
496 throw std::out_of_range("SequenceTree: rotation range");
497 }
498 const auto n = right - left;
499 if (n == 0 || (distance %= n) == 0) {
500 return;
501 }
502 if (rotate_local(left, right, distance)) {
503 return;
504 }
505 if constexpr (!IndexBias && sizeof(value_type) <= sizeof(std::uint64_t) &&
506 std::is_trivially_copyable_v<value_type> &&
507 std::is_default_constructible_v<value_type> &&
508 requires(Block& block, std::size_t i, value_type value) {
509 { block.set_at(i, value) } noexcept;
510 }) {
511 // Bounded short edits keep the existing leaves, lengths and topology.
512 // Gather everything before writing, so overlapping source/destination
513 // ranges are safe. This path neither allocates nor changes ownership.
514 constexpr std::size_t limit = 128;
515 if (n <= limit) {
516 struct Segment {
517 Block* block;
518 std::size_t offset, count;
519 };
520 std::array<value_type, limit> values;
521 std::array<Segment, limit> segments;
522 std::size_t count = 0, done = 0;
523 while (done < n) {
524 const auto position = locate(root_, left + done);
525 auto& block = position.leaf->block;
526 const auto take = std::min(n - done, block.size() - position.offset);
527 segments[count++] = {&block, position.offset, take};
528 for (std::size_t i = 0; i < take; ++i) {
529 values[done + i] = std::as_const(block)[position.offset + i];
530 }
531 done += take;
532 }
533 auto source = distance;
534 for (std::size_t j = 0; j < count; ++j) {
535 const auto& segment = segments[j];
536 for (std::size_t i = 0; i < segment.count; ++i) {
537 segment.block->set_at(segment.offset + i, values[source]);
538 if (++source == n) {
539 source = 0;
540 }
541 }
542 mutate();
543 }
544 return;
545 }
546 }
547 Pool pool(reserve_.for_rotation(size(), sizeof(value_type)));
548 if (left == 0 && right == size()) {
550 pool.reserve(5 * height() + 4, locate(root_, distance).offset != 0);
551 auto cut = split(std::move(root_), distance, pool);
552 root_ = concatenate(std::move(cut.right), std::move(cut.left), pool);
553 pool.commit();
554 return;
555 }
565 const auto leaf_spares =
566 std::size_t(left != 0 && locate(root_, left).offset != 0) +
567 (locate(root_, left + distance).offset != 0) +
568 (right != size() && locate(root_, right).offset != 0);
569 pool.reserve(15 * height() + 24, leaf_spares);
570 // Split the outer boundaries in separate halves, then rebuild two halves
571 // independently. Avoid repeatedly extending the same growing prefix.
572 auto middle = split(std::move(root_), left + distance, pool);
573 auto a = split(std::move(middle.left), left, pool);
574 auto b = split(std::move(middle.right), right - left - distance, pool);
575 auto first = concatenate(std::move(a.left), std::move(b.left), pool);
576 auto second = concatenate(std::move(a.right), std::move(b.right), pool);
577 root_ = concatenate(std::move(first), std::move(second), pool);
578 pool.commit();
579 }
580
595 void insert_at(std::size_t position, value_type value)
596 requires InsertableSequenceBlock<Block>
597 {
598 if (position > size()) {
599 throw std::out_of_range("SequenceTree: insert position");
600 }
601 if (empty()) {
602 root_ = Owner(allocate<Leaf>(), 1, 0);
603 const value_type data[] = {value};
604 static_cast<Leaf*>(root_.p)->block =
605 Block(std::span<const value_type>(data, 1));
606 return;
607 }
608 struct PathEntry {
609 Node* node;
610 std::size_t child;
611 std::size_t total;
612 };
613 std::array<PathEntry, max_height> path;
614 std::size_t depth = 0;
615 std::uint64_t total_bias = 0;
616 void* p = root_.p;
617 auto total = root_.total;
618 auto local = position;
619 for (auto h = root_.height; h != 0; --h) {
620 visit();
621 auto& node = *static_cast<Node*>(p);
622 if constexpr (IndexBias) {
623 total_bias += node.bias.value;
624 }
625 if constexpr (Layout == LengthLayout::cumulative) {
626 const auto i = node_select(
627 {node.measures.data(), std::size_t(node.count) - 1}, local);
628 const auto prefix = i == 0 ? 0 : node.measures[i - 1];
629 const auto end = i + 1 == node.count ? total : node.measures[i];
630 path[depth++] = {&node, i, total};
631 local -= prefix;
632 total = end - prefix;
633 p = node.child(i, h);
634 } else {
635 std::size_t prefix = 0;
636 for (std::size_t i = 0; i < node.count; ++i) {
637 const auto n = length(node, i, total, prefix);
638 if (local - prefix < n ||
639 (local - prefix == n && i + 1 == node.count)) {
640 path[depth++] = {&node, i, total};
641 local -= prefix;
642 total = n;
643 p = node.child(i, h);
644 break;
645 }
646 prefix += n;
647 }
648 }
649 }
650 auto& leaf = *static_cast<Leaf*>(p);
651 if constexpr (IndexBias) {
652 total_bias += leaf.bias.value;
653 }
654 if (leaf.block.size() < block_capacity) {
655 if constexpr (IndexBias) {
656 assert(total_bias <= value);
657 leaf.block.insert_at(local,
658 static_cast<value_type>(value - total_bias));
659 } else {
660 leaf.block.insert_at(local, value);
661 }
662 mutate();
663 for (std::size_t i = depth; i > 0; --i) {
664 auto& entry = path[i - 1];
665 auto& node = *entry.node;
666 node.regular_prefix =
667 std::min(std::size_t(node.regular_prefix), entry.child);
668 if constexpr (Layout == LengthLayout::cumulative) {
669 for (std::size_t j = entry.child; j + 1 < node.count; ++j) {
670 ++node.measures[j];
671 }
672 } else if (entry.child + 1 < node.count) {
673 ++node.measures[entry.child];
674 }
675 }
676 ++root_.total;
677 return;
678 }
679 if constexpr (!IndexBias) {
680 // A full leaf needs one sibling. Each full ancestor can require one
681 // additional node, and propagation can create one new root. Reserve
682 // before touching ownership or payloads to retain the strong guarantee.
683 Pool pool;
684 std::size_t spares = 1;
685 for (std::size_t i = depth; i != 0; --i) {
686 if (path[i - 1].node->count != Fanout) {
687 break;
688 }
689 ++spares;
690 }
691 pool.reserve(spares, 1);
692 auto sibling = pool.leaf();
693 const auto left_count = (block_capacity + 1) / 2;
694 const bool insert_left = local < left_count;
695 const auto old_left = left_count - std::size_t(insert_left);
696 auto& right = static_cast<Leaf*>(sibling.p)->block;
697 leaf.block.redistribute(right, old_left);
698 if (insert_left) {
699 leaf.block.insert_at(local, value);
700 } else {
701 right.insert_at(local - old_left, value);
702 }
703 mutate();
704 sibling.total = right.size();
705 root_.release();
706 Pair carry{Owner(&leaf, leaf.block.size(), 0), std::move(sibling)};
707 for (std::size_t level = depth; level != 0; --level) {
708 const auto& entry = path[level - 1];
709 auto* node = entry.node;
710 Entries entries;
711 std::size_t count = 0, prefix = 0;
712 for (std::size_t i = 0; i < node->count; ++i) {
713 const auto n = length(*node, i, entry.total, prefix);
714 if (i == entry.child) {
715 entries[count++] = std::move(carry.left);
716 if (carry.right.p) {
717 entries[count++] = std::move(carry.right);
718 }
719 } else {
720 entries[count++] =
721 Owner(node->child(i, depth - level + 1), n, depth - level);
722 }
723 prefix += n;
724 }
725 pool.recycle(node);
726 carry = repack(entries, count, pool);
727 }
728 if (carry.right.p) {
729 Entries entries;
730 entries[0] = std::move(carry.left);
731 entries[1] = std::move(carry.right);
732 root_ = pack(pool.node(), entries, 0, 2);
733 } else {
734 root_ = std::move(carry.left);
735 }
736 return;
737 }
738 // Biased trees retain the split/concatenate path for lazy bias handling.
739 Pool pool;
740 const value_type data[] = {value};
741 if (position == size()) {
742 pool.reserve(2 * height() + 4, 1);
743 auto singleton = pool.leaf();
744 static_cast<Leaf*>(singleton.p)->block =
745 Block(std::span<const value_type>(data, 1));
746 singleton.total = 1;
747 singleton.height = 0;
748 root_ = concatenate(std::move(root_), std::move(singleton), pool);
749 return;
750 }
757 pool.reserve(7 * height() + 12, 2);
758 auto pair = split(std::move(root_), position, pool);
759 auto singleton = pool.leaf();
760 static_cast<Leaf*>(singleton.p)->block =
761 Block(std::span<const value_type>(data, 1));
762 singleton.total = 1;
763 singleton.height = 0;
764 root_ = concatenate(std::move(pair.left), std::move(singleton), pool);
765 root_ = concatenate(std::move(root_), std::move(pair.right), pool);
766 }
767
770 struct MemoryUsage {
771 std::size_t blocks = 0;
772 std::size_t nodes = 0;
773 std::size_t block_bytes = 0;
774 std::size_t node_bytes = 0;
775 std::size_t reserved_nodes = 0;
776 std::size_t reserved_bytes = 0;
777 std::size_t total_bytes = sizeof(SequenceTree);
778 };
787 MemoryUsage memory_usage() const noexcept {
788 MemoryUsage result;
789 auto walk = [&](auto&& self, void* p, std::size_t h) -> void {
790 if (!p) {
791 return;
792 }
793 if (h == 0) {
794 ++result.blocks;
795 } else {
796 ++result.nodes;
797 const auto& node = *static_cast<Node*>(p);
798 for (std::size_t i = 0; i < node.count; ++i) {
799 self(self, node.child(i, h), h - 1);
800 }
801 }
802 };
803 walk(walk, root_.p, height());
804 result.block_bytes = saturated_multiply(result.blocks, sizeof(Leaf));
805 result.node_bytes = saturated_multiply(result.nodes, sizeof(Node));
806 result.reserved_nodes = reserve_.node_count();
807 result.reserved_bytes =
808 saturated_multiply(result.reserved_nodes, sizeof(Node));
809 result.total_bytes = saturated_add(
810 sizeof(*this),
811 saturated_add(result.block_bytes,
812 saturated_add(result.node_bytes, result.reserved_bytes)));
813 return result;
814 }
817 std::size_t memory_usage_bytes() const noexcept {
818 return memory_usage().total_bytes;
819 }
822 std::size_t block_count() const noexcept { return memory_usage().blocks; }
827 std::size_t node_count() const noexcept { return memory_usage().nodes; }
830 std::size_t block_allocation_bytes() const noexcept {
831 return memory_usage().block_bytes;
832 }
835 std::size_t internal_memory_bytes() const noexcept {
836 const auto usage = memory_usage();
837 return saturated_add(usage.node_bytes, usage.reserved_bytes);
838 }
844 std::size_t payload_capacity_bytes() const noexcept
845 requires requires(const Block& b) {
846 { b.payload_capacity_bytes() } noexcept -> std::same_as<std::size_t>;
847 }
848 {
849 return saturated_multiply(block_count(), Block{}.payload_capacity_bytes());
850 }
856 std::size_t metadata_bytes() const noexcept
857 requires requires(const Block& b) {
858 { b.payload_capacity_bytes() } noexcept -> std::same_as<std::size_t>;
859 }
860 {
861 return memory_usage_bytes() - payload_capacity_bytes();
862 }
870 template <class Callback>
871 void for_each_block(Callback&& callback) const
872 requires(!IndexBias)
873 {
874 auto walk = [&](auto&& self, void* p, std::size_t h) -> void {
875 if (!p) {
876 return;
877 }
878 if (h == 0) {
879 callback(std::as_const(static_cast<Leaf*>(p)->block));
880 } else {
881 const auto& node = *static_cast<Node*>(p);
882 for (std::size_t i = 0; i < node.count; ++i) {
883 self(self, node.child(i, h), h - 1);
884 }
885 }
886 };
887 walk(walk, root_.p, height());
888 }
889
890#ifdef PIXIE_SEQUENCE_TREE_TESTING
900 std::vector<std::vector<std::size_t>> test_child_boundaries() const {
901 std::vector<std::vector<std::size_t>> result;
902 auto walk = [&](auto&& self, const void* p, std::size_t total,
903 std::size_t h, std::size_t start) -> void {
904 if (!p || h == 0) {
905 return;
906 }
907 const auto& node = *static_cast<const Node*>(p);
908 std::vector<std::size_t> boundaries{start};
909 std::size_t prefix = 0;
910 for (std::size_t i = 0; i < node.count; ++i) {
911 prefix += length(node, i, total, prefix);
912 boundaries.push_back(start + prefix);
913 }
914 result.push_back(boundaries);
915 for (std::size_t i = 0; i < node.count; ++i) {
916 self(self, node.child(i, h), boundaries[i + 1] - boundaries[i], h - 1,
917 boundaries[i]);
918 }
919 };
920 walk(walk, root_.p, root_.total, root_.height, 0);
921 return result;
922 }
923
930 bool test_validate() const {
931 if (!root_.p) {
932 return size() == 0 && height() == 0;
933 }
934 std::unordered_set<const void*> seen;
935 auto walk = [&](auto&& self, void* p, std::size_t total, std::size_t h,
936 bool first, bool last, bool root,
937 std::uint64_t inherited) -> bool {
938 if (!p || !total || !seen.insert(p).second) {
939 return false;
940 }
941 if (h == 0) {
942 const auto& b = static_cast<Leaf*>(p)->block;
943 if constexpr (IndexBias) {
944 const auto pending = static_cast<Leaf*>(p)->bias.value;
945 const auto maximum = std::numeric_limits<value_type>::max();
946 if (inherited > maximum || pending > maximum - inherited) {
947 return false;
948 }
949 inherited += pending;
950 for (std::size_t i = 0; i < b.size(); ++i) {
951 if (b[i] > maximum - inherited) {
952 return false;
953 }
954 }
955 }
956 return reinterpret_cast<std::uintptr_t>(p) % alignof(Leaf) == 0 &&
957 b.size() == total && total <= Block::capacity &&
958 (first || last || total >= minimum_leaf_size);
959 }
960 const auto& node = *static_cast<Node*>(p);
961 if constexpr (IndexBias) {
962 const auto maximum = std::numeric_limits<value_type>::max();
963 if (inherited > maximum || node.bias.value > maximum - inherited) {
964 return false;
965 }
966 inherited += node.bias.value;
967 }
968 if (reinterpret_cast<std::uintptr_t>(p) % alignof(Node) != 0 ||
969 node.count < (root ? 2 : Fanout / 2) || node.count > Fanout) {
970 return false;
971 }
972 if (node.regular_prefix > node.count) {
973 return false;
974 }
975 if (!node.order.valid(node, h)) {
976 return false;
977 }
978 if (node.regular_prefix) {
979 if constexpr (!std::has_single_bit(Fanout) ||
980 Layout != LengthLayout::cumulative) {
981 return false;
982 } else {
983 const auto shift = (h - 1) * std::countr_zero(Fanout);
984 if (shift >= std::numeric_limits<std::size_t>::digits ||
985 block_capacity >
986 (std::numeric_limits<std::size_t>::max() >> shift)) {
987 return false;
988 }
989 const auto capacity = block_capacity << shift;
990 for (std::size_t i = 0; i < node.regular_prefix; ++i) {
991 const auto end = i + 1 == node.count ? total : node.measures[i];
992 if (end / (i + 1) != capacity || end % (i + 1) != 0) {
993 return false;
994 }
995 }
996 }
997 }
998 std::size_t prefix = 0;
999 for (std::size_t i = 0; i < node.count; ++i) {
1000 if (prefix >= total) {
1001 return false;
1002 }
1003 const auto n = length(node, i, total, prefix);
1004 if (n > total - prefix ||
1005 !self(self, node.child(i, h), n, h - 1, first && i == 0,
1006 last && i + 1 == node.count, false, inherited)) {
1007 return false;
1008 }
1009 prefix += n;
1010 }
1011 return prefix == total;
1012 };
1013 return walk(walk, root_.p, size(), height(), true, true, true, 0);
1014 }
1017 std::vector<const void*> test_leaf_identities() const {
1018 std::vector<const void*> result;
1019 auto walk = [&](auto&& self, const void* p, std::size_t h) -> void {
1020 if (!p) {
1021 return;
1022 }
1023 if (h == 0) {
1024 result.push_back(&static_cast<const Leaf*>(p)->block);
1025 } else {
1026 const auto& node = *static_cast<const Node*>(p);
1027 for (std::size_t i = 0; i < node.count; ++i) {
1028 self(self, node.child(i, h), h - 1);
1029 }
1030 }
1031 };
1032 walk(walk, root_.p, height());
1033 return result;
1034 }
1044 std::vector<std::pair<const void*, std::uint64_t>> test_bias_snapshot() const
1045 requires(IndexBias)
1046 {
1047 std::vector<std::pair<const void*, std::uint64_t>> result;
1048 auto walk = [&](auto&& self, const void* p, std::size_t h) -> void {
1049 if (!p) {
1050 return;
1051 }
1052 if (h == 0) {
1053 result.emplace_back(p, static_cast<const Leaf*>(p)->bias.value);
1054 } else {
1055 const auto& node = *static_cast<const Node*>(p);
1056 result.emplace_back(p, node.bias.value);
1057 for (std::size_t i = 0; i < node.count; ++i) {
1058 self(self, node.child(i, h), h - 1);
1059 }
1060 }
1061 };
1062 walk(walk, root_.p, height());
1063 return result;
1064 }
1070 void test_add_bias(std::uint64_t bias) noexcept
1071 requires(IndexBias)
1072 {
1073 assert(!empty());
1074 add_bias(root_.p, height(), bias);
1075 }
1084 std::vector<const void*> test_internal_node_identities() const {
1085 std::vector<const void*> result;
1086 auto walk = [&](auto&& self, const void* p, std::size_t h) -> void {
1087 if (!p || h == 0) {
1088 return;
1089 }
1090 result.push_back(p);
1091 const auto& node = *static_cast<const Node*>(p);
1092 for (std::size_t i = 0; i < node.count; ++i) {
1093 self(self, node.child(i, h), h - 1);
1094 }
1095 };
1096 walk(walk, root_.p, height());
1097 return result;
1098 }
1099#endif
1100
1101 private:
1102 static void allocation() {
1103#ifdef PIXIE_SEQUENCE_TREE_TESTING
1104 if (failure_ == 0) {
1105 throw std::bad_alloc();
1106 }
1107 if (failure_ > 0) {
1108 --failure_;
1109 }
1110#endif
1111 }
1112 template <class T>
1113 static T* allocate() {
1114 allocation();
1115 auto* p = new T;
1116#ifdef PIXIE_SEQUENCE_TREE_TESTING
1117 ++test_counters.allocations;
1118 ++test_counters.live_allocations;
1119 test_counters.peak_allocations = std::max(test_counters.peak_allocations,
1120 test_counters.live_allocations);
1121 test_counters.live_bytes += sizeof(T);
1122 test_counters.peak_bytes =
1123 std::max(test_counters.peak_bytes, test_counters.live_bytes);
1124#endif
1125 return p;
1126 }
1127 template <class T>
1128 static void dispose(T* p) noexcept {
1129 delete p;
1130#ifdef PIXIE_SEQUENCE_TREE_TESTING
1131 --test_counters.live_allocations;
1132 test_counters.live_bytes -= sizeof(T);
1133#endif
1134 }
1135 static void visit() noexcept {
1136#ifdef PIXIE_SEQUENCE_TREE_TESTING
1137 ++test_counters.node_visits;
1138#endif
1139 }
1140 static void transfer() noexcept {
1141#ifdef PIXIE_SEQUENCE_TREE_TESTING
1142 ++test_counters.child_transfers;
1143#endif
1144 }
1145 static void mutate() noexcept {
1146#ifdef PIXIE_SEQUENCE_TREE_TESTING
1147 ++test_counters.payload_mutations;
1148#endif
1149 }
1150 static void destroy(void* p, std::size_t height) noexcept {
1151 if (!p) {
1152 return;
1153 }
1154 if (height == 0) {
1155 dispose(static_cast<Leaf*>(p));
1156 } else {
1157 auto* node = static_cast<Node*>(p);
1158 for (std::size_t i = 0; i < node->count; ++i) {
1159 destroy(node->child(i, height), height - 1);
1160 }
1161 dispose(node);
1162 }
1163 }
1164 struct Owner {
1165 void* p = nullptr;
1166 std::size_t total = 0;
1167 std::size_t height = 0;
1168 Owner() noexcept = default;
1169 Owner(void* pointer, std::size_t n, std::size_t h) noexcept
1170 : p(pointer), total(n), height(h) {}
1171 Owner(const Owner&) = delete;
1172 Owner& operator=(const Owner&) = delete;
1173 Owner(Owner&& other) noexcept
1174 : p(std::exchange(other.p, nullptr)),
1175 total(std::exchange(other.total, 0)),
1176 height(std::exchange(other.height, 0)) {}
1177 Owner& operator=(Owner&& other) noexcept {
1178 if (this != &other) {
1179 destroy(p, height);
1180 p = std::exchange(other.p, nullptr);
1181 total = std::exchange(other.total, 0);
1182 height = std::exchange(other.height, 0);
1183 }
1184 return *this;
1185 }
1186 ~Owner() { destroy(p, height); }
1187 void* release() noexcept { return std::exchange(p, nullptr); }
1188 };
1189
1190 struct PoolAllocator {
1191 static Node* node() { return allocate<Node>(); }
1192 static Owner leaf() { return Owner(allocate<Leaf>(), 0, 0); }
1193 static void dispose(Node* node) noexcept { SequenceTree::dispose(node); }
1194 static void reset(Node* node) noexcept {
1195 node->count = 0;
1196 node->regular_prefix = 0;
1197 if constexpr (IndexBias) {
1198 node->bias.value = 0;
1199 }
1200 }
1201 };
1202 using Reserve = typename Policy::template Reserve<Node, PoolAllocator>;
1203 using Pool = typename Policy::template Pool<Node, Owner, PoolAllocator>;
1204 void clear_reserve() noexcept {
1205 reserve_.clear();
1206 } // Only initialized slots own subtrees; unused capacity is never touched.
1207 class Entries {
1208 union Slot {
1209 Owner owner;
1210 Slot() noexcept {}
1211 ~Slot() {}
1212 };
1213 std::array<Slot, 2 * Fanout> slots_;
1214 std::size_t count_ = 0;
1215
1216 public:
1217 Entries() noexcept = default;
1218 Entries(const Entries&) = delete;
1219 Entries& operator=(const Entries&) = delete;
1220 ~Entries() {
1221 while (count_ != 0) {
1222 std::destroy_at(&slots_[--count_].owner);
1223 }
1224 }
1225 Owner& operator[](std::size_t index) noexcept {
1226 assert(index < slots_.size());
1227 while (count_ <= index) {
1228 std::construct_at(&slots_[count_++].owner);
1229 }
1230 return slots_[index].owner;
1231 }
1232 void append(void* pointer, std::size_t total, std::size_t height) noexcept {
1233 assert(count_ < slots_.size());
1234 std::construct_at(&slots_[count_++].owner, pointer, total, height);
1235 }
1236 };
1237 struct Pair {
1238 Owner left, right;
1239 };
1240
1241 static void add_bias(void* p, std::size_t height, std::uint64_t bias) noexcept
1242 requires(IndexBias)
1243 {
1244 auto& pending = height == 0 ? static_cast<Leaf*>(p)->bias.value
1245 : static_cast<Node*>(p)->bias.value;
1246 assert(bias <= std::numeric_limits<value_type>::max() - pending);
1247 pending += bias;
1248 }
1249 static void normalize(Leaf& leaf) noexcept {
1250 if constexpr (IndexBias) {
1251 if (leaf.bias.value != 0) {
1252 leaf.block.add_bias(leaf.bias.value);
1253 leaf.bias.value = 0;
1254 mutate();
1255 }
1256 }
1257 }
1258 static void redistribute(Leaf& a, Leaf& b, std::size_t left) noexcept {
1259 normalize(a);
1260 normalize(b);
1261 a.block.redistribute(b.block, left);
1262 mutate();
1263 }
1264
1265 static std::size_t length(const Node& node,
1266 std::size_t i,
1267 std::size_t total,
1268 std::size_t prefix) noexcept {
1269 if (i + 1 == node.count) {
1270 return total - prefix;
1271 }
1272 if constexpr (Layout == LengthLayout::cumulative) {
1273 return node.measures[i] - prefix;
1274 } else {
1275 return node.measures[i];
1276 }
1277 }
1278 static std::size_t unpack(Owner tree, Entries& entries, Pool& pool) noexcept {
1279 visit();
1280 auto* node = static_cast<Node*>(tree.p);
1281 const auto count = node->count;
1282 std::size_t prefix = 0;
1283 for (std::size_t i = 0; i < count; ++i) {
1284 const auto n = length(*node, i, tree.total, prefix);
1285 if constexpr (IndexBias) {
1286 add_bias(node->child(i, tree.height), tree.height - 1,
1287 node->bias.value);
1288 }
1289 entries.append(node->child(i, tree.height), n, tree.height - 1);
1290 prefix += n;
1291 transfer();
1292 }
1293 tree.release();
1294 pool.recycle(node);
1295 return count;
1296 }
1297 static Owner pack(Node* node,
1298 Entries& entries,
1299 std::size_t begin,
1300 std::size_t count) noexcept {
1301 assert(count >= 2 && count <= Fanout);
1302 node->count = count;
1303 const auto height = entries[begin].height + 1;
1304 node->order.reset(*node);
1305 std::size_t child_capacity = 0;
1306 if constexpr (std::has_single_bit(Fanout) &&
1307 Layout == LengthLayout::cumulative) {
1308 const auto shift = (height - 1) * std::countr_zero(Fanout);
1309 if (shift < std::numeric_limits<std::size_t>::digits &&
1310 block_capacity <=
1311 (std::numeric_limits<std::size_t>::max() >> shift)) {
1312 child_capacity = block_capacity << shift;
1313 }
1314 }
1315 node->regular_prefix = 0;
1316 std::size_t total = 0;
1317 for (std::size_t i = 0; i < count; ++i) {
1318 auto& entry = entries[begin + i];
1319 assert(entry.p && entry.height + 1 == height);
1320 total += entry.total;
1321 node->child(i, height) = entry.release();
1322 if (child_capacity != 0 && i == node->regular_prefix &&
1323 entry.total == child_capacity) {
1324 ++node->regular_prefix;
1325 }
1326 if (i + 1 < count) {
1327 node->measures[i] =
1328 Layout == LengthLayout::cumulative ? total : entry.total;
1329 }
1330 transfer();
1331 }
1332 // A partial shortcut adds a data-dependent branch. Keep short relaxed
1333 // nodes on their uniform search path, and use partial prefixes only when
1334 // they dominate a wide node. Fully regular nodes retain their fast path.
1335 if (std::size_t(node->regular_prefix) + 1 < count &&
1336 (count < partial_prefix_min_children ||
1337 std::size_t(node->regular_prefix) < count - count / 4)) {
1338 node->regular_prefix = 0;
1339 }
1340 return Owner(node, total, height);
1341 }
1342 static Owner group(Entries& entries,
1343 std::size_t begin,
1344 std::size_t count,
1345 Pool& pool) noexcept {
1346 if (count == 0) {
1347 return {};
1348 }
1349 if (count == 1) {
1350 return std::move(entries[begin]);
1351 }
1352 return pack(pool.node(), entries, begin, count);
1353 }
1354 static Pair repack(Entries& entries, std::size_t count, Pool& pool) noexcept {
1355 if (count <= Fanout) {
1356 return {group(entries, 0, count, pool), {}};
1357 }
1358 const auto half = count / 2;
1359 return {group(entries, 0, half, pool),
1360 group(entries, half, count - half, pool)};
1361 }
1362 // Join compatible roots, carrying at most two nodes at the taller height.
1363 // Underfull roots are merged/redistributed before becoming nonroot children.
1364 // Pool deficit: equal-height unpack returns two nodes before repack uses at
1365 // most two. Each of gap unequal levels returns one before using at most two;
1366 // join may add one root. Thus gap+1 spares cover every prefix, not just the
1367 // final node count. Without root growth the bound is gap. A taller root with
1368 // a spare child slot cannot split, tightening this to gap-1 when gap > 0.
1369 static Pair join_level(Owner a, Owner b, Pool& pool) noexcept {
1370 if (a.height == b.height) {
1371 if (a.height == 0) {
1372 return {std::move(a), std::move(b)};
1373 }
1374 const auto left_count = static_cast<const Node*>(a.p)->count;
1375 const auto right_count = static_cast<const Node*>(b.p)->count;
1376 if (left_count + right_count > Fanout && left_count >= Fanout / 2 &&
1377 right_count >= Fanout / 2) {
1378 // Both roots are already legal nonroot nodes, and two nodes are
1379 // necessary. Preserve their children, lengths, and pending biases.
1380 // Returning them consumes no pool nodes, just like unpack/repack's
1381 // zero net node deficit in this case.
1382 return {std::move(a), std::move(b)};
1383 }
1384 Entries entries;
1385 const auto n = unpack(std::move(a), entries, pool);
1386 const auto m = unpack(std::move(b), entries, pool);
1387 return repack(entries, n + m, pool);
1388 }
1389 Entries entries;
1390 if (a.height > b.height) {
1391 const auto count = unpack(std::move(a), entries, pool);
1392 auto seam = join_level(std::move(entries[count - 1]), std::move(b), pool);
1393 entries[count - 1] = std::move(seam.left);
1394 const bool extra = seam.right.p != nullptr;
1395 entries[count] = std::move(seam.right);
1396 return repack(entries, count + extra, pool);
1397 }
1398 const auto count = unpack(std::move(b), entries, pool);
1399 auto seam = join_level(std::move(a), std::move(entries[0]), pool);
1400 const bool extra = seam.right.p != nullptr;
1401 if (extra) {
1402 for (std::size_t i = count; i > 1; --i) {
1403 entries[i] = std::move(entries[i - 1]);
1404 }
1405 entries[1] = std::move(seam.right);
1406 }
1407 entries[0] = std::move(seam.left);
1408 return repack(entries, count + extra, pool);
1409 }
1410 static Owner join(Owner a, Owner b, Pool& pool) noexcept {
1411 if (!a.p) {
1412 return b;
1413 }
1414 if (!b.p) {
1415 return a;
1416 }
1417 auto pair = join_level(std::move(a), std::move(b), pool);
1418 if (!pair.right.p) {
1419 return std::move(pair.left);
1420 }
1421 Entries entries;
1422 entries[0] = std::move(pair.left);
1423 entries[1] = std::move(pair.right);
1424 return group(entries, 0, 2, pool);
1425 }
1426
1427 // One cut descent. On ascent, sibling groups are already balanced subtrees.
1428 // Each boundary accumulator rises monotonically: spine gaps traversed by
1429 // join telescope across levels (plus one boundary node per level), O(F*h),
1430 // not a fresh full-height join at every ancestor. Neither result exceeds the
1431 // input height: sibling groups have at most F-1 children, so their root has
1432 // room for a carry; a single sibling joined to a cut grows by at most one.
1433 //
1451 static Pair split(Owner tree, std::size_t p, Pool& pool) noexcept {
1452 if (p == 0) {
1453 return {{}, std::move(tree)};
1454 }
1455 if (p == tree.total) {
1456 return {std::move(tree), {}};
1457 }
1458 if (tree.height == 0) {
1459 auto right = pool.leaf();
1460 redistribute(*static_cast<Leaf*>(tree.p), *static_cast<Leaf*>(right.p),
1461 p);
1462 right.total = tree.total - p;
1463 tree.total = p;
1464 return {std::move(tree), std::move(right)};
1465 }
1466 Entries entries;
1467 const auto count = unpack(std::move(tree), entries, pool);
1468 std::size_t i = 0;
1469 while (p > entries[i].total) {
1470 p -= entries[i++].total;
1471 }
1472 const auto child_height = entries[i].height;
1473 auto cut = split(std::move(entries[i]), p, pool);
1474 const auto fits = [child_height](const Owner& boundary) noexcept {
1475 return boundary.p && boundary.height == child_height &&
1476 (child_height == 0 ||
1477 static_cast<const Node*>(boundary.p)->count >= Fanout / 2);
1478 };
1479 Owner left;
1480 if (fits(cut.left)) {
1481 // The boundary is already a legal child at the siblings' height.
1482 // Pack once instead of grouping the siblings and immediately unpacking
1483 // them again in join_level. Exterior leaves retain split's seam rules.
1484 entries[i] = std::move(cut.left);
1485 left = group(entries, 0, i + 1, pool);
1486 } else {
1487 left = join(group(entries, 0, i, pool), std::move(cut.left), pool);
1488 }
1489 Owner right;
1490 if (fits(cut.right)) {
1491 entries[i] = std::move(cut.right);
1492 right = group(entries, i, count - i, pool);
1493 } else {
1494 right = join(std::move(cut.right),
1495 group(entries, i + 1, count - i - 1, pool), pool);
1496 }
1497 return {std::move(left), std::move(right)};
1498 }
1499
1500 struct Location {
1501 Leaf* leaf;
1502 std::size_t offset;
1503 [[no_unique_address]] Bias bias{};
1504 };
1520 bool rotate_local(std::size_t left,
1521 std::size_t right,
1522 std::size_t distance) noexcept {
1523 const bool includes_first = left == 0;
1524 const bool includes_last = right == size();
1525 void* p = root_.p;
1526 auto total = root_.total;
1527 for (auto h = root_.height; h != 0; --h) {
1528 visit();
1529 auto& node = *static_cast<Node*>(p);
1530 std::array<std::size_t, Fanout> lengths;
1531 std::size_t prefix = 0;
1532 std::size_t begin = Fanout, middle = Fanout, end = Fanout + 1;
1533 bool descend = false;
1534 for (std::size_t i = 0; i < node.count; ++i) {
1535 const auto n = length(node, i, total, prefix);
1536 if (left >= prefix && right <= prefix + n) {
1537 left -= prefix;
1538 right -= prefix;
1539 total = n;
1540 p = node.child(i, h);
1541 descend = true;
1542 break;
1543 }
1544 lengths[i] = n;
1545 if (prefix == left) {
1546 begin = i;
1547 }
1548 if (prefix == left + distance) {
1549 middle = i;
1550 }
1551 prefix += n;
1552 if (prefix == right) {
1553 end = i + 1;
1554 }
1555 }
1556 if (descend) {
1557 continue;
1558 }
1559 if (begin == Fanout || middle == Fanout || end == Fanout + 1) {
1560 return false;
1561 }
1562 // The matched slots already identify the endpoint subtrees. Follow only
1563 // their outer spines, without rescanning ancestors or selecting by rank.
1564 const auto underfull = [h](void* child, bool back) noexcept {
1565 for (auto level = h - 1; level != 0; --level) {
1566 visit();
1567 const auto& edge = *static_cast<Node*>(child);
1568 child = edge.child(back ? edge.count - 1 : 0, level);
1569 }
1570 return static_cast<Leaf*>(child)->block.size() < minimum_leaf_size;
1571 };
1572 if ((includes_first && underfull(node.child(begin, h), false)) ||
1573 (includes_last && underfull(node.child(end - 1, h), true))) {
1574 return false;
1575 }
1576 const auto writes =
1577 node.order.template rotate<SequenceTree>(node, begin, middle, end, h);
1578 for (std::size_t i = 0; i < writes; ++i) {
1579 transfer();
1580 }
1581 const bool equal_lengths =
1582 end <= node.regular_prefix ||
1583 std::all_of(lengths.begin() + begin + 1, lengths.begin() + end,
1584 [&](auto n) { return n == lengths[begin]; });
1585 if (!equal_lengths) {
1586 std::rotate(lengths.begin() + begin, lengths.begin() + middle,
1587 lengths.begin() + end);
1588 prefix = 0;
1589 node.regular_prefix = 0;
1590 for (std::size_t i = 0; i + 1 < node.count; ++i) {
1591 prefix += lengths[i];
1592 node.measures[i] =
1593 Layout == LengthLayout::cumulative ? prefix : lengths[i];
1594 }
1595 }
1596 return true;
1597 }
1598 static_cast<Leaf*>(p)->block.rotate_left(left, right, distance);
1599 mutate();
1600 return true;
1601 }
1602 template <bool Accumulate = false>
1603 static Location locate(const Owner& root, std::size_t index) noexcept {
1604 void* p = root.p;
1605 auto total = root.total;
1606 [[maybe_unused]] Bias bias{};
1607 for (auto h = root.height; h != 0; --h) {
1608 visit();
1609 const auto& node = *static_cast<Node*>(p);
1610 if constexpr (Accumulate && IndexBias) {
1611 bias.value += node.bias.value;
1612 }
1613 if constexpr (Layout == LengthLayout::cumulative) {
1614 std::size_t i;
1615 if constexpr (std::has_single_bit(Fanout)) {
1616 if (node.regular_prefix) {
1617 const auto shift = (h - 1) * std::countr_zero(Fanout);
1618 const auto block_index = index / block_capacity;
1619 i = block_index >> shift;
1620 if constexpr (!IndexBias) {
1621 if (i < node.regular_prefix) {
1622 // Full children begin at multiples of block_capacity. The
1623 // remaining radix digits and leaf offset are unchanged by
1624 // subtracting that prefix, so retain the original quotient
1625 // and avoid rebasing/redividing the element index.
1626 p = node.child(i, h);
1627 for (auto depth = h - 1; depth != 0; --depth) {
1628 visit();
1629 const auto child =
1630 (block_index >>
1631 ((depth - 1) * std::countr_zero(Fanout))) &
1632 (Fanout - 1);
1633 p = static_cast<Node*>(p)->child(child, depth);
1634 }
1635 return {static_cast<Leaf*>(p), index % block_capacity, bias};
1636 }
1637 }
1638 if (Fanout < partial_prefix_min_children ||
1639 i < node.regular_prefix ||
1640 node.regular_prefix + 1 == node.count) {
1641 const auto capacity = block_capacity << shift;
1642 const auto prefix = i * capacity;
1643 index -= prefix;
1644 total = i + 1 == node.count ? total - prefix : capacity;
1645 p = node.child(i, h);
1646 continue;
1647 }
1648 }
1649 // No child exceeds block_capacity * Fanout^(h-1). Therefore
1650 // floor(index / that capacity) cannot lie after the desired child.
1651 // Skip the provably irrelevant prefix even in a relaxed node.
1652 std::size_t first = 0;
1653 if constexpr (Fanout >= 32) {
1654 // Narrow tables already fit in one or two SIMD batches; the
1655 // extra division cost did not pay for itself in those profiles.
1656 const auto shift = (h - 1) * std::countr_zero(Fanout);
1657 if (shift < std::numeric_limits<std::size_t>::digits) {
1658 first = (index / block_capacity) >> shift;
1659 }
1660 }
1661 i = first + node_select({node.measures.data() + first,
1662 std::size_t(node.count) - 1 - first},
1663 index);
1664 } else {
1665 i = node_select({node.measures.data(), std::size_t(node.count) - 1},
1666 index);
1667 }
1668 const auto prefix = i == 0 ? 0 : node.measures[i - 1];
1669 const auto end = i + 1 == node.count ? total : node.measures[i];
1670 index -= prefix;
1671 total = end - prefix;
1672 p = node.child(i, h);
1673 } else {
1674 std::size_t prefix = 0;
1675 for (std::size_t i = 0; i < node.count; ++i) {
1676 const auto n = length(node, i, total, prefix);
1677 if (index - prefix < n) {
1678 index -= prefix;
1679 total = n;
1680 p = node.child(i, h);
1681 break;
1682 }
1683 prefix += n;
1684 }
1685 }
1686 }
1687 if constexpr (Accumulate && IndexBias) {
1688 bias.value += static_cast<Leaf*>(p)->bias.value;
1689 }
1690 return {static_cast<Leaf*>(p), index, bias};
1691 }
1704 static value_type edge_value(void* p,
1705 std::size_t height,
1706 Bias bias,
1707 bool back) noexcept {
1708 for (auto h = height; h != 0; --h) {
1709 auto& node = *static_cast<Node*>(p);
1710 if constexpr (IndexBias) {
1711 bias.value += node.bias.value;
1712 }
1713 p = node.child(back ? node.count - 1 : 0, h);
1714 }
1715 auto& leaf = *static_cast<Leaf*>(p);
1716 if constexpr (IndexBias) {
1717 bias.value += leaf.bias.value;
1718 }
1719 const auto& block = std::as_const(leaf.block);
1720 const auto offset = back ? block.size() - 1 : 0;
1721 const auto raw = block[offset];
1722 if constexpr (IndexBias) {
1723 return static_cast<value_type>(raw + bias.value);
1724 } else {
1725 return raw;
1726 }
1727 }
1728 static Owner pop_edge(Owner& tree, bool back, Pool& pool) noexcept {
1729 // Removing an entire exterior leaf needs no spares. Inductively, reducing
1730 // a child from height k-1 to q returns at least k-1-q nodes. Recycling the
1731 // parent adds one. With >=2 siblings, grouping uses one, but its root has
1732 // <=F-1 children: joining costs at most k-q-1. With one sibling, no group
1733 // node is needed and join costs <=k-q (one less without root growth).
1734 // With none, all credits remain. Every prefix is funded and a height drop
1735 // from k to r leaves at least k-r credits, completing the induction.
1736 const auto n = locate(tree, back ? tree.total - 1 : 0).leaf->block.size();
1737 const auto position = back ? tree.total - n : n;
1738 auto cut = split(std::move(tree), position, pool);
1739 if (back) {
1740 tree = std::move(cut.left);
1741 return std::move(cut.right);
1742 }
1743 tree = std::move(cut.right);
1744 return std::move(cut.left);
1745 }
1746 // Payload seam repair is performed once, not at each structural ancestor.
1747 // Start with the adjacent leaves. If their combined size is below half a
1748 // block and both exterior trees remain, borrow one more left leaf. If a
1749 // left tree still remains after borrowing, that leaf was interior and hence
1750 // at least half full; otherwise the seam becomes exterior and may be short.
1751 // Compact these two or three blocks, then balance the final pair. Thus an
1752 // underfull result can only be an exterior leaf of the whole result.
1753 // Three pop_edge calls never increase the node deficit. Group the <=3 seam
1754 // leaves (4<=F) using <=1 node, then join twice, rather than once per leaf.
1755 // For h>=1, the group has height <=1<=h. The joins cost <=h+1 and <=h+2
1756 // spares and produce height <=h+2. Thus 1+(h+1)+(h+2)=2h+4 covers every
1757 // prefix, including retained results. For h=0 both inputs are extracted in
1758 // full, so only the group can cost a node. No leaf spares are used. An
1759 // underfull group root is repaired by join_level before becoming nonroot.
1760 static Owner concatenate(Owner a, Owner b, Pool& pool) noexcept {
1761 if (!a.p || !b.p) {
1762 return join(std::move(a), std::move(b), pool);
1763 }
1764 if (locate(a, a.total - 1).leaf->block.size() >= minimum_leaf_size &&
1765 locate(b, 0).leaf->block.size() >= minimum_leaf_size) {
1766 return join(std::move(a), std::move(b), pool);
1767 }
1768 Entries seam;
1769 seam[1] = pop_edge(a, true, pool);
1770 seam[2] = pop_edge(b, false, pool);
1771 if (a.p && b.p && seam[1].total + seam[2].total < minimum_leaf_size) {
1772 seam[0] = pop_edge(a, true, pool);
1773 }
1774 std::size_t count = 0;
1775 for (std::size_t i = 0; i < 3; ++i) {
1776 if (seam[i].p) {
1777 if (i != count) {
1778 seam[count] = std::move(seam[i]);
1779 }
1780 ++count;
1781 }
1782 }
1783 for (std::size_t i = 0; i + 1 < count;) {
1784 auto& x = seam[i];
1785 auto& y = seam[i + 1];
1786 const auto total = x.total + y.total;
1787 const auto first = std::min(block_capacity, total);
1788 redistribute(*static_cast<Leaf*>(x.p), *static_cast<Leaf*>(y.p), first);
1789 x.total = first;
1790 y.total = total - first;
1791 if (y.total == 0) {
1792 for (std::size_t j = i + 1; j + 1 < count; ++j) {
1793 seam[j] = std::move(seam[j + 1]);
1794 }
1795 seam[--count] = {};
1796 } else {
1797 ++i;
1798 }
1799 }
1800 if (count >= 2 && seam[count - 1].total < minimum_leaf_size) {
1801 auto& x = seam[count - 2];
1802 auto& y = seam[count - 1];
1803 const auto total = x.total + y.total;
1804 redistribute(*static_cast<Leaf*>(x.p), *static_cast<Leaf*>(y.p),
1805 total / 2);
1806 x.total = total / 2;
1807 y.total = total - x.total;
1808 }
1809 a = join(std::move(a), group(seam, 0, count, pool), pool);
1810 return join(std::move(a), std::move(b), pool);
1811 }
1812 // Keep the O(F*max_height) assembly scratch in a separate call frame. Even
1813 // an early return in that frame pays the compiler's stack-clash page probes.
1814 template <class Iterator, class Sentinel>
1815 static SequenceTree from_blocks_many(Iterator it,
1816 Sentinel end,
1817 Block& pending,
1818 std::size_t total) {
1819 std::array<Entries, max_height> levels;
1820 std::array<std::size_t, max_height> counts{};
1821 auto emit = [&](Block&& block) {
1822 Owner carry(allocate<Leaf>(), block.size(), 0);
1823 static_cast<Leaf*>(carry.p)->block = std::move(block);
1824 std::size_t level = 0;
1825 while (true) {
1826 levels[level][counts[level]++] = std::move(carry);
1827 if (counts[level] != Fanout) {
1828 break;
1829 }
1830 auto* node = allocate<Node>();
1831 carry = pack(node, levels[level], 0, Fanout);
1832 counts[level++] = 0;
1833 assert(level < max_height);
1834 }
1835 };
1836 if (pending.size() == Block::capacity) {
1837 emit(std::move(pending));
1838 pending = Block{};
1839 }
1840 for (; it != end; ++it) {
1841 Block incoming(std::ranges::iter_move(it));
1842 const auto n = incoming.size();
1843 if (n > Block::capacity ||
1844 n > std::numeric_limits<std::size_t>::max() - total) {
1845 throw std::length_error("SequenceTree: construction size");
1846 }
1847 total += n;
1848 if (n == 0) {
1849 continue;
1850 }
1851 const auto combined = pending.size() + n;
1852 pending.redistribute(incoming, std::min(block_capacity, combined));
1853 if (pending.size() == Block::capacity) {
1854 emit(std::move(pending));
1855 pending = std::move(incoming);
1856 }
1857 }
1858 if (pending.size()) {
1859 emit(std::move(pending));
1860 }
1861 SequenceTree result;
1862 if (total <= Block::capacity) {
1863 result.root_ = std::move(levels[0][0]);
1864 return result;
1865 }
1866 // Assemble from low to high: each level's older sibling group precedes the
1867 // existing fringe. Height gaps telescope just as in split's boundary
1868 // assembly, rather than repeatedly descending from the tallest root.
1869 Pool pool;
1870 std::size_t used_height = 0;
1871 for (std::size_t i = 0; i < max_height; ++i) {
1872 if (counts[i]) {
1873 used_height = i;
1874 }
1875 }
1876 // <=h+1 groups and join roots, plus telescoping gaps <=h+1. This covers
1877 // every prefix while the unassembled higher-level Owners remain live.
1878 pool.reserve(3 * (used_height + 1), 0);
1879 for (std::size_t i = 0; i <= used_height; ++i) {
1880 auto prefix = group(levels[i], 0, counts[i], pool);
1881 result.root_ = join(std::move(prefix), std::move(result.root_), pool);
1882 }
1883 return result;
1884 }
1885 // Only Permutation may request rebasing. All allocating preflight paths
1886 // complete before tagging; from that point every ownership transfer and
1887 // bounded normalization is noexcept. The structural pool proofs are
1888 // unchanged.
1889 void merge_rebased(SequenceTree& donor)
1890 requires(IndexBias)
1891 {
1892 merge_impl<true>(donor);
1893 }
1894 template <bool Rebase>
1895 void merge_impl(SequenceTree& donor) {
1896 if (this == &donor || donor.empty()) {
1897 return;
1898 }
1899 if (donor.size() > std::numeric_limits<std::size_t>::max() - size()) {
1900 throw std::length_error("SequenceTree: concatenation size");
1901 }
1902 if (empty()) {
1903 *this = std::move(donor);
1904 return;
1905 }
1906 if constexpr (!IndexBias) {
1907 if (donor.height() == 0 && height() != 0) {
1908 // Growing the exterior leaf changes only the implicit last-child
1909 // length at each ancestor. No stored measure or topology changes.
1910 auto* p = root_.p;
1911 for (auto depth = height(); depth != 0; --depth) {
1912 visit();
1913 const auto& node = *static_cast<Node*>(p);
1914 p = node.child(node.count - 1, depth);
1915 }
1916 auto& last = *static_cast<Leaf*>(p);
1917 const auto incoming = donor.size();
1918 if (incoming <= block_capacity - last.block.size()) {
1919 redistribute(last, *static_cast<Leaf*>(donor.root_.p),
1920 last.block.size() + incoming);
1921 root_.total += incoming;
1922 donor.root_ = {};
1923 donor.clear_reserve();
1924 return;
1925 }
1926 }
1927 }
1928 Pool pool;
1929 auto tag = [&]() noexcept {
1930 donor.clear_reserve();
1931 if constexpr (Rebase) {
1932 add_bias(donor.root_.p, donor.height(), size());
1933 }
1934 };
1935 if (height() == 0 && donor.height() == 0) {
1936 // Two leaf roots have no spine to rebuild. A fitting total combines in
1937 // place; otherwise seam extraction splits only at endpoints and joining
1938 // the two surviving leaves requires exactly one parent allocation.
1939 const auto total = size() + donor.size();
1940 if (total <= block_capacity) {
1941 tag();
1942 redistribute(*static_cast<Leaf*>(root_.p),
1943 *static_cast<Leaf*>(donor.root_.p), total);
1944 root_.total = total;
1945 donor.root_ = {};
1946 } else {
1947 pool.reserve(1, 0);
1948 tag();
1949 root_ = concatenate(std::move(root_), std::move(donor.root_), pool);
1950 }
1951 return;
1952 }
1953 const auto h = std::max(height(), donor.height());
1954 if (std::as_const(locate(root_, size() - 1).leaf->block).size() >=
1955 minimum_leaf_size &&
1956 std::as_const(locate(donor.root_, 0).leaf->block).size() >=
1957 minimum_leaf_size) {
1958 // Recycled spine nodes cover replacements; only gap+1 additional nodes
1959 // can be simultaneously needed. No payload repair or leaf spares.
1960 const auto gap = h - std::min(height(), donor.height());
1961 pool.reserve(gap + 1, 0);
1962 tag();
1963 root_ = join(std::move(root_), std::move(donor.root_), pool);
1964 return;
1965 }
1966 pool.reserve(2 * h + 4, 0);
1967 tag();
1968 root_ = concatenate(std::move(root_), std::move(donor.root_), pool);
1969 }
1970 static std::size_t saturated_add(std::size_t a, std::size_t b) noexcept {
1971 return b > std::numeric_limits<std::size_t>::max() - a
1972 ? std::numeric_limits<std::size_t>::max()
1973 : a + b;
1974 }
1975 static std::size_t saturated_multiply(std::size_t a, std::size_t b) noexcept {
1976 return b != 0 && a > std::numeric_limits<std::size_t>::max() / b
1977 ? std::numeric_limits<std::size_t>::max()
1978 : a * b;
1979 }
1980#ifdef PIXIE_SEQUENCE_TREE_TESTING
1981 inline static thread_local std::ptrdiff_t failure_ = -1;
1982#endif
1983 Owner root_;
1984 [[no_unique_address]] Reserve reserve_;
1985};
1986
1987} // namespace pixie::permutations::detail
Exclusively owned permutation of the integers in [0, size()).
Definition permutation.h:45
Shared compile-time sequence tuning options.
LengthLayout
Representation of tree child lengths.
Definition sequence_options.h:14