add cx_tree_visitor()

Wed, 20 Mar 2024 23:31:41 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 20 Mar 2024 23:31:41 +0100
changeset 845
2615317311b7
parent 844
3270ea9e41ef
child 846
71f4e0a13bb0

add cx_tree_visitor()

src/cx/tree.h file | annotate | diff | comparison | revisions
src/tree.c file | annotate | diff | comparison | revisions
tests/test_tree.c file | annotate | diff | comparison | revisions
--- a/src/cx/tree.h	Thu Mar 14 22:07:19 2024 +0100
+++ b/src/cx/tree.h	Wed Mar 20 23:31:41 2024 +0100
@@ -45,7 +45,7 @@
 #endif
 
 /**
- * Tree iterator.
+ * A depth-first tree iterator.
  *
  * This iterator is not position-aware in a strict sense, as it does not assume a particular order of elements in the
  * tree. However, the iterator keeps track of the number of nodes it has passed in a counter variable.
@@ -74,11 +74,11 @@
     /**
      * Offset in the node struct for the children linked list.
      */
-    off_t loc_children;
+    ptrdiff_t loc_children;
     /**
      * Offset in the node struct for the next pointer.
      */
-    off_t loc_next;
+    ptrdiff_t loc_next;
     /**
      * The total number of distinct nodes that have been passed so far.
      */
@@ -119,19 +119,105 @@
 } CxTreeIterator;
 
 /**
+ * An element in a visitor queue.
+ */
+struct cx_tree_visitor_queue_s {
+    /**
+     * The tree node to visit.
+     */
+    void *node;
+    /**
+     * The depth of the node.
+     */
+    size_t depth;
+    /**
+     * The next element in the queue or \c NULL.
+     */
+    struct cx_tree_visitor_queue_s *next;
+};
+
+/**
+ * A breadth-first tree iterator.
+ *
+ * This iterator needs to maintain a visitor queue that will be automatically freed once the iterator becomes invalid.
+ * If you want to discard the iterator before, you MUST manually call cxTreeVisitorDispose().
+ *
+ * This iterator is not position-aware in a strict sense, as it does not assume a particular order of elements in the
+ * tree. However, the iterator keeps track of the number of nodes it has passed in a counter variable.
+ * Each node, regardless of the number of passes, is counted only once.
+ *
+ * @note Objects that are pointed to by an iterator are mutable through that iterator. However, if the
+ * underlying data structure is mutated by other means than this iterator (e.g. elements added or removed),
+ * the iterator becomes invalid (regardless of what cxIteratorValid() returns).
+ *
+ * @see CxIterator
+ */
+typedef struct cx_tree_visitor_s {
+    /**
+     * The base properties of this iterator.
+     */
+    struct cx_iterator_base_s base;
+    /**
+     * Offset in the node struct for the children linked list.
+     */
+    ptrdiff_t loc_children;
+    /**
+     * Offset in the node struct for the next pointer.
+     */
+    ptrdiff_t loc_next;
+    /**
+     * The total number of distinct nodes that have been passed so far.
+     */
+    size_t counter;
+    /**
+     * The currently observed node.
+     *
+     * This is the same what cxIteratorCurrent() would return.
+     */
+    void *node;
+    /**
+     * The current depth in the tree.
+     */
+    size_t depth;
+    /**
+     * The next element in the visitor queue.
+     */
+    struct cx_tree_visitor_queue_s *queue_next;
+    /**
+     * The last element in the visitor queue.
+     */
+    struct cx_tree_visitor_queue_s *queue_last;
+} CxTreeVisitor;
+
+/**
  * Releases internal memory of the given tree iterator.
  * @param iter the iterator
  */
