mmcfilters
Public API documentation
Loading...
Searching...
No Matches
TreeEditor.hpp
1#pragma once
2
3#include "MorphologicalTree.hpp"
4
5#include <cstdint>
6#include <memory>
7#include <optional>
8#include <stdexcept>
9#include <vector>
10
11namespace mmcfilters {
12
13template <AltitudeValue T> class ValuedMorphologicalTreeEditor;
14
29 friend class MorphologicalTree;
30 template <AltitudeValue T> friend class ValuedMorphologicalTreeEditor;
31
32 public:
38 friend class TreeEditor;
39
41 const TreeEditor* editor_ = nullptr;
43 const MorphologicalTree* tree_ = nullptr;
45 std::size_t mutationVersion_ = 0;
47 TreeEditValidationMode validationMode_ = TreeEditValidationMode::Complete;
48
55 IncrementalProof(const TreeEditor& editor, TreeEditValidationMode validationMode) noexcept
56 : editor_(&editor), tree_(editor.tree_), mutationVersion_(editor.tree_ != nullptr ? editor.tree_->getMutationVersion() : 0),
57 validationMode_(validationMode) {}
58
59 public:
68
75 : editor_(other.editor_), tree_(other.tree_), mutationVersion_(other.mutationVersion_), validationMode_(other.validationMode_) {
76 other.editor_ = nullptr;
77 other.tree_ = nullptr;
78 }
79
84
90 [[nodiscard]] bool usedCompleteValidation() const noexcept { return validationMode_ == TreeEditValidationMode::Complete; }
91 };
92
93 private:
101 template <class Id, Id InvalidId> class DeltaIdSet {
103 std::vector<Id> ids_;
105 std::vector<Id> table_;
106
113 [[nodiscard]] static std::size_t hash(Id id) noexcept {
114 return static_cast<std::size_t>(static_cast<std::uint32_t>(id) * std::uint32_t{2654435761u});
115 }
116
122 void rebuild(std::size_t capacity) {
123 std::vector<Id> newTable(capacity, InvalidId);
124 const std::size_t mask = capacity - 1;
125 for (Id id : ids_) {
126 std::size_t slot = hash(id) & mask;
127 while (newTable[slot] != InvalidId) {
128 slot = (slot + 1) & mask;
129 }
130 newTable[slot] = id;
131 }
132 table_ = std::move(newTable);
133 }
134
135 public:
142 [[nodiscard]] bool contains(Id id) const noexcept {
143 if (id == InvalidId || table_.empty()) {
144 return false;
145 }
146 const std::size_t mask = table_.size() - 1;
147 std::size_t slot = hash(id) & mask;
148 while (table_[slot] != InvalidId) {
149 if (table_[slot] == id) {
150 return true;
151 }
152 slot = (slot + 1) & mask;
153 }
154 return false;
155 }
156
163 [[nodiscard]] bool insert(Id id) {
164 if (id == InvalidId) {
165 return false;
166 }
167 if (table_.empty()) {
168 ids_.reserve(16);
169 rebuild(32);
170 } else if ((ids_.size() + 1) * 10 > table_.size() * 7) {
171 rebuild(table_.size() * 2);
172 }
173
174 const std::size_t mask = table_.size() - 1;
175 std::size_t slot = hash(id) & mask;
176 while (table_[slot] != InvalidId) {
177 if (table_[slot] == id) {
178 return false;
179 }
180 slot = (slot + 1) & mask;
181 }
182 ids_.push_back(id);
183 table_[slot] = id;
184 return true;
185 }
186
187 [[nodiscard]] const std::vector<Id>&
193 entries() const noexcept {
194 return ids_;
195 }
196 };
197
199 using DeltaNodeSet = DeltaIdSet<NodeId, InvalidNode>;
200
202 using DeltaPixelSet = DeltaIdSet<PixelId, InvalidPixel>;
203
205 struct NodeRollbackState {
207 NodeId node = InvalidNode;
209 NodeId parent = InvalidNode;
211 NodeId firstChild = InvalidNode;
213 NodeId nextSibling = InvalidNode;
215 NodeId prevSibling = InvalidNode;
217 NodeId lastChild = InvalidNode;
219 int numChildren = 0;
221 std::uint8_t alive = 0;
223 PixelId properHead = InvalidPixel;
225 PixelId properTail = InvalidPixel;
227 int properPartCardinality = 0;
228 };
229
231 struct PixelRollbackState {
233 PixelId pixel = InvalidPixel;
235 NodeId smallestNodeId = InvalidNode;
237 PixelId next = InvalidPixel;
239 PixelId previous = InvalidPixel;
240 };
241
243 enum class FreeListMutation { Popped, Pushed };
244
246 struct FreeListRollbackState {
248 FreeListMutation mutation = FreeListMutation::Popped;
250 NodeId node = InvalidNode;
251 };
252
260 struct RollbackJournal {
262 std::size_t originalNodeSlots = 0;
264 NodeId root = InvalidNode;
266 int numNodes = 0;
268 std::optional<NodeId> preservedExternalNodeIdOffset;
270 std::size_t nodeStructureVersion = 0;
272 std::size_t topologyVersion = 0;
274 std::size_t properPartVersion = 0;
276 std::size_t mutationVersion = 0;
278 DeltaNodeSet capturedNodes;
280 DeltaPixelSet capturedPixels;
282 std::vector<NodeRollbackState> nodes;
284 std::vector<PixelRollbackState> pixels;
286 std::vector<FreeListRollbackState> freeListMutations;
287 };
288
290 MorphologicalTree* tree_ = nullptr;
292 bool active_ = false;
294 bool recoverable_ = false;
296 std::unique_ptr<RollbackJournal> rollbackJournal_;
298 DeltaNodeSet touchedNodes_;
300 int detachedNodeBalance_ = 0;
302 int unsupportedLeafBalance_ = 0;
304 bool incrementalValidationSupported_ = true;
306 bool invariantsEstablishedByConstruction_ = false;
307
315 class EditSessionPause {
316 private:
318 MorphologicalTree& tree_;
320 bool wasEditing_ = false;
321
322 public:
328 explicit EditSessionPause(MorphologicalTree& tree) noexcept : tree_(tree), wasEditing_(tree.editSessionOpen_) { tree_.editSessionOpen_ = false; }
329
333 ~EditSessionPause() noexcept { tree_.editSessionOpen_ = wasEditing_; }
334 };
335
342 explicit TreeEditor(MorphologicalTree& tree, bool invariantsEstablishedByConstruction = false)
343 : tree_(&tree), active_(true), recoverable_(!invariantsEstablishedByConstruction),
344 invariantsEstablishedByConstruction_(invariantsEstablishedByConstruction) {
345 tree_->beginEditSession();
346 }
347
351 void ensureRollbackJournal() {
352 if (!recoverable_ || rollbackJournal_) {
353 return;
354 }
355 auto journal = std::make_unique<RollbackJournal>();
356 journal->originalNodeSlots = tree_->nodeParent_.size();
357 journal->root = tree_->rootNodeId_;
358 journal->numNodes = tree_->numNodes_;
359 journal->preservedExternalNodeIdOffset = tree_->preservedExternalNodeIdOffset_;
360 journal->nodeStructureVersion = tree_->nodeStructureVersion_;
361 journal->topologyVersion = tree_->topologyVersion_;
362 journal->properPartVersion = tree_->properPartVersion_;
363 journal->mutationVersion = tree_->mutationVersion_;
364 rollbackJournal_ = std::move(journal);
365 }
366
372 MorphologicalTree& tree() const {
373 if (!active_ || tree_ == nullptr) {
374 throw std::logic_error("TreeEditor operation requires an active edit session.");
375 }
376 return *tree_;
377 }
378
389 void finishCommit(TreeEditValidationMode validationMode) noexcept {
390 if (active_ && tree_ != nullptr) {
391 tree_->recordEditCommit(validationMode);
392 tree_->endEditSession();
393 active_ = false;
394 recoverable_ = false;
395 rollbackJournal_.reset();
396 }
397 }
398
404 void captureNodeForRollback(NodeId node) {
405 if (!recoverable_ || node < 0 || static_cast<std::size_t>(node) >= tree_->nodeParent_.size()) {
406 return;
407 }
408 ensureRollbackJournal();
409 if (static_cast<std::size_t>(node) >= rollbackJournal_->originalNodeSlots || rollbackJournal_->capturedNodes.contains(node)) {
410 return;
411 }
412 rollbackJournal_->nodes.reserve(rollbackJournal_->nodes.size() + 1);
413 if (!rollbackJournal_->capturedNodes.insert(node)) {
414 return;
415 }
416 const std::size_t slot = static_cast<std::size_t>(node);
417 rollbackJournal_->nodes.push_back({node, tree_->nodeParent_[slot], tree_->firstChild_[slot], tree_->nextSibling_[slot], tree_->prevSibling_[slot],
418 tree_->lastChild_[slot], tree_->numChildrenByNode_[slot], tree_->alive_[slot], tree_->properHead_[slot],
419 tree_->properTail_[slot], tree_->properPartCardinalityByNode_[slot]});
420 }
421
427 void capturePixelForRollback(PixelId pixel) {
428 if (!recoverable_ || !tree_->isPixel(pixel)) {
429 return;
430 }
431 ensureRollbackJournal();
432 if (rollbackJournal_->capturedPixels.contains(pixel)) {
433 return;
434 }
435 rollbackJournal_->pixels.reserve(rollbackJournal_->pixels.size() + 1);
436 if (!rollbackJournal_->capturedPixels.insert(pixel)) {
437 return;
438 }
439 const std::size_t slot = static_cast<std::size_t>(pixel);
440 rollbackJournal_->pixels.push_back({pixel, tree_->smallestNodeMap_[slot], tree_->nextProperPart_[slot], tree_->prevProperPart_[slot]});
441 }
442
448 void captureNodeLinkNeighborhood(NodeId node) {
449 if (node < 0 || static_cast<std::size_t>(node) >= tree_->nodeParent_.size()) {
450 return;
451 }
452 captureNodeForRollback(node);
453 captureNodeForRollback(tree_->nodeParent_[static_cast<std::size_t>(node)]);
454 captureNodeForRollback(tree_->prevSibling_[static_cast<std::size_t>(node)]);
455 captureNodeForRollback(tree_->nextSibling_[static_cast<std::size_t>(node)]);
456 }
457
463 void capturePixelLinkNeighborhood(PixelId pixel) {
464 if (!tree_->isPixel(pixel)) {
465 return;
466 }
467 capturePixelForRollback(pixel);
468 capturePixelForRollback(tree_->prevProperPart_[static_cast<std::size_t>(pixel)]);
469 capturePixelForRollback(tree_->nextProperPart_[static_cast<std::size_t>(pixel)]);
470 }
471
477 void prepareFreeListGrowth(std::size_t count) {
478 if (!recoverable_ || count == 0) {
479 return;
480 }
481 ensureRollbackJournal();
482 rollbackJournal_->freeListMutations.reserve(rollbackJournal_->freeListMutations.size() + count);
483 tree_->freeNodeIds_.reserve(tree_->freeNodeIds_.size() + count);
484 }
485
491 void recordFreeListGrowth(std::size_t previousSize) noexcept {
492 if (!rollbackJournal_) {
493 return;
494 }
495 for (std::size_t i = previousSize; i < tree_->freeNodeIds_.size(); ++i) {
496 rollbackJournal_->freeListMutations.push_back({FreeListMutation::Pushed, tree_->freeNodeIds_[i]});
497 }
498 }
499
503 void restoreRollbackJournal() noexcept {
504 if (!active_ || tree_ == nullptr || !recoverable_) {
505 return;
506 }
507 if (!rollbackJournal_) {
508 tree_->endEditSession();
509 recoverable_ = false;
510 active_ = false;
511 return;
512 }
513
514 for (auto it = rollbackJournal_->freeListMutations.rbegin(); it != rollbackJournal_->freeListMutations.rend(); ++it) {
515 if (it->mutation == FreeListMutation::Pushed) {
516 assert(!tree_->freeNodeIds_.empty());
517 assert(tree_->freeNodeIds_.back() == it->node);
518 tree_->freeNodeIds_.pop_back();
519 } else {
520 tree_->freeNodeIds_.push_back(it->node);
521 }
522 }
523
524 const std::size_t originalSlots = rollbackJournal_->originalNodeSlots;
525 tree_->nodeParent_.resize(originalSlots);
526 tree_->firstChild_.resize(originalSlots);
527 tree_->nextSibling_.resize(originalSlots);
528 tree_->prevSibling_.resize(originalSlots);
529 tree_->lastChild_.resize(originalSlots);
530 tree_->numChildrenByNode_.resize(originalSlots);
531 tree_->alive_.resize(originalSlots);
532 tree_->properHead_.resize(originalSlots);
533 tree_->properTail_.resize(originalSlots);
534 tree_->properPartCardinalityByNode_.resize(originalSlots);
535
536 for (const NodeRollbackState& state : rollbackJournal_->nodes) {
537 const std::size_t slot = static_cast<std::size_t>(state.node);
538 tree_->nodeParent_[slot] = state.parent;
539 tree_->firstChild_[slot] = state.firstChild;
540 tree_->nextSibling_[slot] = state.nextSibling;
541 tree_->prevSibling_[slot] = state.prevSibling;
542 tree_->lastChild_[slot] = state.lastChild;
543 tree_->numChildrenByNode_[slot] = state.numChildren;
544 tree_->alive_[slot] = state.alive;
545 tree_->properHead_[slot] = state.properHead;
546 tree_->properTail_[slot] = state.properTail;
547 tree_->properPartCardinalityByNode_[slot] = state.properPartCardinality;
548 }
549
550 for (const PixelRollbackState& state : rollbackJournal_->pixels) {
551 const std::size_t slot = static_cast<std::size_t>(state.pixel);
552 tree_->smallestNodeMap_[slot] = state.smallestNodeId;
553 tree_->nextProperPart_[slot] = state.next;
554 tree_->prevProperPart_[slot] = state.previous;
555 }
556
557 tree_->rootNodeId_ = rollbackJournal_->root;
558 tree_->numNodes_ = rollbackJournal_->numNodes;
559 tree_->preservedExternalNodeIdOffset_ = rollbackJournal_->preservedExternalNodeIdOffset;
560 tree_->nodeStructureVersion_ = rollbackJournal_->nodeStructureVersion;
561 tree_->topologyVersion_ = rollbackJournal_->topologyVersion;
562 tree_->properPartVersion_ = rollbackJournal_->properPartVersion;
563 tree_->mutationVersion_ = rollbackJournal_->mutationVersion;
564 tree_->invalidateDfsIntervalCache();
565 tree_->invalidateLcaCache();
566 tree_->endEditSession();
567 rollbackJournal_.reset();
568 recoverable_ = false;
569 active_ = false;
570 }
571
579 [[nodiscard]] static bool isDetached(const MorphologicalTree& tree, NodeId node) noexcept {
580 return tree.isAlive(node) && node != tree.rootNodeId_ && tree.nodeParent_[static_cast<std::size_t>(node)] == node;
581 }
582
589 void recordDetachedTransition(bool wasDetached, bool isNowDetached) noexcept {
590 if (invariantsEstablishedByConstruction_) {
591 return;
592 }
593 detachedNodeBalance_ += static_cast<int>(isNowDetached) - static_cast<int>(wasDetached);
594 }
595
603 [[nodiscard]] static bool isUnsupportedLeaf(const MorphologicalTree& tree, NodeId node) noexcept {
604 return tree.isAlive(node) && tree.numChildrenByNode_[static_cast<std::size_t>(node)] == 0 &&
605 tree.properPartCardinalityByNode_[static_cast<std::size_t>(node)] == 0;
606 }
607
614 void recordUnsupportedLeafTransition(bool wasUnsupported, bool isNowUnsupported) noexcept {
615 if (invariantsEstablishedByConstruction_) {
616 return;
617 }
618 unsupportedLeafBalance_ += static_cast<int>(isNowUnsupported) - static_cast<int>(wasUnsupported);
619 }
620
626 void touch(NodeId node) {
627 if (invariantsEstablishedByConstruction_) {
628 return;
629 }
630 try {
631 static_cast<void>(touchedNodes_.insert(node));
632 } catch (...) {
633 incrementalValidationSupported_ = false;
634 throw;
635 }
636 }
637
638 public:
642 TreeEditor(const TreeEditor&) = delete;
646 TreeEditor& operator=(const TreeEditor&) = delete;
647
654 : tree_(other.tree_), active_(other.active_), recoverable_(other.recoverable_), rollbackJournal_(std::move(other.rollbackJournal_)),
655 touchedNodes_(std::move(other.touchedNodes_)), detachedNodeBalance_(other.detachedNodeBalance_),
656 unsupportedLeafBalance_(other.unsupportedLeafBalance_), incrementalValidationSupported_(other.incrementalValidationSupported_),
657 invariantsEstablishedByConstruction_(other.invariantsEstablishedByConstruction_) {
658 other.tree_ = nullptr;
659 other.active_ = false;
660 other.recoverable_ = false;
661 other.rollbackJournal_.reset();
662 other.detachedNodeBalance_ = 0;
663 other.unsupportedLeafBalance_ = 0;
664 other.incrementalValidationSupported_ = false;
665 other.invariantsEstablishedByConstruction_ = false;
666 }
667
672
679 ~TreeEditor() { restoreRollbackJournal(); }
680
686 [[nodiscard]] bool canRollback() const noexcept { return active_ && recoverable_; }
687
691 void rollback() {
692 if (!active_ || tree_ == nullptr) {
693 throw std::logic_error("TreeEditor::rollback requires an active edit session.");
694 }
695 if (!recoverable_) {
696 throw std::logic_error("TreeEditor::rollback is unavailable for the internal journal-free editor.");
697 }
698 restoreRollbackJournal();
699 }
700
709 [[nodiscard("Discarding a detached node id makes the staged edit impossible to complete safely")]] NodeId createDetachedNode() {
710 MorphologicalTree& t = tree();
711 if (invariantsEstablishedByConstruction_) {
712 return t.createDetachedNode();
713 }
714
715 ensureRollbackJournal();
716 const bool reusesFreeSlot = !t.freeNodeIds_.empty();
717 const NodeId candidate = reusesFreeSlot ? t.freeNodeIds_.back() : static_cast<NodeId>(t.nodeParent_.size());
718 captureNodeForRollback(candidate);
719 if (reusesFreeSlot) {
720 rollbackJournal_->freeListMutations.reserve(rollbackJournal_->freeListMutations.size() + 1);
721 rollbackJournal_->freeListMutations.push_back({FreeListMutation::Popped, candidate});
722 }
723 const NodeId nodeId = t.createDetachedNode();
724 recordDetachedTransition(false, isDetached(t, nodeId));
725 recordUnsupportedLeafTransition(false, isUnsupportedLeaf(t, nodeId));
726 touch(nodeId);
727 return nodeId;
728 }
729
736 MorphologicalTree& t = tree();
737 if (!t.isAlive(nodeId)) {
738 throw std::invalid_argument("TreeEditor::detach requires a live node.");
739 }
740 if (t.isRoot(nodeId)) {
741 throw std::invalid_argument("TreeEditor::detach cannot detach the connected root.");
742 }
743 if (invariantsEstablishedByConstruction_) {
744 t.detachNode(nodeId);
745 return;
746 }
747 const NodeId oldParent = t.parent(nodeId);
748 captureNodeLinkNeighborhood(nodeId);
749 touch(nodeId);
750 const bool wasDetached = isDetached(t, nodeId);
751 const bool oldParentWasUnsupported = isUnsupportedLeaf(t, oldParent);
752 t.detachNode(nodeId);
753 recordDetachedTransition(wasDetached, isDetached(t, nodeId));
754 recordUnsupportedLeafTransition(oldParentWasUnsupported, isUnsupportedLeaf(t, oldParent));
755 }
756
767 MorphologicalTree& t = tree();
768 if (!t.isAlive(nodeId) || !t.isAlive(newParentId)) {
769 throw std::invalid_argument("TreeEditor::reparent requires live node ids.");
770 }
771 if (t.isRoot(nodeId)) {
772 throw std::invalid_argument("TreeEditor::reparent cannot move the connected root.");
773 }
774 if (nodeId == newParentId) {
775 throw std::invalid_argument("TreeEditor::reparent requires distinct node ids.");
776 }
777 if (invariantsEstablishedByConstruction_) {
778 t.moveNode(nodeId, newParentId);
779 return;
780 }
781 const NodeId oldParent = t.parent(nodeId);
782 captureNodeLinkNeighborhood(nodeId);
783 captureNodeForRollback(newParentId);
784 captureNodeForRollback(t.lastChild_[static_cast<std::size_t>(newParentId)]);
785 touch(nodeId);
786 const bool wasDetached = isDetached(t, nodeId);
787 const bool oldParentWasUnsupported = isUnsupportedLeaf(t, oldParent);
788 const bool newParentWasUnsupported = isUnsupportedLeaf(t, newParentId);
789 t.moveNode(nodeId, newParentId);
790 recordDetachedTransition(wasDetached, isDetached(t, nodeId));
791 recordUnsupportedLeafTransition(oldParentWasUnsupported, isUnsupportedLeaf(t, oldParent));
792 if (oldParent != newParentId) {
793 recordUnsupportedLeafTransition(newParentWasUnsupported, isUnsupportedLeaf(t, newParentId));
794 }
795 }
796
807 MorphologicalTree& t = tree();
808 if (!t.isAlive(parentId) || !t.isAlive(detachedNodeId)) {
809 throw std::invalid_argument("TreeEditor::attach requires live node ids.");
810 }
811 if (parentId == detachedNodeId) {
812 throw std::invalid_argument("TreeEditor::attach requires distinct node ids.");
813 }
814 if (t.parent(detachedNodeId) != detachedNodeId) {
815 throw std::invalid_argument("TreeEditor::attach expects a detached self-parented node.");
816 }
817 if (invariantsEstablishedByConstruction_) {
818 t.attachNode(parentId, detachedNodeId);
819 return;
820 }
821 captureNodeLinkNeighborhood(detachedNodeId);
822 captureNodeForRollback(parentId);
823 captureNodeForRollback(t.lastChild_[static_cast<std::size_t>(parentId)]);
824 touch(detachedNodeId);
825 const bool wasDetached = isDetached(t, detachedNodeId);
826 const bool parentWasUnsupported = isUnsupportedLeaf(t, parentId);
827 t.attachNode(parentId, detachedNodeId);
828 recordDetachedTransition(wasDetached, isDetached(t, detachedNodeId));
829 recordUnsupportedLeafTransition(parentWasUnsupported, isUnsupportedLeaf(t, parentId));
830 }
831
843 MorphologicalTree& t = tree();
844 if (!t.isAlive(parentId) || !t.isAlive(sourceId)) {
845 throw std::invalid_argument("TreeEditor::moveChildren requires live node ids.");
846 }
847 if (parentId == sourceId) {
848 throw std::invalid_argument("TreeEditor::moveChildren requires distinct node ids.");
849 }
850 if (invariantsEstablishedByConstruction_) {
851 t.moveChildren(parentId, sourceId);
852 return;
853 }
854 captureNodeForRollback(parentId);
855 captureNodeForRollback(sourceId);
856 captureNodeForRollback(t.lastChild_[static_cast<std::size_t>(parentId)]);
857 for (NodeId child = t.firstChild_[static_cast<std::size_t>(sourceId)]; child != InvalidNode; child = t.nextSibling_[static_cast<std::size_t>(child)]) {
858 captureNodeForRollback(child);
859 }
860 touch(parentId);
861 const bool parentWasUnsupported = isUnsupportedLeaf(t, parentId);
862 const bool sourceWasUnsupported = isUnsupportedLeaf(t, sourceId);
863 t.moveChildren(parentId, sourceId);
864 recordUnsupportedLeafTransition(parentWasUnsupported, isUnsupportedLeaf(t, parentId));
865 recordUnsupportedLeafTransition(sourceWasUnsupported, isUnsupportedLeaf(t, sourceId));
866 }
867
879 MorphologicalTree& t = tree();
880 if (!t.isAlive(targetNodeId) || !t.isAlive(sourceNodeId)) {
881 throw std::invalid_argument("TreeEditor::movePixelToProperPart requires live node ids.");
882 }
883 if (targetNodeId == sourceNodeId) {
884 throw std::invalid_argument("TreeEditor::movePixelToProperPart requires distinct source and target nodes.");
885 }
886 if (!t.isPixel(pixel)) {
887 throw std::invalid_argument("TreeEditor::movePixelToProperPart requires a valid proper-part id.");
888 }
889 if (invariantsEstablishedByConstruction_) {
890 t.movePixelToProperPart(targetNodeId, sourceNodeId, pixel);
891 return;
892 }
893 captureNodeForRollback(targetNodeId);
894 captureNodeForRollback(sourceNodeId);
895 capturePixelLinkNeighborhood(pixel);
896 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(targetNodeId)]);
897 const bool targetWasUnsupported = isUnsupportedLeaf(t, targetNodeId);
898 const bool sourceWasUnsupported = isUnsupportedLeaf(t, sourceNodeId);
899 t.movePixelToProperPart(targetNodeId, sourceNodeId, pixel);
900 recordUnsupportedLeafTransition(targetWasUnsupported, isUnsupportedLeaf(t, targetNodeId));
901 recordUnsupportedLeafTransition(sourceWasUnsupported, isUnsupportedLeaf(t, sourceNodeId));
902 }
903
914 MorphologicalTree& t = tree();
915 if (!t.isAlive(targetNodeId) || !t.isAlive(sourceNodeId)) {
916 throw std::invalid_argument("TreeEditor::mergeProperParts requires live node ids.");
917 }
918 if (targetNodeId == sourceNodeId) {
919 throw std::invalid_argument("TreeEditor::mergeProperParts requires distinct source and target nodes.");
920 }
921 if (invariantsEstablishedByConstruction_) {
922 t.mergeProperParts(targetNodeId, sourceNodeId);
923 return;
924 }
925 captureNodeForRollback(targetNodeId);
926 captureNodeForRollback(sourceNodeId);
927 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(targetNodeId)]);
928 for (PixelId pixel = t.properHead_[static_cast<std::size_t>(sourceNodeId)]; pixel != InvalidPixel;
929 pixel = t.nextProperPart_[static_cast<std::size_t>(pixel)]) {
930 capturePixelForRollback(pixel);
931 }
932 const bool targetWasUnsupported = isUnsupportedLeaf(t, targetNodeId);
933 const bool sourceWasUnsupported = isUnsupportedLeaf(t, sourceNodeId);
934 t.mergeProperParts(targetNodeId, sourceNodeId);
935 recordUnsupportedLeafTransition(targetWasUnsupported, isUnsupportedLeaf(t, targetNodeId));
936 recordUnsupportedLeafTransition(sourceWasUnsupported, isUnsupportedLeaf(t, sourceNodeId));
937 }
938
947 MorphologicalTree& t = tree();
948 if (!t.isAlive(parentNodeId) || !t.isAlive(childId)) {
949 throw std::invalid_argument("TreeEditor::removeChild requires live node ids.");
950 }
951 if (!t.hasChild(parentNodeId, childId)) {
952 throw std::invalid_argument("TreeEditor::removeChild requires a direct parent-child relation.");
953 }
954 if (invariantsEstablishedByConstruction_) {
955 t.removeChild(parentNodeId, childId, releaseNodeFlag);
956 return;
957 }
958 captureNodeLinkNeighborhood(childId);
959 captureNodeForRollback(parentNodeId);
960 const bool willRelease =
961 releaseNodeFlag && t.numChildrenByNode_[static_cast<std::size_t>(childId)] == 0 && t.properPartCardinalityByNode_[static_cast<std::size_t>(childId)] == 0;
962 if (willRelease) {
963 prepareFreeListGrowth(1);
964 }
965 touch(childId);
966 const bool wasDetached = isDetached(t, childId);
967 const bool parentWasUnsupported = isUnsupportedLeaf(t, parentNodeId);
968 const bool childWasUnsupported = isUnsupportedLeaf(t, childId);
969 const std::size_t freeListSize = t.freeNodeIds_.size();
970 t.removeChild(parentNodeId, childId, releaseNodeFlag);
971 recordFreeListGrowth(freeListSize);
972 recordDetachedTransition(wasDetached, isDetached(t, childId));
973 recordUnsupportedLeafTransition(parentWasUnsupported, isUnsupportedLeaf(t, parentNodeId));
974 recordUnsupportedLeafTransition(childWasUnsupported, isUnsupportedLeaf(t, childId));
975 }
976
983 MorphologicalTree& t = tree();
984 if (!t.isAlive(nodeId)) {
985 throw std::invalid_argument("TreeEditor::releaseNode requires a live node.");
986 }
987 if (t.isRoot(nodeId)) {
988 throw std::invalid_argument("TreeEditor::releaseNode cannot release the connected root.");
989 }
990 if (t.parent(nodeId) != nodeId) {
991 throw std::invalid_argument("TreeEditor::releaseNode expects a detached self-parented node.");
992 }
993 if (invariantsEstablishedByConstruction_) {
994 t.releaseNode(nodeId);
995 return;
996 }
997 captureNodeForRollback(nodeId);
998 const bool willRelease = t.numChildrenByNode_[static_cast<std::size_t>(nodeId)] == 0 && t.properPartCardinalityByNode_[static_cast<std::size_t>(nodeId)] == 0;
999 if (willRelease) {
1000 prepareFreeListGrowth(1);
1001 }
1002 const bool wasDetached = isDetached(t, nodeId);
1003 const bool wasUnsupported = isUnsupportedLeaf(t, nodeId);
1004 const std::size_t freeListSize = t.freeNodeIds_.size();
1005 t.releaseNode(nodeId);
1006 recordFreeListGrowth(freeListSize);
1007 recordDetachedTransition(wasDetached, isDetached(t, nodeId));
1008 recordUnsupportedLeafTransition(wasUnsupported, isUnsupportedLeaf(t, nodeId));
1009 }
1010
1020 MorphologicalTree& t = tree();
1021 if (!t.isAlive(nodeId)) {
1022 throw std::invalid_argument("TreeEditor::setRoot requires a live node.");
1023 }
1024 if (invariantsEstablishedByConstruction_) {
1025 t.setRoot(nodeId);
1026 return;
1027 }
1028 const NodeId oldRoot = t.root();
1029 const NodeId oldParent = t.parent(nodeId);
1030 captureNodeForRollback(oldRoot);
1031 captureNodeLinkNeighborhood(nodeId);
1032 captureNodeForRollback(oldParent);
1033 touch(oldRoot);
1034 touch(oldParent);
1035 touch(nodeId);
1036 const bool oldRootWasDetached = isDetached(t, oldRoot);
1037 const bool nodeWasDetached = isDetached(t, nodeId);
1038 t.setRoot(nodeId);
1039 recordDetachedTransition(oldRootWasDetached, isDetached(t, oldRoot));
1040 if (nodeId != oldRoot) {
1041 recordDetachedTransition(nodeWasDetached, isDetached(t, nodeId));
1042 }
1043 }
1044
1051 MorphologicalTree& t = tree();
1052 if (invariantsEstablishedByConstruction_) {
1053 EditSessionPause pause(t);
1054 t.pruneNode(nodeId);
1055 return;
1056 }
1057 incrementalValidationSupported_ = false;
1058
1059 if (t.isAlive(nodeId) && !t.isRoot(nodeId)) {
1060 const NodeId parent = t.nodeParent_[static_cast<std::size_t>(nodeId)];
1061 if (parent != InvalidNode && parent != nodeId) {
1062 std::vector<NodeId> subtree;
1063 subtree.push_back(nodeId);
1064 for (std::size_t i = 0; i < subtree.size(); ++i) {
1065 const NodeId current = subtree[i];
1066 for (NodeId child = t.firstChild_[static_cast<std::size_t>(current)]; child != InvalidNode;
1067 child = t.nextSibling_[static_cast<std::size_t>(child)]) {
1068 subtree.push_back(child);
1069 }
1070 }
1071
1072 captureNodeForRollback(parent);
1073 captureNodeLinkNeighborhood(nodeId);
1074 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(parent)]);
1075 for (NodeId current : subtree) {
1076 captureNodeForRollback(current);
1077 for (PixelId pixel = t.properHead_[static_cast<std::size_t>(current)]; pixel != InvalidPixel;
1078 pixel = t.nextProperPart_[static_cast<std::size_t>(pixel)]) {
1079 capturePixelForRollback(pixel);
1080 }
1081 }
1082 prepareFreeListGrowth(subtree.size());
1083 }
1084 }
1085
1086 const std::size_t freeListSize = t.freeNodeIds_.size();
1087 EditSessionPause pause(t);
1088 t.pruneNode(nodeId);
1089 recordFreeListGrowth(freeListSize);
1090 }
1091
1098 MorphologicalTree& t = tree();
1099 if (invariantsEstablishedByConstruction_) {
1100 EditSessionPause pause(t);
1101 t.mergeNodeIntoParent(nodeId);
1102 return;
1103 }
1104 incrementalValidationSupported_ = false;
1105
1106 if (t.isAlive(nodeId) && !t.isRoot(nodeId)) {
1107 const NodeId parent = t.nodeParent_[static_cast<std::size_t>(nodeId)];
1108 if (parent != InvalidNode && parent != nodeId) {
1109 captureNodeForRollback(parent);
1110 captureNodeLinkNeighborhood(nodeId);
1111 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(parent)]);
1112 for (PixelId pixel = t.properHead_[static_cast<std::size_t>(nodeId)]; pixel != InvalidPixel;
1113 pixel = t.nextProperPart_[static_cast<std::size_t>(pixel)]) {
1114 capturePixelForRollback(pixel);
1115 }
1116 for (NodeId child = t.firstChild_[static_cast<std::size_t>(nodeId)]; child != InvalidNode;
1117 child = t.nextSibling_[static_cast<std::size_t>(child)]) {
1118 captureNodeForRollback(child);
1119 }
1120 prepareFreeListGrowth(1);
1121 }
1122 }
1123
1124 const std::size_t freeListSize = t.freeNodeIds_.size();
1125 EditSessionPause pause(t);
1126 t.mergeNodeIntoParent(nodeId);
1127 recordFreeListGrowth(freeListSize);
1128 }
1129
1135 [[nodiscard]] bool hasDetachedAliveNodes() const noexcept { return active_ && tree_ != nullptr && tree_->hasDetachedAliveNodes(); }
1136
1137 private:
1145 [[nodiscard]] TreeValidationResult validateIncrementalTopology(bool changedParentCyclesExcluded) const noexcept {
1146 try {
1147 if (!active_ || tree_ == nullptr) {
1148 return {false, "Incremental topology validation requires an active edit session."};
1149 }
1150 if (detachedNodeBalance_ != 0) {
1151 return {false, "Incremental topology validation found detached alive nodes."};
1152 }
1153 if (unsupportedLeafBalance_ != 0) {
1154 return {false, "Incremental topology validation found a live node whose subtree support is empty."};
1155 }
1156
1157 const NodeId root = tree_->root();
1158 if (!tree_->isAlive(root) || tree_->parent(root) != root) {
1159 return {false, "Incremental topology validation requires a live self-parented root."};
1160 }
1161
1162 const int numNodes = tree_->numNodes();
1163 for (NodeId node : touchedNodes_.entries()) {
1164 if (!tree_->isAlive(node)) {
1165 continue;
1166 }
1167
1168 if (!changedParentCyclesExcluded) {
1169 NodeId cursor = node;
1170 int pathLength = 0;
1171 while (cursor != root) {
1172 if (!tree_->isAlive(cursor)) {
1173 return {false, "Incremental topology validation found a parent path outside the alive node domain."};
1174 }
1175 const NodeId parent = tree_->parent(cursor);
1176 if (parent == InvalidNode || parent == cursor || !tree_->isAlive(parent)) {
1177 return {false, "Incremental topology validation found a detached or invalid parent path."};
1178 }
1179 cursor = parent;
1180 if (++pathLength > numNodes) {
1181 return {false, "Incremental topology validation found a parent cycle."};
1182 }
1183 }
1184 }
1185 }
1186 return {true, ""};
1187 } catch (const std::exception& ex) {
1188 return {false, ex.what()};
1189 } catch (...) {
1190 return {false, "Incremental topology validation failed with an unknown error."};
1191 }
1192 }
1193
1200 [[nodiscard]] IncrementalProof proveIncrementalImpl(bool changedParentCyclesExcluded) {
1201 TreeValidationResult result;
1202 TreeEditValidationMode validationMode = TreeEditValidationMode::Incremental;
1203 if (invariantsEstablishedByConstruction_ && incrementalValidationSupported_) {
1204 result = {true, ""};
1205 } else if (incrementalValidationSupported_) {
1206 result = validateIncrementalTopology(changedParentCyclesExcluded);
1207 } else {
1208 result = validate();
1209 validationMode = TreeEditValidationMode::Complete;
1210 }
1211 if (!result.ok) {
1212 throw std::runtime_error(result.message);
1213 }
1214#ifndef NDEBUG
1215 if (validationMode == TreeEditValidationMode::Incremental) {
1216 const TreeValidationResult oracle = validate();
1217 if (!oracle.ok) {
1218 throw std::runtime_error(std::string("Incremental topology proof disagrees with the complete validation oracle: ") + oracle.message);
1219 }
1220 }
1221#endif
1222 return IncrementalProof(*this, validationMode);
1223 }
1224
1233 [[nodiscard]] IncrementalProof proveIncrementalWithStrictAltitudeAcyclicity() { return proveIncrementalImpl(true); }
1234
1235 public:
1245 [[nodiscard]] IncrementalProof proveIncremental() { return proveIncrementalImpl(false); }
1246
1253 if (!active_ || tree_ == nullptr || proof.editor_ != this || proof.tree_ != tree_ || proof.mutationVersion_ != tree_->getMutationVersion()) {
1254 throw std::logic_error("Incremental topology proof is stale or belongs to another edit session.");
1255 }
1256
1257 const TreeEditValidationMode validationMode = proof.validationMode_;
1258 proof.editor_ = nullptr;
1259 proof.tree_ = nullptr;
1260 finishCommit(validationMode);
1261 }
1262
1269 if (!active_ || tree_ == nullptr) {
1270 return {false, "TreeEditor validation requires an active edit session."};
1271 }
1272 return tree_->validateConnectedRootedTreeResult();
1273 }
1274
1280 [[nodiscard("Inspect the validation result or use commit() for exception-based failure handling")]] TreeValidationResult validateAndCommit() noexcept {
1282 if (!result.ok) {
1283 return result;
1284 }
1285 finishCommit(TreeEditValidationMode::Complete);
1286 return result;
1287 }
1288
1295 void commit() {
1297 if (!result.ok) {
1298 throw std::runtime_error(result.message);
1299 }
1300 }
1301};
1302
1304
1305} // namespace mmcfilters
int PixelId
Pixel identifier type used by source and active construction domains.
Definition Common.hpp:26
int NodeId
Node identifier type used throughout the project.
Definition Common.hpp:17
constexpr NodeId InvalidNode
Sentinel value used to denote an invalid node identifier.
Definition Common.hpp:34
constexpr PixelId InvalidPixel
Sentinel value used to denote an invalid pixel identifier.
Definition Common.hpp:43
Mutable connected-subset tree on a finite pixel domain.
TreeEditor edit()
Opens the only public entrypoint for staged structural mutations.
Move-only evidence that the current edit revision satisfies the generic topology invariants.
bool usedCompleteValidation() const noexcept
Returns whether this proof was established by complete validation.
IncrementalProof(const IncrementalProof &)=delete
Disables copy construction.
IncrementalProof & operator=(IncrementalProof &&)=delete
Disables move assignment.
IncrementalProof & operator=(const IncrementalProof &)=delete
Disables copy assignment.
IncrementalProof(IncrementalProof &&other) noexcept
Transfers proof ownership and invalidates other.
Thin edit-session facade for multi-step topology updates.
TreeValidationResult validate() const noexcept
Runs strong validation without closing the session.
void detach(NodeId nodeId)
Detaches one non-root node from the connected rooted component.
TreeEditor(const TreeEditor &)=delete
Disables copy construction.
void commit(IncrementalProof &&proof)
Commits the exact edit revision represented by proof.
bool hasDetachedAliveNodes() const noexcept
Returns whether the staged edit still has detached alive nodes.
void mergeNodeIntoParent(NodeId nodeId)
Applies the committed-safe parent merge inside the staged edit.
TreeEditor & operator=(TreeEditor &&)=delete
Disables move assignment.
void reparent(NodeId nodeId, NodeId newParentId)
Reparents one live non-root node under another live node.
void attach(NodeId parentId, NodeId detachedNodeId)
Attaches one detached node back under the connected rooted tree.
NodeId createDetachedNode()
Creates a live detached node in the topological hierarchy.
void setRoot(NodeId nodeId)
Promotes nodeId to become the connected root.
void removeChild(NodeId parentNodeId, NodeId childId, bool releaseNodeFlag)
Detaches a direct child from its parent and optionally releases an empty detached slot.
void movePixelToProperPart(NodeId targetNodeId, NodeId sourceNodeId, PixelId pixel)
Transfers one direct proper part from sourceNodeId to targetNodeId.
void releaseNode(NodeId nodeId)
Releases an empty detached non-root node slot.
TreeEditor(TreeEditor &&other) noexcept
Transfers the open edit-session handle without closing it.
void commit()
Finalizes the edit by validating that the tree is connected again.
~TreeEditor()
Rolls back an unfinished recoverable public edit.
TreeEditor & operator=(const TreeEditor &)=delete
Disables copy assignment.
void moveChildren(NodeId parentId, NodeId sourceId)
Transfers every direct child of sourceId under parentId.
void rollback()
Aborts a recoverable edit and restores its original state.
bool canRollback() const noexcept
Tests whether this editor owns a delta rollback journal.
void pruneNode(NodeId nodeId)
Applies the committed-safe subtree prune inside the staged edit.
IncrementalProof proveIncremental()
Produces move-only evidence for the current edit revision.
TreeValidationResult validateAndCommit() noexcept
Validates and closes the edit session on success.
void mergeProperParts(NodeId targetNodeId, NodeId sourceNodeId)
Transfers every direct proper part from sourceNodeId to targetNodeId.
Edit-session facade for ValuedMorphologicalTree.
Owning result for one computed scalar attribute layout and buffer.
Non-throwing validation result returned by edit-session checks.