Skip to main content

tldr_cli/commands/
available.rs

1//! Available Expressions Analysis CLI command
2//!
3//! Computes available expressions at each program point for Common Subexpression
4//! Elimination (CSE) detection.
5//!
6//! # Usage
7//!
8//! ```bash
9//! tldr available <file> <function> [-f json|text]
10//! tldr available src/main.py process_data --check "a + b"
11//! tldr available src/main.py process_data --at_line 42
12//! ```
13//!
14//! # Output
15//!
16//! - JSON: Full AvailableExprsInfo with avail_in/avail_out per block
17//! - Text: Human-readable summary with redundant computations highlighted
18//!
19//! # Reference
20//! - dataflow/spec.md (CAP-AE-01 through CAP-AE-12)
21
22use std::path::PathBuf;
23
24use anyhow::Result;
25use clap::Args;
26
27use tldr_core::dataflow::{compute_available_exprs_with_source_and_lang, AvailableExprsInfo};
28use tldr_core::{get_cfg_context, get_dfg_context, Language};
29
30use crate::output::OutputFormat;
31
32/// Analyze available expressions for CSE detection
33#[derive(Debug, Args)]
34pub struct AvailableArgs {
35    /// Source file to analyze
36    pub file: PathBuf,
37
38    /// Function name to analyze
39    pub function: String,
40
41    /// Programming language (auto-detected from file extension if not specified)
42    #[arg(long, short = 'l')]
43    pub lang: Option<Language>,
44
45    /// Check if a specific expression is available (e.g., "a + b")
46    #[arg(long)]
47    pub check: Option<String>,
48
49    /// Show expressions available at a specific line number
50    #[arg(long)]
51    pub at_line: Option<usize>,
52
53    /// Show what kills a specific expression
54    #[arg(long)]
55    pub killed_by: Option<String>,
56
57    /// Show only CSE opportunities, skip per-block details
58    #[arg(long)]
59    pub cse_only: bool,
60}
61
62impl AvailableArgs {
63    /// Run the available expressions analysis command
64    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
65        use crate::output::OutputWriter;
66
67        let writer = OutputWriter::new(format, quiet);
68
69        // Determine language from file extension or argument
70        let language = self
71            .lang
72            .unwrap_or_else(|| Language::from_path(&self.file).unwrap_or(Language::Python));
73
74        writer.progress(&format!(
75            "Analyzing available expressions for {} in {}...",
76            self.function,
77            self.file.display()
78        ));
79
80        // Read source file - ensure it exists
81        if !self.file.exists() {
82            return Err(anyhow::anyhow!("File not found: {}", self.file.display()));
83        }
84
85        // Read source for line mapping
86        let source = std::fs::read_to_string(&self.file)?;
87        let source_lines: Vec<String> = source.lines().map(|s| s.to_string()).collect();
88
89        // Get CFG for the function
90        let cfg = get_cfg_context(
91            self.file.to_str().unwrap_or_default(),
92            &self.function,
93            language,
94        )?;
95
96        // Get DFG for expression extraction
97        let dfg = get_dfg_context(
98            self.file.to_str().unwrap_or_default(),
99            &self.function,
100            language,
101        )?;
102
103        // Compute available expressions (with AST-based extraction for multi-language support)
104        let result = compute_available_exprs_with_source_and_lang(
105            &cfg,
106            &dfg,
107            &source_lines,
108            Some(language),
109        )?;
110
111        // Handle specific queries
112        if let Some(ref expr) = self.check {
113            return self.handle_check_query(&result, expr, &writer);
114        }
115
116        if let Some(line) = self.at_line {
117            return self.handle_at_line_query(&result, line, &writer);
118        }
119
120        if let Some(ref expr) = self.killed_by {
121            return self.handle_killed_by_query(&result, expr, &writer);
122        }
123
124        // Default: output full result
125        match format {
126            OutputFormat::Json => {
127                let json = serde_json::to_string_pretty(&result)
128                    .map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
129                writer.write_text(&json)?;
130            }
131            OutputFormat::Text => {
132                let text = self.format_text_output(&result);
133                writer.write_text(&text)?;
134            }
135            OutputFormat::Compact => {
136                let json = serde_json::to_string(&result)
137                    .map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
138                writer.write_text(&json)?;
139            }
140            _ => {
141                let json = serde_json::to_string_pretty(&result)
142                    .map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
143                writer.write_text(&json)?;
144            }
145        }
146
147        Ok(())
148    }
149
150    fn handle_check_query(
151        &self,
152        result: &AvailableExprsInfo,
153        expr: &str,
154        writer: &crate::output::OutputWriter,
155    ) -> Result<()> {
156        // Find blocks where this expression is available
157        let mut available_in_blocks = Vec::new();
158
159        for (block_id, exprs) in &result.avail_in {
160            if exprs.iter().any(|e| e.text == expr) {
161                available_in_blocks.push(*block_id);
162            }
163        }
164
165        let output = serde_json::json!({
166            "expression": expr,
167            "available_in_blocks": available_in_blocks,
168            "is_redundant": result.redundant_computations().iter().any(|(text, _, _)| text == expr),
169        });
170
171        let json = serde_json::to_string_pretty(&output)
172            .map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
173        writer.write_text(&json)?;
174        Ok(())
175    }
176
177    fn handle_at_line_query(
178        &self,
179        result: &AvailableExprsInfo,
180        line: usize,
181        writer: &crate::output::OutputWriter,
182    ) -> Result<()> {
183        // Find expressions available at the given line
184        let mut available_exprs = Vec::new();
185
186        for exprs in result.avail_in.values() {
187            for expr in exprs {
188                if expr.line <= line && !available_exprs.contains(&expr.text) {
189                    available_exprs.push(expr.text.clone());
190                }
191            }
192        }
193
194        // Also check avail_out for completeness
195        for exprs in result.avail_out.values() {
196            for expr in exprs {
197                if expr.line <= line && !available_exprs.contains(&expr.text) {
198                    available_exprs.push(expr.text.clone());
199                }
200            }
201        }
202
203        let output = serde_json::json!({
204            "line": line,
205            "available_expressions": available_exprs,
206        });
207
208        let json = serde_json::to_string_pretty(&output)
209            .map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
210        writer.write_text(&json)?;
211        Ok(())
212    }
213
214    fn handle_killed_by_query(
215        &self,
216        result: &AvailableExprsInfo,
217        expr: &str,
218        writer: &crate::output::OutputWriter,
219    ) -> Result<()> {
220        // Find what variables kill this expression
221        let mut killers = Vec::new();
222
223        // Find the expression to get its operands
224        for exprs in result.avail_in.values() {
225            for e in exprs {
226                if e.text == expr {
227                    killers.extend(e.operands.iter().cloned());
228                    break;
229                }
230            }
231        }
232
233        // Also check avail_out
234        for exprs in result.avail_out.values() {
235            for e in exprs {
236                if e.text == expr {
237                    for op in &e.operands {
238                        if !killers.contains(op) {
239                            killers.push(op.clone());
240                        }
241                    }
242                    break;
243                }
244            }
245        }
246
247        let output = serde_json::json!({
248            "expression": expr,
249            "killed_by_redefinition_of": killers,
250        });
251
252        let json = serde_json::to_string_pretty(&output)
253            .map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
254        writer.write_text(&json)?;
255        Ok(())
256    }
257
258    fn format_text_output(&self, result: &AvailableExprsInfo) -> String {
259        let mut output = String::new();
260
261        output.push_str(&format!(
262            "Available Expressions Analysis: {} in {}\n\n",
263            self.function,
264            self.file.display()
265        ));
266
267        // Show redundant computations (CSE opportunities)
268        // redundant_computations returns Vec<(text, first_line, redundant_line)>
269        let redundant = result.redundant_computations();
270        if !redundant.is_empty() {
271            output.push_str("CSE Opportunities (redundant computations):\n");
272            for (expr_text, first_line, redundant_line) in &redundant {
273                output.push_str(&format!(
274                    "  - '{}' first at line {}, redundant at line {}\n",
275                    expr_text, first_line, redundant_line
276                ));
277            }
278            output.push('\n');
279        } else {
280            output.push_str("No redundant computations detected.\n\n");
281        }
282
283        // Show available expressions per block (unless --cse-only)
284        if !self.cse_only {
285            output.push_str("Available expressions by block:\n");
286            let mut blocks: Vec<_> = result.avail_in.keys().collect();
287            blocks.sort();
288
289            for block_id in blocks {
290                if let Some(exprs) = result.avail_in.get(block_id) {
291                    if !exprs.is_empty() {
292                        let expr_strs: Vec<_> = exprs.iter().map(|e| e.text.as_str()).collect();
293                        output.push_str(&format!(
294                            "  Block {}: {}\n",
295                            block_id,
296                            expr_strs.join(", ")
297                        ));
298                    }
299                }
300            }
301        }
302
303        output
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use std::collections::{HashMap, HashSet};
311    use std::path::PathBuf;
312
313    use tldr_core::dataflow::available::{
314        AvailableExprsInfo, Confidence, ExprInstance, Expression,
315    };
316
317    /// Build an AvailableArgs with the given cse_only flag.
318    fn make_args(cse_only: bool) -> AvailableArgs {
319        AvailableArgs {
320            file: PathBuf::from("test.py"),
321            function: "example".to_string(),
322            lang: None,
323            check: None,
324            at_line: None,
325            killed_by: None,
326            cse_only,
327        }
328    }
329
330    /// Build an AvailableExprsInfo with one CSE opportunity and one block
331    /// of available expressions.
332    fn make_result_with_cse() -> AvailableExprsInfo {
333        let expr_a = Expression::new("a + b", vec!["a", "b"], 2);
334        let expr_b = Expression::new("c * d", vec!["c", "d"], 3);
335        // Duplicate of expr_a at a later line -- triggers CSE
336        let expr_a_dup = Expression::new("a + b", vec!["a", "b"], 4);
337
338        let mut avail_in: HashMap<usize, HashSet<Expression>> = HashMap::new();
339        let mut set = HashSet::new();
340        set.insert(expr_a.clone());
341        set.insert(expr_b.clone());
342        avail_in.insert(0, set);
343
344        let avail_out: HashMap<usize, HashSet<Expression>> = HashMap::new();
345
346        let mut all_exprs = HashSet::new();
347        all_exprs.insert(expr_a.clone());
348        all_exprs.insert(expr_b.clone());
349
350        // ExprInstance list that will trigger redundant_computations()
351        let instances_with_blocks = vec![
352            ExprInstance::new(expr_a.clone(), 0),
353            ExprInstance::new(expr_b.clone(), 0),
354            ExprInstance::new(expr_a_dup.clone(), 0),
355        ];
356
357        AvailableExprsInfo {
358            avail_in,
359            avail_out,
360            all_exprs,
361            entry_block: 0,
362            expr_instances: vec![expr_a.clone(), expr_b.clone(), expr_a_dup.clone()],
363            expr_instances_with_blocks: instances_with_blocks,
364            defs_per_line: HashMap::new(),
365            line_to_block: HashMap::new(),
366            uncertain_exprs: Vec::new(),
367            confidence: Confidence::High,
368        }
369    }
370
371    #[test]
372    fn test_cse_only_flag_hides_blocks() {
373        let args = make_args(true);
374        let result = make_result_with_cse();
375        let output = args.format_text_output(&result);
376
377        // CSE section should always be present
378        assert!(
379            output.contains("CSE Opportunities"),
380            "CSE Opportunities section must be present with --cse-only. Got:\n{}",
381            output,
382        );
383
384        // Per-block section should be hidden
385        assert!(
386            !output.contains("Available expressions by block:"),
387            "Per-block section must be hidden with --cse-only. Got:\n{}",
388            output,
389        );
390    }
391
392    #[test]
393    fn test_default_shows_blocks() {
394        let args = make_args(false);
395        let result = make_result_with_cse();
396        let output = args.format_text_output(&result);
397
398        // CSE section should be present
399        assert!(
400            output.contains("CSE Opportunities"),
401            "CSE Opportunities section must be present by default. Got:\n{}",
402            output,
403        );
404
405        // Per-block section should also be present
406        assert!(
407            output.contains("Available expressions by block:"),
408            "Per-block section must be present by default. Got:\n{}",
409            output,
410        );
411    }
412}