+ __attribute__((__nonnull__))
 static inline void cxTreeIteratorDispose(CxTreeIterator *iter) {
     free(iter->stack);
     iter->stack = NULL;
 }
 
 /**
+ * Releases internal memory of the given tree visitor.
+ * @param visitor the visitor
+ */
+__attribute__((__nonnull__))
+static inline void cxTreeVisitorDispose(CxTreeVisitor *visitor) {
+    struct cx_tree_visitor_queue_s *q = visitor->queue_next;
+    while (q != NULL) {
+        struct cx_tree_visitor_queue_s *next = q->next;
+        free(q);
+        q = next;
+    }
+}
+
+/**
  * Links a node to a (new) parent.
  *
  * If the node has already a parent, it is unlinked, first.
- * If the parent has children already, the node is prepended to the list
+ * If the parent has children already, the node is \em prepended to the list
  * of all currently existing children.
  *
  * @param parent the parent node
@@ -234,14 +320,14 @@
 );
 
 /**
- * Creates an iterator for a tree with the specified root node.
+ * Creates a depth-first iterator for a tree with the specified root node.
  *
  * @note A tree iterator needs to maintain a stack of visited nodes, which is allocated using stdlib malloc().
  * When the iterator becomes invalid, this memory is automatically released. However, if you wish to cancel the
  * iteration before the iterator becomes invalid by itself, you MUST call cxTreeIteratorDispose() manually to release
  * the memory.
  *
- * @remark At the moment, the returned iterator does not support cxIteratorFlagRemoval().
+ * @remark The returned iterator does not support cxIteratorFlagRemoval().
  *
  * @param root the root node
  * @param visit_on_exit set to true, when the iterator shall visit a node again after processing all children
@@ -258,6 +344,29 @@
         ptrdiff_t loc_next
 );
 
+/**
+ * Creates a breadth-first iterator for a tree with the specified root node.
+ *
+ * @note A tree visitor needs to maintain a queue of to be visited nodes, which is allocated using stdlib malloc().
+ * When the visitor becomes invalid, this memory is automatically released. However, if you wish to cancel the
+ * iteration before the visitor becomes invalid by itself, you MUST call cxTreeVisitorDispose() manually to release
+ * the memory.
+ *
+ * @remark The returned iterator does not support cxIteratorFlagRemoval().
+ *
+ * @param root the root node
+ * @param loc_children offset in the node struct for the children linked list
+ * @param loc_next offset in the node struct for the next pointer
+ * @return the new tree visitor
+ * @see cxTreeVisitorDispose()
+ */
+__attribute__((__nonnull__))
+CxTreeVisitor cx_tree_visitor(
+        void *root,
+        ptrdiff_t loc_children,
+        ptrdiff_t loc_next
+);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
--- a/src/tree.c	Thu Mar 14 22:07:19 2024 +0100
+++ b/src/tree.c	Wed Mar 20 23:31:41 2024 +0100
@@ -178,10 +178,8 @@
 
 static void cx_tree_iter_next(void *it) {
     struct cx_tree_iterator_s *iter = it;
-    off_t const loc_next = iter->loc_next;
-    off_t const loc_children = iter->loc_children;
-
-    // TODO: support mutating iterator
+    ptrdiff_t const loc_next = iter->loc_next;
+    ptrdiff_t const loc_children = iter->loc_children;
 
     void *children;
 
@@ -280,3 +278,116 @@
 
     return iter;
 }
+
+static bool cx_tree_visitor_valid(void const *it) {
+    struct cx_tree_visitor_s const *iter = it;
+    return iter->node != NULL;
+}
+
+static void *cx_tree_visitor_current(void const *it) {
+    struct cx_tree_visitor_s const *iter = it;
+    return iter->node;
+}
+
+__attribute__((__nonnull__))
+static void cx_tree_visitor_enqueue_siblings(
+        struct cx_tree_visitor_s *iter, void *node, ptrdiff_t loc_next) {
+    node = tree_next(node);
+    while (node != NULL) {
+        struct cx_tree_visitor_queue_s *q;
+        q = malloc(sizeof(struct cx_tree_visitor_queue_s));
+        q->depth = iter->queue_last->depth;
+        q->node = node;
+        iter->queue_last->next = q;
+        iter->queue_last = q;
+        node = tree_next(node);
+    }
+    iter->queue_last->next = NULL;
+}
+
+static void cx_tree_visitor_next(void *it) {
+    struct cx_tree_visitor_s *iter = it;
+    ptrdiff_t const loc_next = iter->loc_next;
+    ptrdiff_t const loc_children = iter->loc_children;
+
+    // check if there is a next node
+    if (iter->queue_next == NULL) {
+        iter->node = NULL;
+        return;
+    }
+
+    // dequeue the next node
+    iter->node = iter->queue_next->node;
+    iter->depth = iter->queue_next->depth;
+    {
+        struct cx_tree_visitor_queue_s *q = iter->queue_next;
+        iter->queue_next = q->next;
+        if (iter->queue_next == NULL) {
+            assert(iter->queue_last == q);
+            iter->queue_last = NULL;
+        }
+        free(q);
+    }
+
+    // increment the node counter
+    iter->counter++;
+
+    // add the children of the new node to the queue
+    void *children = tree_children(iter->node);
+    if (children != NULL) {
+        struct cx_tree_visitor_queue_s *q;
+        q = malloc(sizeof(struct cx_tree_visitor_queue_s));
+        q->depth = iter->depth + 1;
+        q->node = children;
+        if (iter->queue_last == NULL) {
+            assert(iter->queue_next == NULL);
+            iter->queue_next = q;
+        } else {
+            iter->queue_last->next = q;
+        }
+        iter->queue_last = q;
+        cx_tree_visitor_enqueue_siblings(iter, children, loc_next);
+    }
+}
+
+CxTreeVisitor cx_tree_visitor(
+        void *root,
+        ptrdiff_t loc_children,
+        ptrdiff_t loc_next
+) {
+    CxTreeVisitor iter;
+    iter.loc_children = loc_children;
+    iter.loc_next = loc_next;
+
+    // allocate stack
+    iter.depth = 0;
+
+    // visit the root node
+    iter.node = root;
+    iter.counter = 1;
+    iter.depth = 1;
+
+    // put all children of root into the queue
+    void *children = tree_children(root);
+    if (children == NULL) {
+        iter.queue_next = NULL;
+        iter.queue_last = NULL;
+    } else {
+        iter.queue_next = malloc(sizeof(struct cx_tree_visitor_queue_s));
+        iter.queue_next->depth = 2;
+        iter.queue_next->node = children;
+        iter.queue_last = iter.queue_next;
+        cx_tree_visitor_enqueue_siblings(&iter, children, loc_next);
+    }
+
+    // assign base iterator functions
+    iter.base.mutating = false;
+    iter.base.remove = false;
+    iter.base.current_impl = NULL;
+    iter.base.valid = cx_tree_visitor_valid;
+    iter.base.next = cx_tree_visitor_next;
+    iter.base.current = cx_tree_visitor_current;
+
+    return iter;
+}
+
--- a/tests/test_tree.c	Thu Mar 14 22:07:19 2024 +0100
+++ b/tests/test_tree.c	Wed Mar 20 23:31:41 2024 +0100
@@ -286,6 +286,14 @@
     tree_node cc = {0};
     tree_node cba = {0};
 
