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_->reserveFreeNodeSlots(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
735 void detach(NodeId nodeId) {
736 MorphologicalTree& t = tree();
737 if (invariantsEstablishedByConstruction_) {
738 t.detachNode(nodeId);
739 return;
740 }
741 if (!t.isAlive(nodeId)) {
742 throw std::invalid_argument("TreeEditor::detach requires a live node.");
743 }
744 if (t.isRoot(nodeId)) {
745 throw std::invalid_argument("TreeEditor::detach cannot detach the connected root.");
746 }
747
748 const NodeId oldParent = t.parent(nodeId);
749 captureNodeLinkNeighborhood(nodeId);
750 touch(nodeId);
751 const bool wasDetached = isDetached(t, nodeId);
752 const bool oldParentWasUnsupported = isUnsupportedLeaf(t, oldParent);
753 t.detachNode(nodeId);
754 recordDetachedTransition(wasDetached, isDetached(t, nodeId));
755 recordUnsupportedLeafTransition(oldParentWasUnsupported, isUnsupportedLeaf(t, oldParent));
756 }
757
768 MorphologicalTree& t = tree();
769 if (invariantsEstablishedByConstruction_) {
770 t.moveNode(nodeId, newParentId);
771 return;
772 }
773 if (!t.isAlive(nodeId) || !t.isAlive(newParentId)) {
774 throw std::invalid_argument("TreeEditor::reparent requires live node ids.");
775 }
776 if (t.isRoot(nodeId)) {
777 throw std::invalid_argument("TreeEditor::reparent cannot move the connected root.");
778 }
779 if (nodeId == newParentId) {
780 throw std::invalid_argument("TreeEditor::reparent requires distinct node ids.");
781 }
782
783 const NodeId oldParent = t.parent(nodeId);
784 captureNodeLinkNeighborhood(nodeId);
785 captureNodeForRollback(newParentId);
786 captureNodeForRollback(t.lastChild_[static_cast<std::size_t>(newParentId)]);
787 touch(nodeId);
788 const bool wasDetached = isDetached(t, nodeId);
789 const bool oldParentWasUnsupported = isUnsupportedLeaf(t, oldParent);
790 const bool newParentWasUnsupported = isUnsupportedLeaf(t, newParentId);
791 t.moveNode(nodeId, newParentId);
792 recordDetachedTransition(wasDetached, isDetached(t, nodeId));
793 recordUnsupportedLeafTransition(oldParentWasUnsupported, isUnsupportedLeaf(t, oldParent));
794 if (oldParent != newParentId) {
795 recordUnsupportedLeafTransition(newParentWasUnsupported, isUnsupportedLeaf(t, newParentId));
796 }
797 }
798
809 MorphologicalTree& t = tree();
810 if (invariantsEstablishedByConstruction_) {
811 t.attachNode(parentId, detachedNodeId);
812 return;
813 }
814 if (!t.isAlive(parentId) || !t.isAlive(detachedNodeId)) {
815 throw std::invalid_argument("TreeEditor::attach requires live node ids.");
816 }
817 if (parentId == detachedNodeId) {
818 throw std::invalid_argument("TreeEditor::attach requires distinct node ids.");
819 }
820 if (t.parent(detachedNodeId) != detachedNodeId) {
821 throw std::invalid_argument("TreeEditor::attach expects a detached self-parented node.");
822 }
823 captureNodeLinkNeighborhood(detachedNodeId);
824 captureNodeForRollback(parentId);
825 captureNodeForRollback(t.lastChild_[static_cast<std::size_t>(parentId)]);
826 touch(detachedNodeId);
827 const bool wasDetached = isDetached(t, detachedNodeId);
828 const bool parentWasUnsupported = isUnsupportedLeaf(t, parentId);
829 t.attachNode(parentId, detachedNodeId);
830 recordDetachedTransition(wasDetached, isDetached(t, detachedNodeId));
831 recordUnsupportedLeafTransition(parentWasUnsupported, isUnsupportedLeaf(t, parentId));
832 }
833
845 MorphologicalTree& t = tree();
846 if (invariantsEstablishedByConstruction_) {
847 t.moveChildren(parentId, sourceId);
848 return;
849 }
850 if (!t.isAlive(parentId) || !t.isAlive(sourceId)) {
851 throw std::invalid_argument("TreeEditor::moveChildren requires live node ids.");
852 }
853 if (parentId == sourceId) {
854 throw std::invalid_argument("TreeEditor::moveChildren requires distinct node ids.");
855 }
856 captureNodeForRollback(parentId);
857 captureNodeForRollback(sourceId);
858 captureNodeForRollback(t.lastChild_[static_cast<std::size_t>(parentId)]);
859 for (NodeId child = t.firstChild_[static_cast<std::size_t>(sourceId)]; child != InvalidNode; child = t.nextSibling_[static_cast<std::size_t>(child)]) {
860 captureNodeForRollback(child);
861 }
862 touch(parentId);
863 const bool parentWasUnsupported = isUnsupportedLeaf(t, parentId);
864 const bool sourceWasUnsupported = isUnsupportedLeaf(t, sourceId);
865 t.moveChildren(parentId, sourceId);
866 recordUnsupportedLeafTransition(parentWasUnsupported, isUnsupportedLeaf(t, parentId));
867 recordUnsupportedLeafTransition(sourceWasUnsupported, isUnsupportedLeaf(t, sourceId));
868 }
869
881 MorphologicalTree& t = tree();
882 if (invariantsEstablishedByConstruction_) {
883 t.movePixelToProperPart(targetNodeId, sourceNodeId, pixel);
884 return;
885 }
886 if (!t.isAlive(targetNodeId) || !t.isAlive(sourceNodeId)) {
887 throw std::invalid_argument("TreeEditor::movePixelToProperPart requires live node ids.");
888 }
889 if (targetNodeId == sourceNodeId) {
890 throw std::invalid_argument("TreeEditor::movePixelToProperPart requires distinct source and target nodes.");
891 }
892 if (!t.isPixel(pixel)) {
893 throw std::invalid_argument("TreeEditor::movePixelToProperPart requires a valid proper-part id.");
894 }
895 captureNodeForRollback(targetNodeId);
896 captureNodeForRollback(sourceNodeId);
897 capturePixelLinkNeighborhood(pixel);
898 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(targetNodeId)]);
899 const bool targetWasUnsupported = isUnsupportedLeaf(t, targetNodeId);
900 const bool sourceWasUnsupported = isUnsupportedLeaf(t, sourceNodeId);
901 t.movePixelToProperPart(targetNodeId, sourceNodeId, pixel);
902 recordUnsupportedLeafTransition(targetWasUnsupported, isUnsupportedLeaf(t, targetNodeId));
903 recordUnsupportedLeafTransition(sourceWasUnsupported, isUnsupportedLeaf(t, sourceNodeId));
904 }
905
916 MorphologicalTree& t = tree();
917 if (invariantsEstablishedByConstruction_) {
918 t.mergeProperParts(targetNodeId, sourceNodeId);
919 return;
920 }
921 if (!t.isAlive(targetNodeId) || !t.isAlive(sourceNodeId)) {
922 throw std::invalid_argument("TreeEditor::mergeProperParts requires live node ids.");
923 }
924 if (targetNodeId == sourceNodeId) {
925 throw std::invalid_argument("TreeEditor::mergeProperParts requires distinct source and target nodes.");
926 }
927 captureNodeForRollback(targetNodeId);
928 captureNodeForRollback(sourceNodeId);
929 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(targetNodeId)]);
930 for (PixelId pixel = t.properHead_[static_cast<std::size_t>(sourceNodeId)]; pixel != InvalidPixel;
931 pixel = t.nextProperPart_[static_cast<std::size_t>(pixel)]) {
932 capturePixelForRollback(pixel);
933 }
934 const bool targetWasUnsupported = isUnsupportedLeaf(t, targetNodeId);
935 const bool sourceWasUnsupported = isUnsupportedLeaf(t, sourceNodeId);
936 t.mergeProperParts(targetNodeId, sourceNodeId);
937 recordUnsupportedLeafTransition(targetWasUnsupported, isUnsupportedLeaf(t, targetNodeId));
938 recordUnsupportedLeafTransition(sourceWasUnsupported, isUnsupportedLeaf(t, sourceNodeId));
939 }
940
949 MorphologicalTree& t = tree();
950 if (invariantsEstablishedByConstruction_) {
951 t.removeChild(parentNodeId, childId, releaseNodeFlag);
952 return;
953 }
954 if (!t.isAlive(parentNodeId) || !t.isAlive(childId)) {
955 throw std::invalid_argument("TreeEditor::removeChild requires live node ids.");
956 }
957 if (!t.hasChild(parentNodeId, childId)) {
958 throw std::invalid_argument("TreeEditor::removeChild requires a direct parent-child relation.");
959 }
960 captureNodeLinkNeighborhood(childId);
961 captureNodeForRollback(parentNodeId);
962 const bool willRelease =
963 releaseNodeFlag && t.numChildrenByNode_[static_cast<std::size_t>(childId)] == 0 && t.properPartCardinalityByNode_[static_cast<std::size_t>(childId)] == 0;
964 if (willRelease) {
965 prepareFreeListGrowth(1);
966 }
967 touch(childId);
968 const bool wasDetached = isDetached(t, childId);
969 const bool parentWasUnsupported = isUnsupportedLeaf(t, parentNodeId);
970 const bool childWasUnsupported = isUnsupportedLeaf(t, childId);
971 const std::size_t freeListSize = t.freeNodeIds_.size();
972 t.removeChild(parentNodeId, childId, releaseNodeFlag);
973 recordFreeListGrowth(freeListSize);
974 recordDetachedTransition(wasDetached, isDetached(t, childId));
975 recordUnsupportedLeafTransition(parentWasUnsupported, isUnsupportedLeaf(t, parentNodeId));
976 recordUnsupportedLeafTransition(childWasUnsupported, isUnsupportedLeaf(t, childId));
977 }
978
984 void releaseNode(NodeId nodeId) {
985 MorphologicalTree& t = tree();
986 if (invariantsEstablishedByConstruction_) {
987 t.releaseNode(nodeId);
988 return;
989 }
990 if (!t.isAlive(nodeId)) {
991 throw std::invalid_argument("TreeEditor::releaseNode requires a live node.");
992 }
993 if (t.isRoot(nodeId)) {
994 throw std::invalid_argument("TreeEditor::releaseNode cannot release the connected root.");
995 }
996 if (t.parent(nodeId) != nodeId) {
997 throw std::invalid_argument("TreeEditor::releaseNode expects a detached self-parented node.");
998 }
999 captureNodeForRollback(nodeId);
1000 const bool willRelease = t.numChildrenByNode_[static_cast<std::size_t>(nodeId)] == 0 && t.properPartCardinalityByNode_[static_cast<std::size_t>(nodeId)] == 0;
1001 if (willRelease) {
1002 prepareFreeListGrowth(1);
1003 }
1004 const bool wasDetached = isDetached(t, nodeId);
1005 const bool wasUnsupported = isUnsupportedLeaf(t, nodeId);
1006 const std::size_t freeListSize = t.freeNodeIds_.size();
1007 t.releaseNode(nodeId);
1008 recordFreeListGrowth(freeListSize);
1009 recordDetachedTransition(wasDetached, isDetached(t, nodeId));
1010 recordUnsupportedLeafTransition(wasUnsupported, isUnsupportedLeaf(t, nodeId));
1011 }
1012
1021 void setRoot(NodeId nodeId) {
1022 MorphologicalTree& t = tree();
1023 if (invariantsEstablishedByConstruction_) {
1024 t.setRoot(nodeId);
1025 return;
1026 }
1027 if (!t.isAlive(nodeId)) {
1028 throw std::invalid_argument("TreeEditor::setRoot requires a live node.");
1029 }
1030
1031 const NodeId oldRoot = t.root();
1032 const NodeId oldParent = t.parent(nodeId);
1033 captureNodeForRollback(oldRoot);
1034 captureNodeLinkNeighborhood(nodeId);
1035 captureNodeForRollback(oldParent);
1036 touch(oldRoot);
1037 touch(oldParent);
1038 touch(nodeId);
1039 const bool oldRootWasDetached = isDetached(t, oldRoot);
1040 const bool nodeWasDetached = isDetached(t, nodeId);
1041 t.setRoot(nodeId);
1042 recordDetachedTransition(oldRootWasDetached, isDetached(t, oldRoot));
1043 if (nodeId != oldRoot) {
1044 recordDetachedTransition(nodeWasDetached, isDetached(t, nodeId));
1045 }
1046 }
1047
1053 void pruneNode(NodeId nodeId) {
1054 MorphologicalTree& t = tree();
1055 if (invariantsEstablishedByConstruction_) {
1056 EditSessionPause pause(t);
1057 t.pruneNode(nodeId);
1058 return;
1059 }
1060 incrementalValidationSupported_ = false;
1061
1062 if (t.isAlive(nodeId) && !t.isRoot(nodeId)) {
1063 const NodeId parent = t.nodeParent_[static_cast<std::size_t>(nodeId)];
1064 if (parent != InvalidNode && parent != nodeId) {
1065 std::vector<NodeId> subtree;
1066 subtree.push_back(nodeId);
1067 for (std::size_t i = 0; i < subtree.size(); ++i) {
1068 const NodeId current = subtree[i];
1069 for (NodeId child = t.firstChild_[static_cast<std::size_t>(current)]; child != InvalidNode;
1070 child = t.nextSibling_[static_cast<std::size_t>(child)]) {
1071 subtree.push_back(child);
1072 }
1073 }
1074
1075 captureNodeForRollback(parent);
1076 captureNodeLinkNeighborhood(nodeId);
1077 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(parent)]);
1078 for (NodeId current : subtree) {
1079 captureNodeForRollback(current);
1080 for (PixelId pixel = t.properHead_[static_cast<std::size_t>(current)]; pixel != InvalidPixel;
1081 pixel = t.nextProperPart_[static_cast<std::size_t>(pixel)]) {
1082 capturePixelForRollback(pixel);
1083 }
1084 }
1085 prepareFreeListGrowth(subtree.size());
1086 }
1087 }
1088
1089 const std::size_t freeListSize = t.freeNodeIds_.size();
1090 EditSessionPause pause(t);
1091 t.pruneNode(nodeId);
1092 recordFreeListGrowth(freeListSize);
1093 }
1094
1101 MorphologicalTree& t = tree();
1102 if (invariantsEstablishedByConstruction_) {
1103 EditSessionPause pause(t);
1104 t.mergeNodeIntoParent(nodeId);
1105 return;
1106 }
1107 incrementalValidationSupported_ = false;
1108
1109 if (t.isAlive(nodeId) && !t.isRoot(nodeId)) {
1110 const NodeId parent = t.nodeParent_[static_cast<std::size_t>(nodeId)];
1111 if (parent != InvalidNode && parent != nodeId) {
1112 captureNodeForRollback(parent);
1113 captureNodeLinkNeighborhood(nodeId);
1114 capturePixelForRollback(t.properTail_[static_cast<std::size_t>(parent)]);
1115 for (PixelId pixel = t.properHead_[static_cast<std::size_t>(nodeId)]; pixel != InvalidPixel;
1116 pixel = t.nextProperPart_[static_cast<std::size_t>(pixel)]) {
1117 capturePixelForRollback(pixel);
1118 }
1119 for (NodeId child = t.firstChild_[static_cast<std::size_t>(nodeId)]; child != InvalidNode;
1120 child = t.nextSibling_[static_cast<std::size_t>(child)]) {
1121 captureNodeForRollback(child);
1122 }
1123 prepareFreeListGrowth(1);
1124 }
1125 }
1126
1127 const std::size_t freeListSize = t.freeNodeIds_.size();
1128 EditSessionPause pause(t);
1129 t.mergeNodeIntoParent(nodeId);
1130 recordFreeListGrowth(freeListSize);
1131 }
1132
1138 [[nodiscard]] bool hasDetachedAliveNodes() const noexcept { return active_ && tree_ != nullptr && tree_->hasDetachedAliveNodes(); }
1139
1140 private:
1148 [[nodiscard]] TreeValidationResult validateIncrementalTopology(bool changedParentCyclesExcluded) const noexcept {
1149 try {
1150 if (!active_ || tree_ == nullptr) {
1151 return {false, "Incremental topology validation requires an active edit session."};
1152 }
1153 if (detachedNodeBalance_ != 0) {
1154 return {false, "Incremental topology validation found detached alive nodes."};
1155 }
1156 if (unsupportedLeafBalance_ != 0) {
1157 return {false, "Incremental topology validation found a live node whose subtree support is empty."};
1158 }
1159
1160 const NodeId root = tree_->root();
1161 if (!tree_->isAlive(root) || tree_->parent(root) != root) {
1162 return {false, "Incremental topology validation requires a live self-parented root."};
1163 }
1164
1165 const int numNodes = tree_->numNodes();
1166 for (NodeId node : touchedNodes_.entries()) {
1167 if (!tree_->isAlive(node)) {
1168 continue;
1169 }
1170
1171 if (!changedParentCyclesExcluded) {
1172 NodeId cursor = node;
1173 int pathLength = 0;
1174 while (cursor != root) {
1175 if (!tree_->isAlive(cursor)) {
1176 return {false, "Incremental topology validation found a parent path outside the alive node domain."};
1177 }
1178 const NodeId parent = tree_->parent(cursor);
1179 if (parent == InvalidNode || parent == cursor || !tree_->isAlive(parent)) {
1180 return {false, "Incremental topology validation found a detached or invalid parent path."};
1181 }
1182 cursor = parent;
1183 if (++pathLength > numNodes) {
1184 return {false, "Incremental topology validation found a parent cycle."};
1185 }
1186 }
1187 }
1188 }
1189 return {true, ""};
1190 } catch (const std::exception& ex) {
1191 return {false, ex.what()};
1192 } catch (...) {
1193 return {false, "Incremental topology validation failed with an unknown error."};
1194 }
1195 }
1196
1203 [[nodiscard]] IncrementalProof proveIncrementalImpl(bool changedParentCyclesExcluded) {
1204 TreeValidationResult result;
1205 TreeEditValidationMode validationMode = TreeEditValidationMode::Incremental;
1206 if (invariantsEstablishedByConstruction_ && incrementalValidationSupported_) {
1207 result = {true, ""};
1208 } else if (incrementalValidationSupported_) {
1209 result = validateIncrementalTopology(changedParentCyclesExcluded);
1210 } else {
1211 result = validate();
1212 validationMode = TreeEditValidationMode::Complete;
1213 }
1214 if (!result.ok) {
1215 throw std::runtime_error(result.message);
1216 }
1217#ifndef NDEBUG
1218 if (validationMode == TreeEditValidationMode::Incremental) {
1219 const TreeValidationResult oracle = validate();
1220 if (!oracle.ok) {
1221 throw std::runtime_error(std::string("Incremental topology proof disagrees with the complete validation oracle: ") + oracle.message);
1222 }
1223 }
1224#endif
1225 return IncrementalProof(*this, validationMode);
1226 }
1227
1236 [[nodiscard]] IncrementalProof proveIncrementalWithStrictAltitudeAcyclicity() { return proveIncrementalImpl(true); }
1237
1238 public:
1248 [[nodiscard]] IncrementalProof proveIncremental() { return proveIncrementalImpl(false); }
1249
1256 if (!active_ || tree_ == nullptr || proof.editor_ != this || proof.tree_ != tree_ || proof.mutationVersion_ != tree_->getMutationVersion()) {
1257 throw std::logic_error("Incremental topology proof is stale or belongs to another edit session.");
1258 }
1259
1260 const TreeEditValidationMode validationMode = proof.validationMode_;
1261 proof.editor_ = nullptr;
1262 proof.tree_ = nullptr;
1263 finishCommit(validationMode);
1264 }
1265
1272 if (!active_ || tree_ == nullptr) {
1273 return {false, "TreeEditor validation requires an active edit session."};
1274 }
1275 return tree_->validateConnectedRootedTreeResult();
1276 }
1277
1283 [[nodiscard("Inspect the validation result or use commit() for exception-based failure handling")]] TreeValidationResult validateAndCommit() noexcept {
1285 if (!result.ok) {
1286 return result;
1287 }
1288 finishCommit(TreeEditValidationMode::Complete);
1289 return result;
1290 }
1291
1298 void commit() {
1300 if (!result.ok) {
1301 throw std::runtime_error(result.message);
1302 }
1303 }
1304};
1305
1307
1308} // 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.