From bc64619b43346ff023a85f1c5e09a567a6825094 Mon Sep 17 00:00:00 2001 From: Abdelrahman Said Date: Sat, 4 Jul 2026 20:40:21 +0100 Subject: [PATCH] Add topological sort and cycle detection --- scratchpad/dag_man.c | 178 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 29 deletions(-) diff --git a/scratchpad/dag_man.c b/scratchpad/dag_man.c index daf7401..cd352f2 100644 --- a/scratchpad/dag_man.c +++ b/scratchpad/dag_man.c @@ -6,6 +6,8 @@ #include #include +wp_intern WpLogger _log = { .name = wpStr8LitRo("dag_man") }; + /* --------------------------------------------------------------------------- * Pool allocator (arena-backed, intrusive free list) * -------------------------------------------------------------------------*/ @@ -63,6 +65,7 @@ typedef struct { u64 index; u64 generation; } PrNodeId; +typedef PrNodeId *PrNodeIdArray; typedef struct { union { @@ -124,6 +127,11 @@ wp_intern void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity) graph->max_count_ever = 0; graph->count = 0; + if (!graph->nodes || !graph->vertices) { + graph->capacity = 0; + return; + } + prPoolInit(&graph->vertex_pool, allocator, sizeof(PrEdgeVertex)); memset(graph->nodes, 0, capacity * sizeof(PrNode)); memset(graph->vertices, 0, capacity * sizeof(PrEdgeVertex)); @@ -134,40 +142,73 @@ wp_intern void prGraphInit(PrGraph *graph, WpAllocator *allocator, u64 capacity) } } -wp_intern void prGraphAddEdge(PrGraph *graph, PrNodeId from, PrNodeId to) { - if (!prGraphIsActiveNode(graph, from) || !prGraphIsActiveNode(graph, to)) { return; } - if (from.index == to.index) { return; } +/* --------------------------------------------------------------------------- + * Kahn's algorithm — topological sort / cycle detection + * + * Returns a WpArray of PrNodeId (sorted topologically). If the result count + * is less than graph->count, the graph contains a cycle. + * -------------------------------------------------------------------------*/ - PrEdgeVertex *edge = (PrEdgeVertex *)prPoolAlloc(&graph->vertex_pool); +wp_intern PrNodeId *prGraphTopologicalSort(PrGraph *graph, const WpAllocator *allocator) { + if (graph->count == 0) { return NULL; } + if (!graph->nodes || graph->capacity == 0) { return NULL; } - PrEdgeVertex *src = &graph->vertices[from.index]; - PrEdgeVertex *dst = &graph->vertices[to.index]; + PrNodeIdArray result = wpArrayAllocCapacity(PrNodeId, allocator, graph->count, WP_ARRAY_INIT_NONE); + if (!result) { return NULL; } - edge->source = from; - edge->target = to; - edge->next_forward = src->next_forward; - edge->next_backward = dst->next_backward; - src->next_forward = edge; - dst->next_backward = edge; + WpAllocator local_arena = wpMemArenaAllocatorInitZero(KiB(16)); + + WpU64Array in_degree = wpArrayAllocCapacity(u64, &local_arena, graph->capacity, WP_ARRAY_INIT_FILLED); + if (!in_degree) { return result; } + memset(in_degree, 0, wpArrayCapacity(in_degree) * sizeof(u64)); + + for (u64 i = 0; i < graph->max_count_ever; i++) { + PrNodeId id = prGraphGetNode(graph, i); + if (!prGraphIsActiveNode(graph, id)) { continue; } + + PrEdgeVertex *vertex = &graph->vertices[i]; + PrEdgeVertex *curr = vertex->next_forward; + while (curr) { + in_degree[curr->target.index]++; + curr = curr->next_forward; + } + } + + WpQueue queue = wpQueueAlloc(u64, &local_arena, graph->count); + + for (u64 i = 0; i < graph->max_count_ever; i++) { + PrNodeId id = prGraphGetNode(graph, i); + if (!prGraphIsActiveNode(graph, id)) { continue; } + if (in_degree[i] == 0) { + wpQueuePush(u64, &queue, &i); + } + } + + while (queue.count > 0) { + u64 *node_idx = wpQueuePop(u64, &queue); + if (!node_idx) { break; } + + PrNodeId id = prGraphGetNode(graph, *node_idx); + wpArrayAppendCapped(PrNodeId, result, &id); + + PrEdgeVertex *vertex = &graph->vertices[*node_idx]; + PrEdgeVertex *curr = vertex->next_forward; + while (curr) { + u64 target_idx = curr->target.index; + if (in_degree[target_idx] > 0) { + in_degree[target_idx]--; + if (in_degree[target_idx] == 0) { + wpQueuePush(u64, &queue, &target_idx); + } + } + curr = curr->next_forward; + } + } + + return result; } -wp_intern PrNodeId prGraphAddNode(PrGraph *graph, PrNodeType type) { - u64 idx = graph->free_head; - if (idx == INVALID_NODE_INDEX) { return INVALID_NODE_ID; } - - PrNode *node = &graph->nodes[idx]; - - graph->free_head = node->next_free; - node->next_free = INVALID_NODE_INDEX; - node->type = type; - memset(&node->params, 0, sizeof(node->params)); - - graph->count++; - - if (idx + 1 > graph->max_count_ever) { graph->max_count_ever = idx + 1; } - - return (PrNodeId){ .index = idx, .generation = node->generation }; -} +/* --- edge management -------------------------------------------------- */ wp_intern void _unlinkForward(PrGraph *graph, PrNodeId from, PrEdgeVertex *edge) { if (!prGraphIsActiveNode(graph, from)) { return; } @@ -211,6 +252,55 @@ wp_intern void _unlinkBackward(PrGraph *graph, PrNodeId to, PrEdgeVertex *edge) } } +wp_intern void prGraphAddEdge(PrGraph *graph, PrNodeId from, PrNodeId to) { + if (!prGraphIsActiveNode(graph, from) || !prGraphIsActiveNode(graph, to)) { return; } + if (from.index == to.index) { return; } + + PrEdgeVertex *edge = (PrEdgeVertex *)prPoolAlloc(&graph->vertex_pool); + if (!edge) { return; } + + edge->source = from; + edge->target = to; + + /* Link into adjacency chains */ + PrEdgeVertex *src = &graph->vertices[from.index]; + PrEdgeVertex *dst = &graph->vertices[to.index]; + edge->next_forward = src->next_forward; + edge->next_backward = dst->next_backward; + src->next_forward = edge; + dst->next_backward = edge; + + /* Check whether the new edge created a cycle */ + WpAllocator scratch = wpMemArenaAllocatorInitZero(KiB(16)); + PrNodeIdArray sorted = prGraphTopologicalSort(graph, &scratch); + u64 sorted_count = sorted ? wpArrayCount(sorted) : 0; + if (sorted_count < graph->count) { + _unlinkForward(graph, from, edge); + _unlinkBackward(graph, to, edge); + prPoolFree(&graph->vertex_pool, edge); + } + + wpMemArenaAllocatorDestroy(&scratch); +} + +wp_intern PrNodeId prGraphAddNode(PrGraph *graph, PrNodeType type) { + u64 idx = graph->free_head; + if (idx == INVALID_NODE_INDEX) { return INVALID_NODE_ID; } + + PrNode *node = &graph->nodes[idx]; + + graph->free_head = node->next_free; + node->next_free = INVALID_NODE_INDEX; + node->type = type; + memset(&node->params, 0, sizeof(node->params)); + + graph->count++; + + if (idx + 1 > graph->max_count_ever) { graph->max_count_ever = idx + 1; } + + return (PrNodeId){ .index = idx, .generation = node->generation }; +} + wp_intern void prGraphRemoveNode(PrGraph *graph, PrNodeId id) { if (!prGraphIsActiveNode(graph, id)) { return; } @@ -297,6 +387,10 @@ wp_intern void prGraphDump(const PrGraph *graph) { i32 main(void) { WpAllocator arena = wpMemArenaAllocatorInitZero(MiB(16)); + if (wpMemAllocatorInvalid(&arena)) { + wpLogFatal(&_log, wpStr8Lit("arena init failed")); + return 1; + } PrGraph graph = {0}; prGraphInit(&graph, &arena, 128); @@ -349,6 +443,32 @@ i32 main(void) { printf("\n"); prGraphDump(&graph); + printf("\n=== Try adding cycle 8→1 (should be rejected) ===\n"); + prGraphAddEdge(&graph, n8, n1); + + printf("\n=== Try adding 8→1 again (still rejected) ===\n"); + prGraphAddEdge(&graph, n8, n1); + + printf("\n=== Try adding cycle 7→2 (should be rejected) ===\n\n"); + prGraphAddEdge(&graph, n7, n2); + + prGraphDump(&graph); + + printf("\n=== Topological sort ===\n"); + PrNodeId *sorted = prGraphTopologicalSort(&graph, &arena); + if (sorted) { + u64 n = wpArrayCount(sorted); + printf(" count: %llu / %llu active\n", + (unsigned long long)n, + (unsigned long long)graph.count); + for (u64 i = 0; i < n; i++) { + printf(" [%llu] idx=%llu gen=%llu\n", + (unsigned long long)i, + (unsigned long long)sorted[i].index, + (unsigned long long)sorted[i].generation); + } + } + wpMemArenaAllocatorDestroy(&arena); return 0;