Skip to main content

tldr_cli/commands/
slice.rs

1//! Slice command - Program slicing
2//!
3//! Computes backward or forward program slices from a line.
4//! Auto-routes through daemon when available for ~35x speedup.
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::Args;
10use serde::{Deserialize, Serialize};
11
12use tldr_core::{get_slice_rich, Language, SliceDirection};
13
14use crate::commands::daemon_router::{params_with_file_function_line, try_daemon_route};
15use crate::output::{OutputFormat, OutputWriter};
16
17/// Compute program slice from a line
18#[derive(Debug, Args)]
19pub struct SliceArgs {
20    /// Source file path
21    pub file: PathBuf,
22
23    /// Function name containing the line
24    pub function: String,
25
26    /// Line number to slice from
27    pub line: u32,
28
29    /// Slice direction: backward (what affects this line) or forward (what this line affects)
30    #[arg(long, short = 'd', default_value = "backward")]
31    pub direction: SliceDirectionArg,
32
33    /// Variable to filter by (optional - traces all if not specified)
34    #[arg(long)]
35    pub variable: Option<String>,
36
37    /// Programming language (auto-detected from file extension if not specified)
38    #[arg(long, short = 'l')]
39    pub lang: Option<Language>,
40}
41
42/// CLI wrapper for slice direction
43#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
44pub enum SliceDirectionArg {
45    /// Backward slice - what affects this line?
46    #[default]
47    Backward,
48    /// Forward slice - what does this line affect?
49    Forward,
50}
51
52impl From<SliceDirectionArg> for SliceDirection {
53    fn from(arg: SliceDirectionArg) -> Self {
54        match arg {
55            SliceDirectionArg::Backward => SliceDirection::Backward,
56            SliceDirectionArg::Forward => SliceDirection::Forward,
57        }
58    }
59}
60
61/// Rich slice line for output
62#[derive(Debug, Serialize, Deserialize)]
63struct SliceLine {
64    line: u32,
65    code: String,
66    #[serde(skip_serializing_if = "Vec::is_empty")]
67    definitions: Vec<String>,
68    #[serde(skip_serializing_if = "Vec::is_empty")]
69    uses: Vec<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    dep_type: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    dep_label: Option<String>,
74}
75
76/// Edge in slice output
77#[derive(Debug, Serialize, Deserialize)]
78struct SliceEdgeOutput {
79    from_line: u32,
80    to_line: u32,
81    dep_type: String,
82    label: String,
83}
84
85/// Slice result output format (backward-compatible: keeps `lines` as Vec<u32>)
86#[derive(Debug, Serialize, Deserialize)]
87struct SliceOutput {
88    file: PathBuf,
89    function: String,
90    criterion_line: u32,
91    direction: String,
92    variable: Option<String>,
93    /// Bare line numbers (backward-compatible)
94    lines: Vec<u32>,
95    /// Rich line data with code and metadata
96    #[serde(skip_serializing_if = "Vec::is_empty")]
97    slice_lines: Vec<SliceLine>,
98    /// Dependency edges within the slice
99    #[serde(skip_serializing_if = "Vec::is_empty")]
100    edges: Vec<SliceEdgeOutput>,
101    line_count: usize,
102}
103
104/// Legacy daemon output (old format without rich data)
105#[derive(Debug, Serialize, Deserialize)]
106struct LegacySliceOutput {
107    file: PathBuf,
108    function: String,
109    criterion_line: u32,
110    direction: String,
111    variable: Option<String>,
112    lines: Vec<u32>,
113    line_count: usize,
114}
115
116impl SliceArgs {
117    /// Run the slice command
118    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
119        let writer = OutputWriter::new(format, quiet);
120
121        // Determine language from file extension or argument
122        let language = self
123            .lang
124            .unwrap_or_else(|| Language::from_path(&self.file).unwrap_or(Language::Python));
125
126        let direction: SliceDirection = self.direction.into();
127        let direction_str = match direction {
128            SliceDirection::Backward => "backward",
129            SliceDirection::Forward => "forward",
130        };
131
132        // Try daemon first for cached result (use file's parent as project root)
133        let project = self.file.parent().unwrap_or(&self.file);
134        if let Some(output) = try_daemon_route::<LegacySliceOutput>(
135            project,
136            "slice",
137            params_with_file_function_line(&self.file, &self.function, self.line),
138        ) {
139            // Daemon returns legacy format -- enrich with source code if possible
140            let source_lines = read_file_lines(&self.file);
141            if writer.is_text() {
142                let mut text = String::new();
143                text.push_str(&format!(
144                    "Program Slice ({} from line {})\n",
145                    output.direction, output.criterion_line
146                ));
147                text.push_str(&format!(
148                    "Function: {}::{}\n",
149                    output.file.display(),
150                    output.function
151                ));
152                if let Some(var) = &output.variable {
153                    text.push_str(&format!("Variable: {}\n", var));
154                }
155                text.push_str(&format!(
156                    "\nSlice contains {} lines:\n\n",
157                    output.lines.len()
158                ));
159                for &line_num in &output.lines {
160                    let code = source_lines
161                        .get((line_num as usize).wrapping_sub(1))
162                        .map(|s| s.trim_end())
163                        .unwrap_or("");
164                    let marker = if line_num == output.criterion_line {
165                        ">"
166                    } else {
167                        " "
168                    };
169                    let criterion_flag = if line_num == output.criterion_line {
170                        "  <-- criterion"
171                    } else {
172                        ""
173                    };
174                    text.push_str(&format!(
175                        "{} {:>5} | {}{}\n",
176                        marker, line_num, code, criterion_flag
177                    ));
178                }
179                writer.write_text(&text)?;
180                return Ok(());
181            } else {
182                // Convert legacy to new format for JSON
183                let slice_lines: Vec<SliceLine> = output
184                    .lines
185                    .iter()
186                    .map(|&l| {
187                        let code = source_lines
188                            .get((l as usize).wrapping_sub(1))
189                            .map(|s| s.trim_end().to_string())
190                            .unwrap_or_default();
191                        SliceLine {
192                            line: l,
193                            code,
194                            definitions: Vec::new(),
195                            uses: Vec::new(),
196                            dep_type: None,
197                            dep_label: None,
198                        }
199                    })
200                    .collect();
201                let rich_output = SliceOutput {
202                    file: output.file,
203                    function: output.function,
204                    criterion_line: output.criterion_line,
205                    direction: output.direction,
206                    variable: output.variable,
207                    line_count: output.line_count,
208                    lines: output.lines,
209                    slice_lines,
210                    edges: Vec::new(),
211                };
212                writer.write(&rich_output)?;
213                return Ok(());
214            }
215        }
216
217        // Fallback to direct compute with rich output
218        writer.progress(&format!(
219            "Computing {} slice for line {} in {}::{}...",
220            direction_str,
221            self.line,
222            self.file.display(),
223            self.function
224        ));
225
226        // Get rich slice
227        let rich = get_slice_rich(
228            self.file.to_str().unwrap_or_default(),
229            &self.function,
230            self.line,
231            direction,
232            self.variable.as_deref(),
233            language,
234        )?;
235
236        // Build backward-compatible line list
237        let lines: Vec<u32> = rich.nodes.iter().map(|n| n.line).collect();
238
239        // Build rich line data
240        let slice_lines: Vec<SliceLine> = rich
241            .nodes
242            .iter()
243            .map(|n| SliceLine {
244                line: n.line,
245                code: n.code.clone(),
246                definitions: n.definitions.clone(),
247                uses: n.uses.clone(),
248                dep_type: n.dep_type.clone(),
249                dep_label: n.dep_label.clone(),
250            })
251            .collect();
252
253        // Build edge output
254        let edges: Vec<SliceEdgeOutput> = rich
255            .edges
256            .iter()
257            .map(|e| SliceEdgeOutput {
258                from_line: e.from_line,
259                to_line: e.to_line,
260                dep_type: e.dep_type.clone(),
261                label: e.label.clone(),
262            })
263            .collect();
264
265        let data_count = edges.iter().filter(|e| e.dep_type == "data").count();
266        let ctrl_count = edges.iter().filter(|e| e.dep_type == "control").count();
267
268        let output = SliceOutput {
269            file: self.file.clone(),
270            function: self.function.clone(),
271            criterion_line: self.line,
272            direction: direction_str.to_string(),
273            variable: self.variable.clone(),
274            line_count: lines.len(),
275            lines,
276            slice_lines,
277            edges,
278        };
279
280        // Output based on format
281        if writer.is_text() {
282            let text = format_rich_text(&output, data_count, ctrl_count);
283            writer.write_text(&text)?;
284        } else {
285            writer.write(&output)?;
286        }
287
288        Ok(())
289    }
290}
291
292/// Format rich slice as compact text for LLM consumption
293fn format_rich_text(output: &SliceOutput, data_count: usize, ctrl_count: usize) -> String {
294    let mut text = String::new();
295
296    text.push_str(&format!(
297        "Program Slice ({} from line {})\n",
298        output.direction, output.criterion_line
299    ));
300    text.push_str(&format!(
301        "Function: {}::{}\n",
302        output.file.display(),
303        output.function
304    ));
305    if let Some(var) = &output.variable {
306        text.push_str(&format!("Variable: {}\n", var));
307    }
308
309    // Count non-blank lines for accurate summary
310    let non_blank_count = output
311        .slice_lines
312        .iter()
313        .filter(|sl| !sl.code.trim().is_empty())
314        .count();
315
316    // Summary line with dep counts
317    if data_count > 0 || ctrl_count > 0 {
318        text.push_str(&format!(
319            "\nSlice contains {} lines ({} data deps, {} control deps):\n\n",
320            non_blank_count, data_count, ctrl_count
321        ));
322    } else {
323        text.push_str(&format!("\nSlice contains {} lines:\n\n", non_blank_count));
324    }
325
326    // Code lines with annotations
327    // Track previous defs/uses to avoid repeating identical annotations
328    // (PDG nodes span multiple lines but carry one set of defs/uses)
329    let mut prev_defs: Option<&Vec<String>> = None;
330    let mut prev_uses: Option<&Vec<String>> = None;
331
332    for sl in &output.slice_lines {
333        // Skip blank lines — they waste tokens and carry no insight
334        if sl.code.trim().is_empty() {
335            continue;
336        }
337
338        let marker = if sl.line == output.criterion_line {
339            ">"
340        } else {
341            " "
342        };
343
344        // Only show defs/uses on the first line of each node span
345        let same_as_prev = prev_defs == Some(&sl.definitions) && prev_uses == Some(&sl.uses);
346
347        let mut annotations = Vec::new();
348        if !same_as_prev {
349            if !sl.definitions.is_empty() {
350                annotations.push(format!("[defines: {}]", sl.definitions.join(", ")));
351            }
352            if !sl.uses.is_empty() {
353                annotations.push(format!("[uses: {}]", sl.uses.join(", ")));
354            }
355        }
356        if let Some(dt) = &sl.dep_type {
357            if dt == "control" && !same_as_prev {
358                annotations.push("ctrl".to_string());
359            }
360        }
361
362        prev_defs = Some(&sl.definitions);
363        prev_uses = Some(&sl.uses);
364
365        let criterion_flag = if sl.line == output.criterion_line {
366            "  <-- criterion"
367        } else {
368            ""
369        };
370
371        let annotation_str = if annotations.is_empty() {
372            String::new()
373        } else {
374            format!("     {}", annotations.join(" "))
375        };
376
377        text.push_str(&format!(
378            "{} {:>5} | {}{}{}\n",
379            marker, sl.line, sl.code, annotation_str, criterion_flag
380        ));
381    }
382
383    // Dependencies section
384    if !output.edges.is_empty() {
385        text.push_str("\nDependencies:\n");
386        for edge in &output.edges {
387            if edge.dep_type == "data" && !edge.label.is_empty() {
388                text.push_str(&format!(
389                    "  {}@{} <- {}@{} (data: {})\n",
390                    edge.label, edge.to_line, edge.label, edge.from_line, edge.label
391                ));
392            } else {
393                text.push_str(&format!(
394                    "  {} <- {} ({})\n",
395                    edge.to_line, edge.from_line, edge.dep_type
396                ));
397            }
398        }
399    }
400
401    text
402}
403
404/// Read file lines for source enrichment
405fn read_file_lines(path: &PathBuf) -> Vec<String> {
406    std::fs::read_to_string(path)
407        .map(|c| c.lines().map(|l| l.to_string()).collect())
408        .unwrap_or_default()
409}