daedalus_planner/
lib.rs

1//! Planner passes and execution plan model scaffolding. See `PLAN.md` for staged tasks.
2//! Exposes a deterministic pass pipeline from registry-sourced graphs to an `ExecutionPlan`.
3//!
4//! Pass order (stubs today, contract documented):
5//! hydrate_registry -> typecheck -> convert -> align -> gpu -> schedule -> lint.
6
7pub mod debug;
8mod diagnostics;
9mod graph;
10pub mod helpers;
11mod passes;
12
13pub use diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSpan};
14pub use graph::{
15    ComputeAffinity, DEFAULT_PLAN_VERSION, Edge, EdgeBufferInfo, ExecutionPlan, GpuSegment, Graph,
16    NodeInstance, NodeRef, PortRef, StableHash,
17};
18pub use passes::{PlannerConfig, PlannerInput, PlannerOutput, build_plan};
19
20#[cfg(test)]
21mod tests {
22    use super::*;
23    use daedalus_data::model::{TypeExpr, ValueType};
24    use daedalus_registry::store::NodeDescriptorBuilder;
25    use daedalus_registry::store::Registry;
26
27    #[test]
28    fn stable_hash_changes_with_edges() {
29        let mut graph = Graph::default();
30        graph.nodes.push(NodeInstance {
31            id: daedalus_registry::ids::NodeId::new("n1"),
32            bundle: None,
33            label: None,
34            inputs: vec![],
35            outputs: vec![],
36            compute: ComputeAffinity::CpuOnly,
37            const_inputs: vec![],
38            sync_groups: vec![],
39            metadata: Default::default(),
40        });
41        graph.nodes.push(NodeInstance {
42            id: daedalus_registry::ids::NodeId::new("n2"),
43            bundle: None,
44            label: None,
45            inputs: vec![],
46            outputs: vec![],
47            compute: ComputeAffinity::CpuOnly,
48            const_inputs: vec![],
49            sync_groups: vec![],
50            metadata: Default::default(),
51        });
52        let g1 = graph.clone();
53        let p1 = build_plan(
54            PlannerInput {
55                graph: g1,
56                registry: &Registry::new(),
57            },
58            PlannerConfig::default(),
59        )
60        .plan;
61
62        graph.edges.push(Edge {
63            from: PortRef {
64                node: NodeRef(0),
65                port: "out".into(),
66            },
67            to: PortRef {
68                node: NodeRef(1),
69                port: "in".into(),
70            },
71            metadata: Default::default(),
72        });
73        let p2 = build_plan(
74            PlannerInput {
75                graph,
76                registry: &Registry::new(),
77            },
78            PlannerConfig::default(),
79        )
80        .plan;
81
82        assert_ne!(p1.hash, p2.hash);
83    }
84
85    #[test]
86    fn reports_missing_node_and_ports_and_converter_gap() {
87        // Registry with node a (out:int) and b (in:bool)
88        let mut registry = Registry::new();
89        let a = NodeDescriptorBuilder::new("a")
90            .output("out", TypeExpr::Scalar(ValueType::Int))
91            .build()
92            .unwrap();
93        registry.register_node(a).unwrap();
94        let b = NodeDescriptorBuilder::new("b")
95            .input("in", TypeExpr::Scalar(ValueType::Bool))
96            .build()
97            .unwrap();
98        registry.register_node(b).unwrap();
99
100        let mut graph = Graph::default();
101        graph.nodes.push(NodeInstance {
102            id: daedalus_registry::ids::NodeId::new("a"),
103            bundle: None,
104            label: None,
105            inputs: vec![],
106            outputs: vec![],
107            compute: ComputeAffinity::CpuOnly,
108            const_inputs: vec![],
109            sync_groups: vec![],
110            metadata: Default::default(),
111        });
112        graph.nodes.push(NodeInstance {
113            id: daedalus_registry::ids::NodeId::new("b"),
114            bundle: None,
115            label: None,
116            inputs: vec![],
117            outputs: vec![],
118            compute: ComputeAffinity::CpuOnly,
119            const_inputs: vec![],
120            sync_groups: vec![],
121            metadata: Default::default(),
122        });
123        // Edge uses wrong output port name to trigger port missing + type mismatch
124        graph.edges.push(Edge {
125            from: PortRef {
126                node: NodeRef(0),
127                port: "missing".into(),
128            },
129            to: PortRef {
130                node: NodeRef(1),
131                port: "in".into(),
132            },
133            metadata: Default::default(),
134        });
135
136        let out = build_plan(
137            PlannerInput {
138                graph,
139                registry: &registry,
140            },
141            PlannerConfig::default(),
142        );
143
144        // Expect port-missing on source, no type mismatch because missing source type.
145        assert!(
146            out.diagnostics
147                .iter()
148                .any(|d| matches!(d.code, DiagnosticCode::PortMissing)
149                    && d.span.node.as_deref() == Some("a"))
150        );
151
152        // Now add a correct port but wrong type to trigger converter resolution.
153        let mut registry2 = Registry::new();
154        let a2 = NodeDescriptorBuilder::new("a")
155            .output("out", TypeExpr::Scalar(ValueType::Int))
156            .build()
157            .unwrap();
158        registry2.register_node(a2).unwrap();
159        let b2 = NodeDescriptorBuilder::new("b")
160            .input("in", TypeExpr::Scalar(ValueType::Bool))
161            .build()
162            .unwrap();
163        registry2.register_node(b2).unwrap();
164        let mut graph2 = Graph::default();
165        graph2.nodes.push(NodeInstance {
166            id: daedalus_registry::ids::NodeId::new("a"),
167            bundle: None,
168            label: None,
169            inputs: vec![],
170            outputs: vec![],
171            compute: ComputeAffinity::CpuOnly,
172            const_inputs: vec![],
173            sync_groups: vec![],
174            metadata: Default::default(),
175        });
176        graph2.nodes.push(NodeInstance {
177            id: daedalus_registry::ids::NodeId::new("b"),
178            bundle: None,
179            label: None,
180            inputs: vec![],
181            outputs: vec![],
182            compute: ComputeAffinity::CpuOnly,
183            const_inputs: vec![],
184            sync_groups: vec![],
185            metadata: Default::default(),
186        });
187        graph2.edges.push(Edge {
188            from: PortRef {
189                node: NodeRef(0),
190                port: "out".into(),
191            },
192            to: PortRef {
193                node: NodeRef(1),
194                port: "in".into(),
195            },
196            metadata: Default::default(),
197        });
198
199        let out2 = build_plan(
200            PlannerInput {
201                graph: graph2,
202                registry: &registry2,
203            },
204            PlannerConfig::default(),
205        );
206
207        assert!(
208            out2.diagnostics
209                .iter()
210                .any(|d| matches!(d.code, DiagnosticCode::ConverterMissing))
211        );
212
213        // Register a converter to remove the gap.
214        struct IntToBool;
215        impl daedalus_data::convert::Converter for IntToBool {
216            fn id(&self) -> daedalus_data::convert::ConverterId {
217                daedalus_data::convert::ConverterId("int_to_bool".into())
218            }
219            fn input(&self) -> &TypeExpr {
220                static TY: once_cell::sync::Lazy<TypeExpr> =
221                    once_cell::sync::Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
222                &TY
223            }
224            fn output(&self) -> &TypeExpr {
225                static TY: once_cell::sync::Lazy<TypeExpr> =
226                    once_cell::sync::Lazy::new(|| TypeExpr::Scalar(ValueType::Bool));
227                &TY
228            }
229            fn convert(
230                &self,
231                _value: daedalus_data::model::Value,
232            ) -> Result<daedalus_data::model::Value, daedalus_data::errors::DataError> {
233                Ok(daedalus_data::model::Value::Bool(true))
234            }
235            fn cost(&self) -> u64 {
236                1
237            }
238        }
239
240        let mut registry3 = Registry::new();
241        let a3 = NodeDescriptorBuilder::new("a")
242            .output("out", TypeExpr::Scalar(ValueType::Int))
243            .build()
244            .unwrap();
245        registry3.register_node(a3).unwrap();
246        let b3 = NodeDescriptorBuilder::new("b")
247            .input("in", TypeExpr::Scalar(ValueType::Bool))
248            .build()
249            .unwrap();
250        registry3.register_node(b3).unwrap();
251        registry3
252            .register_converter(Box::new(IntToBool))
253            .expect("converter registers");
254
255        let mut graph3 = Graph::default();
256        graph3.nodes.push(NodeInstance {
257            id: daedalus_registry::ids::NodeId::new("a"),
258            bundle: None,
259            label: None,
260            inputs: vec![],
261            outputs: vec![],
262            compute: ComputeAffinity::CpuOnly,
263            const_inputs: vec![],
264            sync_groups: vec![],
265            metadata: Default::default(),
266        });
267        graph3.nodes.push(NodeInstance {
268            id: daedalus_registry::ids::NodeId::new("b"),
269            bundle: None,
270            label: None,
271            inputs: vec![],
272            outputs: vec![],
273            compute: ComputeAffinity::CpuOnly,
274            const_inputs: vec![],
275            sync_groups: vec![],
276            metadata: Default::default(),
277        });
278        graph3.edges.push(Edge {
279            from: PortRef {
280                node: NodeRef(0),
281                port: "out".into(),
282            },
283            to: PortRef {
284                node: NodeRef(1),
285                port: "in".into(),
286            },
287            metadata: Default::default(),
288        });
289
290        let out3 = build_plan(
291            PlannerInput {
292                graph: graph3,
293                registry: &registry3,
294            },
295            PlannerConfig::default(),
296        );
297
298        assert!(
299            !out3
300                .diagnostics
301                .iter()
302                .any(|d| matches!(d.code, DiagnosticCode::ConverterMissing))
303        );
304    }
305
306    #[test]
307    fn detects_cycle_in_align() {
308        let mut registry = Registry::new();
309        let node_desc = NodeDescriptorBuilder::new("n")
310            .input("in", TypeExpr::Scalar(ValueType::Int))
311            .output("out", TypeExpr::Scalar(ValueType::Int))
312            .build()
313            .unwrap();
314        registry.register_node(node_desc).unwrap();
315
316        let mut graph = Graph::default();
317        graph.nodes.push(NodeInstance {
318            id: daedalus_registry::ids::NodeId::new("n"),
319            bundle: None,
320            label: None,
321            inputs: vec![],
322            outputs: vec![],
323            compute: ComputeAffinity::CpuOnly,
324            const_inputs: vec![],
325            sync_groups: vec![],
326            metadata: Default::default(),
327        });
328        graph.nodes.push(NodeInstance {
329            id: daedalus_registry::ids::NodeId::new("n"),
330            bundle: None,
331            label: None,
332            inputs: vec![],
333            outputs: vec![],
334            compute: ComputeAffinity::CpuOnly,
335            const_inputs: vec![],
336            sync_groups: vec![],
337            metadata: Default::default(),
338        });
339
340        // Cycle: 0 -> 1 -> 0
341        graph.edges.push(Edge {
342            from: PortRef {
343                node: NodeRef(0),
344                port: "out".into(),
345            },
346            to: PortRef {
347                node: NodeRef(1),
348                port: "in".into(),
349            },
350            metadata: Default::default(),
351        });
352        graph.edges.push(Edge {
353            from: PortRef {
354                node: NodeRef(1),
355                port: "out".into(),
356            },
357            to: PortRef {
358                node: NodeRef(0),
359                port: "in".into(),
360            },
361            metadata: Default::default(),
362        });
363
364        let out = build_plan(
365            PlannerInput {
366                graph,
367                registry: &registry,
368            },
369            PlannerConfig {
370                enable_lints: true,
371                ..Default::default()
372            },
373        );
374
375        assert!(
376            out.diagnostics
377                .iter()
378                .any(|d| matches!(d.code, DiagnosticCode::ScheduleConflict))
379        );
380    }
381
382    #[test]
383    fn gpu_required_without_flag_reports() {
384        let mut registry = Registry::new();
385        let node_desc = NodeDescriptorBuilder::new("n")
386            .input("in", TypeExpr::Scalar(ValueType::Int))
387            .output("out", TypeExpr::Scalar(ValueType::Int))
388            .build()
389            .unwrap();
390        registry.register_node(node_desc).unwrap();
391
392        let mut graph = Graph::default();
393        graph.nodes.push(NodeInstance {
394            id: daedalus_registry::ids::NodeId::new("n"),
395            bundle: None,
396            label: None,
397            inputs: vec![],
398            outputs: vec![],
399            compute: ComputeAffinity::GpuRequired,
400            const_inputs: vec![],
401            sync_groups: vec![],
402            metadata: Default::default(),
403        });
404
405        let out = build_plan(
406            PlannerInput {
407                graph,
408                registry: &registry,
409            },
410            PlannerConfig {
411                enable_gpu: false,
412                ..Default::default()
413            },
414        );
415
416        assert!(
417            out.diagnostics
418                .iter()
419                .any(|d| matches!(d.code, DiagnosticCode::GpuUnsupported))
420        );
421    }
422}