+    tree_node* expected_order[] = {
+            &root,
+            &c, &cc,&cb, &cba,&ca,
+            &b, &ba,
+            &a, &ab, &aa,
+    };
+    tree_node* actual_order[16]; // reserve more space in case s.t. goes wrong
+
     cx_tree_link(&root, &a, tree_node_layout);
     cx_tree_link(&root, &b, tree_node_layout);
     cx_tree_link(&root, &c, tree_node_layout);
@@ -302,6 +310,7 @@
         cx_foreach(tree_node*, node, iter) {
             CX_TEST_ASSERT(node->data == 0);
             node->data++;
+            actual_order[chk] = node;
             chk++;
             CX_TEST_ASSERT(node == iter.node);
             CX_TEST_ASSERT(!iter.exiting);
@@ -317,18 +326,11 @@
         }
         CX_TEST_ASSERT(iter.counter == 11);
         CX_TEST_ASSERT(chk == 11);
+        for (unsigned i = 0 ; i < chk ; i++) {
+            CX_TEST_ASSERT(actual_order[i] == expected_order[i]);
+            CX_TEST_ASSERT(actual_order[i]->data == 1);
+        }
         CX_TEST_ASSERT(iter.stack == NULL);
-        CX_TEST_ASSERT(root.data == 1);
-        CX_TEST_ASSERT(a.data == 1);
-        CX_TEST_ASSERT(b.data == 1);
-        CX_TEST_ASSERT(c.data == 1);
-        CX_TEST_ASSERT(aa.data == 1);
-        CX_TEST_ASSERT(ab.data == 1);
-        CX_TEST_ASSERT(ba.data == 1);
-        CX_TEST_ASSERT(ca.data == 1);
-        CX_TEST_ASSERT(cb.data == 1);
-        CX_TEST_ASSERT(cc.data == 1);
-        CX_TEST_ASSERT(cba.data == 1);
     }
 }
 
@@ -507,6 +509,120 @@
     cx_testing_allocator_destroy(&talloc);
 }
 
