PyDecisionGraph 0.3.2.post1__cp315-cp315-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. decision_graph/__infra__.pxd +1 -0
  2. decision_graph/__init__.pxd +1 -0
  3. decision_graph/__init__.py +39 -0
  4. decision_graph/decision_tree/__init__.pxd +2 -0
  5. decision_graph/decision_tree/__init__.py +85 -0
  6. decision_graph/decision_tree/bake/__infra__.pxd +502 -0
  7. decision_graph/decision_tree/bake/__init__.pxd +502 -0
  8. decision_graph/decision_tree/bake/__init__.py +29 -0
  9. decision_graph/decision_tree/bake/c_action.cp315-win_amd64.pyd +0 -0
  10. decision_graph/decision_tree/bake/c_action.h +324 -0
  11. decision_graph/decision_tree/bake/c_action.pxd +54 -0
  12. decision_graph/decision_tree/bake/c_action.pyi +126 -0
  13. decision_graph/decision_tree/bake/c_action.pyx +65 -0
  14. decision_graph/decision_tree/bake/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
  15. decision_graph/decision_tree/bake/c_allocator_protocol.pxd +24 -0
  16. decision_graph/decision_tree/bake/c_allocator_protocol.pyi +61 -0
  17. decision_graph/decision_tree/bake/c_allocator_protocol.pyx +109 -0
  18. decision_graph/decision_tree/bake/c_bake.cp315-win_amd64.pyd +0 -0
  19. decision_graph/decision_tree/bake/c_bake.h +629 -0
  20. decision_graph/decision_tree/bake/c_bake.pxd +52 -0
  21. decision_graph/decision_tree/bake/c_bake.pyi +151 -0
  22. decision_graph/decision_tree/bake/c_bake.pyx +142 -0
  23. decision_graph/decision_tree/bake/c_collections.cp315-win_amd64.pyd +0 -0
  24. decision_graph/decision_tree/bake/c_collections.h +692 -0
  25. decision_graph/decision_tree/bake/c_collections.pxd +52 -0
  26. decision_graph/decision_tree/bake/c_collections.pyi +193 -0
  27. decision_graph/decision_tree/bake/c_collections.pyx +161 -0
  28. decision_graph/decision_tree/bake/c_const.cp315-win_amd64.pyd +0 -0
  29. decision_graph/decision_tree/bake/c_const.h +395 -0
  30. decision_graph/decision_tree/bake/c_const.pxd +56 -0
  31. decision_graph/decision_tree/bake/c_const.pyi +305 -0
  32. decision_graph/decision_tree/bake/c_const.pyx +278 -0
  33. decision_graph/decision_tree/bake/c_edge.cp315-win_amd64.pyd +0 -0
  34. decision_graph/decision_tree/bake/c_edge.h +511 -0
  35. decision_graph/decision_tree/bake/c_edge.pxd +117 -0
  36. decision_graph/decision_tree/bake/c_edge.pyi +192 -0
  37. decision_graph/decision_tree/bake/c_edge.pyx +270 -0
  38. decision_graph/decision_tree/bake/c_eval.h +848 -0
  39. decision_graph/decision_tree/bake/c_eval.pxd +40 -0
  40. decision_graph/decision_tree/bake/c_expr.cp315-win_amd64.pyd +0 -0
  41. decision_graph/decision_tree/bake/c_expr.h +1900 -0
  42. decision_graph/decision_tree/bake/c_expr.pxd +129 -0
  43. decision_graph/decision_tree/bake/c_expr.pyi +280 -0
  44. decision_graph/decision_tree/bake/c_expr.pyx +272 -0
  45. decision_graph/decision_tree/bake/c_hierarchy.cp315-win_amd64.pyd +0 -0
  46. decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  47. decision_graph/decision_tree/bake/c_hierarchy.pxd +71 -0
  48. decision_graph/decision_tree/bake/c_hierarchy.pyi +409 -0
  49. decision_graph/decision_tree/bake/c_hierarchy.pyx +277 -0
  50. decision_graph/decision_tree/bake/c_logic_group.cp315-win_amd64.pyd +0 -0
  51. decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  52. decision_graph/decision_tree/bake/c_logic_group.pxd +162 -0
  53. decision_graph/decision_tree/bake/c_logic_group.pyi +264 -0
  54. decision_graph/decision_tree/bake/c_logic_group.pyx +308 -0
  55. decision_graph/decision_tree/bake/c_node.cp315-win_amd64.pyd +0 -0
  56. decision_graph/decision_tree/bake/c_node.h +2660 -0
  57. decision_graph/decision_tree/bake/c_node.pxd +381 -0
  58. decision_graph/decision_tree/bake/c_node.pyi +438 -0
  59. decision_graph/decision_tree/bake/c_node.pyx +639 -0
  60. decision_graph/decision_tree/bake/c_reconstruct.cp315-win_amd64.pyd +0 -0
  61. decision_graph/decision_tree/bake/c_reconstruct.pxd +63 -0
  62. decision_graph/decision_tree/bake/c_reconstruct.pyi +60 -0
  63. decision_graph/decision_tree/bake/c_reconstruct.pyx +252 -0
  64. decision_graph/decision_tree/bake/c_var.cp315-win_amd64.pyd +0 -0
  65. decision_graph/decision_tree/bake/c_var.h +2054 -0
  66. decision_graph/decision_tree/bake/c_var.pxd +205 -0
  67. decision_graph/decision_tree/bake/c_var.pyi +271 -0
  68. decision_graph/decision_tree/bake/c_var.pyx +228 -0
  69. decision_graph/decision_tree/capi/__infra__.pxd +33 -0
  70. decision_graph/decision_tree/capi/__init__.pxd +33 -0
  71. decision_graph/decision_tree/capi/__init__.py +57 -0
  72. decision_graph/decision_tree/capi/c_abc.cp315-win_amd64.pyd +0 -0
  73. decision_graph/decision_tree/capi/c_abc.pxd +246 -0
  74. decision_graph/decision_tree/capi/c_abc.pyi +770 -0
  75. decision_graph/decision_tree/capi/c_abc.pyx +1532 -0
  76. decision_graph/decision_tree/capi/c_collection.cp315-win_amd64.pyd +0 -0
  77. decision_graph/decision_tree/capi/c_collection.pxd +22 -0
  78. decision_graph/decision_tree/capi/c_collection.pyi +245 -0
  79. decision_graph/decision_tree/capi/c_collection.pyx +138 -0
  80. decision_graph/decision_tree/capi/c_node.cp315-win_amd64.pyd +0 -0
  81. decision_graph/decision_tree/capi/c_node.pxd +95 -0
  82. decision_graph/decision_tree/capi/c_node.pyi +492 -0
  83. decision_graph/decision_tree/capi/c_node.pyx +551 -0
  84. decision_graph/decision_tree/exc.py +189 -0
  85. decision_graph/decision_tree/native/__init__.py +57 -0
  86. decision_graph/decision_tree/native/abc.py +1167 -0
  87. decision_graph/decision_tree/native/collection.py +134 -0
  88. decision_graph/decision_tree/native/node.py +521 -0
  89. decision_graph/includes/decision_graph/decision_tree/bake/c_action.c +16320 -0
  90. decision_graph/includes/decision_graph/decision_tree/bake/c_action.h +324 -0
  91. decision_graph/includes/decision_graph/decision_tree/bake/c_allocator_protocol.c +12473 -0
  92. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.c +16485 -0
  93. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.h +629 -0
  94. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.c +17381 -0
  95. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.h +692 -0
  96. decision_graph/includes/decision_graph/decision_tree/bake/c_const.c +20815 -0
  97. decision_graph/includes/decision_graph/decision_tree/bake/c_const.h +395 -0
  98. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.c +20696 -0
  99. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.h +511 -0
  100. decision_graph/includes/decision_graph/decision_tree/bake/c_eval.h +848 -0
  101. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.c +22065 -0
  102. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.h +1900 -0
  103. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.c +23267 -0
  104. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  105. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.c +22434 -0
  106. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  107. decision_graph/includes/decision_graph/decision_tree/bake/c_node.c +28385 -0
  108. decision_graph/includes/decision_graph/decision_tree/bake/c_node.h +2660 -0
  109. decision_graph/includes/decision_graph/decision_tree/bake/c_reconstruct.c +15955 -0
  110. decision_graph/includes/decision_graph/decision_tree/bake/c_var.c +16881 -0
  111. decision_graph/includes/decision_graph/decision_tree/bake/c_var.h +2054 -0
  112. decision_graph/includes/decision_graph/decision_tree/capi/c_abc.c +59196 -0
  113. decision_graph/includes/decision_graph/decision_tree/capi/c_collection.c +19967 -0
  114. decision_graph/includes/decision_graph/decision_tree/capi/c_node.c +39229 -0
  115. decision_graph/logic_group/__init__.py +25 -0
  116. decision_graph/logic_group/base.py +100 -0
  117. decision_graph/logic_group/pending_request.py +384 -0
  118. decision_graph/webui/__init__.py +24 -0
  119. decision_graph/webui/bake/__init__.py +65 -0
  120. decision_graph/webui/bake/app.py +488 -0
  121. decision_graph/webui/bake/static/d3.v7.min.js +2 -0
  122. decision_graph/webui/bake/static/script.js +1440 -0
  123. decision_graph/webui/bake/static/style.css +920 -0
  124. decision_graph/webui/bake/templates/index.html +183 -0
  125. decision_graph/webui/bake/templates/offline.html +184 -0
  126. decision_graph/webui/capi/__init__.py +54 -0
  127. decision_graph/webui/capi/app.py +366 -0
  128. decision_graph/webui/capi/main.py +53 -0
  129. decision_graph/webui/capi/static/d3.v7.min.js +2 -0
  130. decision_graph/webui/capi/static/script.js +1026 -0
  131. decision_graph/webui/capi/static/style.css +656 -0
  132. decision_graph/webui/capi/templates/index.html +93 -0
  133. decision_graph/webui/capi/templates/offline.html +94 -0
  134. decision_graph/webui/native/__init__.py +54 -0
  135. decision_graph/webui/native/app.py +366 -0
  136. decision_graph/webui/native/main.py +53 -0
  137. decision_graph/webui/native/static/d3.v7.min.js +2 -0
  138. decision_graph/webui/native/static/script.js +1026 -0
  139. decision_graph/webui/native/static/style.css +656 -0
  140. decision_graph/webui/native/templates/index.html +93 -0
  141. decision_graph/webui/native/templates/offline.html +94 -0
  142. pydecisiongraph-0.3.2.post1.dist-info/METADATA +136 -0
  143. pydecisiongraph-0.3.2.post1.dist-info/RECORD +148 -0
  144. pydecisiongraph-0.3.2.post1.dist-info/WHEEL +5 -0
  145. pydecisiongraph-0.3.2.post1.dist-info/licenses/LICENSE +373 -0
  146. pydecisiongraph-0.3.2.post1.dist-info/top_level.txt +2 -0
  147. tests/bake_c_evaluator.cp315-win_amd64.pyd +0 -0
  148. tests/bake_eval_probe.cp315-win_amd64.pyd +0 -0
