45 std::size_t mutationVersion_ = 0;
47 TreeEditValidationMode validationMode_ = TreeEditValidationMode::Complete;
56 : editor_(&
editor), tree_(
editor.tree_), mutationVersion_(
editor.tree_ !=
nullptr ?
editor.tree_->getMutationVersion() : 0),
75 : editor_(
other.editor_), tree_(
other.tree_), mutationVersion_(
other.mutationVersion_), validationMode_(
other.validationMode_) {
76 other.editor_ =
nullptr;
77 other.tree_ =
nullptr;
101 template <
class Id, Id Inval
idId>
class DeltaIdSet {
103 std::vector<Id> ids_;
105 std::vector<Id> table_;
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});
122 void rebuild(std::size_t capacity) {
123 std::vector<Id> newTable(capacity, InvalidId);
124 const std::size_t mask = capacity - 1;
126 std::size_t slot = hash(
id) & mask;
127 while (newTable[slot] != InvalidId) {
128 slot = (slot + 1) & mask;
132 table_ = std::move(newTable);
142 [[nodiscard]]
bool contains(Id
id)
const noexcept {
143 if (
id == InvalidId || table_.empty()) {
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) {
152 slot = (slot + 1) & mask;
163 [[nodiscard]]
bool insert(Id
id) {
164 if (
id == InvalidId) {
167 if (table_.empty()) {
170 }
else if ((ids_.size() + 1) * 10 > table_.size() * 7) {
171 rebuild(table_.size() * 2);
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) {
180 slot = (slot + 1) & mask;
187 [[nodiscard]]
const std::vector<Id>&
193 entries() const noexcept {
199 using DeltaNodeSet = DeltaIdSet<NodeId, InvalidNode>;
202 using DeltaPixelSet = DeltaIdSet<PixelId, InvalidPixel>;
205 struct NodeRollbackState {
221 std::uint8_t alive = 0;
227 int properPartCardinality = 0;
231 struct PixelRollbackState {
243 enum class FreeListMutation { Popped, Pushed };
246 struct FreeListRollbackState {
248 FreeListMutation mutation = FreeListMutation::Popped;
260 struct RollbackJournal {
262 std::size_t originalNodeSlots = 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;
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;
315 class EditSessionPause {
318 MorphologicalTree& tree_;
320 bool wasEditing_ =
false;
328 explicit EditSessionPause(MorphologicalTree& tree) noexcept : tree_(tree), wasEditing_(tree.editSessionOpen_) { tree_.editSessionOpen_ =
false; }
333 ~EditSessionPause() noexcept { tree_.editSessionOpen_ = wasEditing_; }
342 explicit TreeEditor(MorphologicalTree& tree,
bool invariantsEstablishedByConstruction =
false)
343 : tree_(&tree), active_(true), recoverable_(!invariantsEstablishedByConstruction),
344 invariantsEstablishedByConstruction_(invariantsEstablishedByConstruction) {
345 tree_->beginEditSession();
351 void ensureRollbackJournal() {
352 if (!recoverable_ || rollbackJournal_) {
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);
372 MorphologicalTree& tree()
const {
373 if (!active_ || tree_ ==
nullptr) {
374 throw std::logic_error(
"TreeEditor operation requires an active edit session.");
389 void finishCommit(TreeEditValidationMode validationMode)
noexcept {
390 if (active_ && tree_ !=
nullptr) {
391 tree_->recordEditCommit(validationMode);
392 tree_->endEditSession();
394 recoverable_ =
false;
395 rollbackJournal_.reset();
404 void captureNodeForRollback(NodeId node) {
405 if (!recoverable_ || node < 0 ||
static_cast<std::size_t
>(node) >= tree_->nodeParent_.size()) {
408 ensureRollbackJournal();
409 if (
static_cast<std::size_t
>(node) >= rollbackJournal_->originalNodeSlots || rollbackJournal_->capturedNodes.contains(node)) {
412 rollbackJournal_->nodes.reserve(rollbackJournal_->nodes.size() + 1);
413 if (!rollbackJournal_->capturedNodes.insert(node)) {
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]});
427 void capturePixelForRollback(PixelId pixel) {
428 if (!recoverable_ || !tree_->isPixel(pixel)) {
431 ensureRollbackJournal();
432 if (rollbackJournal_->capturedPixels.contains(pixel)) {
435 rollbackJournal_->pixels.reserve(rollbackJournal_->pixels.size() + 1);
436 if (!rollbackJournal_->capturedPixels.insert(pixel)) {
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]});
448 void captureNodeLinkNeighborhood(NodeId node) {
449 if (node < 0 ||
static_cast<std::size_t
>(node) >= tree_->nodeParent_.size()) {
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)]);
463 void capturePixelLinkNeighborhood(PixelId pixel) {
464 if (!tree_->isPixel(pixel)) {
467 capturePixelForRollback(pixel);
468 capturePixelForRollback(tree_->prevProperPart_[
static_cast<std::size_t
>(pixel)]);
469 capturePixelForRollback(tree_->nextProperPart_[
static_cast<std::size_t
>(pixel)]);
477 void prepareFreeListGrowth(std::size_t count) {
478 if (!recoverable_ || count == 0) {
481 ensureRollbackJournal();
482 rollbackJournal_->freeListMutations.reserve(rollbackJournal_->freeListMutations.size() + count);
483 tree_->reserveFreeNodeSlots(count);
491 void recordFreeListGrowth(std::size_t previousSize)
noexcept {
492 if (!rollbackJournal_) {
495 for (std::size_t i = previousSize; i < tree_->freeNodeIds_.size(); ++i) {
496 rollbackJournal_->freeListMutations.push_back({FreeListMutation::Pushed, tree_->freeNodeIds_[i]});
503 void restoreRollbackJournal() noexcept {
504 if (!active_ || tree_ ==
nullptr || !recoverable_) {
507 if (!rollbackJournal_) {
508 tree_->endEditSession();
509 recoverable_ =
false;
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();
520 tree_->freeNodeIds_.push_back(it->node);
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);
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;
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;
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;
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;
589 void recordDetachedTransition(
bool wasDetached,
bool isNowDetached)
noexcept {
590 if (invariantsEstablishedByConstruction_) {
593 detachedNodeBalance_ +=
static_cast<int>(isNowDetached) -
static_cast<int>(wasDetached);
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;
614 void recordUnsupportedLeafTransition(
bool wasUnsupported,
bool isNowUnsupported)
noexcept {
615 if (invariantsEstablishedByConstruction_) {
618 unsupportedLeafBalance_ +=
static_cast<int>(isNowUnsupported) -
static_cast<int>(wasUnsupported);
626 void touch(NodeId node) {
627 if (invariantsEstablishedByConstruction_) {
631 static_cast<void>(touchedNodes_.insert(node));
633 incrementalValidationSupported_ =
false;
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;
692 if (!active_ || tree_ ==
nullptr) {
693 throw std::logic_error(
"TreeEditor::rollback requires an active edit session.");
696 throw std::logic_error(
"TreeEditor::rollback is unavailable for the internal journal-free editor.");
698 restoreRollbackJournal();
711 if (invariantsEstablishedByConstruction_) {
712 return t.createDetachedNode();
715 ensureRollbackJournal();
720 rollbackJournal_->freeListMutations.reserve(rollbackJournal_->freeListMutations.size() + 1);
721 rollbackJournal_->freeListMutations.push_back({FreeListMutation::Popped,
candidate});
723 const NodeId nodeId =
t.createDetachedNode();
724 recordDetachedTransition(
false, isDetached(
t, nodeId));
725 recordUnsupportedLeafTransition(
false, isUnsupportedLeaf(
t, nodeId));
737 if (invariantsEstablishedByConstruction_) {
738 t.detachNode(nodeId);
741 if (!
t.isAlive(nodeId)) {
742 throw std::invalid_argument(
"TreeEditor::detach requires a live node.");
744 if (
t.isRoot(nodeId)) {
745 throw std::invalid_argument(
"TreeEditor::detach cannot detach the connected root.");
749 captureNodeLinkNeighborhood(nodeId);
753 t.detachNode(nodeId);
754 recordDetachedTransition(
wasDetached, isDetached(
t, nodeId));
769 if (invariantsEstablishedByConstruction_) {
774 throw std::invalid_argument(
"TreeEditor::reparent requires live node ids.");
776 if (
t.isRoot(nodeId)) {
777 throw std::invalid_argument(
"TreeEditor::reparent cannot move the connected root.");
780 throw std::invalid_argument(
"TreeEditor::reparent requires distinct node ids.");
784 captureNodeLinkNeighborhood(nodeId);
786 captureNodeForRollback(
t.lastChild_[
static_cast<std::size_t
>(
newParentId)]);
792 recordDetachedTransition(
wasDetached, isDetached(
t, nodeId));
810 if (invariantsEstablishedByConstruction_) {
815 throw std::invalid_argument(
"TreeEditor::attach requires live node ids.");
818 throw std::invalid_argument(
"TreeEditor::attach requires distinct node ids.");
821 throw std::invalid_argument(
"TreeEditor::attach expects a detached self-parented node.");
825 captureNodeForRollback(
t.lastChild_[
static_cast<std::size_t
>(
parentId)]);
846 if (invariantsEstablishedByConstruction_) {
851 throw std::invalid_argument(
"TreeEditor::moveChildren requires live node ids.");
854 throw std::invalid_argument(
"TreeEditor::moveChildren requires distinct node ids.");
858 captureNodeForRollback(
t.lastChild_[
static_cast<std::size_t
>(
parentId)]);
860 captureNodeForRollback(
child);
882 if (invariantsEstablishedByConstruction_) {
887 throw std::invalid_argument(
"TreeEditor::movePixelToProperPart requires live node ids.");
890 throw std::invalid_argument(
"TreeEditor::movePixelToProperPart requires distinct source and target nodes.");
892 if (!
t.isPixel(pixel)) {
893 throw std::invalid_argument(
"TreeEditor::movePixelToProperPart requires a valid proper-part id.");
897 capturePixelLinkNeighborhood(pixel);
898 capturePixelForRollback(
t.properTail_[
static_cast<std::size_t
>(
targetNodeId)]);
917 if (invariantsEstablishedByConstruction_) {
922 throw std::invalid_argument(
"TreeEditor::mergeProperParts requires live node ids.");
925 throw std::invalid_argument(
"TreeEditor::mergeProperParts requires distinct source and target nodes.");
929 capturePixelForRollback(
t.properTail_[
static_cast<std::size_t
>(
targetNodeId)]);
931 pixel =
t.nextProperPart_[
static_cast<std::size_t
>(pixel)]) {
932 capturePixelForRollback(pixel);
950 if (invariantsEstablishedByConstruction_) {
955 throw std::invalid_argument(
"TreeEditor::removeChild requires live node ids.");
958 throw std::invalid_argument(
"TreeEditor::removeChild requires a direct parent-child relation.");
960 captureNodeLinkNeighborhood(
childId);
963 releaseNodeFlag &&
t.numChildrenByNode_[
static_cast<std::size_t
>(
childId)] == 0 &&
t.properPartCardinalityByNode_[
static_cast<std::size_t
>(
childId)] == 0;
965 prepareFreeListGrowth(1);
986 if (invariantsEstablishedByConstruction_) {
987 t.releaseNode(nodeId);
990 if (!
t.isAlive(nodeId)) {
991 throw std::invalid_argument(
"TreeEditor::releaseNode requires a live node.");
993 if (
t.isRoot(nodeId)) {
994 throw std::invalid_argument(
"TreeEditor::releaseNode cannot release the connected root.");
996 if (
t.parent(nodeId) != nodeId) {
997 throw std::invalid_argument(
"TreeEditor::releaseNode expects a detached self-parented node.");
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;
1002 prepareFreeListGrowth(1);
1007 t.releaseNode(nodeId);
1009 recordDetachedTransition(
wasDetached, isDetached(
t, nodeId));
1010 recordUnsupportedLeafTransition(
wasUnsupported, isUnsupportedLeaf(
t, nodeId));
1023 if (invariantsEstablishedByConstruction_) {
1027 if (!
t.isAlive(nodeId)) {
1028 throw std::invalid_argument(
"TreeEditor::setRoot requires a live node.");
1033 captureNodeForRollback(
oldRoot);
1034 captureNodeLinkNeighborhood(nodeId);
1055 if (invariantsEstablishedByConstruction_) {
1056 EditSessionPause
pause(
t);
1057 t.pruneNode(nodeId);
1060 incrementalValidationSupported_ =
false;
1062 if (
t.isAlive(nodeId) && !
t.isRoot(nodeId)) {
1063 const NodeId parent =
t.nodeParent_[
static_cast<std::size_t
>(nodeId)];
1067 for (std::size_t
i = 0;
i <
subtree.size(); ++
i) {
1070 child =
t.nextSibling_[
static_cast<std::size_t
>(
child)]) {
1075 captureNodeForRollback(parent);
1076 captureNodeLinkNeighborhood(nodeId);
1077 capturePixelForRollback(
t.properTail_[
static_cast<std::size_t
>(parent)]);
1079 captureNodeForRollback(
current);
1081 pixel =
t.nextProperPart_[
static_cast<std::size_t
>(pixel)]) {
1082 capturePixelForRollback(pixel);
1085 prepareFreeListGrowth(
subtree.size());
1090 EditSessionPause
pause(
t);
1091 t.pruneNode(nodeId);
1102 if (invariantsEstablishedByConstruction_) {
1103 EditSessionPause
pause(
t);
1104 t.mergeNodeIntoParent(nodeId);
1107 incrementalValidationSupported_ =
false;
1109 if (
t.isAlive(nodeId) && !
t.isRoot(nodeId)) {
1110 const NodeId parent =
t.nodeParent_[
static_cast<std::size_t
>(nodeId)];
1112 captureNodeForRollback(parent);
1113 captureNodeLinkNeighborhood(nodeId);
1114 capturePixelForRollback(
t.properTail_[
static_cast<std::size_t
>(parent)]);
1116 pixel =
t.nextProperPart_[
static_cast<std::size_t
>(pixel)]) {
1117 capturePixelForRollback(pixel);
1120 child =
t.nextSibling_[
static_cast<std::size_t
>(
child)]) {
1121 captureNodeForRollback(
child);
1123 prepareFreeListGrowth(1);
1128 EditSessionPause
pause(
t);
1129 t.mergeNodeIntoParent(nodeId);
1150 if (!active_ || tree_ ==
nullptr) {
1151 return {
false,
"Incremental topology validation requires an active edit session."};
1153 if (detachedNodeBalance_ != 0) {
1154 return {
false,
"Incremental topology validation found detached alive nodes."};
1156 if (unsupportedLeafBalance_ != 0) {
1157 return {
false,
"Incremental topology validation found a live node whose subtree support is empty."};
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."};
1165 const int numNodes = tree_->numNodes();
1166 for (NodeId node : touchedNodes_.entries()) {
1167 if (!tree_->isAlive(node)) {
1171 if (!changedParentCyclesExcluded) {
1174 while (cursor != root) {
1175 if (!tree_->isAlive(cursor)) {
1176 return {
false,
"Incremental topology validation found a parent path outside the alive node domain."};
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."};
1183 if (++pathLength > numNodes) {
1184 return {
false,
"Incremental topology validation found a parent cycle."};
1190 }
catch (
const std::exception& ex) {
1191 return {
false, ex.what()};
1193 return {
false,
"Incremental topology validation failed with an unknown error."};
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);
1212 validationMode = TreeEditValidationMode::Complete;
1215 throw std::runtime_error(result.message);
1218 if (validationMode == TreeEditValidationMode::Incremental) {
1219 const TreeValidationResult oracle =
validate();
1221 throw std::runtime_error(std::string(
"Incremental topology proof disagrees with the complete validation oracle: ") + oracle.message);
1225 return IncrementalProof(*
this, validationMode);
1236 [[nodiscard]] IncrementalProof proveIncrementalWithStrictAltitudeAcyclicity() {
return proveIncrementalImpl(
true); }
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.");
1261 proof.editor_ =
nullptr;
1262 proof.tree_ =
nullptr;
1272 if (!active_ || tree_ ==
nullptr) {
1273 return {
false,
"TreeEditor validation requires an active edit session."};
1275 return tree_->validateConnectedRootedTreeResult();
1288 finishCommit(TreeEditValidationMode::Complete);
1301 throw std::runtime_error(
result.message);