Skip to main content

synwire_agent/dataflow/
tracer.rs

1//! Heuristic dataflow tracer using assignment pattern matching.
2
3/// A dataflow origin: where a value comes from.
4#[non_exhaustive]
5pub struct DataflowOrigin {
6    /// Source file.
7    pub file: String,
8    /// Line number (1-based).
9    pub line: u32,
10    /// Kind of origin: `"definition"`, `"assignment"`, `"parameter"`, or `"return"`.
11    pub kind: String,
12    /// Code snippet at this origin.
13    pub snippet: String,
14}
15
16/// A hop in the dataflow trace.
17#[non_exhaustive]
18pub struct DataflowHop {
19    /// Origin at this hop.
20    pub origin: DataflowOrigin,
21    /// Hop number (0 = immediate assignment site).
22    pub depth: u32,
23}
24
25/// Traces data flow for a variable up to `max_hops` hops backward.
26///
27/// Uses heuristic pattern matching (assignment operators, `let` bindings).
28/// For a full implementation, integrate with LSP type inference.
29pub struct DataflowTracer {
30    /// Maximum number of backward hops to trace.
31    pub max_hops: u32,
32}
33
34impl DataflowTracer {
35    /// Create a tracer with the given hop limit.
36    pub const fn new(max_hops: u32) -> Self {
37        Self { max_hops }
38    }
39
40    /// Trace dataflow for a variable in the given source text.
41    ///
42    /// Returns origins found within [`DataflowTracer::max_hops`] backward hops.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use synwire_agent::dataflow::DataflowTracer;
48    /// let tracer = DataflowTracer::new(5);
49    /// let source = "let x = 5;\nlet y = x + 1;\nx = compute();\n";
50    /// let hops = tracer.trace(source, "x", "test.rs");
51    /// assert!(!hops.is_empty());
52    /// ```
53    pub fn trace(&self, source: &str, variable: &str, file: &str) -> Vec<DataflowHop> {
54        let mut hops = Vec::new();
55        let assign_pattern1 = format!("{variable} =");
56        let assign_pattern2 = format!("{variable}=");
57        let let_pattern = format!("let {variable}");
58
59        for (line_idx, line) in source.lines().enumerate() {
60            if line.contains(&assign_pattern1)
61                || line.contains(&assign_pattern2)
62                || line.contains(&let_pattern)
63            {
64                let kind = if line.contains("let ") {
65                    "definition".to_owned()
66                } else {
67                    "assignment".to_owned()
68                };
69                hops.push(DataflowHop {
70                    origin: DataflowOrigin {
71                        file: file.to_owned(),
72                        line: u32::try_from(line_idx + 1).unwrap_or(u32::MAX),
73                        kind,
74                        snippet: line.trim().to_owned(),
75                    },
76                    depth: 0,
77                });
78            }
79            if hops.len() >= self.max_hops as usize {
80                break;
81            }
82        }
83        hops
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn dataflow_finds_assignment() {
93        let tracer = DataflowTracer::new(5);
94        let source = "let x = 5;\nlet y = x + 1;\nx = compute();\n";
95        let hops = tracer.trace(source, "x", "test.rs");
96        assert!(!hops.is_empty());
97        assert!(hops.iter().any(|h| h.origin.kind == "definition"));
98    }
99
100    #[test]
101    fn dataflow_respects_max_hops() {
102        let tracer = DataflowTracer::new(2);
103        let source = "x = 1;\nx = 2;\nx = 3;\nx = 4;\n";
104        let hops = tracer.trace(source, "x", "test.rs");
105        assert!(hops.len() <= 2);
106    }
107
108    #[test]
109    fn dataflow_no_match_returns_empty() {
110        let tracer = DataflowTracer::new(5);
111        let source = "let y = 10;\nz = 20;\n";
112        let hops = tracer.trace(source, "nonexistent_var", "test.rs");
113        assert!(hops.is_empty());
114    }
115}