+CX_TEST(test_tree_visitor) {
+    tree_node root = {0};
+    tree_node a = {0};
+    tree_node b = {0};
+    tree_node c = {0};
+    tree_node aa = {0};
+    tree_node ab = {0};
+    tree_node ba = {0};
+    tree_node ca = {0};
+    tree_node cb = {0};
+    tree_node cc = {0};
+    tree_node cba = {0};
+
+    tree_node* expected_order[] = {
+            &root,
+            &c, &b, &a,
+            &cc, &cb, &ca, &ba, &ab, &aa,
+            &cba
+    };
+    tree_node* actual_order[16]; // reserve more space in case s.t. goes wrong
+
+    cx_tree_link(&root, &a, tree_node_layout);
+    cx_tree_link(&root, &b, tree_node_layout);
+    cx_tree_link(&root, &c, tree_node_layout);
+    cx_tree_link(&a, &aa, tree_node_layout);
+    cx_tree_link(&a, &ab, tree_node_layout);
+    cx_tree_link(&b, &ba, tree_node_layout);
+    cx_tree_link(&c, &ca, tree_node_layout);
+    cx_tree_link(&c, &cb, tree_node_layout);
+    cx_tree_link(&c, &cc, tree_node_layout);
+    cx_tree_link(&cb, &cba, tree_node_layout);
+    CX_TEST_DO {
+        CxTreeVisitor iter = cx_tree_visitor(&root, tree_child_list);
+        unsigned chk = 0;
+        cx_foreach(tree_node*, node, iter) {
+            CX_TEST_ASSERT(node->data == 0);
+            node->data++;
+            actual_order[chk] = node;
+            chk++;
+            CX_TEST_ASSERT(node == iter.node);
+            if (node == &root) {
+                CX_TEST_ASSERT(iter.depth == 1);
+            } else if (node == &a || node == &b || node == &c) {
+                CX_TEST_ASSERT(iter.depth == 2);
+            } else if (node == &cba) {
+                CX_TEST_ASSERT(iter.depth == 4);
+            } else {
+                CX_TEST_ASSERT(iter.depth == 3);
+            }
+        }
+        CX_TEST_ASSERT(iter.counter == 11);
+        CX_TEST_ASSERT(chk == 11);
+        for (unsigned i = 0 ; i < chk ; i++) {
+            CX_TEST_ASSERT(actual_order[i] == expected_order[i]);
+            CX_TEST_ASSERT(actual_order[i]->data == 1);
+        }
+        CX_TEST_ASSERT(iter.queue_next == NULL);
+        CX_TEST_ASSERT(iter.queue_last == NULL);
+    }
+}
+
+CX_TEST(test_tree_visitor_no_children) {
+    tree_node root = {0};
+
+    CX_TEST_DO {
+        CxTreeVisitor iter = cx_tree_visitor(&root, tree_child_list);
+        unsigned chk = 0;
+        cx_foreach(tree_node*, node, iter) {
+            CX_TEST_ASSERT(node == iter.node);
+            chk++;
+        }
+        CX_TEST_ASSERT(iter.counter == 1);
+        CX_TEST_ASSERT(chk == 1);
+        CX_TEST_ASSERT(iter.queue_next == NULL);
+        CX_TEST_ASSERT(iter.queue_last == NULL);
+    }
+}
+
+CX_TEST(test_tree_visitor_no_branching) {
+    tree_node root = {0};
+    tree_node a = {0};
+    tree_node b = {0};
+    tree_node c = {0};
+
+    tree_node* expected_order[] = {
+            &root, &a, &b, &c
+    };
+    tree_node* actual_order[4];
+
+    cx_tree_link(&root, &a, tree_node_layout);
+    cx_tree_link(&a, &b, tree_node_layout);
+    cx_tree_link(&b, &c, tree_node_layout);
+
+    CX_TEST_DO {
+        CxTreeVisitor iter = cx_tree_visitor(&root, tree_child_list);
+        unsigned chk = 0;
+        cx_foreach(tree_node*, node, iter) {
+            CX_TEST_ASSERT(node == iter.node);
+            CX_TEST_ASSERT(node->data == 0);
+            node->data++;
+            actual_order[chk] = node;
+            chk++;
+        }
+        CX_TEST_ASSERT(iter.counter == 4);
+        CX_TEST_ASSERT(chk == 4);
+        CX_TEST_ASSERT(iter.queue_next == NULL);
+        CX_TEST_ASSERT(iter.queue_last == NULL);
+        for (unsigned i = 0 ; i < chk ; i++) {
+            CX_TEST_ASSERT(actual_order[i] == expected_order[i]);
+            CX_TEST_ASSERT(actual_order[i]->data == 1);
+        }
+    }
+}
+
 CxTestSuite *cx_test_suite_tree_low_level(void) {
     CxTestSuite *suite = cx_test_suite_new("tree (low level)");
 
@@ -520,6 +636,9 @@
     cx_test_register(suite, test_tree_iterator_basic_enter_and_exit);
     cx_test_register(suite, test_tree_iterator_xml);
     cx_test_register(suite, test_tree_iterator_free_nodes);
+    cx_test_register(suite, test_tree_visitor);
+    cx_test_register(suite, test_tree_visitor_no_children);
+    cx_test_register(suite, test_tree_visitor_no_branching);
 
     return suite;
 }

mercurial