Skip to main content

ryo_executor/engine/impls/
introduce_variable.rs

1//! V2 ASTRegApply implementation for IntroduceVariableMutation
2//!
3//! Extracts complex expressions into named variables:
4//! - `foo(a + b * c, a + b * c)` → `let x = a + b * c; foo(x, x)`
5//!
6//! # Implementation Strategy
7//!
8//! 1. Find target function(s) in the registry
9//! 2. Recursively traverse the function body to find matching expressions
10//! 3. Replace matching expressions with variable references
11//! 4. Insert a `let` statement at the beginning of the function body
12
13use ryo_analysis::SymbolKind;
14use ryo_mutations::idiom::IntroduceVariableMutation;
15use ryo_mutations::{Mutation, MutationResult};
16use ryo_source::pure::{PureBlock, PureExpr, PureItem, PurePattern, PureStmt};
17
18use crate::engine::{ASTMutationContext, ASTRegApply};
19
20impl ASTRegApply for IntroduceVariableMutation {
21    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
22        let mut total_replacements = 0;
23
24        // Collect function IDs to modify
25        let fn_ids: Vec<_> = if let Some(target_id) = self.target_fn {
26            // Target function specified: direct lookup
27            vec![target_id]
28        } else {
29            // No target: collect all functions
30            ctx.symbol_registry
31                .iter()
32                .filter(|(id, _)| {
33                    matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function))
34                })
35                .map(|(id, _)| id)
36                .collect()
37        };
38
39        for fn_id in fn_ids {
40            if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
41                let mut new_func = func.clone();
42                let replacements =
43                    replace_expr_in_block(&mut new_func.body, &self.target_expr, &self.var_name);
44
45                if replacements > 0 {
46                    // Insert the variable declaration at the beginning
47                    let let_stmt = PureStmt::Local {
48                        pattern: PurePattern::Ident {
49                            name: self.var_name.clone(),
50                            is_mut: self.is_mut,
51                            by_ref: false,
52                        },
53                        ty: None, // Type inference
54                        init: Some(self.target_expr.clone()),
55                        else_branch: None,
56                    };
57                    new_func.body.stmts.insert(0, let_stmt);
58
59                    ctx.set_ast(fn_id, PureItem::Fn(new_func));
60                    total_replacements += replacements;
61                }
62            }
63        }
64
65        MutationResult {
66            mutation_type: self.mutation_type().to_string(),
67            changes: total_replacements,
68            description: if total_replacements > 0 {
69                format!(
70                    "Introduced variable '{}' ({} replacements)",
71                    self.var_name, total_replacements
72                )
73            } else {
74                format!("No matching expressions found for '{}'", self.var_name)
75            },
76        }
77    }
78}
79
80/// Replace all occurrences of `target` expression with a variable reference in a block.
81/// Returns the number of replacements made.
82fn replace_expr_in_block(block: &mut PureBlock, target: &PureExpr, var_name: &str) -> usize {
83    let mut count = 0;
84    for stmt in &mut block.stmts {
85        count += replace_expr_in_stmt(stmt, target, var_name);
86    }
87    count
88}
89
90/// Replace expressions in a statement
91fn replace_expr_in_stmt(stmt: &mut PureStmt, target: &PureExpr, var_name: &str) -> usize {
92    match stmt {
93        PureStmt::Local { init, .. } => {
94            if let Some(expr) = init {
95                replace_expr(expr, target, var_name)
96            } else {
97                0
98            }
99        }
100        PureStmt::Semi(expr) | PureStmt::Expr(expr) => replace_expr(expr, target, var_name),
101        PureStmt::Item(_) => 0, // Don't descend into nested items
102        // Verbatim is opaque raw bytes — leaf (B-3-cont).
103        PureStmt::Verbatim(_) => 0,
104    }
105}
106
107/// Replace target expression with variable reference, recursively.
108/// Returns the number of replacements made.
109fn replace_expr(expr: &mut PureExpr, target: &PureExpr, var_name: &str) -> usize {
110    // Check if this expression matches the target
111    if expr == target {
112        *expr = PureExpr::Path(var_name.to_string());
113        return 1;
114    }
115
116    // Recursively check children
117    match expr {
118        PureExpr::Binary { left, right, .. } => {
119            replace_expr(left, target, var_name) + replace_expr(right, target, var_name)
120        }
121        PureExpr::Unary { expr: inner, .. } => replace_expr(inner, target, var_name),
122        PureExpr::Call { func, args } => {
123            let mut count = replace_expr(func, target, var_name);
124            for arg in args {
125                count += replace_expr(arg, target, var_name);
126            }
127            count
128        }
129        PureExpr::MethodCall { receiver, args, .. } => {
130            let mut count = replace_expr(receiver, target, var_name);
131            for arg in args {
132                count += replace_expr(arg, target, var_name);
133            }
134            count
135        }
136        PureExpr::Field { expr: inner, .. } => replace_expr(inner, target, var_name),
137        PureExpr::Index { expr: e, index } => {
138            replace_expr(e, target, var_name) + replace_expr(index, target, var_name)
139        }
140        PureExpr::Block { block, .. } => replace_expr_in_block(block, target, var_name),
141        PureExpr::If {
142            cond,
143            then_branch,
144            else_branch,
145        } => {
146            let mut count = replace_expr(cond, target, var_name);
147            count += replace_expr_in_block(then_branch, target, var_name);
148            if let Some(else_expr) = else_branch {
149                count += replace_expr(else_expr, target, var_name);
150            }
151            count
152        }
153        PureExpr::Match { expr: e, arms } => {
154            let mut count = replace_expr(e, target, var_name);
155            for arm in arms {
156                count += replace_expr(&mut arm.body, target, var_name);
157                if let Some(guard) = &mut arm.guard {
158                    count += replace_expr(guard, target, var_name);
159                }
160            }
161            count
162        }
163        PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
164            replace_expr_in_block(block, target, var_name)
165        }
166        PureExpr::For { expr: e, body, .. } => {
167            replace_expr(e, target, var_name) + replace_expr_in_block(body, target, var_name)
168        }
169        PureExpr::Return(Some(inner))
170        | PureExpr::Break {
171            expr: Some(inner), ..
172        } => replace_expr(inner, target, var_name),
173        PureExpr::Closure { body, .. } => replace_expr(body, target, var_name),
174        PureExpr::Struct { fields, .. } => {
175            let mut count = 0;
176            for (_, field_expr) in fields {
177                count += replace_expr(field_expr, target, var_name);
178            }
179            count
180        }
181        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
182            let mut count = 0;
183            for e in exprs {
184                count += replace_expr(e, target, var_name);
185            }
186            count
187        }
188        PureExpr::Ref { expr: inner, .. } => replace_expr(inner, target, var_name),
189        PureExpr::Await(inner) | PureExpr::Try(inner) => replace_expr(inner, target, var_name),
190        PureExpr::Range { start, end, .. } => {
191            let mut count = 0;
192            if let Some(s) = start {
193                count += replace_expr(s, target, var_name);
194            }
195            if let Some(e) = end {
196                count += replace_expr(e, target, var_name);
197            }
198            count
199        }
200        PureExpr::Cast { expr: inner, .. } => replace_expr(inner, target, var_name),
201        PureExpr::Let { expr: inner, .. } => replace_expr(inner, target, var_name),
202        PureExpr::Async { body, .. } | PureExpr::Unsafe(body) => {
203            replace_expr_in_block(body, target, var_name)
204        }
205        PureExpr::Repeat { expr: e, len } => {
206            replace_expr(e, target, var_name) + replace_expr(len, target, var_name)
207        }
208        // Leaf nodes - no children to recurse into
209        PureExpr::Lit(_)
210        | PureExpr::Path(_)
211        | PureExpr::Macro { .. }
212        | PureExpr::Return(None)
213        | PureExpr::Break { expr: None, .. }
214        | PureExpr::Continue { .. }
215        | PureExpr::Other(_)
216        // Verbatim is opaque raw bytes — no inner expressions for
217        // introduce-variable to inspect (B-3 carrier).
218        | PureExpr::Verbatim(_) => 0,
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::engine::ASTMutationEngine;
226    use ryo_analysis::testing::ContextBuilder;
227
228    #[test]
229    fn test_v2_introduce_variable() {
230        let mut ctx = ContextBuilder::new()
231            .with_file(
232                "src/lib.rs",
233                r#"
234fn compute() -> i32 {
235    let x = 1 + 2;
236    let y = 1 + 2;
237    x + y
238}
239"#,
240            )
241            .build();
242
243        // Create target expression: 1 + 2
244        let target = PureExpr::Binary {
245            op: "+".to_string(),
246            left: Box::new(PureExpr::Lit("1".to_string())),
247            right: Box::new(PureExpr::Lit("2".to_string())),
248        };
249
250        let mutation = IntroduceVariableMutation {
251            target_expr: target,
252            var_name: "sum".to_string(),
253            target_fn: None, // Note: target_fn requires SymbolId, not name
254            is_mut: false,
255        };
256
257        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
258
259        // Should have found and replaced the expression
260        // Note: The actual number depends on how the parser represents the code
261        println!("Result: {:?}", result.result);
262    }
263
264    #[test]
265    fn test_v2_introduce_variable_no_match() {
266        let mut ctx = ContextBuilder::new()
267            .with_file(
268                "src/lib.rs",
269                r#"
270fn simple() -> i32 {
271    42
272}
273"#,
274            )
275            .build();
276
277        let target = PureExpr::Binary {
278            op: "+".to_string(),
279            left: Box::new(PureExpr::Lit("1".to_string())),
280            right: Box::new(PureExpr::Lit("2".to_string())),
281        };
282
283        let mutation = IntroduceVariableMutation {
284            target_expr: target,
285            var_name: "sum".to_string(),
286            target_fn: None,
287            is_mut: false,
288        };
289
290        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
291
292        // No matching expression should be found
293        assert_eq!(result.result.changes, 0);
294    }
295}