Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
topological_sort.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19#include "system/macros.h"
20#include "system/mem_alloc.h"
22#include <stdio.h>
23#include <string.h>
24
25/* ------------------------- flatten_nodes() ------------------------- */
26
27static dt_digraph_node_t *_get_or_create_node(GHashTable *by_id, const char *id)
28{
29 dt_digraph_node_t *n = g_hash_table_lookup(by_id, id);
30 if(n) return n;
31
32 n = g_new0(dt_digraph_node_t, 1);
33 n->id = g_strdup(id);
34 n->tag = NULL;
35 n->previous = NULL;
36 n->user_data = NULL;
37
38 g_hash_table_insert(by_id, (gpointer)n->id, n);
39 return n;
40}
41
43{
45 if(IS_NULL_PTR(n)) return NULL;
46 n->id = g_strdup(id);
47 n->tag = NULL;
48 n->previous = NULL;
49 n->user_data = NULL;
50 return n;
51}
52
53int flatten_nodes(GList *input_nodes, GList **out_nodes)
54{
55 if(IS_NULL_PTR(out_nodes)) return 1;
56 *out_nodes = NULL;
57
58 GHashTable *by_id = g_hash_table_new(g_str_hash, g_str_equal);
59 if(IS_NULL_PTR(by_id)) return 1;
60
61 // 1) Create canonical nodes for every id we see (and keep first non-NULL user_data)
62 for(GList *it = g_list_first(input_nodes); it; it = g_list_next(it))
63 {
64 dt_digraph_node_t *in = (dt_digraph_node_t *)it->data;
65 if(IS_NULL_PTR(in) || !in->id) continue;
66
67 dt_digraph_node_t *canon = _get_or_create_node(by_id, in->id);
68 if(!canon->user_data && in->user_data) canon->user_data = in->user_data;
69 if(!canon->tag && in->tag) canon->tag = g_strdup(in->tag);
70 if(canon->tag && in->tag && strcmp(canon->tag, in->tag))
71 {
72 // Minimal provenance merging: keep both when we see a conflict.
73 if(((!strcmp(canon->tag, "dst") && !strcmp(in->tag, "src"))
74 || (!strcmp(canon->tag, "src") && !strcmp(in->tag, "dst"))))
75 {
76 dt_free(canon->tag);
77 canon->tag = g_strdup("dst+src");
78 }
79 else if(strcmp(canon->tag, "dst+src"))
80 {
81 dt_free(canon->tag);
82 canon->tag = g_strdup("mixed");
83 }
84 }
85 }
86
87 // 2) Merge previous lists onto canonical nodes, remapping to canonical nodes by id and deduplicating
88 for(GList *it = g_list_first(input_nodes); it; it = g_list_next(it))
89 {
90 dt_digraph_node_t *in = (dt_digraph_node_t *)it->data;
91 if(IS_NULL_PTR(in) || !in->id) continue;
92
93 dt_digraph_node_t *self = _get_or_create_node(by_id, in->id);
94
95 for(GList *p = g_list_first(in->previous); p; p = g_list_next(p))
96 {
97 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
98 if(IS_NULL_PTR(pred) || !pred->id) continue;
99
100 dt_digraph_node_t *cpred = _get_or_create_node(by_id, pred->id);
101
102 // add cpred to self->previous if not already present
103 gboolean found = FALSE;
104 for(GList *q = g_list_first(self->previous); q; q = g_list_next(q))
105 {
106 if(q->data == cpred) { found = TRUE; break; }
107 }
108 if(!found) self->previous = g_list_prepend(self->previous, cpred);
109 }
110 }
111
112 // 3) Build output list with unique nodes, preserving first-seen order from input_nodes
113 GHashTable *added = g_hash_table_new(g_str_hash, g_str_equal);
114 if(IS_NULL_PTR(added))
115 {
116 g_hash_table_destroy(by_id);
117 return 1;
118 }
119
120 for(GList *it = g_list_first(input_nodes); it; it = g_list_next(it))
121 {
122 dt_digraph_node_t *in = (dt_digraph_node_t *)it->data;
123 if(IS_NULL_PTR(in) || !in->id) continue;
124
125 if(g_hash_table_contains(added, in->id)) continue;
126
127 dt_digraph_node_t *canon = g_hash_table_lookup(by_id, in->id);
128 if(canon)
129 {
130 *out_nodes = g_list_append(*out_nodes, canon);
131 g_hash_table_add(added, (gpointer)canon->id);
132 }
133 }
134
135 // Also include nodes that exist only because they were referenced by previous
136 GHashTableIter iter;
137 gpointer key = NULL, val = NULL;
138 g_hash_table_iter_init(&iter, by_id);
139 while(g_hash_table_iter_next(&iter, &key, &val))
140 {
141 const char *id = (const char *)key;
142 dt_digraph_node_t *canon = (dt_digraph_node_t *)val;
143 if(!g_hash_table_contains(added, id)) *out_nodes = g_list_append(*out_nodes, canon);
144 }
145
146 g_hash_table_destroy(added);
147 g_hash_table_destroy(by_id);
148
149 return 0;
150}
151
152
153/* ---------------------- topological_sort() (DFS) ---------------------- */
154
155/*
156 Constraint semantics used here (common with this structure):
157 - Each dt_digraph_node_constraints_t is stored in node->constraints, so "self" is the owner.
158 - If !IS_NULL_PTR(c->previous): edge (c->previous -> self)
159 - If c->next != NULL: edge (self -> c->next)
160
161 Returns 0 on success, 1 on cycle / unsatisfiable.
162*/
163
170
171static gboolean _add_edge(GHashTable *outgoing, dt_digraph_node_t *from, dt_digraph_node_t *to)
172{
173 if(IS_NULL_PTR(from) || IS_NULL_PTR(to)) return FALSE;
174
175 GList *lst = (GList *)g_hash_table_lookup(outgoing, from);
176 lst = g_list_prepend(lst, to);
177 g_hash_table_replace(outgoing, from, lst);
178 return TRUE;
179}
180
181static GList *_toposort_extract_cycle_from_stack(const GList *dfs_stack, const dt_digraph_node_t *gray)
182{
183 /* Build a cycle list from the current DFS recursion stack.
184 *
185 * We keep `dfs_stack` as a LIFO list whose head is the currently explored node.
186 * When DFS reaches a GRAY node again, that node is an ancestor present somewhere in the stack.
187 *
188 * The cycle nodes are the sub-path:
189 * gray -> ... -> current
190 * in traversal order. The returned list contains each node once (no closing duplicate).
191 *
192 * Ownership:
193 * - The returned list container is newly allocated and must be freed with g_list_free() by the caller.
194 * - The node pointers are not owned (they belong to the canonical node graph).
195 */
196 if(IS_NULL_PTR(dfs_stack) || IS_NULL_PTR(gray)) return NULL;
197
198 GList *cycle = NULL;
199 for(const GList *it = dfs_stack; it; it = g_list_next(it))
200 {
202 if(IS_NULL_PTR(n)) continue;
203 cycle = g_list_prepend(cycle, n);
204 if(n == gray) break;
205 }
206 return g_list_reverse(cycle);
207}
208
210 GHashTable *outgoing,
211 GHashTable *color,
212 GList **result,
213 int *dfs_count,
214 GList **dfs_stack,
215 GList **cycle_out)
216{
217 dt_visit_color_t c = (dt_visit_color_t)GPOINTER_TO_INT(g_hash_table_lookup(color, n));
218
219 if(c == DT_VISIT_GRAY)
220 {
221 fprintf(stderr, "[toposort] Cycle detected visiting node '%s'\n", n->id);
222 if(cycle_out && !*cycle_out) *cycle_out = _toposort_extract_cycle_from_stack(*dfs_stack, n);
223 return TRUE; // cycle
224 }
225 if(c == DT_VISIT_BLACK)
226 {
227 fprintf(stderr, "[toposort] Already finished node '%s'\n", n->id);
228 return FALSE; // already done
229 }
230
231 *dfs_count = *dfs_count + 1;
232
233 // Push on the recursion stack so we can reconstruct a cycle path if needed.
234 if(dfs_stack) *dfs_stack = g_list_prepend(*dfs_stack, n);
235
236 g_hash_table_replace(color, n, GINT_TO_POINTER(DT_VISIT_GRAY));
237
238 GList *nbrs = (GList *)g_hash_table_lookup(outgoing, n);
239 for(GList *it = nbrs; it; it = g_list_next(it))
240 {
242 if(_dfs_visit(m, outgoing, color, result, dfs_count, dfs_stack, cycle_out))
243 {
244 // Pop before propagating the cycle error.
245 if(dfs_stack && *dfs_stack) *dfs_stack = g_list_delete_link(*dfs_stack, *dfs_stack);
246 return TRUE;
247 }
248 }
249
250 g_hash_table_replace(color, n, GINT_TO_POINTER(DT_VISIT_BLACK));
251 *result = g_list_prepend(*result, n);
252
253 // Pop on normal exit.
254 if(dfs_stack && *dfs_stack) *dfs_stack = g_list_delete_link(*dfs_stack, *dfs_stack);
255 return FALSE;
256}
257
258int topological_sort(GList *nodes, GList **sorted, GList **cycle_out)
259{
260 if(IS_NULL_PTR(sorted)) return 1;
261 *sorted = NULL;
262 if(cycle_out) *cycle_out = NULL;
263
264 // Debug: print initial nodes and their predecessors
265 fprintf(stderr, "[toposort] Initial node list and constraints:\n");
266 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
267 {
269 fprintf(stderr, "[toposort] node '%s'", n->id);
270 if(n->tag) fprintf(stderr, " [%s]", n->tag);
271 fprintf(stderr, " (predecessors:");
272 for(GList *p = g_list_first(n->previous); p; p = g_list_next(p))
273 {
274 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
275 fprintf(stderr, " '%s'", pred->id);
276 }
277 fprintf(stderr, ")\n");
278 }
279
280 // Debug: print all edges (previous -> node)
281 fprintf(stderr, "[toposort] Edges:\n");
282 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
283 {
285 for(GList *p = g_list_first(n->previous); p; p = g_list_next(p))
286 {
287 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
288 fprintf(stderr, "[toposort] '%s' -> '%s'\n", pred->id, n->id);
289 }
290 }
291
292 GHashTable *outgoing = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL);
293 GHashTable *color = g_hash_table_new(g_direct_hash, g_direct_equal);
294 if(IS_NULL_PTR(outgoing) || IS_NULL_PTR(color))
295 {
296 if(outgoing) g_hash_table_destroy(outgoing);
297 if(color) g_hash_table_destroy(color);
298 return 1;
299 }
300
301 // Ensure every node appears as a key (even if it has no outgoing edges)
302 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
303 {
305 if(IS_NULL_PTR(n)) continue;
306
307 if(!g_hash_table_contains(outgoing, n)) g_hash_table_insert(outgoing, n, NULL);
308 if(!g_hash_table_contains(color, n)) g_hash_table_insert(color, n, GINT_TO_POINTER(DT_VISIT_WHITE));
309 }
310
311 // Build edges from previous lists: for each node, for each pred in node->previous, add edge pred -> node
312 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
313 {
314 dt_digraph_node_t *self = (dt_digraph_node_t *)it->data;
315 if(IS_NULL_PTR(self)) continue;
316
317 for(GList *p = g_list_first(self->previous); p; p = g_list_next(p))
318 {
319 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
320 if(IS_NULL_PTR(pred)) continue;
321
322 if(!g_hash_table_contains(outgoing, pred)) g_hash_table_insert(outgoing, pred, NULL);
323 if(!g_hash_table_contains(color, pred))
324 g_hash_table_insert(color, pred, GINT_TO_POINTER(DT_VISIT_WHITE));
325
326 _add_edge(outgoing, pred, self);
327 }
328 }
329
330 // DFS over all nodes (including any that appeared only in previous)
331 GList *result = NULL;
332 GList *cycle = NULL;
333 GList *dfs_stack = NULL;
334
335 int dfs_count = 0;
336
337 GHashTableIter iter;
338 gpointer key = NULL, val = NULL;
339 g_hash_table_iter_init(&iter, outgoing);
340 while(g_hash_table_iter_next(&iter, &key, &val))
341 {
343 dt_visit_color_t c = (dt_visit_color_t)GPOINTER_TO_INT(g_hash_table_lookup(color, n));
344 if(c == DT_VISIT_WHITE)
345 {
346 if(_dfs_visit(n, outgoing, color, &result, &dfs_count, &dfs_stack, &cycle))
347 {
348 g_list_free(result);
349 result = NULL;
350 if(dfs_stack)
351 {
352 g_list_free(dfs_stack);
353 dfs_stack = NULL;
354 }
355 g_hash_table_destroy(outgoing);
356 g_hash_table_destroy(color);
357 *sorted = NULL;
358 if(cycle_out) *cycle_out = cycle;
359 else if(cycle) g_list_free(cycle);
360 return 1;
361 }
362 dfs_count++;
363 }
364 }
365
366 if(dfs_stack)
367 {
368 g_list_free(dfs_stack);
369 dfs_stack = NULL;
370 }
371 if(cycle)
372 {
373 g_list_free(cycle); // should never happen on success, but keep cleanup symmetric
374 cycle = NULL;
375 }
376
377 g_hash_table_destroy(outgoing);
378 g_hash_table_destroy(color);
379
380 *sorted = result;
381
382 // Debug: print the sorted solution
383 fprintf(stderr, "[toposort] Solution order:\n");
384 int idx = 0;
385 for(GList *it = g_list_first(result); it; it = g_list_next(it), idx++)
386 {
388 fprintf(stderr, "[toposort] %d: '%s'", idx, n->id);
389 if(n->tag) fprintf(stderr, " [%s]", n->tag);
390 fprintf(stderr, "\n");
391 }
392
393 fprintf(stderr, "[toposort] DFS visits performed: %d\n", dfs_count);
394
395 return 0;
396}
397
398/*
399 Frees one node and everything owned by it.
400 - constraints
401 - constraint objects
402 - node
403 Optional:
404 - id
405 - user_data
406*/
407
409{
410 if(IS_NULL_PTR(node)) return;
411
412 if(node->previous)
413 {
414 g_list_free(node->previous);
415 node->previous = NULL;
416 }
417
418 if(user_destroy && node->user_data) user_destroy(node->user_data);
419
420 if(node->tag)
421 {
422 dt_free(node->tag);
423 }
424 dt_free(node->id);
425
426 dt_free(node);
427}
428
429static void _dt_digraph_nodes_free_full(GList *nodes, dt_node_user_data_destroy_t user_destroy)
430{
431 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
432 dt_digraph_node_free_full(it->data, user_destroy);
433
434 g_list_free(nodes);
435 nodes = NULL;
436}
437
439{
440 if(IS_NULL_PTR(ht)) return;
441
442 GHashTableIter iter;
443 gpointer key, val;
444
445 g_hash_table_iter_init(&iter, ht);
446 while(g_hash_table_iter_next(&iter, &key, &val))
447 {
448 dt_digraph_node_t *node = val;
449 dt_digraph_node_free_full(node, user_destroy);
450 }
451
452 g_hash_table_destroy(ht);
453}
454
455void dt_digraph_cleanup_full(GList *nodes, GHashTable *node_ht, dt_node_user_data_destroy_t user_destroy)
456{
457 if(node_ht)
458 _dt_digraph_nodes_hashtable_free_full(node_ht, user_destroy);
459 else
460 _dt_digraph_nodes_free_full(nodes, user_destroy);
461}
462
463// clang-format off
464// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
465// vim: shiftwidth=2 expandtab tabstop=2 cindent
466// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
467// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
#define m
Definition basecurve.c:283
GdkRGBA color[]
Definition geotagging.c:541
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
char * key
Directed graph node.
static dt_digraph_node_t * _get_or_create_node(GHashTable *by_id, const char *id)
static void _dt_digraph_nodes_hashtable_free_full(GHashTable *ht, dt_node_user_data_destroy_t user_destroy)
dt_visit_color_t
@ DT_VISIT_BLACK
@ DT_VISIT_GRAY
@ DT_VISIT_WHITE
static GList * _toposort_extract_cycle_from_stack(const GList *dfs_stack, const dt_digraph_node_t *gray)
static void dt_digraph_node_free_full(dt_digraph_node_t *node, dt_node_user_data_destroy_t user_destroy)
static void _dt_digraph_nodes_free_full(GList *nodes, dt_node_user_data_destroy_t user_destroy)
static gboolean _add_edge(GHashTable *outgoing, dt_digraph_node_t *from, dt_digraph_node_t *to)
void dt_digraph_cleanup_full(GList *nodes, GHashTable *node_ht, dt_node_user_data_destroy_t user_destroy)
Free a canonical graph (nodes, constraints, ids) in one call.
static gboolean _dfs_visit(dt_digraph_node_t *n, GHashTable *outgoing, GHashTable *color, GList **result, int *dfs_count, GList **dfs_stack, GList **cycle_out)
int flatten_nodes(GList *input_nodes, GList **out_nodes)
Canonicalize / merge duplicated nodes by id.
dt_digraph_node_t * dt_digraph_node_new(const char *id)
Allocate and initialize a new digraph node with the given id.
int topological_sort(GList *nodes, GList **sorted, GList **cycle_out)
Perform a topological sort using depth-first search (DFS).
Small directed-graph helper for constraint aggregation and topological sorting.
void(* dt_node_user_data_destroy_t)(void *data)
Optional destructor for node payloads.