1use std::fmt::Write;
4
5use super::ActionDag;
6use crate::{Action, BlockGraphError};
7
8impl ActionDag {
9 pub fn to_svg(&self) -> Result<String, BlockGraphError> {
32 self.validate_semantics()?;
33 let nodes = self.ordered_nodes().collect::<Vec<_>>();
34 let labels = nodes
35 .iter()
36 .map(|node| {
37 let label = match &node.action {
38 Action::Branch { target, condition } => self
39 .branch_region(*target)
40 .map(|region| format!("resolve {} if {condition}", region.name))
41 .unwrap_or_else(|| node.action.to_string()),
42 _ => node.action.to_string(),
43 };
44 match &node.owner {
45 Some(owner) => format!(
46 "[{} @ {}] {label}",
47 owner.definition,
48 if owner.instance_path.is_empty() {
49 "root"
50 } else {
51 &owner.instance_path
52 }
53 ),
54 None => label,
55 }
56 })
57 .collect::<Vec<_>>();
58 let lines = labels
59 .iter()
60 .map(|label| {
61 label
62 .chars()
63 .collect::<Vec<_>>()
64 .chunks(42)
65 .map(|chunk| chunk.iter().collect::<String>())
66 .collect::<Vec<_>>()
67 })
68 .collect::<Vec<_>>();
69 let sizes = nodes
70 .iter()
71 .zip(&lines)
72 .map(|(node, lines)| {
73 (
74 node.ordinal as u32,
75 (340.0, 38.0 + lines.len() as f32 * 17.0),
76 )
77 })
78 .collect::<Vec<_>>();
79 let mut edges = self.dependencies().collect::<Vec<_>>();
80 edges.sort_by_key(|&(from, to, reason)| (from, to, reason as u8));
81 let pairs = edges
82 .iter()
83 .map(|&(from, to, _)| (from as u32, to as u32))
84 .collect::<Vec<_>>();
85 let placed = bloq_utils::graph_layout::compute_relative(&sizes, &pairs, 48.0);
86 let width = placed.content.0.max(460.0) + 48.0;
87 let height = placed.content.1.max(40.0) + 96.0;
88 let status = if self.is_analyzed() {
89 "Analyzed"
90 } else {
91 "Source"
92 };
93 let mut svg = format!(
94 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" viewBox=\"0 0 {width} {height}\" role=\"img\" aria-label=\"{status} action dependency graph\"><title>{status} action DAG</title><style>text{{font:12px monospace;fill:#202536}}rect{{stroke-width:1.5}}.measure{{fill:#e6edff;stroke:#4a65b8}}.binding{{fill:#e8f6ed;stroke:#4b8c67}}.resolve{{fill:#fff1df;stroke:#b66b21}}.feedback{{fill:#f3e9fc;stroke:#8859af}}.discard{{fill:#fde8e8;stroke:#bc5151}}</style><defs><marker id=\"arrow\" markerWidth=\"7\" markerHeight=\"7\" refX=\"6\" refY=\"3\" orient=\"auto\"><path d=\"M0 0 L6 3 L0 6 Z\" fill=\"#758198\"/></marker></defs><rect width=\"100%\" height=\"100%\" fill=\"white\"/><g transform=\"translate(24 48)\">"
95 );
96 for (from, to, reason) in edges {
97 let (x, y) = placed.positions[&(from as u32)];
98 let (tx, ty) = placed.positions[&(to as u32)];
99 let sx = x + sizes[from].1.0;
100 let sy = y + sizes[from].1.1 / 2.0;
101 let ey = ty + sizes[to].1.1 / 2.0;
102 let middle = sx.midpoint(tx);
103 let dash = if reason.is_implicit() {
104 " stroke-dasharray=\"5 4\""
105 } else {
106 ""
107 };
108 write!(svg, "<path data-from=\"{from}\" data-to=\"{to}\" data-dependency=\"{reason:?}\" d=\"M{sx} {sy} C{middle} {sy} {middle} {ey} {tx} {ey}\" fill=\"none\" stroke=\"#758198\" stroke-width=\"1.5\"{dash} marker-end=\"url(#arrow)\"><title>{reason:?}</title></path>").expect("write SVG string");
109 }
110 for ((node, label), lines) in nodes.iter().zip(&labels).zip(&lines) {
111 let id = node.ordinal;
112 let (x, y) = placed.positions[&(id as u32)];
113 let (w, h) = sizes[id].1;
114 let kind = match node.action {
115 Action::Measure { .. } => "measure",
116 Action::Let { .. } => "binding",
117 Action::Resolve { .. } | Action::Branch { .. } => "resolve",
118 Action::Feedback { .. } => "feedback",
119 Action::DiscardIf(_) => "discard",
120 };
121 let owner = node
122 .owner
123 .as_ref()
124 .map(|owner| {
125 format!(
126 " data-definition=\"{}\" data-instance=\"{}\"",
127 escape(&owner.definition),
128 escape(&owner.instance_path)
129 )
130 })
131 .unwrap_or_default();
132 write!(svg, "<g data-node=\"{id}\"{owner}><title>{}</title><rect class=\"{kind}\" x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" rx=\"7\"/><text x=\"{}\" y=\"{}\">A{id}</text>", escape(label), x + 12.0, y + 20.0).expect("write SVG string");
133 for (line, text) in lines.iter().enumerate() {
134 write!(
135 svg,
136 "<text x=\"{}\" y=\"{}\">{}</text>",
137 x + 12.0,
138 y + 38.0 + line as f32 * 17.0,
139 escape(text)
140 )
141 .expect("write SVG string");
142 }
143 svg.push_str("</g>");
144 }
145 if nodes.is_empty() {
146 svg.push_str("<text y=\"20\">No actions</text>");
147 }
148 svg.push_str("</g><text x=\"24\" y=\"24\">");
149 write!(svg, "{status} action DAG · {} actions</text><text x=\"24\" y=\"{}\">Solid: variable dependencies · Dashed: inferred dependencies</text></svg>", nodes.len(), height - 16.0).expect("write SVG string");
150 Ok(svg)
151 }
152}
153
154fn escape(text: &str) -> String {
155 text.replace('&', "&")
156 .replace('<', "<")
157 .replace('>', ">")
158 .replace('"', """)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::ActionDependency;
165 use crate::{Expr, GalleryItem, MeasureTarget};
166
167 #[test]
168 fn svg_keeps_isolated_actions_dependency_reasons_and_full_labels() {
169 let mut dag = ActionDag::from_actions(&[
170 Action::Measure {
171 target: MeasureTarget::Node(glam::IVec3::ZERO),
172 name: "m".into(),
173 },
174 Action::Let {
175 name: "both".into(),
176 expr: Expr::Binary(
177 crate::BinaryOp::And,
178 Box::new(Expr::Var("m".into())),
179 Box::new(Expr::Var("m".into())),
180 ),
181 },
182 Action::Feedback {
183 targets: vec![crate::FeedbackTarget {
184 pauli: crate::PauliBasis::Z,
185 target: glam::IVec3::ZERO,
186 direction: None,
187 }],
188 condition: None,
189 },
190 ]);
191 dag.add_dependency(
192 dag.ordered[0],
193 dag.ordered[1],
194 ActionDependency::ReadoutParity,
195 );
196 let svg = dag.to_svg().unwrap();
197 assert_eq!(svg.matches("data-node=").count(), 3);
198 assert!(svg.contains("both = m & m"));
199 assert!(svg.contains("data-dependency=\"Classical\""));
200 assert!(svg.contains("data-dependency=\"ReadoutParity\""));
201 assert!(svg.contains("stroke-dasharray"));
202 assert_eq!(svg, dag.to_svg().unwrap());
203 assert!(
204 ActionDag::default()
205 .to_svg()
206 .unwrap()
207 .contains("No actions")
208 );
209 }
210
211 #[test]
212 fn svg_labels_analyzed_graph_and_rejects_cycles() {
213 let flat = GalleryItem::T.build().flatten().unwrap();
214 let (analyzed, _) = flat.analyze_actions().unwrap();
215 let svg = analyzed.action_graph().to_svg().unwrap();
216 assert!(svg.contains("Analyzed action DAG"));
217 let mut cyclic = analyzed.action_graph().clone();
218 cyclic.add_dependency(
219 cyclic.ordered[1],
220 cyclic.ordered[0],
221 ActionDependency::Classical,
222 );
223 assert!(matches!(
224 cyclic.to_svg().unwrap_err(),
225 BlockGraphError::InvalidAction(
226 crate::validate::InvalidActionError::DependencyCycle { .. }
227 )
228 ));
229 }
230
231 #[test]
232 fn continuing_branch_analysis_waits_for_the_selected_physical_readout() {
233 let source = crate::BlockGraph::from_text(
234 r#"BLOG 1.0
235module main {
236 in driver: data = 0
237 0: Port [2, 0, -1] role=input
238 1: ZXZ [2, 0, 0]
239 0 -> +Z
240 2: ZXZ [0, 0, 0]
241 3: ZXZ [0, 0, 2]
242 branch middle {
243 false {
244 4: ZXZ [0, 0, 1]
245 [0, 0, 0] -> +Z
246 [0, 0, 1] -> +Z
247 }
248 true {
249 5: XZX [0, 0, 1]
250 [0, 0, 0] -H> +Z
251 [0, 0, 1] -H> +Z
252 }
253 }
254 selector = measure 1
255 resolve middle if selector
256 result = measure 3
257}
258"#,
259 )
260 .unwrap();
261 let flat = source.flatten().unwrap();
262 let before = flat.to_blog_text();
263 let dag = flat.analyze_action_graph().unwrap();
264 assert!(dag.is_analyzed());
265 assert!(
266 dag.dependencies()
267 .any(|edge| edge == (1, 2, ActionDependency::BranchSupport))
268 );
269 assert_eq!(flat.to_blog_text(), before);
270 let mut constant = flat.clone();
271 let mut actions = constant.actions();
272 if let Action::Branch { condition, .. } = &mut actions[1] {
273 *condition = Expr::Binary(
274 crate::BinaryOp::Xor,
275 Box::new(Expr::Var("selector".into())),
276 Box::new(Expr::Var("selector".into())),
277 );
278 }
279 constant.set_actions(actions).unwrap();
280 assert!(
281 constant
282 .analyze_action_graph()
283 .unwrap()
284 .dependencies()
285 .any(|edge| edge == (1, 2, ActionDependency::BranchSupport))
286 );
287 }
288}