@@ -0,0 +1,2660 @@
1
+ #ifndef C_DCG_BAKE_NODE_H
2
+ #define C_DCG_BAKE_NODE_H
3
+
4
+ #include <stdarg.h>
5
+ #include <stdbool.h>
6
+ #include <stdint.h>
7
+ #include <stdio.h>
8
+ #include <string.h>
9
+ #include <sys/types.h>
10
+
11
+ #include <cbase/allocator_protocol/c_allocator_protocol.h>
12
+ #include <cbase/bytemap/c_bytemap.h>
13
+ #include <cbase/bytemap/xxh3.h>
14
+
15
+ #include <decision_graph/decision_tree/bake/c_edge.h>
16
+ #include <decision_graph/decision_tree/bake/c_var.h>
17
+
18
+ #ifdef _WIN32
19
+ #include <process.h>
20
+ #else
21
+ #include <unistd.h>
22
+ #endif
23
+
24
+ // ========== Constants ==========
25
+
26
+ /** Capacity of the local buffer used to render a node prefix / uid. */
27
+ #ifndef DCG_NODE_STRING_MAXLEN
28
+ #define DCG_NODE_STRING_MAXLEN 256
29
+ #endif
30
+
31
+ /** Depth at which the renderer stops descending (0 = unlimited). */
32
+ #ifndef DCG_NODE_RENDER_MAX_DEPTH
33
+ #define DCG_NODE_RENDER_MAX_DEPTH 0
34
+ #endif
35
+
36
+ /*
37
+ * Display text of the auto-generated no-action. It lives here rather than with
38
+ * the rest of the action family's default reprs because two headers need it and
39
+ * only one of them can see c_action.h: the fill that adds one (c_action.h), and
40
+ * the consolidation that turns an unfilled placeholder into one (this header).
41
+ */
42
+ #ifndef DCG_DEF_REPR_NOACTION
43
+ #define DCG_DEF_REPR_NOACTION "NoAction"
44
+ #endif
45
+
46
+ /*
47
+ * Display text of the auto-generated placeholder. It lives here rather than with
48
+ * the action family's default reprs because the placeholder is not an action: it
49
+ * is a plain node the builder leaves behind, and both its constructor and the
50
+ * consolidation that retires it are in this header.
51
+ */
52
+ #ifndef DCG_DEF_REPR_PLACEHOLDER
53
+ #define DCG_DEF_REPR_PLACEHOLDER "Placeholder"
54
+ #endif
55
+
56
+ // ========== Structs ==========
57
+
58
+ /**
59
+ * @brief A node's identity: 16 raw bytes, laid out as an RFC 4122 UUID.
60
+ *
61
+ * Deliberately a byte array rather than a 128-bit integer. The value is never
62
+ * arithmetic - it is compared bytewise, copied whole, and sliced into the 16
63
+ * bytes a Python `uuid.UUID` is built from - and as a char array it asks the
64
+ * node block for no alignment beyond what its widest integer member already
65
+ * needs.
66
+ */
67
+ typedef unsigned char uuid_t[16];
68
+
69
+ typedef struct dcg_node dcg_node;
70
+
71
+ typedef struct dcg_logic_group dcg_logic_group;
72
+
73
+ typedef struct dcg_logic_group_manager dcg_logic_group_manager;
74
+
75
+ typedef struct dcg_node_ctx_ops {
76
+ int (*enter_fn)(dcg_node* node, dcg_logic_group_manager* mgr);
77
+ int (*exit_fn)(dcg_node* node, dcg_logic_group_manager* mgr);
78
+ } dcg_node_ctx_ops;
79
+
80
+ /**
81
+ * @brief Type of a node, encoded as family | variant.
82
+ *
83
+ * The high nibble is the family and is what the family predicates test
84
+ * (masked with DCG_NODE_FAMILY_MASK and compared against the family head);
85
+ * the low byte identifies the variant. The "Mask and Generics" members are
86
+ * the family heads - a generic member is legal wherever the family is
87
+ * expected.
88
+ */
89
+ typedef enum dcg_node_type {
90
+ // === Input Node ===
91
+ DCG_NODE_INPUT = 0x0000, // <- Mask and Generics
92
+ DCG_NODE_TRUE = 0x0001, // Boolean true constant.
93
+ DCG_NODE_FALSE = 0x0002, // Boolean false constant.
94
+ DCG_NODE_DOUBLE = 0x0003, // Double constant.
95
+ DCG_NODE_STRING = 0x0004, // String constant (payload borrowed).
96
+ DCG_NODE_INT = 0x0005, // Integer constant.
97
+ DCG_NODE_VARIABLE = 0x0006, // A live read from a logic group's store.
98
+ // === Operator Node ===
99
+ DCG_NODE_OP = 0x0100, // <- Mask and Generics
100
+ DCG_NODE_UNARY = 0x0101, // One operand child.
101
+ DCG_NODE_BINARY = 0x0102, // Two operand children.
102
+ DCG_NODE_TERNARY = 0x0103, // Three operand children (cond, then, else).
103
+ DCG_NODE_CALL = 0x0104, // Variadic operator (call-like).
104
+ // 0x0200 - RESERVED. The collection family stood here; a mapping, a
105
+ // sequence and a generator are logic groups, not nodes, and live in
106
+ // c_logic_group.h and c_collections.h.
107
+ // === Action Node ===
108
+ DCG_NODE_ACTION = 0x0400, // <- Mask and Generics
109
+ DCG_NODE_NOACTION = 0x0401, // Leaf action: do nothing.
110
+ DCG_NODE_LONGACTION = 0x0402, // Leaf action: long position signal.
111
+ DCG_NODE_SHORTACTION = 0x0403, // Leaf action: short position signal.
112
+ DCG_NODE_CANCELACTION = 0x0404, // Leaf action: cancel outstanding signal.
113
+ DCG_NODE_CLEARACTION = 0x0405, // Leaf action: flatten.
114
+ DCG_NODE_PLACEHOLDER = 0x0406, // Auto-generated stand-in, replaced on consolidation.
115
+ // === Special Node ===
116
+ DCG_NODE_SPECIAL = 0x0800, // <- Mask and Generics
117
+ DCG_NODE_ROOT = 0x0801, // Graph entry point - the only parentless node.
118
+ DCG_NODE_BREAKPOINT = 0x0802 // Inspection sink - evaluation stops here.
119
+ } dcg_node_type;
120
+
121
+ /**
122
+ * @brief Bit masks over a node type: family nibble and variant number.
123
+ */
124
+ typedef enum dcg_node_mask {
125
+ DCG_NODE_FAMILY_MASK = 0x0F00, // Extracts the family nibble of a dcg_node_type.
126
+ DCG_NODE_VARIANT_MASK = 0x00FF // Extracts the variant number of a dcg_node_type.
127
+ } dcg_node_mask;
128
+
129
+ /**
130
+ * @brief Per-node behaviour flags.
131
+ */
132
+ typedef enum dcg_node_flag {
133
+ DCG_NODE_FLAG_NONE = 0, // No flags.
134
+ DCG_NODE_FLAG_FROZEN = 1 << 0, // Baked: structural mutation is refused.
135
+ DCG_NODE_FLAG_VISITED = 1 << 1, // Traversal scratch; cleared by the walker.
136
+ DCG_NODE_FLAG_PRUNED = 1 << 2 // Removed by a bake-time optimisation, kept for inspection.
137
+ } dcg_node_flag;
138
+
139
+ /**
140
+ * @brief A node label - one entry of the node's label list.
141
+ */
142
+ typedef struct dcg_node_label {
143
+ const char* label; // Label text.
144
+ bool owned; // true when the node owns the text and frees it.
145
+ struct dcg_node_label* next; // Next label in the list.
146
+ } dcg_node_label;
147
+
148
+ /**
149
+ * @brief Compile-time switch: teach each node its own evaluation as it is built.
150
+ *
151
+ * A node's built-in value comes from its type. Each family INSTALLS its own rule
152
+ * as the node's eval hook as the node is built, so the evaluator calls a function
153
+ * pointer: the dispatch that would find the rule from the type happens once, at
154
+ * bake time, and a hot walk pays only the call. Define this to 0 and nothing is
155
+ * installed, so the evaluator finds the rule by dispatching on the type every
156
+ * time instead - one switch per evaluated node (see c_dcg_node_eval_default in
157
+ * c_eval.h).
158
+ *
159
+ * It costs one pointer per node (the ctx's type_eval_fn, which a caller's hook
160
+ * still overrides) and a pointer test per evaluation; what it saves is the
161
+ * dispatch. Which way that nets out is what tests/bake/bench_c_eval.c measures -
162
+ * build it both ways and compare.
163
+ *
164
+ * One rule is installed either way: a read a STORE makes carries the store's own
165
+ * rule from birth, because that rule is the read's (see c_collections.h) and the
166
+ * dispatch is not asked to find it.
167
+ *
168
+ * This is a BAKE-TIME switch and not a run-time one on purpose: a node that
169
+ * installs a hook has it installed for good, so the layer it belongs to has to
170
+ * be built the same way everywhere (a graph built by a TU with the switch off
171
+ * and evaluated by one with it on still works - it just dispatches).
172
+ */
173
+ #ifndef DCG_EVAL_DIRECT_HOOKS
174
+ #define DCG_EVAL_DIRECT_HOOKS 1
175
+ #endif
176
+
177
+ /**
178
+ * @brief Flags describing the state of a running evaluation.
179
+ */
180
+ typedef enum dcg_eval_flag {
181
+ DCG_EVAL_FLAG_NONE = 0, // No flags.
182
+ DCG_EVAL_FLAG_SKIP_CHILDREN = 1 << 0, // Do not descend below this node.
183
+ DCG_EVAL_FLAG_CACHED = 1 << 1, // `out` is already valid; the EVAL hook may be skipped.
184
+ DCG_EVAL_FLAG_BREAKPOINT = 1 << 2, // Evaluation is suspended at this node.
185
+ DCG_EVAL_FLAG_TRACE = 1 << 3 // The node wants its hook calls traced.
186
+ } dcg_eval_flag;
187
+
188
+ /**
189
+ * @brief How far a node's own evaluation got, as the bits it completed.
190
+ *
191
+ * The stages are a mask rather than a position because a node that failed in
192
+ * the middle has genuinely completed the ones before it, and a caller asking
193
+ * "did the value get produced?" wants an answer that does not depend on where
194
+ * the failure happened to land. The producer bits ride in the same word for the
195
+ * same reason: which stage ran and what ran it are one answer about one
196
+ * evaluation, and splitting them across two fields would let them disagree.
197
+ *
198
+ * It lives in the NODE layer rather than with the protocol that fills it, because
199
+ * the node's own context records it (see dcg_node_eval_ctx.stage) and a context
200
+ * cannot name a type from a header above it.
201
+ */
202
+ typedef enum dcg_eval_stage {
203
+ DCG_EVAL_STAGE_NONE = 0, // Nothing ran.
204
+ DCG_EVAL_STAGE_PRE_EVAL = 1 << 0, // The pre hook ran and returned OK.
205
+ DCG_EVAL_STAGE_EVAL = 1 << 1, // The value is in the node's out slot.
206
+ DCG_EVAL_STAGE_POST_EVAL = 1 << 2, // The post hook ran and returned OK.
207
+ DCG_EVAL_STAGE_DONE = 1 << 3, // The node's own evaluation completed.
208
+
209
+ /* WHO produced the value, in the same word rather than in a field of its own:
210
+ * exactly one of these accompanies DCG_EVAL_STAGE_EVAL, so the mask says both
211
+ * how far the node got and what got it there - and a failure names the
212
+ * producer as readily as the stage it stopped at. */
213
+ DCG_EVAL_STAGE_HOOK = 1 << 4, // ... from the caller's eval hook.
214
+ DCG_EVAL_STAGE_TYPE_RULE = 1 << 5, // ... from the rule the node's type gave it.
215
+ DCG_EVAL_STAGE_BUILTIN = 1 << 6 // ... from the built-in evaluation, found from the type.
216
+ } dcg_eval_stage;
217
+
218
+ /**
219
+ * @brief Which of the three eval hooks of a node.
220
+ */
221
+ typedef enum dcg_node_hook_type {
222
+ DCG_HOOK_PRE_EVAL = 0, // Runs before the node is evaluated.
223
+ DCG_HOOK_EVAL = 1, // Produces the node's value, into node->out.
224
+ DCG_HOOK_POST_EVAL = 2 // Runs after the value is produced.
225
+ } dcg_node_hook_type;
226
+
227
+ /**
228
+ * @brief An eval hook: a strategy the evaluator calls on a node.
229
+ *
230
+ * Unlike the fire-and-forget mutation callbacks, an eval hook produces (or
231
+ * adjusts) a value and can fail, hence the int return code. It needs nothing
232
+ * but its node: the node carries its own context (node->eval_ctx) and its own
233
+ * value slot (node->out), and the opaque data registered with it arrives as
234
+ * `user_data` - the same pointer for all three hooks of that node.
235
+ */
236
+ typedef int (*dcg_node_hook_fn)(dcg_node* node, void* user_data);
237
+
238
+ /**
239
+ * @brief The evaluation context of a node - the host of its callback protocol.
240
+ *
241
+ * Each node carries its own context: the three eval hooks, the opaque data
242
+ * they share, and the state a running evaluation needs. The evaluator reaches
243
+ * all of it through the node, so a hook needs no context parameter.
244
+ *
245
+ * `type_eval_fn` is not one of the three: it is the node's own evaluation, the
246
+ * rule its type gives it, installed as the node is built when the build asks for
247
+ * it (DCG_EVAL_DIRECT_HOOKS) - and, for an operator node, the rule its OPERATOR
248
+ * gives it, installed when the operator is set and in every build (see
249
+ * c_expr.h). A caller's `eval_fn` OVERRIDES it, which is why the two are
250
+ * separate slots rather than one: the layer's rule and somebody's callback are
251
+ * different things, and only the second belongs to an installer.
252
+ *
253
+ * `stage` records the last evaluation of THIS node twice over, in one word: how
254
+ * far it got (the progression the protocol walks) and WHAT produced its value -
255
+ * the caller's hook, the rule the node's type gave it, or the built-in
256
+ * evaluation (see dcg_eval_stage). `err_code` is what that evaluation failed
257
+ * with, and `eval_seq_id` is the id of the run that wrote the value, so a node
258
+ * can be asked which run it last took part in. All of them are written by the
259
+ * protocol; a dry run restores the node's VALUE, not this record, because what
260
+ * the record says is about the evaluation that just happened.
261
+ */
262
+ typedef struct dcg_node_eval_ctx {
263
+ dcg_node_hook_fn pre_eval_fn; // Runs before the node is evaluated.
264
+ dcg_node_hook_fn eval_fn; // Produces the node's value, into node->out.
265
+ dcg_node_hook_fn post_eval_fn; // Runs after the value is produced.
266
+ dcg_node_hook_fn type_eval_fn; // The rule the node's TYPE gives it - see DCG_EVAL_DIRECT_HOOKS.
267
+ void* user_data; // Opaque data shared by the three hooks.
268
+ void* run; // Per-run state owned by the evaluator.
269
+ uint64_t flags; // dcg_eval_flag bits.
270
+ dcg_ret_code err_code; // Outcome of the node's last evaluation.
271
+ dcg_eval_stage stage; // What ran, and what produced the value - see dcg_eval_stage.
272
+ uint64_t eval_seq_id; // The run whose value the node holds.
273
+ size_t depth; // Depth of this node at its last visit.
274
+ size_t visits; // How many times the node was evaluated.
275
+ } dcg_node_eval_ctx;
276
+
277
+ /**
278
+ * @brief Mutation events observed by the registered callbacks.
279
+ */
280
+ typedef enum dcg_node_event {
281
+ DCG_NODE_EVENT_CHILD_ADDED = 0x0101, // A child was linked under this node.
282
+ DCG_NODE_EVENT_CHILD_UPDATED = 0x0102, // A child was linked under this node.
283
+ DCG_NODE_EVENT_CHILD_REMOVED = 0x0103,
284
+ DCG_NODE_EVENT_CHILD_CLEARED = 0x0104,
285
+ DCG_NODE_EVENT_MODIFIED = 0x0201, // Metadata changed (repr, labels, hooks).
286
+ DCG_NODE_EVENT_EVALUATED = 0x0202,
287
+ } dcg_node_event;
288
+
289
+ /**
290
+ * @brief Fire-and-forget notification callback.
291
+ *
292
+ * @param event What happened.
293
+ * @param self The node the callback is registered on.
294
+ * @param subject The other node involved (the child), or NULL.
295
+ * @param seq_id Sequence id of the mutating caller, for self-suppression.
296
+ * @param user_data Opaque data passed at registration.
297
+ */
298
+ typedef void (*dcg_node_callback_fn)(dcg_node_event event, dcg_node* self, dcg_node* subject, uint64_t seq_id, void* user_data);
299
+
300
+ /**
301
+ * @brief Linked-list node of a registered mutation callback.
302
+ */
303
+ typedef struct dcg_node_callback_ctx {
304
+ dcg_node_callback_fn fn; // Callback implementation.
305
+ void* user_data; // Opaque data from registration.
306
+ uintptr_t id; // Opaque id for unregistration.
307
+ struct dcg_node_callback_ctx* next; // Next registered callback.
308
+ } dcg_node_callback_ctx;
309
+
310
+ /**
311
+ * @brief A node of the decision graph.
312
+ *
313
+ * The node owns its `repr`, its labels and their text, a string `out` and any
314
+ * standalone condition adopted onto it. Everything it owns is allocated as a
315
+ * nested child block, so one c_ap_free_owned() - what c_dcg_node_free() ends
316
+ * with - releases the whole of it without a teardown walk. The five built-in
317
+ * edge conditions are static objects, never blocks, and are left alone.
318
+ *
319
+ * What a node does NOT own is its graph: `children` names blocks of its own
320
+ * type that happen to hang below it. c_dcg_node_free() is therefore a LOCAL
321
+ * operation - it unlinks the node from its parent and lets its children go
322
+ * (they become parentless fragments and stay alive). Tearing a whole graph
323
+ * down is the hierarchy's job: c_dcg_node_teardown_root() walks it and frees
324
+ * every node through the free of its type, leaf first.
325
+ *
326
+ * The payload a family adds after the base header - the operands of an
327
+ * expression, the key of a variable - is released by that family's _free,
328
+ * which is what c_dcg_node_free_generic() reaches for a node in hand as a plain
329
+ * dcg_node*.
330
+ */
331
+ typedef struct dcg_node {
332
+ // === Eval Callbacks ===
333
+ dcg_node_eval_ctx eval_ctx; // Hooks + run state of this node.
334
+ // === Context Callbacks ===
335
+ dcg_node_ctx_ops ctx_ops; // What entering and leaving this type means.
336
+ // === Out Buffer ===
337
+ dcg_var_t out; // Value produced by the last evaluation. OWNED if a string.
338
+ // === Meta Data ===
339
+ dcg_node_type ntype; // Node type.
340
+ const char* repr; // Display text. // OWNED - a nested copy.
341
+ uuid_t uid; // Stable identity: minted at init, mirrored by the Python layer.
342
+ dcg_node_label* labels; // Label list.
343
+ bool autogen; // Generated by the builder rather than written by the user.
344
+ uint32_t flags; // dcg_node_flag bits.
345
+ void* user_payload; // Opaque slot for the binding layer.
346
+ dcg_node_callback_ctx* callbacks; // Registered mutation observers (calloc/free).
347
+ // === Hierarchy ===
348
+ const dcg_node_edge_condition* condition_to_parent; // Edge condition. // OWNED if OWN_CONDITION.
349
+ struct dcg_node* parent; // Parent node, NULL for the root.
350
+ struct dcg_node* children; // First child, NULL for a leaf.
351
+ struct dcg_node* next_sibling; // Next child of the same parent.
352
+ struct dcg_node* prev_sibling; // Previous child of the same parent.
353
+ } dcg_node;
354
+
355
+ /**
356
+ * @brief Result of a validation walk.
357
+ */
358
+ typedef struct dcg_validate_report {
359
+ dcg_ret_code code; // First problem found (DCG_OK when the graph is valid).
360
+ dcg_node* node; // Node that produced the first problem.
361
+ size_t errors; // Total problems.
362
+ size_t warnings; // Total warnings (unresolved AUTO on a non-baked graph).
363
+ size_t nodes; // Nodes visited.
364
+ size_t depth; // Deepest level visited.
365
+ } dcg_validate_report;
366
+
367
+ /**
368
+ * @brief Tree drawing style of the console renderer.
369
+ */
370
+ typedef enum dcg_render_style {
371
+ DCG_RENDER_ASCII = 0, // +-- / |-- style.
372
+ DCG_RENDER_UNICODE = 1 // Box-drawing glyphs for the branch guides.
373
+ } dcg_render_style;
374
+
375
+ /**
376
+ * @brief Console renderer options.
377
+ *
378
+ * Zero-initialize and call c_dcg_render_opts_default() to get the standard
379
+ * layout, then override the fields you care about.
380
+ */
381
+ typedef struct dcg_render_opts {
382
+ dcg_render_style style; // Drawing style.
383
+ size_t max_depth; // Stop below this depth (0 = unlimited).
384
+ bool show_uid; // Render each node's uid.
385
+ bool show_labels; // Render each node's labels.
386
+ bool show_condition; // Render the edge condition.
387
+ bool show_out; // Render the node's out value.
388
+ bool show_flags; // Render the flag bits.
389
+ bool show_hooks; // Render which eval hooks are registered.
390
+ bool show_children; // Render the child count.
391
+ bool show_address; // Render the node address.
392
+ } dcg_render_opts;
393
+
394
+ /**
395
+ * @brief A bounded string builder over a caller-provided buffer.
396
+ *
397
+ * Appends never overflow: once `used` reaches `cap` the builder keeps
398
+ * counting the characters that WOULD have been written, so the caller can
399
+ * detect truncation (used >= cap) instead of silently losing text.
400
+ */
401
+ typedef struct dcg_strbuf {
402
+ char* data; // Destination buffer. NOT owned.
403
+ size_t cap; // Capacity of data.
404
+ size_t used; // Characters that fit (excluding the terminating NUL).
405
+ } dcg_strbuf;
406
+
407
+ // ========== Forward Declarations ==========
408
+
409
+ // Utilities
410
+ static inline const char* c_dcg_node_type_name(dcg_node_type ntype);
411
+ static inline bool c_dcg_node_type_is_input(dcg_node_type ntype);
412
+ static inline bool c_dcg_node_type_is_op(dcg_node_type ntype);
413
+ static inline bool c_dcg_node_type_is_flat(dcg_node_type ntype);
414
+ static inline bool c_dcg_node_type_is_action(dcg_node_type ntype);
415
+ static inline bool c_dcg_node_type_is_special(dcg_node_type ntype);
416
+ static inline size_t c_dcg_node_type_arity(dcg_node_type ntype);
417
+ static inline uint64_t c_dcg_node_gen_seq_id(const void* ptr);
418
+ static inline void c_dcg_node_unlink(dcg_node* node);
419
+ static inline void c_dcg_node_adopt_condition(dcg_node* node, const dcg_node_edge_condition* condition);
420
+
421
+ // Lifecycle
422
+ static inline int c_dcg_node_init(dcg_node* node, dcg_node_type ntype, const char* repr);
423
+ static inline dcg_node_ctx_ops c_dcg_node_ctx_ops_for(dcg_node_type ntype);
424
+ static inline dcg_node* c_dcg_node_new(dcg_node_type ntype, const char* repr, allocator_protocol* allocator);
425
+ static inline void c_dcg_node_dealloc(dcg_node* node);
426
+ static inline void c_dcg_node_free(dcg_node* node);
427
+
428
+ // Node metadata
429
+ static inline int c_dcg_node_set_repr(dcg_node* node, const char* repr);
430
+ static inline int c_dcg_node_set_string(dcg_node* node, const char* value);
431
+
432
+ // Eval hooks
433
+ static inline int c_dcg_node_register_eval_hook(dcg_node* node, dcg_node_hook_type hook, dcg_node_hook_fn fn, void* user_data);
434
+ static inline void c_dcg_node_unregister_eval_hooks(dcg_node* node);
435
+
436
+ // Mutation callbacks
437
+ static inline int c_dcg_node_register_callback(dcg_node* node, dcg_node_callback_fn fn, void* user_data, uintptr_t* out_id);
438
+ static inline int c_dcg_node_unregister_callback(dcg_node* node, uintptr_t callback_id);
439
+ static inline void c_dcg_node_invoke_callbacks(dcg_node* node, dcg_node_event event, dcg_node* subject, uint64_t seq_id);
440
+ static inline size_t c_dcg_node_callback_count(const dcg_node* node);
441
+
442
+ // Graph building
443
+ static inline int c_dcg_node_append(dcg_node* parent, dcg_node* child, const dcg_node_edge_condition* condition);
444
+ static inline int c_dcg_node_append_auto(dcg_node* parent, dcg_node* child);
445
+ static inline int c_dcg_node_append_at(dcg_node* parent, dcg_node* child, const dcg_node_edge_condition* condition, size_t index);
446
+ static inline int c_dcg_node_append_at_binary(dcg_node* parent, dcg_node* child, bool condition, size_t index);
447
+ static inline int c_dcg_node_detach(dcg_node* node);
448
+ static inline int c_dcg_node_replace(dcg_node* old_node, dcg_node* new_node);
449
+ static inline int c_dcg_node_replace_shared(dcg_node* old_node, dcg_node* new_node);
450
+ static inline dcg_node* c_dcg_node_new_placeholder(allocator_protocol* allocator);
451
+ static inline size_t c_dcg_node_consolidate_placeholder(dcg_node* node);
452
+ static inline dcg_node* c_dcg_node_get_placeholder(dcg_node* node);
453
+
454
+ // Queries
455
+ static inline size_t c_dcg_node_child_count(const dcg_node* node);
456
+ static inline dcg_node* c_dcg_node_first_child(const dcg_node* node);
457
+ static inline dcg_node* c_dcg_node_last_child(const dcg_node* node);
458
+ static inline dcg_node* c_dcg_node_child_at(const dcg_node* node, size_t index);
459
+ static inline ssize_t c_dcg_node_child_index(const dcg_node* node);
460
+ static inline dcg_node* c_dcg_node_child_by_condition(const dcg_node* node, const dcg_node_edge_condition* condition);
461
+ static inline dcg_node* c_dcg_node_next_sibling(const dcg_node* node);
462
+ static inline dcg_node* c_dcg_node_prev_sibling(const dcg_node* node);
463
+ static inline dcg_node* c_dcg_node_root(dcg_node* node);
464
+ static inline size_t c_dcg_node_depth(const dcg_node* node);
465
+ static inline size_t c_dcg_node_height(const dcg_node* node);
466
+ static inline size_t c_dcg_node_subtree_size(const dcg_node* node);
467
+ static inline size_t c_dcg_node_leaf_count(const dcg_node* node);
468
+ static inline bool c_dcg_node_is_leaf(const dcg_node* node);
469
+ static inline bool c_dcg_node_is_root(const dcg_node* node);
470
+ static inline bool c_dcg_node_is_ancestor(const dcg_node* node, const dcg_node* descendant);
471
+ static inline dcg_node* c_dcg_node_find_by_uid(const dcg_node* root, const uuid_t uid);
472
+ static inline size_t c_dcg_node_collect_leaves(const dcg_node* root, dcg_node** out, size_t cap);
473
+ static inline size_t c_dcg_node_collect_descendants(const dcg_node* root, dcg_node** out, size_t cap);
474
+ static inline size_t c_dcg_node_path_to(const dcg_node* node, const dcg_node** out, size_t cap);
475
+
476
+ // Labels
477
+ static inline int c_dcg_node_add_label(dcg_node* node, const char* label);
478
+ static inline bool c_dcg_node_has_label(const dcg_node* node, const char* label);
479
+ static inline int c_dcg_node_remove_label(dcg_node* node, const char* label);
480
+ static inline size_t c_dcg_node_label_count(const dcg_node* node);
481
+
482
+ // Validation
483
+ static inline bool c_dcg_node_validate(const dcg_node* root, dcg_validate_report* report);
484
+ static inline int c_dcg_node_validate_print(const dcg_node* root, FILE* stream);
485
+
486
+ // Rendering
487
+ static inline void c_dcg_render_opts_default(dcg_render_opts* opts);
488
+ static inline int c_dcg_node_render(const dcg_node* node, FILE* stream, const dcg_render_opts* opts);
489
+ static inline int c_dcg_node_print(const dcg_node* node);
490
+ static inline int c_dcg_node_render_to_string(const dcg_node* node, char* out, size_t cap, const dcg_render_opts* opts);
491
+
492
+ // Identity
493
+ static inline void c_dcg_node_arm_uuid(dcg_node* node);
494
+ static inline int c_dcg_node_format_uid(const uuid_t uid, char* out, size_t cap);
495
+
496
+ // Internal helpers (exposed for reuse and testing - not part of the stable surface)
497
+ static inline uint64_t c_dcg_node_getpid(void);
498
+ static inline void c_dcg_sb_init(dcg_strbuf* buf, char* data, size_t cap);
499
+ static inline void c_dcg_sb_puts(dcg_strbuf* buf, const char* text);
500
+ static inline void c_dcg_sb_printf(dcg_strbuf* buf, const char* fmt, ...);
501
+ static inline const dcg_node_edge_condition* c_dcg_node_infer_condition(const dcg_node* parent);
502
+ static inline int c_dcg_node_link(dcg_node* parent, dcg_node* child, const dcg_node_edge_condition* condition, dcg_node* anchor);
503
+ static inline void c_dcg_validate_fail(dcg_validate_report* report, dcg_node* node, dcg_ret_code err, bool* valid);
504
+ static inline bool c_dcg_node_validate_walk(const dcg_node* node, dcg_validate_report* report, size_t depth);
505
+ static inline int c_dcg_node_format_flags(uint32_t flags, char* out, size_t cap);
506
+ static inline int c_dcg_node_format_line(const dcg_node* node, char* out, size_t cap, const dcg_render_opts* opts);
507
+ static inline int c_dcg_node_render_walk(const dcg_node* node, FILE* stream, const dcg_render_opts* opts, const char* prefix, bool is_last, size_t depth);
508
+ static inline size_t c_dcg_node_collect_leaves_walk(const dcg_node* node, dcg_node** out, size_t cap, size_t* written);
509
+ static inline size_t c_dcg_node_collect_descendants_walk(const dcg_node* node, dcg_node** out, size_t cap, size_t* written);
510
+
511
+ // ========== Utility Functions ==========
512
+
513
+ /*
514
+ * One name table per family, indexed by the variant number (the low byte of
515
+ * the type). Adding a variant means adding a name in the right table and
516
+ * nothing else - the family is selected by the mask, never by parsing.
517
+ */
518
+ static const char* const DCG_NODE_INPUT_NAMES[] = {"INPUT", "TRUE", "FALSE", "DOUBLE", "STRING", "INT", "VARIABLE"};
519
+ static const char* const DCG_NODE_OP_NAMES[] = {"OP", "UNARY", "BINARY", "TERNARY", "CALL"};
520
+ static const char* const DCG_NODE_ACTION_NAMES[] = {"ACTION", "NOACTION", "LONGACTION", "SHORTACTION", "CANCELACTION", "CLEARACTION", "PLACEHOLDER"};
521
+ static const char* const DCG_NODE_SPECIAL_NAMES[] = {"SPECIAL", "ROOT", "BREAKPOINT"};
522
+
523
+ /**
524
+ * @brief Look a variant number up in its family's name table.
525
+ *
526
+ * @param names Name table of the family.
527
+ * @param count Entries in the table.
528
+ * @param variant Variant number (the low byte of the type).
529
+ * @return The name, or "UNKNOWN" past the end of the table.
530
+ */
531
+ static inline const char* c_dcg_name_at(const char* const* names, size_t count, size_t variant) {
532
+ if (!names || variant >= count) return "UNKNOWN";
533
+ return names[variant];
534
+ }
535
+
536
+ /**
537
+ * @brief Stable display name of a node type (the enum name without its prefix).
538
+ *
539
+ * @param ntype Node type.
540
+ * @return Static string; "UNKNOWN" for an out-of-range type.
541
+ */
542
+ static inline const char* c_dcg_node_type_name(dcg_node_type ntype) {
543
+ size_t variant = (size_t) ((int) ntype & DCG_NODE_VARIANT_MASK);
544
+
545
+ switch ((int) ntype & DCG_NODE_FAMILY_MASK) {
546
+ case DCG_NODE_INPUT:
547
+ return c_dcg_name_at(DCG_NODE_INPUT_NAMES, sizeof(DCG_NODE_INPUT_NAMES) / sizeof(char*), variant);
548
+ case DCG_NODE_OP:
549
+ return c_dcg_name_at(DCG_NODE_OP_NAMES, sizeof(DCG_NODE_OP_NAMES) / sizeof(char*), variant);
550
+ case DCG_NODE_ACTION:
551
+ return c_dcg_name_at(DCG_NODE_ACTION_NAMES, sizeof(DCG_NODE_ACTION_NAMES) / sizeof(char*), variant);
552
+ case DCG_NODE_SPECIAL:
553
+ return c_dcg_name_at(DCG_NODE_SPECIAL_NAMES, sizeof(DCG_NODE_SPECIAL_NAMES) / sizeof(char*), variant);
554
+ default:
555
+ return "UNKNOWN";
556
+ }
557
+ }
558
+
559
+ /**
560
+ * @brief Predicate: is the type in the input family?
561
+ *
562
+ * The input family is what a graph is fed: a literal it carries, or a read it
563
+ * takes from the store of a logic group.
564
+ *
565
+ * @param ntype Node type.
566
+ * @return true for input types (the family head included).
567
+ */
568
+ static inline bool c_dcg_node_type_is_input(dcg_node_type ntype) {
569
+ return ((int) ntype & DCG_NODE_FAMILY_MASK) == DCG_NODE_INPUT;
570
+ }
571
+
572
+ /**
573
+ * @brief Predicate: is the type in the operator family?
574
+ *
575
+ * @param ntype Node type.
576
+ * @return true for operator types.
577
+ */
578
+ static inline bool c_dcg_node_type_is_op(dcg_node_type ntype) {
579
+ return ((int) ntype & DCG_NODE_FAMILY_MASK) == DCG_NODE_OP;
580
+ }
581
+
582
+ /**
583
+ * @brief Predicate: does the type live in the base header alone?
584
+ *
585
+ * A literal IS the whole struct - its variant adds no field of its own - so a
586
+ * dcg_node block holds it. An expression carries its operand array after the
587
+ * header, a variable its key and its group, an action its placement fields, a
588
+ * root or a breakpoint its walk state: those need their family's constructor,
589
+ * because only the family knows how big the block has to be.
590
+ *
591
+ * @param ntype Node type.
592
+ * @return true when a plain dcg_node block is enough for the type.
593
+ */
594
+ static inline bool c_dcg_node_type_is_flat(dcg_node_type ntype) {
595
+ // A placeholder is the one action-family type that carries nothing beyond
596
+ // the base node - no signal, no payload, no connect flag - so its block is
597
+ // one dcg_node like an input's, and the base constructor can build it. It
598
+ // is asked first because the family switch below reads "input, and nothing
599
+ // else", which would otherwise refuse it.
600
+ if (ntype == DCG_NODE_PLACEHOLDER) return true;
601
+
602
+ switch ((int) ntype & DCG_NODE_FAMILY_MASK) {
603
+ case DCG_NODE_INPUT:
604
+ return ntype != DCG_NODE_VARIABLE; // a variable carries its key and its group
605
+ default:
606
+ return false; // an operator carries operands, the rest carry their own state
607
+ }
608
+ }
609
+
610
+ /**
611
+ * @brief Predicate: is the type in the action family?
612
+ *
613
+ * Action nodes are leaves: the graph terminates (or hands over control) at them.
614
+ *
615
+ * @param ntype Node type.
616
+ * @return true for action types.
617
+ */
618
+ static inline bool c_dcg_node_type_is_action(dcg_node_type ntype) {
619
+ return ((int) ntype & DCG_NODE_FAMILY_MASK) == DCG_NODE_ACTION;
620
+ }
621
+
622
+ /**
623
+ * @brief Predicate: is the type in the special family (root / breakpoint)?
624
+ *
625
+ * @param ntype Node type.
626
+ * @return true for special types.
627
+ */
628
+ static inline bool c_dcg_node_type_is_special(dcg_node_type ntype) {
629
+ return ((int) ntype & DCG_NODE_FAMILY_MASK) == DCG_NODE_SPECIAL;
630
+ }
631
+
632
+ /**
633
+ * @brief Fixed operand count of a node type.
634
+ *
635
+ * @param ntype Node type.
636
+ * @return 1 / 2 / 3 for the fixed-arity operators; 0 for everything else
637
+ * (inputs, actions, and the variadic CALL).
638
+ */
639
+ static inline size_t c_dcg_node_type_arity(dcg_node_type ntype) {
640
+ switch (ntype) {
641
+ case DCG_NODE_UNARY:
642
+ return 1;
643
+ case DCG_NODE_BINARY:
644
+ return 2;
645
+ case DCG_NODE_TERNARY:
646
+ return 3;
647
+ default:
648
+ return 0;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * @brief Process id, used as the seq_id seed (portable across POSIX and Windows).
654
+ *
655
+ * @return The current process id.
656
+ */
657
+ static inline uint64_t c_dcg_node_getpid(void) {
658
+ #ifdef _WIN32
659
+ return (uint64_t) _getpid();
660
+ #else
661
+ return (uint64_t) getpid();
662
+ #endif
663
+ }
664
+
665
+ /**
666
+ * @brief Generate a sequence id for a node - the project-standard pointer/PID mix.
667
+ *
668
+ * A seq_id lets a callback recognise the mutation it caused itself
669
+ * (self-suppression): the Python wrapper regenerates its id whenever it is
670
+ * (re)bound, so a callback that sees `seq_id == wrapper->seq_id` knows the
671
+ * wrapper already applied the change locally and can return early.
672
+ *
673
+ * @param ptr Address to derive the id from - the node block start.
674
+ * @return The sequence id (0 when ptr is NULL).
675
+ */
676
+ static inline uint64_t c_dcg_node_gen_seq_id(const void* ptr) {
677
+ if (!ptr) return 0;
678
+ uint64_t addr = (uint64_t) (uintptr_t) ptr;
679
+ return XXH3_64bits_withSeed(&addr, sizeof(addr), c_dcg_node_getpid());
680
+ }
681
+
682
+ /**
683
+ * @brief Mint a node's identity from what makes it distinct, and arm the field.
684
+ *
685
+ * Hashed, not counted: a process-unique id needs no registry to hand out and no
686
+ * allocation to store, and the three inputs below are exactly what tells one
687
+ * node from another - the block it lives in, the type it is, and the text a
688
+ * reader of the graph knows it by. `c_dcg_node_init` calls this once the node
689
+ * is whole (type, out and repr all in place); calling it again later re-mints.
690
+ *
691
+ * The last two assignments lay the digest out as a valid RFC 4122 UUID -
692
+ * version 4, variant 1 - so the 16 bytes round-trip through `uuid.UUID` at the
693
+ * Python boundary without further massaging.
694
+ *
695
+ * @param node Node to arm (NULL-safe).
696
+ */
697
+ static inline void c_dcg_node_arm_uuid(dcg_node* node) {
698
+ if (!node) return;
699
+
700
+ /* Stack state, no allocation. */
701
+ XXH3_state_t st;
702
+ XXH3_128bits_reset(&st);
703
+
704
+ /* 1. block identity - unique within the process while the node lives */
705
+ uintptr_t addr = (uintptr_t) node;
706
+ XXH3_128bits_update(&st, &addr, sizeof(addr));
707
+
708
+ /* 2. the type, so a recycled block never inherits its predecessor's id */
709
+ XXH3_128bits_update(&st, &node->ntype, sizeof(node->ntype));
710
+
711
+ /* 3. the repr, which is what the tree is read by */
712
+ if (node->repr) XXH3_128bits_update(&st, node->repr, strlen(node->repr));
713
+
714
+ XXH128_hash_t h = XXH3_128bits_digest(&st);
715
+ memcpy(node->uid, &h.low64, sizeof(h.low64));
716
+ memcpy(node->uid + sizeof(h.low64), &h.high64, sizeof(h.high64));
717
+
718
+ /* RFC 4122 layout: version 4, variant 1. */
719
+ node->uid[6] = (node->uid[6] & 0x0F) | 0x40;
720
+ node->uid[8] = (node->uid[8] & 0x3F) | 0x80;
721
+ }
722
+
723
+ /**
724
+ * @brief Bind a string builder to a caller-provided buffer.
725
+ *
726
+ * @param buf Builder to initialize.
727
+ * @param data Destination buffer.
728
+ * @param cap Capacity of data.
729
+ */
730
+ static inline void c_dcg_sb_init(dcg_strbuf* buf, char* data, size_t cap) {
731
+ if (!buf) return;
732
+ buf->data = data;
733
+ buf->cap = cap;
734
+ buf->used = 0;
735
+ if (data && cap > 0) data[0] = '\0';
736
+ }
737
+
738
+ /**
739
+ * @brief Append a string to a builder.
740
+ *
741
+ * @param buf Builder (NULL-safe).
742
+ * @param text Text to append (NULL is skipped).
743
+ */
744
+ static inline void c_dcg_sb_puts(dcg_strbuf* buf, const char* text) {
745
+ if (!buf || !text) return;
746
+ size_t len = strlen(text);
747
+ if (buf->used + len < buf->cap) {
748
+ memcpy(buf->data + buf->used, text, len);
749
+ buf->data[buf->used + len] = '\0';
750
+ }
751
+ buf->used += len;
752
+ }
753
+
754
+ /**
755
+ * @brief Append formatted text to a builder.
756
+ *
757
+ * @param buf Builder (NULL-safe).
758
+ * @param fmt printf format.
759
+ * @param ... Format arguments.
760
+ */
761
+ static inline void c_dcg_sb_printf(dcg_strbuf* buf, const char* fmt, ...) {
762
+ if (!buf || !fmt) return;
763
+
764
+ char scratch[DCG_NODE_STRING_MAXLEN];
765
+ va_list args;
766
+ va_start(args, fmt);
767
+ int n = vsnprintf(scratch, sizeof(scratch), fmt, args);
768
+ va_end(args);
769
+
770
+ if (n < 0) return;
771
+ if ((size_t) n < sizeof(scratch)) {
772
+ c_dcg_sb_puts(buf, scratch);
773
+ return;
774
+ }
775
+
776
+ // Long payload: re-format straight into the destination buffer.
777
+ if (buf->used >= buf->cap) {
778
+ buf->used += (size_t) n;
779
+ return;
780
+ }
781
+ size_t room = buf->cap - buf->used;
782
+ va_start(args, fmt);
783
+ int written = vsnprintf(buf->data + buf->used, room, fmt, args);
784
+ va_end(args);
785
+ if (written >= 0 && (size_t) written < room) buf->used += (size_t) written;
786
+ else buf->used += (size_t) n;
787
+ }
788
+
789
+ /**
790
+ * @brief Unlink a node from its parent's child list, without any policy check.
791
+ *
792
+ * Shared by the detach API and by teardown: a node being torn down must
793
+ * leave the parent's list even when the parent is FROZEN, otherwise the
794
+ * parent would keep a dangling pointer.
795
+ *
796
+ * @param node Node to unlink (NULL-safe: a parentless node is a no-op).
797
+ */
798
+ static inline void c_dcg_node_unlink(dcg_node* node) {
799
+ if (!node || !node->parent) return;
800
+
801
+ dcg_node* parent = node->parent;
802
+ if (node->prev_sibling) node->prev_sibling->next_sibling = node->next_sibling;
803
+ else parent->children = node->next_sibling;
804
+ if (node->next_sibling) node->next_sibling->prev_sibling = node->prev_sibling;
805
+
806
+ node->parent = NULL;
807
+ node->next_sibling = NULL;
808
+ node->prev_sibling = NULL;
809
+ node->condition_to_parent = DCG_NO_CONDITION;
810
+ }
811
+
812
+ /**
813
+ * @brief Adopt an edge condition onto a node.
814
+ *
815
+ * A node owns the condition of its edge: the condition becomes a child block
816
+ * of the node, so it is released with the node and needs no teardown step of
817
+ * its own. The five built-ins are static objects, never blocks, and are left
818
+ * alone - which is the one case worth distinguishing, and it is a plain
819
+ * pointer comparison.
820
+ *
821
+ * Everything else is expected to be an allocator-protocol block, which is what
822
+ * c_dcg_edge_new() hands out. Sniffing for that is deliberately NOT done here:
823
+ * asking whether a pointer is a block means reading the bytes before it, which
824
+ * is out of bounds for anything that is not one.
825
+ *
826
+ * @param node Node to adopt onto (NULL-safe).
827
+ * @param condition Condition to adopt (NULL-safe).
828
+ */
829
+ static inline void c_dcg_node_adopt_condition(dcg_node* node, const dcg_node_edge_condition* condition) {
830
+ if (!node || !condition) return;
831
+ if (c_dcg_condition_is_sentinel(condition)) return; // a static built-in, never a block
832
+ c_ap_acquire_ownership((void*) condition, node); // re-parent it under the node
833
+ }
834
+
835
+ // ========== Lifecycle Methods ==========
836
+
837
+ /**
838
+ * @brief Initialize a node buf, taking a copy of its display text.
839
+ *
840
+ * The buf is populated from scratch (zeroing it first, so a reused buf is
841
+ * safe). The repr is always copied into a block owned by the node - a node
842
+ * never borrows display text - and both the node block and that copy come from
843
+ * the allocator protocol: a node is always an allocator block, never an
844
+ * embedded field, which is what lets it own everything it carries.
845
+ *
846
+ * @param node Node to initialize.
847
+ * @param ntype Node type.
848
+ * @param repr Display text to copy (may be NULL).
849
+ * @return DCG_OK, DCG_ERR_INVALID_ARG, DCG_ERR_INVALID_BUF or DCG_ERR_OOM.
850
+ */
851
+ static inline int c_dcg_node_init(dcg_node* node, dcg_node_type ntype, const char* repr) {
852
+ if (!node) return DCG_ERR_INVALID_ARG;
853
+
854
+ memset(node, 0, sizeof(*node));
855
+ node->ntype = ntype;
856
+ (void) c_dcg_var_init(&node->out);
857
+
858
+ int ret_code = c_dcg_node_set_repr(node, repr);
859
+ if (ret_code != DCG_OK) return ret_code;
860
+
861
+ node->ctx_ops = c_dcg_node_ctx_ops_for(ntype);
862
+
863
+ /* Identity last: the digest reads the type and the repr the lines above set. */
864
+ c_dcg_node_arm_uuid(node);
865
+ return DCG_OK;
866
+ }
867
+
868
+ /* ========== Context Enter ========== */
869
+
870
+ /**
871
+ * @brief Enter a BINARY branching node: reserve one arm for each of its two branches.
872
+ *
873
+ * The two-way case, and the enter every type gets unless its own arity says
874
+ * otherwise: a branching constant or expression, and a breakpoint, all reserve
875
+ * this pair. A type whose entering differs says so with its own callback - the
876
+ * root reserves a single arm instead, and a leaf or a stand-in refuses.
877
+ *
878
+ * The arms go in TRUE-then-FALSE - the order they are READ in - and
879
+ * `get_placeholder` hands back the FIRST standing one, so the first branch
880
+ * built inside this node takes the TRUE arm and the pair stays in that order in
881
+ * the child list. The capi keeps the pair the other way round, FALSE first, and
882
+ * this layer deliberately does not: here the child order is the reading order,
883
+ * so a sequential check tests the true branch first and a render shows it above
884
+ * the fallback.
885
+ *
886
+ * Each arm is a placeholder the node owns for as long as it stands: an arm that
887
+ * gets taken is replaced, and the stand-in it displaced is released here.
888
+ *
889
+ * @param node Node being entered.
890
+ * @param mgr Manager holding the build (unused: reserving is the node's own).
891
+ * @return DCG_OK, or a DCG_ERR_* code.
892
+ */
893
+ static inline int c_dcg_node_ctx_enter_binary(dcg_node* node, dcg_logic_group_manager* mgr) {
894
+ (void) mgr;
895
+ if (!node) return DCG_ERR_INVALID_ARG;
896
+
897
+ dcg_node* true_arm = c_dcg_node_new_placeholder(c_ap_protocol_from_ptr(node));
898
+ if (!true_arm) return DCG_ERR_OOM;
899
+
900
+ int ret = c_dcg_node_append(node, true_arm, DCG_TRUE_CONDITION);
901
+ if (ret != DCG_OK) {
902
+ c_dcg_node_free(true_arm);
903
+ return ret;
904
+ }
905
+
906
+ dcg_node* false_arm = c_dcg_node_new_placeholder(c_ap_protocol_from_ptr(node));
907
+ if (!false_arm) return DCG_ERR_OOM;
908
+
909
+ ret = c_dcg_node_append(node, false_arm, DCG_FALSE_CONDITION);
910
+ if (ret != DCG_OK) {
911
+ c_dcg_node_free(false_arm);
912
+ return ret;
913
+ }
914
+ return DCG_OK;
915
+ }
916
+
917
+ /**
918
+ * @brief Enter the graph's entry point: reserve its single unconditioned arm.
919
+ *
920
+ * A root takes exactly one child and takes it unconditionally, so it reserves
921
+ * one arm rather than the binary pair every other branching node reserves.
922
+ *
923
+ * @param node The root being entered.
924
+ * @param mgr Manager holding the build (unused: reserving is the node's own).
925
+ * @return DCG_OK, or a DCG_ERR_* code.
926
+ */
927
+ static inline int c_dcg_node_ctx_enter_root(dcg_node* node, dcg_logic_group_manager* mgr) {
928
+ (void) mgr;
929
+ if (!node) return DCG_ERR_INVALID_ARG;
930
+
931
+ dcg_node* arm = c_dcg_node_new_placeholder(c_ap_protocol_from_ptr(node));
932
+ if (!arm) return DCG_ERR_OOM;
933
+
934
+ int ret = c_dcg_node_append(node, arm, DCG_NO_CONDITION);
935
+ if (ret != DCG_OK) {
936
+ c_dcg_node_free(arm);
937
+ return ret;
938
+ }
939
+ return DCG_OK;
940
+ }
941
+
942
+ /**
943
+ * @brief Refuse to enter: a leaf and a stand-in hold nothing a build continues in.
944
+ *
945
+ * @param node Node being entered (unused).
946
+ * @param mgr Manager holding the build (unused).
947
+ * @return DCG_ERR_TYPE, always.
948
+ */
949
+ static inline int c_dcg_node_ctx_enter_refused(dcg_node* node, dcg_logic_group_manager* mgr) {
950
+ (void) node;
951
+ (void) mgr;
952
+ return DCG_ERR_TYPE;
953
+ }
954
+
955
+ /**
956
+ * @brief The enter half of a type's context ops.
957
+ *
958
+ * The type is what decides, so this is the single place the mapping lives. The
959
+ * special family is not uniform: a root reserves one arm instead of the binary
960
+ * pair, a breakpoint reserves the pair like any other branching node. The exit
961
+ * half is left NULL here and installed by the manager - see c_dcg_node_ctx_ops.
962
+ *
963
+ * @param ntype Node type.
964
+ * @return The pair, with `exit_fn` NULL.
965
+ */
966
+ static inline dcg_node_ctx_ops c_dcg_node_ctx_ops_for(dcg_node_type ntype) {
967
+ dcg_node_ctx_ops ops;
968
+ ops.enter_fn = NULL;
969
+ ops.exit_fn = NULL;
970
+
971
+ switch (ntype) {
972
+ case DCG_NODE_ROOT:
973
+ ops.enter_fn = c_dcg_node_ctx_enter_root;
974
+ break;
975
+ case DCG_NODE_PLACEHOLDER:
976
+ ops.enter_fn = c_dcg_node_ctx_enter_refused;
977
+ break;
978
+ default:
979
+ ops.enter_fn = c_dcg_node_type_is_action(ntype) ? c_dcg_node_ctx_enter_refused : c_dcg_node_ctx_enter_binary;
980
+ break;
981
+ }
982
+ return ops;
983
+ }
984
+
985
+ /**
986
+ * @brief Give a node its own copy of a display text.
987
+ *
988
+ * The copy is nested under the node and replaces (and releases) whatever repr
989
+ * the node held before, so a node's repr is always owned and always dies with
990
+ * the node.
991
+ *
992
+ * @param node Node to modify.
993
+ * @param repr Text to copy (NULL clears the repr).
994
+ * @return DCG_OK, DCG_ERR_INVALID_ARG, DCG_ERR_INVALID_BUF or DCG_ERR_OOM.
995
+ */
996
+ static inline int c_dcg_node_set_repr(dcg_node* node, const char* repr) {
997
+ if (!node) return DCG_ERR_INVALID_ARG;
998
+
999
+ char* copy = NULL;
1000
+ if (repr) {
1001
+ size_t len = strlen(repr);
1002
+ copy = (char*) c_ap_alloc_child(len + 1, NULL, node);
1003
+ if (!copy) return DCG_ERR_OOM;
1004
+ memcpy(copy, repr, len + 1);
1005
+ }
1006
+
1007
+ if (node->repr) c_ap_free_owned((void*) node->repr); // the copy we are replacing
1008
+ node->repr = copy;
1009
+ return DCG_OK;
1010
+ }
1011
+
1012
+ /**
1013
+ * @brief Allocate and initialize a node.
1014
+ *
1015
+ * @param ntype Node type.
1016
+ * @param repr Borrowed display text (may be NULL).
1017
+ * @param allocator Allocator for the block; NULL falls back to the plain heap.
1018
+ * @return The node, or NULL on OOM.
1019
+ */
1020
+ static inline dcg_node* c_dcg_node_new(dcg_node_type ntype, const char* repr, allocator_protocol* allocator) {
1021
+ /* The block is one dcg_node, so the types that carry more are refused here
1022
+ * and built by the constructor named after their family - see c_const.h,
1023
+ * c_expr.h and c_action.h. */
1024
+ if (!c_dcg_node_type_is_flat(ntype)) return NULL;
1025
+
1026
+ dcg_node* node = (dcg_node*) c_ap_alloc(sizeof(dcg_node), allocator);
1027
+ if (!node) return NULL;
1028
+
1029
+ if (c_dcg_node_init(node, ntype, repr) != DCG_OK) {
1030
+ c_ap_free_owned(node);
1031
+ return NULL;
1032
+ }
1033
+ return node;
1034
+ }
1035
+
1036
+ /**
1037
+ * @brief Tear down a node - leaves a zeroed buf.
1038
+ *
1039
+ * Releases what the BASE owns: the labels and their text, the callback
1040
+ * contexts, the repr, a string out and an adopted condition. The node is then
1041
+ * unlinked from its parent and its children are let go - each one is unlinked
1042
+ * (c_dcg_node_unlink), so it survives as a parentless fragment. A node owns
1043
+ * its block, not its graph: the subtree below it is c_dcg_node_teardown_root's
1044
+ * business, and the payload a family adds after the header is that family's
1045
+ * own _free (see c_dcg_node_free_generic).
1046
+ *
1047
+ * The node's own buf is NOT freed - that is _free's job. A borrowed repr is
1048
+ * left untouched; a built-in condition is a static object, never a block.
1049
+ *
1050
+ * @param node Node to tear down (NULL-safe).
1051
+ */
1052
+ static inline void c_dcg_node_dealloc(dcg_node* node) {
1053
+ if (!node) return;
1054
+
1055
+ // An adopted condition is a nested block of this node. It goes before the
1056
+ // unlink, which drops the pointer to it.
1057
+ if (node->condition_to_parent && !c_dcg_condition_is_sentinel(node->condition_to_parent)) {
1058
+ c_ap_free_owned((void*) node->condition_to_parent);
1059
+ }
1060
+
1061
+ // Unlink from the parent first: a teardown must never leave a dangling
1062
+ // link behind in the parent's child list. This deliberately bypasses the
1063
+ // FROZEN guard - a frozen parent must not be able to pin a dead child.
1064
+ dcg_node* parent = node->parent;
1065
+ c_dcg_node_unlink(node);
1066
+ if (parent) c_dcg_node_invoke_callbacks(parent, DCG_NODE_EVENT_CHILD_REMOVED, node, (uint64_t) -1);
1067
+
1068
+ // The children are blocks of their own: they are let go, not freed, and
1069
+ // each keeps its own subtree. Unlinking pops one child per round, so the
1070
+ // list we are walking is the one being emptied.
1071
+ while (node->children) c_dcg_node_unlink(node->children);
1072
+
1073
+ // Labels are nested blocks, and each entry's text is nested under the
1074
+ // entry: one free per entry releases the pair.
1075
+ dcg_node_label* label = node->labels;
1076
+ node->labels = NULL;
1077
+ while (label) {
1078
+ dcg_node_label* next = label->next;
1079
+ c_ap_free_owned(label);
1080
+ label = next;
1081
+ }
1082
+
1083
+ // Callback contexts are short-lived registration state: calloc / free.
1084
+ dcg_node_callback_ctx* cb = node->callbacks;
1085
+ while (cb) {
1086
+ dcg_node_callback_ctx* next = cb->next;
1087
+ free(cb);
1088
+ cb = next;
1089
+ }
1090
+
1091
+ // The repr and a string out are nested blocks, released here so the block
1092
+ // owns nothing once the teardown is done.
1093
+ if (node->repr) c_ap_free_owned((void*) node->repr);
1094
+ if (node->out.dtype == VAR_TYPE_STRING && node->out.value.as_string) {
1095
+ c_ap_free_owned((void*) node->out.value.as_string);
1096
+ }
1097
+
1098
+ memset(node, 0, sizeof(*node));
1099
+ }
1100
+
1101
+ /**
1102
+ * @brief Tear down a node and free its buf - the node itself, nothing below it.
1103
+ *
1104
+ * The children are unlinked and left alive (see c_dcg_node_dealloc), so this
1105
+ * is the free of a node, not of a subtree. Free a whole graph from its top
1106
+ * with c_dcg_node_teardown_root() - never by calling this on a branch.
1107
+ *
1108
+ * @param node Node to free (NULL-safe): a base node, or one of the types the
1109
+ * base alone holds. A family that adds state after the header -
1110
+ * an expression, a variable, a root, a breakpoint - is
1111
+ * torn down by its own _free, or by c_dcg_node_free_generic() when
1112
+ * only its dcg_node* is in hand.
1113
+ */
1114
+ static inline void c_dcg_node_free(dcg_node* node) {
1115
+ if (!node) return;
1116
+ c_dcg_node_dealloc(node);
1117
+ c_ap_free_owned(node); // everything the node nested is released here
1118
+ }
1119
+
1120
+ // ========== Public APIs - Typed Constructors ==========
1121
+
1122
+ /*
1123
+ * The payload-free types. Everything with state of its own - literals,
1124
+ * expressions, variables - is constructed by its own header.
1125
+ */
1126
+
1127
+ /**
1128
+ * @brief Give a node an owned copy of a string value.
1129
+ *
1130
+ * The copy is nested under the node, so it dies with the node; a previous
1131
+ * string value of the node is released first.
1132
+ *
1133
+ * @param node Node to modify.
1134
+ * @param value Text to copy (NULL stores an empty value).
1135
+ * @return DCG_OK, or a DCG_ERR_* code.
1136
+ */
1137
+ static inline int c_dcg_node_set_string(dcg_node* node, const char* value) {
1138
+ if (!node) return DCG_ERR_INVALID_ARG;
1139
+
1140
+ if (node->out.dtype == VAR_TYPE_STRING && node->out.value.as_string) {
1141
+ c_ap_free_owned((void*) node->out.value.as_string);
1142
+ (void) c_dcg_var_init(&node->out);
1143
+ }
1144
+ if (!value) return c_dcg_var_init_string(&node->out, NULL);
1145
+
1146
+ size_t len = strlen(value);
1147
+ char* copy = (char*) c_ap_alloc_child(len + 1, NULL, node);
1148
+ if (!copy) return DCG_ERR_OOM;
1149
+ memcpy(copy, value, len + 1);
1150
+
1151
+ return c_dcg_var_init_string(&node->out, copy);
1152
+ }
1153
+
1154
+ // ========== Eval Hooks ==========
1155
+
1156
+ /**
1157
+ * @brief Register one eval hook of a node.
1158
+ *
1159
+ * The three hooks of a node share ONE `user_data` - it is a property of the
1160
+ * node, not of the hook - so the first registration sets it and the rest have
1161
+ * to agree: registering with a different pointer is refused rather than quietly
1162
+ * splitting the three hooks of one node across two owners.
1163
+ *
1164
+ * A hook is never replaced by accident either. A slot that already holds one is
1165
+ * refused with DCG_ERR_BUSY, and a node that wants a different set gives the
1166
+ * three back first (c_dcg_node_unregister_eval_hooks) and registers again. A
1167
+ * hook is a function POINTER, and NULL is a hook that is not there - which is
1168
+ * how the evaluator decides whether to call one at all.
1169
+ *
1170
+ * @param node Node to attach the hook to.
1171
+ * @param hook Which of the three (a dcg_node_hook_type).
1172
+ * @param fn The hook to install (never NULL - a hook is given back, not
1173
+ * blanked, which is what unregistering is for).
1174
+ * @param user_data Opaque data the node's three hooks share.
1175
+ * @return DCG_OK, DCG_ERR_INVALID_ARG (a NULL node or hook, a hook type that is
1176
+ * not one of the three - reported, since a switch that says nothing
1177
+ * would leave a caller believing its hook was installed - or a
1178
+ * `user_data` that disagrees with the node's), or DCG_ERR_BUSY (a hook
1179
+ * is already installed there).
1180
+ */
1181
+ static inline int c_dcg_node_register_eval_hook(dcg_node* node, dcg_node_hook_type hook, dcg_node_hook_fn fn, void* user_data) {
1182
+ if (!node || !fn) return DCG_ERR_INVALID_ARG;
1183
+
1184
+ dcg_node_hook_fn* slot = NULL;
1185
+ switch (hook) {
1186
+ case DCG_HOOK_PRE_EVAL:
1187
+ slot = &node->eval_ctx.pre_eval_fn;
1188
+ break;
1189
+ case DCG_HOOK_EVAL:
1190
+ slot = &node->eval_ctx.eval_fn;
1191
+ break;
1192
+ case DCG_HOOK_POST_EVAL:
1193
+ slot = &node->eval_ctx.post_eval_fn;
1194
+ break;
1195
+ default:
1196
+ (void) fprintf(stderr, "c_dcg_node_register_eval_hook: no hook %d on node type %s (0x%04x) at %p - nothing registered\n", (int) hook, c_dcg_node_type_name(node->ntype), (unsigned) node->ntype, (const void*) node);
1197
+ return DCG_ERR_INVALID_ARG;
1198
+ }
1199
+
1200
+ if (*slot) return DCG_ERR_BUSY; /* one hook, one install - give the set back to change it */
1201
+
1202
+ /* An empty node has no data yet, so this registration is what sets it; one
1203
+ * that already carries a hook has to agree with what is there. The rule the
1204
+ * node's type gives it is not one of the three and holds no data, so it
1205
+ * neither blocks a registration nor disagrees with one. */
1206
+ bool empty = node->eval_ctx.pre_eval_fn == NULL && node->eval_ctx.eval_fn == NULL && node->eval_ctx.post_eval_fn == NULL;
1207
+ if (!empty && node->eval_ctx.user_data != user_data) return DCG_ERR_INVALID_ARG;
1208
+
1209
+ *slot = fn;
1210
+ node->eval_ctx.user_data = user_data;
1211
+ return DCG_OK;
1212
+ }
1213
+
1214
+ /**
1215
+ * @brief Give a node's eval hooks back, and the data they shared.
1216
+ *
1217
+ * The three go together and are released together, because they are one
1218
+ * protocol: a node left carrying a pre hook and no eval hook has lost half of
1219
+ * what it was told to do, and a node left carrying a hook it cannot describe is
1220
+ * worse than one with none. What it is for is changing them - registering is
1221
+ * only allowed where nothing is installed.
1222
+ *
1223
+ * @param node Node to clear (NULL-safe).
1224
+ */
1225
+ static inline void c_dcg_node_unregister_eval_hooks(dcg_node* node) {
1226
+ if (!node) return;
1227
+
1228
+ node->eval_ctx.pre_eval_fn = NULL;
1229
+ node->eval_ctx.eval_fn = NULL;
1230
+ node->eval_ctx.post_eval_fn = NULL;
1231
+ node->eval_ctx.user_data = NULL; /* the type's own rule is the node's, not an installer's: it stays */
1232
+ }
1233
+
1234
+ // ========== Mutation Callbacks ==========
1235
+
1236
+ /**
1237
+ * @brief Register a mutation observer on a node.
1238
+ *
1239
+ * Observers are a LIST, not a slot: any number of them can watch one node, and
1240
+ * each is handed back its own id to unregister with. Unlike an eval hook, a
1241
+ * mutation callback cannot fail and reports nothing - it is fire-and-forget.
1242
+ *
1243
+ * @param node Node to observe.
1244
+ * @param fn Callback to invoke on every mutation.
1245
+ * @param user_data Opaque data handed back to the callback.
1246
+ * @param out_id Receives the id for unregistration (may be NULL).
1247
+ * @return DCG_OK, or DCG_ERR_INVALID_ARG / DCG_ERR_OOM.
1248
+ */
1249
+ static inline int c_dcg_node_register_callback(dcg_node* node, dcg_node_callback_fn fn, void* user_data, uintptr_t* out_id) {
1250
+ if (!node || !fn) return DCG_ERR_INVALID_ARG;
1251
+
1252
+ dcg_node_callback_ctx* cb = (dcg_node_callback_ctx*) calloc(1, sizeof(dcg_node_callback_ctx));
1253
+ if (!cb) return DCG_ERR_OOM;
1254
+
1255
+ cb->fn = fn;
1256
+ cb->user_data = user_data;
1257
+ cb->id = (uintptr_t) cb;
1258
+
1259
+ if (!node->callbacks) {
1260
+ node->callbacks = cb;
1261
+ }
1262
+ else {
1263
+ dcg_node_callback_ctx* tail = node->callbacks;
1264
+ while (tail->next) tail = tail->next;
1265
+ tail->next = cb;
1266
+ }
1267
+
1268
+ if (out_id) *out_id = cb->id;
1269
+ return DCG_OK;
1270
+ }
1271
+
1272
+ /**
1273
+ * @brief Unregister a mutation observer by its id.
1274
+ *
1275
+ * @param node Node to modify.
1276
+ * @param callback_id Id returned by c_dcg_node_register_callback.
1277
+ * @return DCG_OK, DCG_ERR_INVALID_ARG or DCG_ERR_NOT_FOUND.
1278
+ */
1279
+ static inline int c_dcg_node_unregister_callback(dcg_node* node, uintptr_t callback_id) {
1280
+ if (!node) return DCG_ERR_INVALID_ARG;
1281
+
1282
+ dcg_node_callback_ctx* prev = NULL;
1283
+ dcg_node_callback_ctx* curr = node->callbacks;
1284
+ while (curr) {
1285
+ if (curr->id == callback_id) {
1286
+ if (prev) prev->next = curr->next;
1287
+ else node->callbacks = curr->next;
1288
+ free(curr);
1289
+ return DCG_OK;
1290
+ }
1291
+ prev = curr;
1292
+ curr = curr->next;
1293
+ }
1294
+ return DCG_ERR_NOT_FOUND;
1295
+ }
1296
+
1297
+ /**
1298
+ * @brief Notify every registered observer of a mutation.
1299
+ *
1300
+ * The next pointer is snapshotted before each call, so a callback may
1301
+ * unregister itself (or another callback) while being invoked.
1302
+ *
1303
+ * @param node Node whose observers to notify (NULL-safe).
1304
+ * @param event What happened.
1305
+ * @param subject The other node involved (may be NULL).
1306
+ * @param seq_id Id of the mutating caller; pass (uint64_t) -1 for
1307
+ * lifecycle events that have no self-suppression semantics.
1308
+ */
1309
+ static inline void c_dcg_node_invoke_callbacks(dcg_node* node, dcg_node_event event, dcg_node* subject, uint64_t seq_id) {
1310
+ if (!node || !node->callbacks) return;
1311
+
1312
+ dcg_node_callback_ctx* cb = node->callbacks;
1313
+ while (cb) {
1314
+ dcg_node_callback_ctx* next = cb->next;
1315
+ if (cb->fn) cb->fn(event, node, subject, seq_id, cb->user_data);
1316
+ cb = next;
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * @brief How many mutation observers are registered on a node.
1322
+ *
1323
+ * @param node Node to inspect (NULL-safe).
1324
+ * @return The observer count.
1325
+ */
1326
+ static inline size_t c_dcg_node_callback_count(const dcg_node* node) {
1327
+ if (!node) return 0;
1328
+ size_t count = 0;
1329
+ dcg_node_callback_ctx* cb = node->callbacks;
1330
+ while (cb) {
1331
+ count++;
1332
+ cb = cb->next;
1333
+ }
1334
+ return count;
1335
+ }
1336
+
1337
+ // ========== Graph Building ==========
1338
+
1339
+ /**
1340
+ * @brief Infer the edge condition of a child that is about to be appended.
1341
+ *
1342
+ * This is the bake port of the capi's c_infer_condition(): the rule reads
1343
+ * the parent's EXISTING branches - the node's dtype plays no part in it.
1344
+ *
1345
+ * - no children yet -> TRUE (the first branch is always TRUE)
1346
+ * - one binary child -> the opposite binary
1347
+ * - one else child -> TRUE, when that child is auto-generated
1348
+ * - one other child -> unresolved
1349
+ * - two or more children -> the newest auto-generated child's condition,
1350
+ * so the builder can find and replace the edge
1351
+ * it created itself; otherwise unresolved
1352
+ *
1353
+ * @param parent Parent the edge will be attached to (NULL-safe).
1354
+ * @return The inferred condition, or NULL when nothing can be inferred.
1355
+ */
1356
+ static inline const dcg_node_edge_condition* c_dcg_node_infer_condition(const dcg_node* parent) {
1357
+ size_t count = c_dcg_node_child_count(parent);
1358
+ dcg_node* last = c_dcg_node_last_child(parent);
1359
+
1360
+ if (count == 0) return DCG_TRUE_CONDITION;
1361
+
1362
+ if (count == 1 && last) {
1363
+ const dcg_node_edge_condition* last_condition = last->condition_to_parent;
1364
+ if (c_dcg_condition_is_binary(last_condition)) {
1365
+ return c_dcg_condition_is_true(last_condition) ? DCG_FALSE_CONDITION : DCG_TRUE_CONDITION;
1366
+ }
1367
+ if (c_dcg_condition_is_else(last_condition) && last->autogen) return DCG_TRUE_CONDITION;
1368
+ return NULL;
1369
+ }
1370
+
1371
+ if (last && last->autogen) return last->condition_to_parent;
1372
+ dcg_node* second = last ? last->prev_sibling : NULL;
1373
+ if (second && second->autogen) return second->condition_to_parent;
1374
+ return NULL;
1375
+ }
1376
+
1377
+ /**
1378
+ * @brief Link a child into a parent's child list, applying every edge rule.
1379
+ *
1380
+ * Shared by c_dcg_node_append() and c_dcg_node_append_at(). The condition
1381
+ * is resolved (AUTO inference), the root's unary/unconditioned rule is
1382
+ * enforced, and one edge per condition is guaranteed - the capi keys its
1383
+ * children dict by condition, so a duplicate edge is a build error there
1384
+ * and here alike.
1385
+ *
1386
+ * @param parent Parent to link into.
1387
+ * @param child Child to link (must be parentless).
1388
+ * @param condition Requested edge condition (never NULL).
1389
+ * @param anchor Insert before this child, or NULL to append at the tail.
1390
+ * @return DCG_OK or a DCG_ERR_* code.
1391
+ */
1392
+ static inline int c_dcg_node_link(dcg_node* parent, dcg_node* child, const dcg_node_edge_condition* condition, dcg_node* anchor) {
1393
+ if (parent == child) return DCG_ERR_CYCLE;
1394
+ if (child->parent) return DCG_ERR_BUSY;
1395
+ if (parent->flags & DCG_NODE_FLAG_FROZEN) return DCG_ERR_BUSY;
1396
+ if (c_dcg_node_is_ancestor(child, parent)) return DCG_ERR_CYCLE;
1397
+
1398
+ if (parent->ntype == DCG_NODE_ROOT) {
1399
+ // A root is strictly unary (one entry point) and strictly unconditioned.
1400
+ if (parent->children) return DCG_ERR_TYPE;
1401
+ if (!c_dcg_condition_is_none(condition) && !c_dcg_condition_is_auto(condition)) return DCG_ERR_EDGE;
1402
+ condition = DCG_NO_CONDITION;
1403
+ }
1404
+ else if (c_dcg_condition_is_auto(condition)) {
1405
+ condition = c_dcg_node_infer_condition(parent);
1406
+ if (!condition) return DCG_ERR_UNRESOLVED;
1407
+ }
1408
+
1409
+ if (c_dcg_node_child_by_condition(parent, condition)) return DCG_ERR_DUPLICATE;
1410
+
1411
+ // An else branch is the fallback, so it is always the last child: nothing
1412
+ // may be appended behind it, and it can never be inserted in front of
1413
+ // another child. Inserting a regular branch somewhere before a trailing
1414
+ // else is fine - the else stays last.
1415
+ if (!anchor && c_dcg_node_child_by_condition(parent, DCG_ELSE_CONDITION)) return DCG_ERR_EDGE;
1416
+ if (anchor && c_dcg_condition_is_else(condition)) return DCG_ERR_EDGE;
1417
+
1418
+ child->parent = parent;
1419
+ child->condition_to_parent = condition;
1420
+ c_dcg_node_adopt_condition(child, condition); // a standalone condition dies with its child
1421
+
1422
+ if (anchor) {
1423
+ child->next_sibling = anchor;
1424
+ child->prev_sibling = anchor->prev_sibling;
1425
+ if (child->prev_sibling) child->prev_sibling->next_sibling = child;
1426
+ else parent->children = child;
1427
+ anchor->prev_sibling = child;
1428
+ }
1429
+ else {
1430
+ child->prev_sibling = c_dcg_node_last_child(parent);
1431
+ child->next_sibling = NULL;
1432
+ if (child->prev_sibling) child->prev_sibling->next_sibling = child;
1433
+ else parent->children = child;
1434
+ }
1435
+
1436
+ c_dcg_node_invoke_callbacks(parent, DCG_NODE_EVENT_CHILD_ADDED, child, (uint64_t) -1);
1437
+ return DCG_OK;
1438
+ }
1439
+
1440
+ /**
1441
+ * @brief Append a child at the tail of a node's child list.
1442
+ *
1443
+ * DCG_AUTO_CONDITION is resolved by inference over the parent's existing
1444
+ * branches (see c_dcg_node_infer_condition) before it is stored; every
1445
+ * other condition is used verbatim. A NULL condition is rejected - the
1446
+ * capi makes the same call, refusing to guess an edge.
1447
+ *
1448
+ * Rejected: a NULL argument, a child that already has a parent (detach it
1449
+ * first), a child that is the parent or one of its ancestors (a cycle), a
1450
+ * condition already registered on this parent, an else branch registered
1451
+ * anywhere but last, a root that already has its entry child, a condition
1452
+ * a root may not carry, an edge that cannot be inferred, and any mutation
1453
+ * of a FROZEN (baked) node.
1454
+ *
1455
+ * @param parent Node to append to.
1456
+ * @param child Node to append (must be currently parentless).
1457
+ * @param condition Edge condition, never NULL. A standalone (allocator
1458
+ * block) condition is adopted by the child and dies with
1459
+ * it; the built-ins and borrowed conditions are not.
1460
+ * @return DCG_OK, or DCG_ERR_INVALID_ARG / BUSY / CYCLE / DUPLICATE / EDGE /
1461
+ * TYPE / UNRESOLVED.
1462
+ */
1463
+ static inline int c_dcg_node_append(dcg_node* parent, dcg_node* child, const dcg_node_edge_condition* condition) {
1464
+ if (!parent || !child || !condition) return DCG_ERR_INVALID_ARG;
1465
+ return c_dcg_node_link(parent, child, condition, NULL);
1466
+ }
1467
+
1468
+ /**
1469
+ * @brief Append a child, letting the parent infer the branch condition.
1470
+ *
1471
+ * Equivalent to c_dcg_node_append(parent, child, NULL) for a non-boolean
1472
+ * parent and to the AUTO inference for a boolean one.
1473
+ *
1474
+ * @param parent Node to append to.
1475
+ * @param child Node to append (must be currently parentless).
1476
+ * @return DCG_OK or a DCG_ERR_* code.
1477
+ */
1478
+ static inline int c_dcg_node_append_auto(dcg_node* parent, dcg_node* child) {
1479
+ return c_dcg_node_append(parent, child, DCG_AUTO_CONDITION);
1480
+ }
1481
+
1482
+ /**
1483
+ * @brief Insert a child at a given position of a node's child list.
1484
+ *
1485
+ * Applies the same rules as c_dcg_node_append and inserts at `index`
1486
+ * instead of the tail; an index at or past the end appends. The else
1487
+ * ordering rule makes an insert behind an else branch impossible, and an
1488
+ * else branch itself can only ever be appended at the tail.
1489
+ *
1490
+ * @param parent Node to insert into.
1491
+ * @param child Node to insert (must be currently parentless).
1492
+ * @param condition Edge condition, never NULL.
1493
+ * @param index Position to insert at; >= child_count appends.
1494
+ * @return DCG_OK or a DCG_ERR_* code.
1495
+ */
1496
+ static inline int c_dcg_node_append_at(dcg_node* parent, dcg_node* child, const dcg_node_edge_condition* condition, size_t index) {
1497
+ if (!parent || !child || !condition) return DCG_ERR_INVALID_ARG;
1498
+ return c_dcg_node_link(parent, child, condition, c_dcg_node_child_at(parent, index));
1499
+ }
1500
+
1501
+ /**
1502
+ * @brief Insert a child at a position, with a boolean branch edge.
1503
+ *
1504
+ * The convenience form of c_dcg_node_append_at() for the common two-way case:
1505
+ * the condition is the branch itself, not a condition object.
1506
+ *
1507
+ * @param parent Node to insert into.
1508
+ * @param child Node to insert (must be currently parentless).
1509
+ * @param condition true inserts under DCG_TRUE_CONDITION, false under DCG_FALSE_CONDITION.
1510
+ * @param index Position to insert at; >= child_count appends.
1511
+ * @return DCG_OK or a DCG_ERR_* code.
1512
+ */
1513
+ static inline int c_dcg_node_append_at_binary(dcg_node* parent, dcg_node* child, bool condition, size_t index) {
1514
+ return c_dcg_node_append_at(parent, child, condition ? DCG_TRUE_CONDITION : DCG_FALSE_CONDITION, index);
1515
+ }
1516
+
1517
+ /**
1518
+ * @brief Unlink a node from its parent, keeping its own subtree intact.
1519
+ *
1520
+ * The node keeps its subtree but loses its edge: the condition falls back
1521
+ * to NO_CONDITION, exactly as a displaced node ends up in the capi.
1522
+ *
1523
+ * @param node Node to unlink (NULL-safe: a parentless node is a no-op).
1524
+ * @return DCG_OK, or DCG_ERR_BUSY when the parent is FROZEN.
1525
+ */
1526
+ static inline int c_dcg_node_detach(dcg_node* node) {
1527
+ if (!node || !node->parent) return DCG_OK;
1528
+ if (node->parent->flags & DCG_NODE_FLAG_FROZEN) return DCG_ERR_BUSY;
1529
+
1530
+ dcg_node* parent = node->parent;
1531
+ c_dcg_node_unlink(node);
1532
+ c_dcg_node_invoke_callbacks(parent, DCG_NODE_EVENT_CHILD_REMOVED, node, (uint64_t) -1);
1533
+ return DCG_OK;
1534
+ }
1535
+
1536
+ /**
1537
+ * @brief Put a node into the slot of another one, inheriting its edge.
1538
+ *
1539
+ * The replacement takes over the old node's position and condition; the old
1540
+ * node is detached but NOT freed (its subtree stays alive), so the caller
1541
+ * decides what happens to it.
1542
+ *
1543
+ * @param old_node Node currently in the graph.
1544
+ * @param new_node Node to put in its place (must be parentless).
1545
+ * @return DCG_OK, DCG_ERR_INVALID_ARG, DCG_ERR_BUSY or DCG_ERR_CYCLE.
1546
+ */
1547
+ static inline int c_dcg_node_replace(dcg_node* old_node, dcg_node* new_node) {
1548
+ if (!old_node || !new_node) return DCG_ERR_INVALID_ARG;
1549
+ if (old_node == new_node) return DCG_OK;
1550
+ if (new_node->parent) return DCG_ERR_BUSY;
1551
+ if (c_dcg_node_is_ancestor(old_node, new_node)) return DCG_ERR_CYCLE;
1552
+
1553
+ dcg_node* parent = old_node->parent;
1554
+ if (!parent) return DCG_ERR_INVALID_ARG;
1555
+
1556
+ const dcg_node_edge_condition* condition = old_node->condition_to_parent;
1557
+ dcg_node* anchor = old_node->next_sibling;
1558
+
1559
+ int ret = c_dcg_node_detach(old_node);
1560
+ if (ret != DCG_OK) return ret;
1561
+
1562
+ // The slot is reused as-is, so the else-ordering rule cannot be violated
1563
+ // here: link straight into the anchor instead of going through append_at.
1564
+ // link() adopts the condition onto the replacement, so an owned condition
1565
+ // travels with the edge instead of dying with the displaced node.
1566
+ return c_dcg_node_link(parent, new_node, condition, anchor);
1567
+ }
1568
+
1569
+ /**
1570
+ * @brief Put a node into another's slot when a parent elsewhere already holds it.
1571
+ *
1572
+ * c_dcg_node_replace() refuses a replacement that has a parent, and in a tree
1573
+ * that is right: one node, one parent. A control-flow JOIN is the exception.
1574
+ * The node a breakpoint resumes into is reached from the breakpoint AND goes on
1575
+ * being the branch it was entered as, so it has to be placed here while the
1576
+ * parent that handed it over keeps its pointer to it - which is what the capi
1577
+ * does when it re-parents the node a breakpoint just connected to.
1578
+ *
1579
+ * What moves is the TREE link, not the list entry: the node's `parent` becomes
1580
+ * the node it displaces, while its former parent goes on naming it as a child.
1581
+ * That former parent is then a second entry point to the same block - a walk
1582
+ * reaches it twice and frees it once, which is what c_dcg_node_teardown_root()
1583
+ * is built to survive.
1584
+ *
1585
+ * It is representable only while the node is ALONE in the list it is leaving,
1586
+ * because the sibling links belong to the list being entered: a node with
1587
+ * neighbours still attached would leave them pointing into a chain it is no
1588
+ * longer in. A node that is not alone is therefore refused rather than
1589
+ * silently corrupting the walk that reads those links.
1590
+ *
1591
+ * @param old_node Node currently in the graph.
1592
+ * @param new_node Node to put in its place (alone in its own list, if it has one).
1593
+ * @return DCG_OK, DCG_ERR_INVALID_ARG, DCG_ERR_BUSY or DCG_ERR_CYCLE.
1594
+ */
1595
+ static inline int c_dcg_node_replace_shared(dcg_node* old_node, dcg_node* new_node) {
1596
+ if (!old_node || !new_node) return DCG_ERR_INVALID_ARG;
1597
+ if (!old_node->parent) return DCG_ERR_INVALID_ARG;
1598
+ if (new_node->next_sibling || new_node->prev_sibling) return DCG_ERR_BUSY; /* not alone in its own list */
1599
+
1600
+ dcg_node* parent = old_node->parent;
1601
+ const dcg_node_edge_condition* condition = old_node->condition_to_parent;
1602
+
1603
+ /*
1604
+ * Shed the tree link, keep the list entry: the former parent is what makes
1605
+ * this a join, and dropping its pointer would make it nothing.
1606
+ *
1607
+ * The join lands at the END of this parent's list rather than in the slot
1608
+ * the displaced node held. A node carries one sibling chain, and its former
1609
+ * parent walks that chain to reach it - landing it mid-list would hand that
1610
+ * parent the rest of this list to walk into, and from there back round to
1611
+ * itself. At the end, the chain it walks stops where it should.
1612
+ */
1613
+ new_node->parent = NULL;
1614
+
1615
+ int ret = c_dcg_node_detach(old_node);
1616
+ if (ret != DCG_OK) return ret;
1617
+
1618
+ return c_dcg_node_append(parent, new_node, condition);
1619
+ }
1620
+
1621
+ /**
1622
+ * @brief Allocate an auto-generated placeholder.
1623
+ *
1624
+ * A placeholder is what an unfinished branch leaves behind. It is a plain node,
1625
+ * not an action: it carries none of the fields the action family's struct exists
1626
+ * for - no signal, no payload, no connect flag - and it is flagged autogen so
1627
+ * consolidation can tell it from a node the caller built.
1628
+ *
1629
+ * The opening half of the placeholder discipline: a branch reserves its slot
1630
+ * with this and fills it later, and whatever is still a placeholder when the
1631
+ * graph closes is retired by c_dcg_node_consolidate_placeholder below.
1632
+ *
1633
+ * @param allocator Allocator for the block; NULL falls back to the plain heap.
1634
+ * @return The node, or NULL on OOM.
1635
+ */
1636
+ static inline dcg_node* c_dcg_node_new_placeholder(allocator_protocol* allocator) {
1637
+ dcg_node* node = c_dcg_node_new(DCG_NODE_PLACEHOLDER, DCG_DEF_REPR_PLACEHOLDER, allocator);
1638
+ if (!node) return NULL;
1639
+
1640
+ node->autogen = true;
1641
+
1642
+ /* A stand-in is an action like any other, so it stands for itself from the
1643
+ * moment it exists - which is what a branch built over it reads before the
1644
+ * build has put anything there (see c_dcg_node_new_action). */
1645
+ if (c_dcg_var_init_node(&node->out, node) != DCG_OK) {
1646
+ c_dcg_node_free(node);
1647
+ return NULL;
1648
+ }
1649
+ return node;
1650
+ }
1651
+
1652
+ /**
1653
+ * @brief Turn every placeholder of a subtree into an auto-generated no-action.
1654
+ *
1655
+ * The closing half of the placeholder discipline: the builder reserves a
1656
+ * branch with a placeholder and fills it later, so whatever is STILL a
1657
+ * placeholder when the graph closes becomes a NoAction leaf - a baked graph
1658
+ * never contains a placeholder.
1659
+ *
1660
+ * The conversion happens in place: the node keeps its slot, its edge, its
1661
+ * labels, its hooks and whatever subtree it grew, and only its type and its
1662
+ * display text change (reported as a MODIFIED mutation). The retitle is the
1663
+ * one step that allocates, so a node whose new text cannot be had keeps the
1664
+ * text it had and says so on stderr; the type has already flipped either way,
1665
+ * which is the structural half. A placeholder that was left holding children
1666
+ * keeps them - and validation will say so, which is the honest report of a
1667
+ * builder that never filled the slot properly.
1668
+ *
1669
+ * The walk is depth-first over the whole subtree and idempotent, so it can
1670
+ * be called once at the root or at every level like the capi does.
1671
+ *
1672
+ * @param node Subtree to consolidate (NULL-safe).
1673
+ * @return Number of placeholders converted.
1674
+ */
1675
+ static inline size_t c_dcg_node_consolidate_placeholder(dcg_node* node) {
1676
+ if (!node) return 0;
1677
+
1678
+ size_t replaced = 0;
1679
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
1680
+ replaced += c_dcg_node_consolidate_placeholder(child);
1681
+
1682
+ if (child->ntype == DCG_NODE_PLACEHOLDER) {
1683
+ child->ntype = DCG_NODE_NOACTION;
1684
+ child->autogen = true;
1685
+ if (c_dcg_node_set_repr(child, DCG_DEF_REPR_NOACTION) != DCG_OK) {
1686
+ (void) fprintf(stderr, "c_dcg_node_consolidate_placeholder: no text for the no-action at %p - keeping \"%s\"\n", (void*) child, child->repr ? child->repr : "");
1687
+ }
1688
+ /* The conversion is a change to the PARENT's child, so the parent is
1689
+ * told: the edge still holds a node, but it is no longer a
1690
+ * placeholder, and a wrapper of the parent has to rebuild it as what
1691
+ * it now is. The converted node cannot carry that news itself - a
1692
+ * node the C layer grew has no wrapper and no callback to hear a
1693
+ * MODIFIED. */
1694
+ c_dcg_node_invoke_callbacks(node, DCG_NODE_EVENT_CHILD_UPDATED, child, (uint64_t) -1);
1695
+ c_dcg_node_invoke_callbacks(child, DCG_NODE_EVENT_MODIFIED, child, (uint64_t) -1);
1696
+ replaced++;
1697
+ }
1698
+ }
1699
+ return replaced;
1700
+ }
1701
+
1702
+ /**
1703
+ * @brief The placeholder slot under a node: the one already reserved, or a new one.
1704
+ *
1705
+ * The opening half of the placeholder discipline - the closing half is
1706
+ * c_dcg_node_consolidate_placeholder(). A branch that is about to be built
1707
+ * reserves its slot first, and whatever fills the slot replaces the
1708
+ * placeholder; a slot nothing ever fills becomes a no-action when the graph
1709
+ * closes.
1710
+ *
1711
+ * The rules:
1712
+ *
1713
+ * - the FIRST standing placeholder is reused, so a build that descends into a
1714
+ * node fills the slot that was reserved first rather than reserving a
1715
+ * second. First and not last, because a node entered by a `with` block
1716
+ * reserves its arms in the order they are read - TRUE then FALSE - and the
1717
+ * arm taken first is the one the evaluator would test first. Taking the
1718
+ * last one here would fill the fallback first and leave the pair in the
1719
+ * order the read does not use;
1720
+ * - otherwise a placeholder is appended on the INFERRED edge, which is what
1721
+ * makes this fail exactly when the node has no room left for a branch -
1722
+ * a node with one non-binary child and no free edge, say. A root is no
1723
+ * exception: the inference hands back the unconditioned edge it requires.
1724
+ *
1725
+ * The placeholder is allocated from the same allocator the node came from, so
1726
+ * the two are one allocation family; it is NOT nested under the node, because
1727
+ * a node does not own its children (see c_dcg_node_free).
1728
+ *
1729
+ * @param node Node to reserve a slot under (NULL-safe).
1730
+ * @return The placeholder, or NULL on OOM / when no edge can be inferred.
1731
+ */
1732
+ static inline dcg_node* c_dcg_node_get_placeholder(dcg_node* node) {
1733
+ if (!node) return NULL;
1734
+
1735
+ dcg_node* standing = NULL;
1736
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
1737
+ if (child->ntype == DCG_NODE_PLACEHOLDER) {
1738
+ standing = child;
1739
+ break;
1740
+ }
1741
+ }
1742
+ if (standing) return standing;
1743
+
1744
+ dcg_node* placeholder = c_dcg_node_new_placeholder(c_ap_protocol_from_ptr(node));
1745
+ if (!placeholder) return NULL;
1746
+
1747
+ if (c_dcg_node_append_auto(node, placeholder) != DCG_OK) {
1748
+ c_dcg_node_free(placeholder);
1749
+ return NULL;
1750
+ }
1751
+ return placeholder;
1752
+ }
1753
+
1754
+ // ========== Queries ==========
1755
+
1756
+ /**
1757
+ * @brief Number of direct children.
1758
+ *
1759
+ * @param node Node to inspect (NULL-safe).
1760
+ * @return The child count.
1761
+ */
1762
+ static inline size_t c_dcg_node_child_count(const dcg_node* node) {
1763
+ if (!node) return 0;
1764
+ size_t count = 0;
1765
+ dcg_node* child = node->children;
1766
+ while (child) {
1767
+ count++;
1768
+ child = child->next_sibling;
1769
+ }
1770
+ return count;
1771
+ }
1772
+
1773
+ /**
1774
+ * @brief First child of a node.
1775
+ *
1776
+ * @param node Node to inspect (NULL-safe).
1777
+ * @return The first child, or NULL for a leaf.
1778
+ */
1779
+ static inline dcg_node* c_dcg_node_first_child(const dcg_node* node) {
1780
+ if (!node) return NULL;
1781
+ return node->children;
1782
+ }
1783
+
1784
+ /**
1785
+ * @brief Last child of a node.
1786
+ *
1787
+ * @param node Node to inspect (NULL-safe).
1788
+ * @return The last child, or NULL for a leaf.
1789
+ */
1790
+ static inline dcg_node* c_dcg_node_last_child(const dcg_node* node) {
1791
+ if (!node) return NULL;
1792
+ dcg_node* child = node->children;
1793
+ if (!child) return NULL;
1794
+ while (child->next_sibling) child = child->next_sibling;
1795
+ return child;
1796
+ }
1797
+
1798
+ /**
1799
+ * @brief Child at a position.
1800
+ *
1801
+ * @param node Node to inspect (NULL-safe).
1802
+ * @param index Zero-based position.
1803
+ * @return The child, or NULL when out of range.
1804
+ */
1805
+ static inline dcg_node* c_dcg_node_child_at(const dcg_node* node, size_t index) {
1806
+ if (!node) return NULL;
1807
+ dcg_node* child = node->children;
1808
+ while (child && index > 0) {
1809
+ child = child->next_sibling;
1810
+ index--;
1811
+ }
1812
+ return child;
1813
+ }
1814
+
1815
+ /**
1816
+ * @brief Position of a node among its siblings.
1817
+ *
1818
+ * @param node Node to inspect (NULL-safe).
1819
+ * @return The zero-based index, or -1 for the root (a node without a parent).
1820
+ */
1821
+ static inline ssize_t c_dcg_node_child_index(const dcg_node* node) {
1822
+ if (!node || !node->parent) return -1;
1823
+
1824
+ ssize_t index = 0;
1825
+ dcg_node* child = node->parent->children;
1826
+ while (child) {
1827
+ if (child == node) return index;
1828
+ index++;
1829
+ child = child->next_sibling;
1830
+ }
1831
+ return -1;
1832
+ }
1833
+
1834
+ /**
1835
+ * @brief First child registered under a given edge condition.
1836
+ *
1837
+ * Conditions compare by identity for the built-ins and by payload for the
1838
+ * rest (see c_dcg_condition_equals), which is what backs both the capi's
1839
+ * condition-keyed children dict and the one-edge-per-condition rule.
1840
+ *
1841
+ * @param node Node to inspect (NULL-safe).
1842
+ * @param condition Condition to look for (NULL = the unconditional edge).
1843
+ * @return The child, or NULL when there is none.
1844
+ */
1845
+ static inline dcg_node* c_dcg_node_child_by_condition(const dcg_node* node, const dcg_node_edge_condition* condition) {
1846
+ if (!node) return NULL;
1847
+ dcg_node* child = node->children;
1848
+ while (child) {
1849
+ if (c_dcg_condition_equals(child->condition_to_parent, condition)) return child;
1850
+ child = child->next_sibling;
1851
+ }
1852
+ return NULL;
1853
+ }
1854
+
1855
+ /**
1856
+ * @brief Next sibling of a node.
1857
+ *
1858
+ * @param node Node to inspect (NULL-safe).
1859
+ * @return The next sibling, or NULL.
1860
+ */
1861
+ static inline dcg_node* c_dcg_node_next_sibling(const dcg_node* node) {
1862
+ if (!node) return NULL;
1863
+ return node->next_sibling;
1864
+ }
1865
+
1866
+ /**
1867
+ * @brief Previous sibling of a node.
1868
+ *
1869
+ * @param node Node to inspect (NULL-safe).
1870
+ * @return The previous sibling, or NULL.
1871
+ */
1872
+ static inline dcg_node* c_dcg_node_prev_sibling(const dcg_node* node) {
1873
+ if (!node) return NULL;
1874
+ return node->prev_sibling;
1875
+ }
1876
+
1877
+ /**
1878
+ * @brief Walk up to the topmost ancestor - the root of the graph.
1879
+ *
1880
+ * @param node Node to start from (NULL-safe).
1881
+ * @return The root, or NULL.
1882
+ */
1883
+ static inline dcg_node* c_dcg_node_root(dcg_node* node) {
1884
+ if (!node) return NULL;
1885
+ while (node->parent) node = node->parent;
1886
+ return node;
1887
+ }
1888
+
1889
+ /**
1890
+ * @brief Number of edges between a node and its root.
1891
+ *
1892
+ * @param node Node to inspect (NULL-safe).
1893
+ * @return The depth (0 for a root).
1894
+ */
1895
+ static inline size_t c_dcg_node_depth(const dcg_node* node) {
1896
+ size_t depth = 0;
1897
+ while (node && node->parent) {
1898
+ depth++;
1899
+ node = node->parent;
1900
+ }
1901
+ return depth;
1902
+ }
1903
+
1904
+ /**
1905
+ * @brief Longest downward path from a node to one of its leaves.
1906
+ *
1907
+ * @param node Node to inspect (NULL-safe).
1908
+ * @return The height (0 for a leaf).
1909
+ */
1910
+ static inline size_t c_dcg_node_height(const dcg_node* node) {
1911
+ if (!node || !node->children) return 0;
1912
+
1913
+ size_t best = 0;
1914
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
1915
+ size_t h = c_dcg_node_height(child);
1916
+ if (h > best) best = h;
1917
+ }
1918
+ return best + 1;
1919
+ }
1920
+
1921
+ /**
1922
+ * @brief Number of nodes in the subtree rooted at a node (the node included).
1923
+ *
1924
+ * @param node Node to inspect (NULL-safe).
1925
+ * @return The subtree size.
1926
+ */
1927
+ static inline size_t c_dcg_node_subtree_size(const dcg_node* node) {
1928
+ if (!node) return 0;
1929
+ size_t size = 1;
1930
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
1931
+ size += c_dcg_node_subtree_size(child);
1932
+ }
1933
+ return size;
1934
+ }
1935
+
1936
+ /**
1937
+ * @brief Number of leaves in the subtree rooted at a node.
1938
+ *
1939
+ * @param node Node to inspect (NULL-safe).
1940
+ * @return The leaf count (a childless node counts as one leaf).
1941
+ */
1942
+ static inline size_t c_dcg_node_leaf_count(const dcg_node* node) {
1943
+ if (!node) return 0;
1944
+ if (!node->children) return 1;
1945
+
1946
+ size_t count = 0;
1947
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
1948
+ count += c_dcg_node_leaf_count(child);
1949
+ }
1950
+ return count;
1951
+ }
1952
+
1953
+ /**
1954
+ * @brief Predicate: does the node have no children?
1955
+ *
1956
+ * @param node Node to inspect (NULL-safe).
1957
+ * @return true for a leaf.
1958
+ */
1959
+ static inline bool c_dcg_node_is_leaf(const dcg_node* node) {
1960
+ return node && node->children == NULL;
1961
+ }
1962
+
1963
+ /**
1964
+ * @brief Predicate: is the node parentless?
1965
+ *
1966
+ * @param node Node to inspect (NULL-safe).
1967
+ * @return true for the root.
1968
+ */
1969
+ static inline bool c_dcg_node_is_root(const dcg_node* node) {
1970
+ return node && node->parent == NULL;
1971
+ }
1972
+
1973
+ /**
1974
+ * @brief Predicate: is one node an ancestor of another?
1975
+ *
1976
+ * @param node Candidate ancestor (NULL-safe).
1977
+ * @param descendant Candidate descendant (NULL-safe).
1978
+ * @return true when descendant sits anywhere below node.
1979
+ */
1980
+ static inline bool c_dcg_node_is_ancestor(const dcg_node* node, const dcg_node* descendant) {
1981
+ if (!node || !descendant) return false;
1982
+ for (const dcg_node* walk = descendant->parent; walk; walk = walk->parent) {
1983
+ if (walk == node) return true;
1984
+ }
1985
+ return false;
1986
+ }
1987
+
1988
+ /**
1989
+ * @brief Depth-first search for the first node carrying a uid.
1990
+ *
1991
+ * @param root Subtree to search (NULL-safe).
1992
+ * @param uid Identity to look for (16 bytes).
1993
+ * @return The node, or NULL when there is none.
1994
+ */
1995
+ static inline dcg_node* c_dcg_node_find_by_uid(const dcg_node* root, const uuid_t uid) {
1996
+ if (!root || !uid) return NULL;
1997
+ if (memcmp(root->uid, uid, sizeof(uuid_t)) == 0) return (dcg_node*) root;
1998
+
1999
+ for (dcg_node* child = root->children; child; child = child->next_sibling) {
2000
+ dcg_node* found = c_dcg_node_find_by_uid(child, uid);
2001
+ if (found) return found;
2002
+ }
2003
+ return NULL;
2004
+ }
2005
+
2006
+ /**
2007
+ * @brief Recursive worker of c_dcg_node_collect_leaves.
2008
+ *
2009
+ * @param node Subtree to walk.
2010
+ * @param out Destination array (may be NULL to only count).
2011
+ * @param cap Capacity of out.
2012
+ * @param written Running number of entries already stored (NULL-safe).
2013
+ * @return Number of leaves in this subtree.
2014
+ */
2015
+ static inline size_t c_dcg_node_collect_leaves_walk(const dcg_node* node, dcg_node** out, size_t cap, size_t* written) {
2016
+ if (!node) return 0;
2017
+
2018
+ if (!node->children) {
2019
+ if (out && written && *written < cap) out[*written] = (dcg_node*) node;
2020
+ if (written) (*written)++;
2021
+ return 1;
2022
+ }
2023
+
2024
+ size_t total = 0;
2025
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
2026
+ total += c_dcg_node_collect_leaves_walk(child, out, cap, written);
2027
+ }
2028
+ return total;
2029
+ }
2030
+
2031
+ /**
2032
+ * @brief Collect the leaves of a subtree, depth-first, left to right.
2033
+ *
2034
+ * @param root Subtree to walk (NULL-safe).
2035
+ * @param out Destination array (may be NULL to only count).
2036
+ * @param cap Capacity of out.
2037
+ * @return The total number of leaves; when it exceeds cap, the array holds
2038
+ * the first cap of them.
2039
+ */
2040
+ static inline size_t c_dcg_node_collect_leaves(const dcg_node* root, dcg_node** out, size_t cap) {
2041
+ size_t written = 0;
2042
+ return c_dcg_node_collect_leaves_walk(root, out, cap, &written);
2043
+ }
2044
+
2045
+ /**
2046
+ * @brief Recursive worker of c_dcg_node_collect_descendants.
2047
+ *
2048
+ * @param node Subtree to walk.
2049
+ * @param out Destination array (may be NULL to only count).
2050
+ * @param cap Capacity of out.
2051
+ * @param written Running number of entries already stored (NULL-safe).
2052
+ * @return Number of descendants in this subtree.
2053
+ */
2054
+ static inline size_t c_dcg_node_collect_descendants_walk(const dcg_node* node, dcg_node** out, size_t cap, size_t* written) {
2055
+ if (!node) return 0;
2056
+
2057
+ size_t total = 0;
2058
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
2059
+ if (out && written && *written < cap) out[*written] = (dcg_node*) child;
2060
+ if (written) (*written)++;
2061
+ total++;
2062
+ total += c_dcg_node_collect_descendants_walk(child, out, cap, written);
2063
+ }
2064
+ return total;
2065
+ }
2066
+
2067
+ /**
2068
+ * @brief Collect every node of a subtree below the root, pre-order.
2069
+ *
2070
+ * @param root Subtree to walk (NULL-safe).
2071
+ * @param out Destination array (may be NULL to only count).
2072
+ * @param cap Capacity of out.
2073
+ * @return The total number of descendants; when it exceeds cap, the array
2074
+ * holds the first cap of them.
2075
+ */
2076
+ static inline size_t c_dcg_node_collect_descendants(const dcg_node* root, dcg_node** out, size_t cap) {
2077
+ size_t written = 0;
2078
+ return c_dcg_node_collect_descendants_walk(root, out, cap, &written);
2079
+ }
2080
+
2081
+ /**
2082
+ * @brief Collect the chain of nodes from the root down to a node.
2083
+ *
2084
+ * The node itself is the last entry. Useful for recording an eval path.
2085
+ *
2086
+ * @param node Node to trace (NULL-safe).
2087
+ * @param out Destination array (may be NULL to only count).
2088
+ * @param cap Capacity of out.
2089
+ * @return The path length (root..node inclusive); entries beyond cap are
2090
+ * dropped from the front when the path does not fit.
2091
+ */
2092
+ static inline size_t c_dcg_node_path_to(const dcg_node* node, const dcg_node** out, size_t cap) {
2093
+ size_t depth = c_dcg_node_depth(node);
2094
+ for (size_t i = 0; i <= depth && node; i++) {
2095
+ size_t index = depth - i;
2096
+ if (out && index < cap) out[index] = node;
2097
+ node = node->parent;
2098
+ }
2099
+ return depth + 1;
2100
+ }
2101
+
2102
+ // ========== Labels ==========
2103
+
2104
+ /**
2105
+ * @brief Attach a label to a node, owning a copy of its text.
2106
+ *
2107
+ * The text is copied into a block nested under the node, and the list entry
2108
+ * itself is a nested block too, so removing the label - or freeing the node -
2109
+ * releases both without a separate step. Duplicates are refused.
2110
+ *
2111
+ * @param node Node to label.
2112
+ * @param label Label text to copy.
2113
+ * @return DCG_OK, DCG_ERR_INVALID_ARG, DCG_ERR_INVALID_BUF or DCG_ERR_TYPE.
2114
+ */
2115
+ static inline int c_dcg_node_add_label(dcg_node* node, const char* label) {
2116
+ if (!node || !label) return DCG_ERR_INVALID_ARG;
2117
+ if (c_dcg_node_has_label(node, label)) return DCG_ERR_TYPE;
2118
+
2119
+ dcg_node_label* entry = (dcg_node_label*) c_ap_alloc_child(sizeof(dcg_node_label), NULL, node);
2120
+ if (!entry) return DCG_ERR_OOM;
2121
+
2122
+ size_t len = strlen(label);
2123
+ char* copy = (char*) c_ap_alloc_child(len + 1, NULL, entry);
2124
+ if (!copy) {
2125
+ c_ap_free_owned(entry);
2126
+ return DCG_ERR_OOM;
2127
+ }
2128
+ memcpy(copy, label, len + 1);
2129
+
2130
+ entry->label = copy;
2131
+ entry->next = node->labels;
2132
+ node->labels = entry;
2133
+
2134
+ c_dcg_node_invoke_callbacks(node, DCG_NODE_EVENT_MODIFIED, node, (uint64_t) -1);
2135
+ return DCG_OK;
2136
+ }
2137
+
2138
+ /**
2139
+ * @brief Predicate: does the node carry a label?
2140
+ *
2141
+ * @param node Node to inspect (NULL-safe).
2142
+ * @param label Label text to look for.
2143
+ * @return true when the label is attached.
2144
+ */
2145
+ static inline bool c_dcg_node_has_label(const dcg_node* node, const char* label) {
2146
+ if (!node || !label) return false;
2147
+ for (const dcg_node_label* entry = node->labels; entry; entry = entry->next) {
2148
+ if (entry->label && strcmp(entry->label, label) == 0) return true;
2149
+ }
2150
+ return false;
2151
+ }
2152
+
2153
+ /**
2154
+ * @brief Detach a label from a node, releasing its copy of the text.
2155
+ *
2156
+ * @param node Node to modify.
2157
+ * @param label Label text to remove.
2158
+ * @return DCG_OK, DCG_ERR_INVALID_ARG or DCG_ERR_NOT_FOUND.
2159
+ */
2160
+ static inline int c_dcg_node_remove_label(dcg_node* node, const char* label) {
2161
+ if (!node || !label) return DCG_ERR_INVALID_ARG;
2162
+
2163
+ dcg_node_label* prev = NULL;
2164
+ dcg_node_label* curr = node->labels;
2165
+ while (curr) {
2166
+ if (curr->label && strcmp(curr->label, label) == 0) {
2167
+ if (prev) prev->next = curr->next;
2168
+ else node->labels = curr->next;
2169
+ if (curr->label) c_ap_free_owned((void*) curr->label);
2170
+ c_ap_free_owned(curr);
2171
+ c_dcg_node_invoke_callbacks(node, DCG_NODE_EVENT_MODIFIED, node, (uint64_t) -1);
2172
+ return DCG_OK;
2173
+ }
2174
+ prev = curr;
2175
+ curr = curr->next;
2176
+ }
2177
+ return DCG_ERR_NOT_FOUND;
2178
+ }
2179
+
2180
+ /**
2181
+ * @brief Number of labels attached to a node.
2182
+ *
2183
+ * @param node Node to inspect (NULL-safe).
2184
+ * @return The label count.
2185
+ */
2186
+ static inline size_t c_dcg_node_label_count(const dcg_node* node) {
2187
+ if (!node) return 0;
2188
+ size_t count = 0;
2189
+ for (const dcg_node_label* entry = node->labels; entry; entry = entry->next) count++;
2190
+ return count;
2191
+ }
2192
+
2193
+ // ========== Validation ==========
2194
+
2195
+ /**
2196
+ * @brief Record a validation problem and mark the graph invalid.
2197
+ *
2198
+ * @param report Report being filled (NULL-safe: counting is then skipped).
2199
+ * @param node Node the problem was found on.
2200
+ * @param err Error code.
2201
+ * @param valid Validity accumulator (NULL-safe).
2202
+ */
2203
+ static inline void c_dcg_validate_fail(dcg_validate_report* report, dcg_node* node, dcg_ret_code err, bool* valid) {
2204
+ if (report) {
2205
+ report->errors++;
2206
+ if (report->code == DCG_OK) {
2207
+ report->code = err;
2208
+ report->node = node;
2209
+ }
2210
+ }
2211
+ if (valid) *valid = false;
2212
+ }
2213
+
2214
+ /**
2215
+ * @brief Recursive worker of c_dcg_node_validate.
2216
+ *
2217
+ * @param node Node to check.
2218
+ * @param report Report being filled (NULL-safe).
2219
+ * @param depth Depth of node.
2220
+ * @return true when the subtree is valid.
2221
+ */
2222
+ static inline bool c_dcg_node_validate_walk(const dcg_node* node, dcg_validate_report* report, size_t depth) {
2223
+ if (!node) return true;
2224
+ if (report) {
2225
+ report->nodes++;
2226
+ if (depth > report->depth) report->depth = depth;
2227
+ }
2228
+
2229
+ bool valid = true;
2230
+ size_t count = c_dcg_node_child_count(node);
2231
+
2232
+ if (node->ntype == DCG_NODE_ROOT) {
2233
+ const dcg_node* entry = node->children;
2234
+ if (node->parent) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_TYPE, &valid);
2235
+ if (count > 1) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_TYPE, &valid);
2236
+ if (entry && !c_dcg_condition_is_none(entry->condition_to_parent)) {
2237
+ c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_EDGE, &valid);
2238
+ }
2239
+ }
2240
+ else if (node->parent == NULL) {
2241
+ c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_TYPE, &valid);
2242
+ }
2243
+
2244
+ if (c_dcg_node_type_is_action(node->ntype) && count > 0) {
2245
+ c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_TYPE, &valid);
2246
+ }
2247
+
2248
+ size_t arity = c_dcg_node_type_arity(node->ntype);
2249
+ if (arity > 0 && count != arity) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_TYPE, &valid);
2250
+
2251
+ size_t index = 0;
2252
+ dcg_node* else_child = NULL;
2253
+ bool has_binary = false;
2254
+ bool has_value = false;
2255
+ bool has_unconditioned = false;
2256
+
2257
+ for (dcg_node* child = node->children; child; child = child->next_sibling, index++) {
2258
+ const dcg_node_edge_condition* cond = child->condition_to_parent;
2259
+
2260
+ if (c_dcg_condition_is_auto(cond)) {
2261
+ c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_UNRESOLVED, &valid);
2262
+ }
2263
+
2264
+ if (c_dcg_condition_is_else(cond)) {
2265
+ if (else_child) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_DUPLICATE, &valid);
2266
+ else_child = child;
2267
+ if (index + 1 != count) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_EDGE, &valid);
2268
+ }
2269
+
2270
+ // Branch shapes: a binary branch, a value branch, an else fallback (a
2271
+ // legitimate branch), or the unconditional/auto pair.
2272
+ if (c_dcg_condition_is_binary(cond)) has_binary = true;
2273
+ else if (c_dcg_condition_is_else(cond)) { /* the fallback is a branch of its own */
2274
+ }
2275
+ else if (c_dcg_condition_is_sentinel(cond)) has_unconditioned = true;
2276
+ else has_value = true;
2277
+
2278
+ if (c_dcg_node_is_ancestor(child, node)) {
2279
+ c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_CYCLE, &valid);
2280
+ continue; // Report it and stop - descending would never come back.
2281
+ }
2282
+
2283
+ if (!c_dcg_node_validate_walk(child, report, depth + 1)) valid = false;
2284
+ }
2285
+
2286
+ // Branch-shape rules apply to decision nodes only: a root is unconditioned
2287
+ // by construction, and everything else branches.
2288
+ if (count > 1 && node->ntype != DCG_NODE_ROOT) {
2289
+ if (has_unconditioned) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_EDGE, &valid);
2290
+ if (has_binary && has_value) c_dcg_validate_fail(report, (dcg_node*) node, DCG_ERR_EDGE, &valid);
2291
+ }
2292
+
2293
+ return valid;
2294
+ }
2295
+
2296
+ /**
2297
+ * @brief Validate a graph, walking it depth-first from the given node.
2298
+ *
2299
+ * Enforces the structural invariants a baked graph must satisfy: exactly one
2300
+ * parentless ROOT, at most one child under a root, fixed operator arity,
2301
+ * action nodes as leaves, at most one trailing ELSE per node, resolved
2302
+ * conditions, consistent branch shapes, and acyclicity.
2303
+ *
2304
+ * Problems are reported as dcg_ret_code values, so a caller handles a bad
2305
+ * graph with the same vocabulary it handles every other failure:
2306
+ * - DCG_ERR_TYPE a detached fragment, a root with too many children,
2307
+ * a wrong operand count, an action holding children
2308
+ * - DCG_ERR_EDGE a root edge that is not unconditional, an else that
2309
+ * is not last, conflicting or unconditioned branches
2310
+ * - DCG_ERR_DUPLICATE more than one else branch
2311
+ * - DCG_ERR_UNRESOLVED an AUTO condition that survived into the graph
2312
+ * - DCG_ERR_CYCLE a cycle (reported, then the walk stops there)
2313
+ *
2314
+ * A cycle is reported and the walk stops there instead of descending into
2315
+ * it. The other tree walks (teardown, size and leaf queries, rendering)
2316
+ * assume an acyclic graph - this is the check that establishes it.
2317
+ *
2318
+ * @param root Node to validate from (typically the graph root).
2319
+ * @param report Receives the outcome; may be NULL when only the verdict matters.
2320
+ * @return true when the graph is valid.
2321
+ */
2322
+ static inline bool c_dcg_node_validate(const dcg_node* root, dcg_validate_report* report) {
2323
+ if (report) memset(report, 0, sizeof(*report));
2324
+ if (!root) return false;
2325
+ return c_dcg_node_validate_walk(root, report, 0);
2326
+ }
2327
+
2328
+ /**
2329
+ * @brief Validate a graph and print every problem found, one per line.
2330
+ *
2331
+ * The report is produced by a plain validation run; this only renders it.
2332
+ *
2333
+ * @param root Node to validate from.
2334
+ * @param stream Destination stream.
2335
+ * @return Number of errors found, or DCG_ERR_INVALID_ARG.
2336
+ */
2337
+ static inline int c_dcg_node_validate_print(const dcg_node* root, FILE* stream) {
2338
+ if (!stream || !root) return DCG_ERR_INVALID_ARG;
2339
+
2340
+ dcg_validate_report report;
2341
+ bool valid = c_dcg_node_validate(root, &report);
2342
+
2343
+ (void) fprintf(
2344
+ stream,
2345
+ "validate: %s nodes=%zu depth=%zu errors=%zu first=%s\n",
2346
+ valid ? "OK" : "INVALID",
2347
+ report.nodes,
2348
+ report.depth,
2349
+ report.errors,
2350
+ c_dcg_ret_code_name(report.code)
2351
+ );
2352
+ if (!valid && report.node) {
2353
+ (void) fprintf(stream, " first error at %s \"%s\"\n", c_dcg_node_type_name(report.node->ntype), report.node->repr ? report.node->repr : "");
2354
+ }
2355
+ return (int) report.errors;
2356
+ }
2357
+
2358
+ // ========== Rendering ==========
2359
+
2360
+ /**
2361
+ * @brief Fill in the standard renderer options.
2362
+ *
2363
+ * @param opts Options to populate (NULL-safe).
2364
+ */
2365
+ static inline void c_dcg_render_opts_default(dcg_render_opts* opts) {
2366
+ if (!opts) return;
2367
+ memset(opts, 0, sizeof(*opts));
2368
+ opts->style = DCG_RENDER_UNICODE;
2369
+ opts->max_depth = DCG_NODE_RENDER_MAX_DEPTH;
2370
+ opts->show_condition = true;
2371
+ opts->show_out = true;
2372
+ opts->show_children = true;
2373
+ }
2374
+
2375
+ /**
2376
+ * @brief Render a node's flags as a compact string.
2377
+ *
2378
+ * @param flags Flag bits.
2379
+ * @param out Destination buffer.
2380
+ * @param cap Capacity of out.
2381
+ * @return Number of characters written, or DCG_ERR_*.
2382
+ */
2383
+ static inline int c_dcg_node_format_flags(uint32_t flags, char* out, size_t cap) {
2384
+ if (!out || cap == 0) return DCG_ERR_INVALID_ARG;
2385
+
2386
+ static const struct {
2387
+ uint32_t bit;
2388
+ const char* name;
2389
+ } entries[] = {
2390
+ {DCG_NODE_FLAG_FROZEN, "frozen"},
2391
+ {DCG_NODE_FLAG_VISITED, "visited"},
2392
+ {DCG_NODE_FLAG_PRUNED, "pruned"},
2393
+ };
2394
+
2395
+ dcg_strbuf buf;
2396
+ c_dcg_sb_init(&buf, out, cap);
2397
+
2398
+ size_t matched = 0;
2399
+ for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
2400
+ if (!(flags & entries[i].bit)) continue;
2401
+ c_dcg_sb_printf(&buf, "%s%s", matched ? "|" : "", entries[i].name);
2402
+ matched++;
2403
+ }
2404
+ if (matched == 0) c_dcg_sb_puts(&buf, "-");
2405
+
2406
+ return buf.used >= buf.cap ? DCG_ERR_FULL : (int) buf.used;
2407
+ }
2408
+
2409
+ /**
2410
+ * @brief Render a node's uid as a 32-character hex string (no dashes).
2411
+ *
2412
+ * Byte order is the array's own: the first byte of the field leads, which is
2413
+ * what makes the string a straight hex dump of the 16 bytes `uuid.UUID.bytes`
2414
+ * hands back.
2415
+ *
2416
+ * @param uid Identity to render (16 bytes).
2417
+ * @param out Destination buffer (at least 33 bytes, terminator included).
2418
+ * @param cap Capacity of out.
2419
+ * @return Number of characters written, or DCG_ERR_*.
2420
+ */
2421
+ static inline int c_dcg_node_format_uid(const uuid_t uid, char* out, size_t cap) {
2422
+ if (!uid || !out) return DCG_ERR_INVALID_ARG;
2423
+ if (cap < sizeof(uuid_t) * 2 + 1) return DCG_ERR_FULL;
2424
+
2425
+ static const char hex[] = "0123456789abcdef";
2426
+ for (size_t i = 0; i < sizeof(uuid_t); i++) {
2427
+ out[i * 2] = hex[(uid[i] >> 4) & 0x0F];
2428
+ out[i * 2 + 1] = hex[uid[i] & 0x0F];
2429
+ }
2430
+ out[sizeof(uuid_t) * 2] = '\0';
2431
+
2432
+ return (int) (sizeof(uuid_t) * 2);
2433
+ }
2434
+
2435
+ /**
2436
+ * @brief Render one node's own line (without the tree prefix).
2437
+ *
2438
+ * @param node Node to render.
2439
+ * @param out Destination buffer.
2440
+ * @param cap Capacity of out.
2441
+ * @param opts Renderer options (NULL = defaults).
2442
+ * @return Number of characters written, or DCG_ERR_*.
2443
+ */
2444
+ static inline int c_dcg_node_format_line(const dcg_node* node, char* out, size_t cap, const dcg_render_opts* opts) {
2445
+ if (!node || !out || cap == 0) return DCG_ERR_INVALID_ARG;
2446
+
2447
+ dcg_render_opts dflt;
2448
+ if (!opts) {
2449
+ c_dcg_render_opts_default(&dflt);
2450
+ opts = &dflt;
2451
+ }
2452
+
2453
+ dcg_strbuf buf;
2454
+ c_dcg_sb_init(&buf, out, cap);
2455
+
2456
+ char scratch[DCG_NODE_STRING_MAXLEN];
2457
+
2458
+ if (opts->show_condition) {
2459
+ (void) c_dcg_condition_format(node->condition_to_parent, "", scratch, sizeof(scratch));
2460
+ c_dcg_sb_printf(&buf, "[%s] ", scratch);
2461
+ }
2462
+
2463
+ /* The operator itself lives in the expression variant, which this header
2464
+ * does not see; an expression's repr carries its symbol (see c_expr.h). */
2465
+ c_dcg_sb_puts(&buf, c_dcg_node_type_name(node->ntype));
2466
+
2467
+ if (node->repr) c_dcg_sb_printf(&buf, " \"%s\"", node->repr);
2468
+
2469
+ if (opts->show_out) {
2470
+ (void) c_dcg_var_format(&node->out, scratch, sizeof(scratch));
2471
+ c_dcg_sb_printf(&buf, " out=%s", scratch);
2472
+ }
2473
+
2474
+ if (opts->show_children) c_dcg_sb_printf(&buf, " children=%zu", c_dcg_node_child_count(node));
2475
+
2476
+ if (opts->show_hooks) {
2477
+ c_dcg_sb_puts(&buf, " hooks=[");
2478
+ if (node->eval_ctx.pre_eval_fn) c_dcg_sb_puts(&buf, "pre");
2479
+ if (node->eval_ctx.eval_fn) c_dcg_sb_printf(&buf, "%seval", node->eval_ctx.pre_eval_fn ? "," : "");
2480
+ if (node->eval_ctx.post_eval_fn) c_dcg_sb_printf(&buf, "%spost", (node->eval_ctx.pre_eval_fn || node->eval_ctx.eval_fn) ? "," : "");
2481
+ c_dcg_sb_puts(&buf, "]");
2482
+ }
2483
+
2484
+ if (opts->show_labels && node->labels) {
2485
+ c_dcg_sb_puts(&buf, " labels=[");
2486
+ size_t index = 0;
2487
+ for (const dcg_node_label* entry = node->labels; entry; entry = entry->next, index++) {
2488
+ c_dcg_sb_printf(&buf, "%s%s", index ? "," : "", entry->label ? entry->label : "?");
2489
+ }
2490
+ c_dcg_sb_puts(&buf, "]");
2491
+ }
2492
+
2493
+ if (opts->show_flags && node->flags) {
2494
+ (void) c_dcg_node_format_flags(node->flags, scratch, sizeof(scratch));
2495
+ c_dcg_sb_printf(&buf, " flags=%s", scratch);
2496
+ }
2497
+
2498
+ if (opts->show_uid) {
2499
+ (void) c_dcg_node_format_uid(node->uid, scratch, sizeof(scratch));
2500
+ c_dcg_sb_printf(&buf, " uid=%s", scratch);
2501
+ }
2502
+
2503
+ if (opts->show_address) c_dcg_sb_printf(&buf, " @%p", (const void*) node);
2504
+
2505
+ return buf.used >= buf.cap ? DCG_ERR_FULL : (int) buf.used;
2506
+ }
2507
+
2508
+ /**
2509
+ * @brief Recursive worker of c_dcg_node_render.
2510
+ *
2511
+ * @param node Node to render.
2512
+ * @param stream Destination stream.
2513
+ * @param opts Renderer options.
2514
+ * @param prefix Prefix drawn to the left of this node's line.
2515
+ * @param is_last true when the node is the last child of its parent.
2516
+ * @param depth Depth of the node.
2517
+ * @return Number of characters written, or DCG_ERR_*.
2518
+ */
2519
+ static inline int c_dcg_node_render_walk(const dcg_node* node, FILE* stream, const dcg_render_opts* opts, const char* prefix, bool is_last, size_t depth) {
2520
+ if (!node) return 0;
2521
+
2522
+ char line[DCG_NODE_STRING_MAXLEN * 2];
2523
+ int n = c_dcg_node_format_line(node, line, sizeof(line), opts);
2524
+ if (n < 0) return n;
2525
+
2526
+ bool unicode = (opts->style == DCG_RENDER_UNICODE);
2527
+ const char* guide = "";
2528
+ /* The unicode guides are spelled as UTF-8 bytes - U+2514 / U+251C elbow and
2529
+ * tee, U+2500 rule - so the source stays ASCII and a compiler reading it
2530
+ * under any code page produces the same bytes. */
2531
+ if (depth > 0) guide = is_last ? (unicode ? "\xE2\x94\x94\xE2\x94\x80\xE2\x94\x80 " : "`-- ") : (unicode ? "\xE2\x94\x9C\xE2\x94\x80\xE2\x94\x80 " : "|-- ");
2532
+
2533
+ int written = fprintf(stream, "%s%s%s\n", prefix, guide, line);
2534
+ if (written < 0) return DCG_ERR_FORMAT;
2535
+
2536
+ char child_prefix[DCG_NODE_STRING_MAXLEN];
2537
+ dcg_strbuf buf;
2538
+ c_dcg_sb_init(&buf, child_prefix, sizeof(child_prefix));
2539
+ c_dcg_sb_puts(&buf, prefix);
2540
+ if (depth > 0) c_dcg_sb_puts(&buf, is_last ? " " : (unicode ? "\xE2\x94\x82 " : "| "));
2541
+
2542
+ if (opts->max_depth && depth + 1 > opts->max_depth) {
2543
+ if (node->children) {
2544
+ n = fprintf(stream, "%s... (%zu children hidden)\n", child_prefix, c_dcg_node_child_count(node));
2545
+ if (n < 0) return DCG_ERR_FORMAT;
2546
+ written += n;
2547
+ }
2548
+ return written;
2549
+ }
2550
+
2551
+ size_t remaining = c_dcg_node_child_count(node);
2552
+ for (dcg_node* child = node->children; child; child = child->next_sibling) {
2553
+ remaining--;
2554
+ n = c_dcg_node_render_walk(child, stream, opts, child_prefix, remaining == 0, depth + 1);
2555
+ if (n < 0) return n;
2556
+ written += n;
2557
+ }
2558
+ return written;
2559
+ }
2560
+
2561
+ /**
2562
+ * @brief Render a node and its subtree to a stream, one node per line.
2563
+ *
2564
+ * The root line carries no branch glyph; every descendant is prefixed with
2565
+ * the usual box-drawing (or ASCII) guide.
2566
+ *
2567
+ * @param node Node to render (typically the graph root).
2568
+ * @param stream Destination stream.
2569
+ * @param opts Renderer options (NULL = defaults).
2570
+ * @return Number of characters written, or DCG_ERR_INVALID_ARG.
2571
+ */
2572
+ static inline int c_dcg_node_render(const dcg_node* node, FILE* stream, const dcg_render_opts* opts) {
2573
+ if (!node || !stream) return DCG_ERR_INVALID_ARG;
2574
+
2575
+ dcg_render_opts dflt;
2576
+ if (!opts) {
2577
+ c_dcg_render_opts_default(&dflt);
2578
+ opts = &dflt;
2579
+ }
2580
+ return c_dcg_node_render_walk(node, stream, opts, "", true, 0);
2581
+ }
2582
+
2583
+ /**
2584
+ * @brief Render a node and its subtree to stdout with the default options.
2585
+ *
2586
+ * @param node Node to render (NULL-safe).
2587
+ * @return Number of characters written, or DCG_ERR_INVALID_ARG.
2588
+ */
2589
+ static inline int c_dcg_node_print(const dcg_node* node) {
2590
+ if (!node) return DCG_ERR_INVALID_ARG;
2591
+ return c_dcg_node_render(node, stdout, NULL);
2592
+ }
2593
+
2594
+ /**
2595
+ * @brief Render a node and its subtree into a caller-provided buffer.
2596
+ *
2597
+ * Rendering goes through a fixed-size stream, so the buffer must be able to
2598
+ * hold the whole subtree: a buffer that runs out is reported as
2599
+ * DCG_ERR_FULL rather than a half-written tree, and the buffer is still
2600
+ * NUL-terminated.
2601
+ *
2602
+ * @param node Node to render.
2603
+ * @param out Destination buffer.
2604
+ * @param cap Capacity of out.
2605
+ * @param opts Renderer options (NULL = defaults).
2606
+ * @return Number of characters written (excluding NUL), or DCG_ERR_INVALID_ARG
2607
+ * / DCG_ERR_FULL / DCG_ERR_FORMAT.
2608
+ */
2609
+ static inline int c_dcg_node_render_to_string(const dcg_node* node, char* out, size_t cap, const dcg_render_opts* opts) {
2610
+ if (!node || !out || cap == 0) return DCG_ERR_INVALID_ARG;
2611
+
2612
+ #if defined(_WIN32)
2613
+ /* fmemopen has no Windows counterpart, so the render goes through a
2614
+ * temporary file instead: the same stream renderer, into a sink that is
2615
+ * read back afterwards. tmpfile() opens in binary update mode, so ftell
2616
+ * counts the bytes written without a text-mode translation in the way,
2617
+ * and the caller's capacity is enforced once the render is done - a tree
2618
+ * that does not fit is reported as FULL, exactly as the fmemopen path
2619
+ * reports it, and never as a silent truncation. */
2620
+ FILE* stream = tmpfile();
2621
+ if (!stream) return DCG_ERR_FORMAT;
2622
+
2623
+ int written = c_dcg_node_render(node, stream, opts);
2624
+ (void) fflush(stream);
2625
+ long end = ftell(stream);
2626
+
2627
+ out[cap - 1] = '\0';
2628
+ if (written < 0 || end < 0 || (size_t) end >= cap) {
2629
+ (void) fclose(stream);
2630
+ return DCG_ERR_FULL;
2631
+ }
2632
+
2633
+ (void) fseek(stream, 0, SEEK_SET);
2634
+ size_t got = fread(out, 1, (size_t) end, stream);
2635
+ (void) fclose(stream);
2636
+ if (got != (size_t) end) return DCG_ERR_FORMAT;
2637
+
2638
+ out[end] = '\0';
2639
+ return written;
2640
+ #else
2641
+ FILE* stream = fmemopen(out, cap, "w");
2642
+ if (!stream) return DCG_ERR_FORMAT;
2643
+
2644
+ int written = c_dcg_node_render(node, stream, opts);
2645
+ long end = ftell(stream);
2646
+ (void) fflush(stream);
2647
+ (void) fclose(stream);
2648
+
2649
+ // Inside a fixed-size stream every failure is a buffer overflow: the
2650
+ // line formatter reports FULL, while a full buffer makes fprintf fail
2651
+ // outright. Both mean "the tree did not fit".
2652
+ out[cap - 1] = '\0';
2653
+ if (written < 0) return DCG_ERR_FULL;
2654
+ if ((end < 0) || ((size_t) end >= cap)) return DCG_ERR_FULL;
2655
+
2656
+ return written;
2657
+ #endif
2658
+ }
2659
+
2660
+ #endif // C_DCG_BAKE_NODE_H