hydro_lang/viz/
debug.rs

1//! Debugging utilities for Hydro IR graph visualization.
2//!
3//! Similar to the DFIR debugging utilities, this module provides convenient
4//! methods for opening graphs in web browsers and VS Code.
5
6use std::fmt::Write;
7use std::io::{Result, Write as IoWrite};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use super::config::VisualizerConfig;
11use super::render::{HydroWriteConfig, render_hydro_ir_dot, render_hydro_ir_mermaid};
12use crate::compile::ir::HydroRoot;
13
14/// Opens Hydro IR roots as a single mermaid diagram.
15pub fn open_mermaid(roots: &[HydroRoot], config: Option<HydroWriteConfig>) -> Result<()> {
16    let mermaid_src = render_with_config(roots, config, render_hydro_ir_mermaid);
17    open_mermaid_browser(&mermaid_src)
18}
19
20/// Opens Hydro IR roots as a single DOT diagram.
21pub fn open_dot(roots: &[HydroRoot], config: Option<HydroWriteConfig>) -> Result<()> {
22    let dot_src = render_with_config(roots, config, render_hydro_ir_dot);
23    open_dot_browser(&dot_src)
24}
25
26/// Saves Hydro IR roots as a Mermaid diagram file.
27/// If no filename is provided, saves to temporary directory.
28pub fn save_mermaid(
29    roots: &[HydroRoot],
30    filename: Option<&str>,
31    config: Option<HydroWriteConfig>,
32) -> Result<std::path::PathBuf> {
33    let content = render_with_config(roots, config, render_hydro_ir_mermaid);
34    save_to_file(content, filename, "hydro_graph.mermaid", "Mermaid diagram")
35}
36
37/// Saves Hydro IR roots as a DOT/Graphviz file.
38/// If no filename is provided, saves to temporary directory.
39pub fn save_dot(
40    roots: &[HydroRoot],
41    filename: Option<&str>,
42    config: Option<HydroWriteConfig>,
43) -> Result<std::path::PathBuf> {
44    let content = render_with_config(roots, config, render_hydro_ir_dot);
45    save_to_file(content, filename, "hydro_graph.dot", "DOT/Graphviz file")
46}
47
48fn open_mermaid_browser(mermaid_src: &str) -> Result<()> {
49    let state = serde_json::json!({
50        "code": mermaid_src,
51        "mermaid": serde_json::json!({
52            "theme": "default"
53        }),
54        "autoSync": true,
55        "updateDiagram": true
56    });
57    let state_json = serde_json::to_vec(&state)?;
58    let state_base64 = data_encoding::BASE64URL.encode(&state_json);
59    webbrowser::open(&format!(
60        "https://mermaid.live/edit#base64:{}",
61        state_base64
62    ))
63}
64
65fn open_dot_browser(dot_src: &str) -> Result<()> {
66    let mut url = "https://dreampuf.github.io/GraphvizOnline/#".to_owned();
67    for byte in dot_src.bytes() {
68        // Lazy percent encoding: https://en.wikipedia.org/wiki/Percent-encoding
69        write!(url, "%{:02x}", byte).unwrap();
70    }
71    webbrowser::open(&url)
72}
73
74/// Helper function to save content to a file with consistent path handling.
75/// If no filename is provided, saves to temporary directory with the default name.
76fn save_to_file(
77    content: String,
78    filename: Option<&str>,
79    default_name: &str,
80    content_type: &str,
81) -> Result<std::path::PathBuf> {
82    let file_path = if let Some(filename) = filename {
83        std::path::PathBuf::from(filename)
84    } else {
85        std::env::temp_dir().join(default_name)
86    };
87
88    std::fs::write(&file_path, content)?;
89    println!("Saved {} to {}", content_type, file_path.display());
90    Ok(file_path)
91}
92
93/// Helper function to handle config unwrapping and rendering.
94fn render_with_config<F>(
95    roots: &[HydroRoot],
96    config: Option<HydroWriteConfig>,
97    renderer: F,
98) -> String
99where
100    F: Fn(&[HydroRoot], &HydroWriteConfig) -> String,
101{
102    let config = config.unwrap_or_default();
103    renderer(roots, &config)
104}
105
106/// Compress JSON content using gzip compression.
107/// Returns the compressed bytes or an error if compression fails.
108fn compress_json(json_content: &str) -> Result<Vec<u8>> {
109    use flate2::Compression;
110    use flate2::write::GzEncoder;
111
112    let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
113    encoder.write_all(json_content.as_bytes())?;
114    encoder.finish()
115}
116
117/// Encode data to base64 URL-safe format without padding.
118/// This format is safe for use in URLs and doesn't require additional escaping.
119fn encode_base64_url_safe(data: &[u8]) -> String {
120    data_encoding::BASE64URL_NOPAD.encode(data)
121}
122
123/// Try to compress and encode JSON content for URL embedding.
124/// Returns (encoded_data, is_compressed, compression_ratio).
125///
126/// Compression is skipped for small JSON (<min_compression_size bytes).
127/// If compression fails or doesn't reduce size, falls back to uncompressed encoding.
128fn try_compress_and_encode(json_content: &str, config: &VisualizerConfig) -> (String, bool, f64) {
129    let original_size = json_content.len();
130
131    // Skip compression for small JSON
132    if !config.enable_compression || original_size < config.min_compression_size {
133        let encoded = encode_base64_url_safe(json_content.as_bytes());
134        return (encoded, false, 1.0);
135    }
136
137    match compress_json(json_content) {
138        Ok(compressed) => {
139            let compressed_size = compressed.len();
140            let ratio = original_size as f64 / compressed_size as f64;
141
142            // Only use compression if it actually reduces size
143            if compressed_size < original_size {
144                let encoded = encode_base64_url_safe(&compressed);
145                (encoded, true, ratio)
146            } else {
147                // Compression didn't help, use uncompressed
148                let encoded = encode_base64_url_safe(json_content.as_bytes());
149                (encoded, false, 1.0)
150            }
151        }
152        Err(e) => {
153            // Compression failed, fall back to uncompressed
154            println!("āš ļø  Compression failed: {}, using uncompressed", e);
155            let encoded = encode_base64_url_safe(json_content.as_bytes());
156            (encoded, false, 1.0)
157        }
158    }
159}
160
161/// Calculate the total URL length for a given encoded data and parameter name.
162/// Returns the total length including base URL, parameter name, and encoded data.
163fn calculate_url_length(base_url: &str, param_name: &str, encoded_data: &str) -> usize {
164    // Format: base_url?param_name=encoded_data
165    base_url.len() + 1 + param_name.len() + 1 + encoded_data.len()
166}
167
168/// Generate a URL for the visualizer with the given JSON content.
169/// Automatically chooses between compressed and uncompressed encoding based on URL length.
170/// Returns (url, is_compressed) or None if the URL would be too long.
171fn generate_visualizer_url(
172    json_content: &str,
173    config: &VisualizerConfig,
174) -> Option<(String, bool)> {
175    let (encoded_data, is_compressed, _ratio) = try_compress_and_encode(json_content, config);
176
177    // Determine parameter name based on compression
178    let param_name = if is_compressed { "compressed" } else { "data" };
179
180    let url_length = calculate_url_length(&config.base_url, param_name, &encoded_data);
181
182    if url_length <= config.max_url_length {
183        let url = format!("{}?{}={}", config.base_url, param_name, encoded_data);
184        Some((url, is_compressed))
185    } else {
186        // message will be displayed by print_fallback_instructions
187        None
188    }
189}
190
191/// Generate a timestamped filename for temporary graph files.
192/// Format: hydro_graph_<timestamp>.json
193fn generate_timestamped_filename() -> String {
194    let timestamp = SystemTime::now()
195        .duration_since(UNIX_EPOCH)
196        .expect("System clock is before Unix epoch - clock may be corrupted")
197        .as_secs();
198    format!("hydro_graph_{}.json", timestamp)
199}
200
201/// Save JSON content to a temporary file with a timestamped filename.
202/// Returns the path to the created file.
203///
204/// Requirements: 9.1, 9.2, 9.3
205fn save_json_to_temp_file(json_content: &str) -> Result<std::path::PathBuf> {
206    let filename = generate_timestamped_filename();
207    let temp_file = std::env::temp_dir().join(filename);
208
209    std::fs::write(&temp_file, json_content)?;
210
211    println!("šŸ“ Saved graph to temporary file: {}", temp_file.display());
212
213    Ok(temp_file)
214}
215
216/// URL-encode a file path for safe transmission in query parameters.
217/// Uses percent encoding to ensure special characters are properly escaped.
218///
219/// Requirements: 9.4
220fn url_encode_file_path(file_path: &std::path::Path) -> String {
221    let path_str = file_path.to_string_lossy();
222    urlencoding::encode(&path_str).to_string()
223}
224
225/// Generate a visualizer URL with a file query parameter.
226/// Format: base_url?file=<encoded_path>
227///
228/// Requirements: 9.4, 9.5
229fn generate_file_based_url(file_path: &std::path::Path, config: &VisualizerConfig) -> String {
230    let encoded_path = url_encode_file_path(file_path);
231    format!("{}?file={}", config.base_url, encoded_path)
232}
233
234/// Print fallback instructions for manual loading of the graph file.
235/// Provides clear guidance if automatic browser opening fails.
236///
237/// Requirements: 9.6, 9.7
238fn print_fallback_instructions(file_path: &std::path::Path, url: &str) {
239    println!("\nšŸ“Š Graph Visualization Instructions");
240    println!("═══════════════════════════════════════════════════════════");
241    println!("The graph is too large to embed in a URL.");
242    println!("It has been saved to a temporary file:");
243    println!("  šŸ“ {}", file_path.display());
244    println!();
245    println!("Opening visualizer in browser...");
246    println!("  🌐 {}", url);
247    println!();
248    println!("If the browser doesn't open automatically, you can:");
249    println!("  1. Manually open: {}", url);
250    println!(
251        "  2. Or visit {} and drag-and-drop the file",
252        url.split('?').next().unwrap_or(url)
253    );
254    println!("═══════════════════════════════════════════════════════════\n");
255}
256
257/// Handle large graph visualization using file-based fallback.
258/// Saves the JSON to a temporary file and opens the visualizer with a file parameter.
259/// Uses the configured base URL from VisualizerConfig.
260fn handle_large_graph_fallback(json_content: &str, config: &VisualizerConfig) -> Result<()> {
261    let temp_file = save_json_to_temp_file(json_content)?;
262
263    // Generate URL with file parameter using configured base URL
264    let url = generate_file_based_url(&temp_file, config);
265
266    print_fallback_instructions(&temp_file, &url);
267
268    match webbrowser::open(&url) {
269        Ok(_) => {
270            println!("āœ“ Successfully opened visualizer in browser");
271        }
272        Err(e) => {
273            println!("āš ļø  Failed to open browser automatically: {}", e);
274            println!("Please manually open the URL above or drag-and-drop the file.");
275        }
276    }
277
278    Ok(())
279}
280
281/// Open JSON visualizer with automatic fallback to file-based approach for large graphs.
282/// First attempts to embed the JSON in the URL using compression.
283/// If the URL is too long, falls back to saving the file and using a file parameter.
284///
285/// This is the main entry point for opening JSON visualizations.
286///
287/// Requirements: 8.1-8.9, 9.1-9.9
288fn open_json_visualizer_with_fallback(json_content: &str, config: &VisualizerConfig) -> Result<()> {
289    // Try to generate a URL with embedded data
290    match generate_visualizer_url(json_content, config) {
291        Some((url, _is_compressed)) => {
292            // URL fits within length limit, open it directly
293            webbrowser::open(&url)?;
294            println!("āœ“ Successfully opened visualizer in browser");
295            Ok(())
296        }
297        None => {
298            // URL too long, use file-based fallback
299            println!("šŸ“¦ Graph too large for URL embedding, using file-based approach...");
300            handle_large_graph_fallback(json_content, config)
301        }
302    }
303}
304
305/// Opens Hydro IR roots as a JSON visualization in the browser.
306/// Automatically handles compression and file-based fallback for large graphs.
307///
308/// This function generates JSON from the Hydro IR and opens it in the configured
309/// visualizer (defaults to <https://hydro.run/docs/hydroscope>, can be overridden
310/// with HYDRO_VISUALIZER_URL environment variable).
311pub fn open_json_visualizer(roots: &[HydroRoot], config: Option<HydroWriteConfig>) -> Result<()> {
312    let json_content = render_with_config(roots, config, render_hydro_ir_json);
313    let viz_config = VisualizerConfig::default();
314    open_json_visualizer_with_fallback(&json_content, &viz_config)
315}
316
317/// Opens Hydro IR roots as a JSON visualization with custom visualizer configuration.
318/// Allows specifying a custom base URL and compression settings.
319pub fn open_json_visualizer_with_config(
320    roots: &[HydroRoot],
321    config: Option<HydroWriteConfig>,
322    viz_config: VisualizerConfig,
323) -> Result<()> {
324    let json_content = render_with_config(roots, config, render_hydro_ir_json);
325    open_json_visualizer_with_fallback(&json_content, &viz_config)
326}
327
328/// Saves Hydro IR roots as a JSON file.
329/// If no filename is provided, saves to temporary directory.
330pub fn save_json(
331    roots: &[HydroRoot],
332    filename: Option<&str>,
333    config: Option<HydroWriteConfig>,
334) -> Result<std::path::PathBuf> {
335    let content = render_with_config(roots, config, render_hydro_ir_json);
336    save_to_file(content, filename, "hydro_graph.json", "JSON file")
337}
338
339/// Helper function to render multiple Hydro IR roots as JSON.
340fn render_hydro_ir_json(roots: &[HydroRoot], config: &HydroWriteConfig) -> String {
341    super::render::render_hydro_ir_json(roots, config)
342}