1use std::collections::{HashMap, HashSet};
2use std::fmt::Write;
3
4use serde::Serialize;
5use slotmap::{SecondaryMap, SparseSecondaryMap};
6
7use super::render::{
8 GraphWriteError, HydroEdgeProp, HydroGraphWrite, HydroNodeType, HydroWriteConfig,
9 write_hydro_ir_json,
10};
11use crate::compile::ir::HydroRoot;
12use crate::compile::ir::backtrace::Backtrace;
13use crate::location::{LocationKey, LocationType};
14use crate::viz::render::VizNodeKey;
15
16#[derive(Serialize)]
19struct BacktraceFrame {
20 #[serde(rename = "fn")]
22 fn_name: String,
23 function: String,
25 file: String,
27 filename: String,
29 line: Option<u32>,
31 #[serde(rename = "lineNumber")]
33 line_number: Option<u32>,
34}
35
36#[derive(Serialize)]
38struct NodeData {
39 #[serde(rename = "locationKey")]
40 location_key: Option<LocationKey>,
41 #[serde(rename = "locationType")]
42 location_type: Option<LocationType>,
43 backtrace: serde_json::Value,
44}
45
46#[derive(Serialize)]
48struct Node {
49 id: String,
50 #[serde(rename = "nodeType")]
51 node_type: String,
52 #[serde(rename = "fullLabel")]
53 full_label: String,
54 #[serde(rename = "shortLabel")]
55 short_label: String,
56 label: String,
57 data: NodeData,
58}
59
60#[derive(Serialize)]
62struct Edge {
63 id: String,
64 source: String,
65 target: String,
66 #[serde(rename = "semanticTags")]
67 semantic_tags: Vec<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 label: Option<String>,
70}
71
72pub struct HydroJson<'a, W> {
75 write: W,
76 nodes: Vec<serde_json::Value>,
77 edges: Vec<serde_json::Value>,
78 locations: SecondaryMap<LocationKey, (String, Vec<VizNodeKey>)>,
80 node_locations: SecondaryMap<VizNodeKey, LocationKey>,
82 edge_count: usize,
83 location_names: &'a SecondaryMap<LocationKey, String>,
85 node_backtraces: SparseSecondaryMap<VizNodeKey, Backtrace>,
87 use_short_labels: bool,
89}
90
91impl<'a, W> HydroJson<'a, W> {
92 pub fn new(write: W, config: HydroWriteConfig<'a>) -> Self {
93 Self {
94 write,
95 nodes: Vec::new(),
96 edges: Vec::new(),
97 locations: SecondaryMap::new(),
98 node_locations: SecondaryMap::new(),
99 edge_count: 0,
100 location_names: config.location_names,
101 node_backtraces: SparseSecondaryMap::new(),
102 use_short_labels: config.use_short_labels,
103 }
104 }
105
106 fn node_type_to_string(node_type: HydroNodeType) -> &'static str {
108 super::render::node_type_utils::to_string(node_type)
109 }
110
111 fn edge_type_to_string(edge_type: HydroEdgeProp) -> String {
113 match edge_type {
114 HydroEdgeProp::Bounded => "Bounded".to_owned(),
115 HydroEdgeProp::Unbounded => "Unbounded".to_owned(),
116 HydroEdgeProp::TotalOrder => "TotalOrder".to_owned(),
117 HydroEdgeProp::NoOrder => "NoOrder".to_owned(),
118 HydroEdgeProp::Keyed => "Keyed".to_owned(),
119 HydroEdgeProp::Stream => "Stream".to_owned(),
120 HydroEdgeProp::KeyedSingleton => "KeyedSingleton".to_owned(),
121 HydroEdgeProp::KeyedStream => "KeyedStream".to_owned(),
122 HydroEdgeProp::Singleton => "Singleton".to_owned(),
123 HydroEdgeProp::Optional => "Optional".to_owned(),
124 HydroEdgeProp::Network => "Network".to_owned(),
125 HydroEdgeProp::Cycle => "Cycle".to_owned(),
126 }
127 }
128
129 fn get_node_type_definitions() -> Vec<serde_json::Value> {
131 let mut types: Vec<(usize, &'static str)> =
133 super::render::node_type_utils::all_types_with_strings()
134 .into_iter()
135 .enumerate()
136 .map(|(idx, (_, type_str))| (idx, type_str))
137 .collect();
138 types.sort_by(|a, b| a.1.cmp(b.1));
139 types
140 .into_iter()
141 .enumerate()
142 .map(|(color_index, (_, type_str))| {
143 serde_json::json!({
144 "id": type_str,
145 "label": type_str,
146 "colorIndex": color_index
147 })
148 })
149 .collect()
150 }
151
152 fn get_legend_items() -> Vec<serde_json::Value> {
154 Self::get_node_type_definitions()
155 .into_iter()
156 .map(|def| {
157 serde_json::json!({
158 "type": def["id"],
159 "label": def["label"]
160 })
161 })
162 .collect()
163 }
164
165 fn get_edge_style_config() -> serde_json::Value {
167 serde_json::json!({
168 "semanticPriorities": [
169 ["Unbounded", "Bounded"],
170 ["NoOrder", "TotalOrder"],
171 ["Keyed", "NotKeyed"],
172 ["Network", "Local"]
173 ],
174 "semanticMappings": {
175 "NetworkGroup": {
177 "Local": {
178 "line-pattern": "solid",
179 "animation": "static"
180 },
181 "Network": {
182 "line-pattern": "dashed",
183 "animation": "animated"
184 }
185 },
186
187 "OrderingGroup": {
189 "TotalOrder": {
190 "waviness": "straight"
191 },
192 "NoOrder": {
193 "waviness": "wavy"
194 }
195 },
196
197 "BoundednessGroup": {
199 "Bounded": {
200 "halo": "none"
201 },
202 "Unbounded": {
203 "halo": "light-blue"
204 }
205 },
206
207 "KeyednessGroup": {
209 "NotKeyed": {
210 "line-style": "single"
211 },
212 "Keyed": {
213 "line-style": "hash-marks"
214 }
215 },
216
217 "CollectionGroup": {
219 "Stream": {
220 "color": "#2563eb",
221 "arrowhead": "triangle-filled"
222 },
223 "Singleton": {
224 "color": "#000000",
225 "arrowhead": "circle-filled"
226 },
227 "Optional": {
228 "color": "#6b7280",
229 "arrowhead": "diamond-open"
230 }
231 },
232 },
233 "note": "Edge styles are now computed per-edge using the unified edge style system. This config is provided for reference and compatibility."
234 })
235 }
236
237 fn optimize_backtrace(&self, backtrace: &Backtrace) -> serde_json::Value {
242 #[cfg(feature = "build")]
243 {
244 let elements = backtrace.elements();
245
246 let relevant_frames: Vec<BacktraceFrame> = elements
248 .map(|elem| {
249 let short_filename = elem
251 .filename
252 .as_deref()
253 .map(|f| Self::truncate_path(f))
254 .unwrap_or_else(|| "unknown".to_owned());
255
256 let short_fn_name = Self::truncate_function_name(&elem.fn_name).to_owned();
257
258 BacktraceFrame {
259 fn_name: short_fn_name.to_owned(),
260 function: short_fn_name,
261 file: short_filename.clone(),
262 filename: short_filename,
263 line: elem.lineno,
264 line_number: elem.lineno,
265 }
266 })
267 .collect();
268
269 serde_json::to_value(relevant_frames).unwrap_or_else(|_| serde_json::json!([]))
270 }
271 #[cfg(not(feature = "build"))]
272 {
273 serde_json::json!([])
274 }
275 }
276
277 fn truncate_path(path: &str) -> String {
279 let parts: Vec<&str> = path.split('/').collect();
280
281 if let Some(src_idx) = parts.iter().rposition(|&p| p == "src") {
283 parts[src_idx..].join("/")
284 } else if parts.len() > 2 {
285 parts[parts.len().saturating_sub(2)..].join("/")
287 } else {
288 path.to_owned()
289 }
290 }
291
292 fn truncate_function_name(fn_name: &str) -> &str {
294 fn_name.split("::").last().unwrap_or(fn_name)
296 }
297}
298
299impl<W> HydroGraphWrite for HydroJson<'_, W>
300where
301 W: Write,
302{
303 type Err = GraphWriteError;
304
305 fn write_prologue(&mut self) -> Result<(), Self::Err> {
306 self.nodes.clear();
308 self.edges.clear();
309 self.locations.clear();
310 self.node_locations.clear();
311 self.edge_count = 0;
312 Ok(())
313 }
314
315 fn write_node_definition(
316 &mut self,
317 node_id: VizNodeKey,
318 node_label: &super::render::NodeLabel,
319 node_type: HydroNodeType,
320 location_key: Option<LocationKey>,
321 location_type: Option<LocationType>,
322 backtrace: Option<&Backtrace>,
323 ) -> Result<(), Self::Err> {
324 let full_label = match node_label {
326 super::render::NodeLabel::Static(s) => s.clone(),
327 super::render::NodeLabel::WithExprs { op_name, exprs } => {
328 if exprs.is_empty() {
329 format!("{}()", op_name)
330 } else {
331 let expr_strs: Vec<String> = exprs.iter().map(|e| e.to_string()).collect();
333 format!("{}({})", op_name, expr_strs.join(", "))
334 }
335 }
336 };
337
338 let short_label = super::render::extract_short_label(&full_label);
340
341 let full_len = full_label.len();
344 let enhanced_full_label = if short_label.len() >= full_len.saturating_sub(2) {
345 match short_label.as_str() {
347 "inspect" => "inspect [debug output]".to_owned(),
348 "persist" => "persist [state storage]".to_owned(),
349 "tee" => "tee [branch dataflow]".to_owned(),
350 "delta" => "delta [change detection]".to_owned(),
351 "spin" => "spin [delay/buffer]".to_owned(),
352 "send_bincode" => "send_bincode [send data to process/cluster]".to_owned(),
353 "broadcast_bincode" => {
354 "broadcast_bincode [send data to all cluster members]".to_owned()
355 }
356 "source_iter" => "source_iter [iterate over collection]".to_owned(),
357 "source_stream" => "source_stream [receive external data stream]".to_owned(),
358 "network(recv)" => "network(recv) [receive from network]".to_owned(),
359 "network(send)" => "network(send) [send to network]".to_owned(),
360 "dest_sink" => "dest_sink [output destination]".to_owned(),
361 _ => {
362 if full_label.len() < 15 {
363 format!("{} [{}]", node_label, "hydro operator")
364 } else {
365 node_label.to_string()
366 }
367 }
368 }
369 } else {
370 node_label.to_string()
371 };
372
373 let backtrace_json = if let Some(bt) = backtrace {
375 self.node_backtraces.insert(node_id, bt.clone());
377 self.optimize_backtrace(bt)
378 } else {
379 serde_json::json!([])
380 };
381
382 let node_type_str = Self::node_type_to_string(node_type);
384
385 let node = Node {
386 id: node_id.to_string(),
387 node_type: node_type_str.to_owned(),
388 full_label: enhanced_full_label,
389 short_label: short_label.clone(),
390 label: if self.use_short_labels {
392 short_label
393 } else {
394 full_label
395 },
396 data: NodeData {
397 location_key,
398 location_type,
399 backtrace: backtrace_json,
400 },
401 };
402 self.nodes
403 .push(serde_json::to_value(node).expect("Node serialization should not fail"));
404
405 if let Some(loc_key) = location_key {
407 self.node_locations.insert(node_id, loc_key);
408 }
409
410 Ok(())
411 }
412
413 fn write_edge(
414 &mut self,
415 src_id: VizNodeKey,
416 dst_id: VizNodeKey,
417 edge_properties: &HashSet<HydroEdgeProp>,
418 label: Option<&str>,
419 ) -> Result<(), Self::Err> {
420 let edge_id = format!("e{}", self.edge_count);
421 self.edge_count = self.edge_count.saturating_add(1);
422
423 #[expect(
425 clippy::disallowed_methods,
426 reason = "nondeterministic iteration order, TODO(mingwei)"
427 )]
428 let mut semantic_tags: Vec<String> = edge_properties
429 .iter()
430 .map(|p| Self::edge_type_to_string(*p))
431 .collect();
432
433 let src_loc = self.node_locations.get(src_id).copied();
435 let dst_loc = self.node_locations.get(dst_id).copied();
436
437 if let (Some(src), Some(dst)) = (src_loc, dst_loc)
439 && src != dst
440 && !semantic_tags.iter().any(|t| t == "Network")
441 {
442 semantic_tags.push("Network".to_owned());
443 } else if semantic_tags.iter().all(|t| t != "Network") {
444 semantic_tags.push("Local".to_owned());
446 }
447
448 semantic_tags.sort();
450
451 let edge = Edge {
452 id: edge_id,
453 source: src_id.to_string(),
454 target: dst_id.to_string(),
455 semantic_tags,
456 label: label.map(|s| s.to_owned()),
457 };
458
459 self.edges
460 .push(serde_json::to_value(edge).expect("Edge serialization should not fail"));
461 Ok(())
462 }
463
464 fn write_location_start(
465 &mut self,
466 location_key: LocationKey,
467 location_type: LocationType,
468 ) -> Result<(), Self::Err> {
469 let location_label = if let Some(location_name) = self.location_names.get(location_key)
470 && "()" != location_name
471 {
473 format!("{:?} {}", location_type, location_name)
474 } else {
475 format!("{:?} {:?}", location_type, location_key)
476 };
477 self.locations
478 .insert(location_key, (location_label, Vec::new()));
479 Ok(())
480 }
481
482 fn write_node(&mut self, node_id: VizNodeKey) -> Result<(), Self::Err> {
483 if let Some((_, node_ids)) = self.locations.values_mut().last() {
485 node_ids.push(node_id);
486 }
487 Ok(())
488 }
489
490 fn write_location_end(&mut self) -> Result<(), Self::Err> {
491 Ok(())
493 }
494
495 fn write_epilogue(&mut self) -> Result<(), Self::Err> {
496 let mut hierarchy_choices = Vec::new();
498 let mut node_assignments_choices = serde_json::Map::new();
499
500 let (location_hierarchy, location_assignments) = self.create_location_hierarchy();
502 hierarchy_choices.push(serde_json::json!({
503 "id": "location",
504 "name": "Location",
505 "children": location_hierarchy
506 }));
507 node_assignments_choices.insert(
508 "location".to_owned(),
509 serde_json::Value::Object(location_assignments),
510 );
511
512 if self.has_backtrace_data() {
514 let (backtrace_hierarchy, backtrace_assignments) = self.create_backtrace_hierarchy();
515 hierarchy_choices.push(serde_json::json!({
516 "id": "backtrace",
517 "name": "Backtrace",
518 "children": backtrace_hierarchy
519 }));
520 node_assignments_choices.insert(
521 "backtrace".to_owned(),
522 serde_json::Value::Object(backtrace_assignments),
523 );
524 }
525
526 let mut nodes_sorted = self.nodes.clone();
528 nodes_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
529 let mut edges_sorted = self.edges.clone();
530 edges_sorted.sort_by(|a, b| {
531 let a_src = a["source"].as_str();
532 let b_src = b["source"].as_str();
533 match a_src.cmp(&b_src) {
534 std::cmp::Ordering::Equal => {
535 let a_dst = a["target"].as_str();
536 let b_dst = b["target"].as_str();
537 match a_dst.cmp(&b_dst) {
538 std::cmp::Ordering::Equal => a["id"].as_str().cmp(&b["id"].as_str()),
539 other => other,
540 }
541 }
542 other => other,
543 }
544 });
545
546 let node_type_definitions = Self::get_node_type_definitions();
548 let legend_items = Self::get_legend_items();
549
550 let node_type_config = serde_json::json!({
551 "types": node_type_definitions,
552 "defaultType": "Transform"
553 });
554 let legend = serde_json::json!({
555 "title": "Node Types",
556 "items": legend_items
557 });
558
559 let selected_hierarchy = if !hierarchy_choices.is_empty() {
561 hierarchy_choices[0]["id"].as_str()
562 } else {
563 None
564 };
565
566 #[derive(serde::Serialize)]
567 struct GraphPayload<'a> {
568 nodes: Vec<serde_json::Value>,
569 edges: Vec<serde_json::Value>,
570 #[serde(rename = "hierarchyChoices")]
571 hierarchy_choices: &'a [serde_json::Value],
572 #[serde(rename = "nodeAssignments")]
573 node_assignments: serde_json::Map<String, serde_json::Value>,
574 #[serde(rename = "selectedHierarchy", skip_serializing_if = "Option::is_none")]
575 selected_hierarchy: Option<&'a str>,
576 #[serde(rename = "edgeStyleConfig")]
577 edge_style_config: serde_json::Value,
578 #[serde(rename = "nodeTypeConfig")]
579 node_type_config: serde_json::Value,
580 legend: serde_json::Value,
581 }
582
583 let payload = GraphPayload {
584 nodes: nodes_sorted,
585 edges: edges_sorted,
586 hierarchy_choices: &hierarchy_choices,
587 node_assignments: node_assignments_choices,
588 selected_hierarchy,
589 edge_style_config: Self::get_edge_style_config(),
590 node_type_config,
591 legend,
592 };
593
594 let final_json = serde_json::to_string_pretty(&payload).unwrap();
595
596 write!(self.write, "{}", final_json)
597 }
598}
599
600impl<W> HydroJson<'_, W> {
601 fn has_backtrace_data(&self) -> bool {
603 self.nodes.iter().any(|node| {
604 if let Some(backtrace_array) = node["data"]["backtrace"].as_array() {
605 backtrace_array.iter().any(|frame| {
607 let filename = frame["file"].as_str().unwrap_or_default();
608 let fn_name = frame["fn"].as_str().unwrap_or_default();
609 !filename.is_empty() || !fn_name.is_empty()
610 })
611 } else {
612 false
613 }
614 })
615 }
616
617 fn create_location_hierarchy(
619 &self,
620 ) -> (
621 Vec<serde_json::Value>,
622 serde_json::Map<String, serde_json::Value>,
623 ) {
624 let mut locs: Vec<(LocationKey, &(String, Vec<VizNodeKey>))> =
626 self.locations.iter().collect();
627 locs.sort_by(|a, b| a.0.cmp(&b.0));
628 let hierarchy: Vec<serde_json::Value> = locs
629 .into_iter()
630 .map(|(location_key, (label, _))| {
631 serde_json::json!({
632 "key": location_key.to_string(),
633 "name": label,
634 "children": [] })
636 })
637 .collect();
638
639 let mut tmp: Vec<(String, serde_json::Value)> = Vec::new();
643 for node in self.nodes.iter() {
644 if let (Some(node_id), location_key) =
645 (node["id"].as_str(), &node["data"]["locationKey"])
646 {
647 tmp.push((node_id.to_owned(), location_key.clone()));
648 }
649 }
650 tmp.sort_by(|a, b| a.0.cmp(&b.0));
651 let mut node_assignments = serde_json::Map::new();
652 for (k, v) in tmp {
653 node_assignments.insert(k, v);
654 }
655
656 (hierarchy, node_assignments)
657 }
658
659 fn create_backtrace_hierarchy(
661 &self,
662 ) -> (
663 Vec<serde_json::Value>,
664 serde_json::Map<String, serde_json::Value>,
665 ) {
666 use std::collections::HashMap;
667
668 let mut hierarchy_map: HashMap<String, (String, usize, Option<String>)> = HashMap::new(); let mut path_to_node_assignments: HashMap<String, Vec<String>> = HashMap::new(); for node in self.nodes.iter() {
673 if let Some(node_id_str) = node["id"].as_str()
674 && let Ok(node_id) = node_id_str.parse::<VizNodeKey>()
675 && let Some(backtrace) = self.node_backtraces.get(node_id)
676 {
677 let elements = backtrace.elements().collect::<Vec<_>>();
678 if elements.is_empty() {
679 continue;
680 }
681
682 let user_frames = elements;
684 if user_frames.is_empty() {
685 continue;
686 }
687
688 let mut hierarchy_path = Vec::new();
690 for (i, elem) in user_frames.iter().rev().enumerate() {
691 let label = if i == 0 {
692 if let Some(filename) = &elem.filename {
693 Self::extract_file_path(filename)
694 } else {
695 format!("fn_{}", Self::truncate_function_name(&elem.fn_name))
696 }
697 } else {
698 Self::truncate_function_name(&elem.fn_name).to_owned()
699 };
700 hierarchy_path.push(label);
701 }
702
703 let mut current_path = String::new();
705 let mut parent_path: Option<String> = None;
706 let mut deepest_path = String::new();
707 let mut deduped: Vec<String> = Vec::new();
709 for seg in hierarchy_path {
710 if deduped.last().map(|s| s == &seg).unwrap_or(false) {
711 continue;
712 }
713 deduped.push(seg);
714 }
715 for (depth, label) in deduped.iter().enumerate() {
716 current_path = if current_path.is_empty() {
717 label.clone()
718 } else {
719 format!("{}/{}", current_path, label)
720 };
721 if !hierarchy_map.contains_key(¤t_path) {
722 hierarchy_map.insert(
723 current_path.clone(),
724 (label.clone(), depth, parent_path.clone()),
725 );
726 }
727 deepest_path = current_path.clone();
728 parent_path = Some(current_path.clone());
729 }
730
731 if !deepest_path.is_empty() {
732 path_to_node_assignments
733 .entry(deepest_path)
734 .or_default()
735 .push(node_id_str.to_owned());
736 }
737 }
738 }
739 let (mut hierarchy, mut path_to_id_map, id_remapping) =
741 self.build_hierarchy_tree_with_ids(&hierarchy_map);
742
743 let root_id = "bt_root";
745 let mut nodes_without_backtrace = Vec::new();
746
747 for node in self.nodes.iter() {
749 if let Some(node_id_str) = node["id"].as_str() {
750 nodes_without_backtrace.push(node_id_str.to_owned());
751 }
752 }
753
754 #[expect(
756 clippy::disallowed_methods,
757 reason = "nondeterministic iteration order, TODO(mingwei)"
758 )]
759 for node_ids in path_to_node_assignments.values() {
760 for node_id in node_ids {
761 nodes_without_backtrace.retain(|id| id != node_id);
762 }
763 }
764
765 if !nodes_without_backtrace.is_empty() {
767 hierarchy.push(serde_json::json!({
768 "id": root_id,
769 "name": "(no backtrace)",
770 "children": []
771 }));
772 path_to_id_map.insert("__root__".to_owned(), root_id.to_owned());
773 }
774
775 let mut node_assignments = serde_json::Map::new();
777 let mut pairs: Vec<(String, Vec<String>)> = path_to_node_assignments.into_iter().collect();
778 pairs.sort_by(|a, b| a.0.cmp(&b.0));
779 for (path, mut node_ids) in pairs {
780 node_ids.sort();
781 if let Some(hierarchy_id) = path_to_id_map.get(&path) {
782 for node_id in node_ids {
783 node_assignments
784 .insert(node_id, serde_json::Value::String(hierarchy_id.clone()));
785 }
786 }
787 }
788
789 for node_id in nodes_without_backtrace {
791 node_assignments.insert(node_id, serde_json::Value::String(root_id.to_owned()));
792 }
793
794 let mut remapped_assignments = serde_json::Map::new();
798 for (node_id, container_id_value) in node_assignments.iter() {
799 if let Some(container_id) = container_id_value.as_str() {
800 let final_container_id = id_remapping
802 .get(container_id)
803 .map(|s| &**s)
804 .unwrap_or(container_id);
805 remapped_assignments.insert(
806 node_id.clone(),
807 serde_json::Value::String(final_container_id.to_owned()),
808 );
809 }
810 }
811
812 (hierarchy, remapped_assignments)
813 }
814
815 fn build_hierarchy_tree_with_ids(
817 &self,
818 hierarchy_map: &HashMap<String, (String, usize, Option<String>)>,
819 ) -> (
820 Vec<serde_json::Value>,
821 HashMap<String, String>,
822 HashMap<String, String>,
823 ) {
824 #[expect(
826 clippy::disallowed_methods,
827 reason = "nondeterministic iteration order, TODO(mingwei)"
828 )]
829 let mut keys: Vec<&String> = hierarchy_map.keys().collect();
830 keys.sort();
831 let mut path_to_id: HashMap<String, String> = HashMap::new();
832 for (i, path) in keys.iter().enumerate() {
833 path_to_id.insert((*path).clone(), format!("bt_{}", i.saturating_add(1)));
834 }
835
836 #[expect(
838 clippy::disallowed_methods,
839 reason = "nondeterministic iteration order, TODO(mingwei)"
840 )]
841 let mut roots: Vec<(String, String)> = hierarchy_map
842 .iter()
843 .filter_map(|(path, (name, depth, _))| {
844 if *depth == 0 {
845 Some((path.clone(), name.clone()))
846 } else {
847 None
848 }
849 })
850 .collect();
851 roots.sort_by(|a, b| a.1.cmp(&b.1));
852 let mut root_nodes = Vec::new();
853 for (path, name) in roots {
854 let tree_node = Self::build_tree_node(&path, &name, hierarchy_map, &path_to_id);
855 root_nodes.push(tree_node);
856 }
857
858 let mut id_remapping: HashMap<String, String> = HashMap::new();
861 root_nodes = root_nodes
862 .into_iter()
863 .map(|node| Self::collapse_single_child_containers(node, None, &mut id_remapping))
864 .collect();
865
866 let mut updated_path_to_id = path_to_id.clone();
868 #[expect(
869 clippy::disallowed_methods,
870 reason = "nondeterministic iteration order, TODO(mingwei)"
871 )]
872 for (path, old_id) in path_to_id.iter() {
873 if let Some(new_id) = id_remapping.get(old_id) {
874 updated_path_to_id.insert(path.clone(), new_id.clone());
875 }
876 }
877
878 (root_nodes, updated_path_to_id, id_remapping)
879 }
880
881 fn build_tree_node(
883 current_path: &str,
884 name: &str,
885 hierarchy_map: &HashMap<String, (String, usize, Option<String>)>,
886 path_to_id: &HashMap<String, String>,
887 ) -> serde_json::Value {
888 let current_id = path_to_id.get(current_path).unwrap().clone();
889
890 #[expect(
892 clippy::disallowed_methods,
893 reason = "nondeterministic iteration order, TODO(mingwei)"
894 )]
895 let mut child_specs: Vec<(&String, &String)> = hierarchy_map
896 .iter()
897 .filter_map(|(child_path, (child_name, _, parent_path))| {
898 if let Some(parent) = parent_path {
899 if parent == current_path {
900 Some((child_path, child_name))
901 } else {
902 None
903 }
904 } else {
905 None
906 }
907 })
908 .collect();
909 child_specs.sort_by(|a, b| a.1.cmp(b.1));
910 let mut children = Vec::new();
911 for (child_path, child_name) in child_specs {
912 let child_node =
913 Self::build_tree_node(child_path, child_name, hierarchy_map, path_to_id);
914 children.push(child_node);
915 }
916
917 if children.is_empty() {
918 serde_json::json!({
919 "id": current_id,
920 "name": name
921 })
922 } else {
923 serde_json::json!({
924 "id": current_id,
925 "name": name,
926 "children": children
927 })
928 }
929 }
930
931 fn collapse_single_child_containers(
937 node: serde_json::Value,
938 parent_name: Option<&str>,
939 id_remapping: &mut HashMap<String, String>,
940 ) -> serde_json::Value {
941 let mut node_obj = match node {
942 serde_json::Value::Object(obj) => obj,
943 _ => return node,
944 };
945
946 let current_name = node_obj
947 .get("name")
948 .and_then(|v| v.as_str())
949 .unwrap_or_default();
950
951 let current_id = node_obj
952 .get("id")
953 .and_then(|v| v.as_str())
954 .unwrap_or_default();
955
956 let effective_name = if let Some(parent) = parent_name {
959 format!("{} → {}", parent, current_name)
960 } else {
961 current_name.to_owned()
962 };
963
964 if let Some(serde_json::Value::Array(children)) = node_obj.get("children") {
966 if children.len() == 1
968 && let Some(child) = children.first()
969 {
970 let child_is_container = child
971 .get("children")
972 .and_then(|v| v.as_array())
973 .is_some_and(|arr| !arr.is_empty());
974
975 if child_is_container {
976 let child_id = child.get("id").and_then(|v| v.as_str()).unwrap_or_default();
977
978 if !current_id.is_empty() && !child_id.is_empty() {
980 id_remapping.insert(current_id.to_owned(), child_id.to_owned());
981 }
982
983 return Self::collapse_single_child_containers(
985 child.clone(),
986 Some(&effective_name),
987 id_remapping,
988 );
989 }
990 }
991
992 let processed_children: Vec<serde_json::Value> = children
994 .iter()
995 .map(|child| {
996 Self::collapse_single_child_containers(child.clone(), None, id_remapping)
997 })
998 .collect();
999
1000 node_obj.insert("name".to_owned(), serde_json::Value::String(effective_name));
1001 node_obj.insert(
1002 "children".to_owned(),
1003 serde_json::Value::Array(processed_children),
1004 );
1005 } else {
1006 node_obj.insert("name".to_owned(), serde_json::Value::String(effective_name));
1008 }
1009
1010 serde_json::Value::Object(node_obj)
1011 }
1012
1013 fn extract_file_path(filename: &str) -> String {
1015 if filename.is_empty() {
1016 return "unknown".to_owned();
1017 }
1018
1019 let parts: Vec<&str> = filename.split('/').collect();
1021 let file_name = parts.last().unwrap_or(&"unknown");
1022
1023 if file_name.ends_with(".rs") && parts.len() > 1 {
1025 let parent_dir = parts[parts.len() - 2];
1026 format!("{}/{}", parent_dir, file_name)
1027 } else {
1028 file_name.to_string()
1029 }
1030 }
1031}
1032
1033pub fn hydro_ir_to_json(
1035 ir: &[HydroRoot],
1036 location_names: &SecondaryMap<LocationKey, String>,
1037) -> Result<String, Box<dyn std::error::Error>> {
1038 let mut output = String::new();
1039
1040 let config = HydroWriteConfig {
1041 show_metadata: false,
1042 show_location_groups: true,
1043 use_short_labels: true, location_names,
1045 };
1046
1047 write_hydro_ir_json(&mut output, ir, config)?;
1048
1049 Ok(output)
1050}
1051
1052pub fn open_json_browser(
1054 ir: &[HydroRoot],
1055 location_names: &SecondaryMap<LocationKey, String>,
1056) -> Result<(), Box<dyn std::error::Error>> {
1057 let config = HydroWriteConfig {
1058 location_names,
1059 ..Default::default()
1060 };
1061
1062 super::debug::open_json_visualizer(ir, Some(config))
1063 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
1064}
1065
1066pub fn save_json(
1068 ir: &[HydroRoot],
1069 location_names: &SecondaryMap<LocationKey, String>,
1070 filename: &str,
1071) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
1072 let config = HydroWriteConfig {
1073 location_names,
1074 ..Default::default()
1075 };
1076
1077 super::debug::save_json(ir, Some(filename), Some(config))
1078 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
1079}
1080
1081#[cfg(feature = "build")]
1083pub fn open_browser(
1084 built_flow: &crate::compile::built::BuiltFlow,
1085) -> Result<(), Box<dyn std::error::Error>> {
1086 open_json_browser(built_flow.ir(), built_flow.location_names())
1087}