1use std::fmt::Write;
7
8use crate::actions::node_name;
9use crate::hash;
10use crate::ir::Graph;
11use crate::types::*;
12
13pub fn render_dsl(graph: &Graph) -> String {
15 let mut out = String::new();
16
17 writeln!(out, "# DirtyData Surface DSL — revision {}", graph.revision.0).unwrap();
19 writeln!(out, "# Hash: blake3:{}", hex_short(&hash::hash_graph(graph))).unwrap();
20 writeln!(out, "# Patches: {}", graph.lineage.applied_patches.len()).unwrap();
21 writeln!(out).unwrap();
22
23 let name_of = |id: &StableId| -> String {
25 graph.topology.nodes.get(id).map(node_name).unwrap_or_else(|| id.to_string())
26 };
27
28 for node in graph.topology.nodes.values() {
30 let kind_str = match &node.kind {
31 NodeKind::Source => "source",
32 NodeKind::Processor => "processor",
33 NodeKind::Analyzer => "analyzer",
34 NodeKind::Sink => "sink",
35 NodeKind::Junction => "junction",
36 NodeKind::Foreign(name) => {
37 writeln!(out, "foreign \"{}\" \"{}\" {{", node_name(node), name).unwrap();
38 render_node_body(&mut out, node);
39 writeln!(out, "}}").unwrap();
40 writeln!(out).unwrap();
41 continue;
42 }
43 NodeKind::Intent => "intent",
44 NodeKind::Metadata => "metadata",
45 NodeKind::Boundary => "boundary",
46 NodeKind::SubGraph => "subgraph",
47 NodeKind::InputProxy => "input_proxy",
48 NodeKind::OutputProxy => "output_proxy",
49 NodeKind::CircuitModule { .. } => "circuit_module",
50 };
51
52 writeln!(out, "{} \"{}\" {{", kind_str, node_name(node)).unwrap();
53 render_node_body(&mut out, node);
54 writeln!(out, "}}").unwrap();
55 writeln!(out).unwrap();
56 }
57
58 if !graph.topology.edges.is_empty() {
60 writeln!(out, "# Connections").unwrap();
61 for edge in graph.topology.edges.values() {
62 let src_name = name_of(&edge.source.node_id);
63 let tgt_name = name_of(&edge.target.node_id);
64
65 let kind_tag = "# normal";
66
67 writeln!(
68 out,
69 "{}.{} -> {}.{} {}",
70 src_name, edge.source.port_name,
71 tgt_name, edge.target.port_name,
72 kind_tag
73 )
74 .unwrap();
75 }
76 }
77
78 out
79}
80
81fn render_node_body(out: &mut String, node: &crate::ir::Node) {
82 for port in &node.ports {
84 let dir = match port.direction {
85 PortDirection::Input => "in",
86 PortDirection::Output => "out",
87 };
88 let domain = match port.domain {
89 ExecutionDomain::Sample => "@sample",
90 ExecutionDomain::Block => "@block",
91 ExecutionDomain::Timeline => "@timeline",
92 ExecutionDomain::Background => "@background",
93 };
94 let dtype = format_data_type(&port.data_type);
95 if port.name == dir {
96 writeln!(out, " {}: {} {}", dir, dtype, domain).unwrap();
97 } else {
98 writeln!(out, " {} \"{}\": {} {}", dir, port.name, dtype, domain).unwrap();
99 }
100 }
101
102 let config_entries: Vec<_> = node
104 .config
105 .iter()
106 .filter(|(k, _)| k.as_str() != "name")
107 .collect();
108
109 if !config_entries.is_empty() {
110 writeln!(out, " config {{").unwrap();
111 for (key, value) in config_entries {
112 writeln!(out, " {}: {}", key, format_config_value(value)).unwrap();
113 }
114 writeln!(out, " }}").unwrap();
115 }
116}
117
118fn format_data_type(dt: &DataType) -> String {
119 match dt {
120 DataType::Audio { channels } => format!("audio({}ch)", channels),
121 DataType::Control => "control".into(),
122 DataType::Midi => "midi".into(),
123 DataType::Spectral { bins } => format!("spectral({})", bins),
124 DataType::Blob => "blob".into(),
125 DataType::Meta => "meta".into(),
126 }
127}
128
129fn format_config_value(v: &ConfigValue) -> String {
130 match v {
131 ConfigValue::Float(f) => format!("{}", f),
132 ConfigValue::Int(i) => format!("{}", i),
133 ConfigValue::Bool(b) => format!("{}", b),
134 ConfigValue::String(s) => format!("\"{}\"", s),
135 ConfigValue::List(items) => {
136 let inner: Vec<_> = items.iter().map(format_config_value).collect();
137 format!("[{}]", inner.join(", "))
138 }
139 ConfigValue::Map(map) => {
140 let inner: Vec<_> = map
141 .iter()
142 .map(|(k, v)| format!("{}: {}", k, format_config_value(v)))
143 .collect();
144 format!("{{{}}}", inner.join(", "))
145 }
146 }
147}
148
149fn hex_short(bytes: &[u8]) -> String {
150 bytes[..8].iter().map(|b| format!("{:02x}", b)).collect()
151}