1use 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#[derive(Debug, Args)]
34pub struct AvailableArgs {
35 pub file: PathBuf,
37
38 pub function: String,
40
41 #[arg(long, short = 'l')]
43 pub lang: Option<Language>,
44
45 #[arg(long)]
47 pub check: Option<String>,
48
49 #[arg(long)]
51 pub at_line: Option<usize>,
52
53 #[arg(long)]
55 pub killed_by: Option<String>,
56
57 #[arg(long)]
59 pub cse_only: bool,
60}
61
62impl AvailableArgs {
63 pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
65 use crate::output::OutputWriter;
66
67 let writer = OutputWriter::new(format, quiet);
68
69 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 if !self.file.exists() {
82 return Err(anyhow::anyhow!("File not found: {}", self.file.display()));
83 }
84
85 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 let cfg = get_cfg_context(
91 self.file.to_str().unwrap_or_default(),
92 &self.function,
93 language,
94 )?;
95
96 let dfg = get_dfg_context(
98 self.file.to_str().unwrap_or_default(),
99 &self.function,
100 language,
101 )?;
102
103 let result = compute_available_exprs_with_source_and_lang(
105 &cfg,
106 &dfg,
107 &source_lines,
108 Some(language),
109 )?;
110
111 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 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 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 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 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 let mut killers = Vec::new();
222
223 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 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 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 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 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 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 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 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 assert!(
379 output.contains("CSE Opportunities"),
380 "CSE Opportunities section must be present with --cse-only. Got:\n{}",
381 output,
382 );
383
384 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 assert!(
400 output.contains("CSE Opportunities"),
401 "CSE Opportunities section must be present by default. Got:\n{}",
402 output,
403 );
404
405 assert!(
407 output.contains("Available expressions by block:"),
408 "Per-block section must be present by default. Got:\n{}",
409 output,
410 );
411 }
412}