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                        },
52                        ty: None, // Type inference
53                        init: Some(self.target_expr.clone()),
54                    };
55                    new_func.body.stmts.insert(0, let_stmt);
56
57                    ctx.set_ast(fn_id, PureItem::Fn(new_func));
58                    total_replacements += replacements;
59                }
60            }
61        }
62
63        MutationResult {
64            mutation_type: self.mutation_type().to_string(),
65            changes: total_replacements,
66            description: if total_replacements > 0 {
67                format!(
68                    "Introduced variable '{}' ({} replacements)",
69                    self.var_name, total_replacements
70                )
71            } else {
72                format!("No matching expressions found for '{}'", self.var_name)
73            },
74        }
75    }
76}
77
78/// Replace all occurrences of `target` expression with a variable reference in a block.
79/// Returns the number of replacements made.
80fn replace_expr_in_block(block: &mut PureBlock, target: &PureExpr, var_name: &str) -> usize {
81    let mut count = 0;
82    for stmt in &mut block.stmts {
83        count += replace_expr_in_stmt(stmt, target, var_name);
84    }
85    count
86}
87
88/// Replace expressions in a statement
89fn replace_expr_in_stmt(stmt: &mut PureStmt, target: &PureExpr, var_name: &str) -> usize {
90    match stmt {
91        PureStmt::Local { init, .. } => {
92            if let Some(expr) = init {
93                replace_expr(expr, target, var_name)
94            } else {
95                0
96            }
97        }
98        PureStmt::Semi(expr) | PureStmt::Expr(expr) => replace_expr(expr, target, var_name),
99        PureStmt::Item(_) => 0, // Don't descend into nested items
100    }
101}
102
103/// Replace target expression with variable reference, recursively.
104/// Returns the number of replacements made.
105fn replace_expr(expr: &mut PureExpr, target: &PureExpr, var_name: &str) -> usize {
106    // Check if this expression matches the target
107    if expr == target {
108        *expr = PureExpr::Path(var_name.to_string());
109        return 1;
110    }
111
112    // Recursively check children
113    match expr {
114        PureExpr::Binary { left, right, .. } => {
115            replace_expr(left, target, var_name) + replace_expr(right, target, var_name)
116        }
117        PureExpr::Unary { expr: inner, .. } => replace_expr(inner, target, var_name),
118        PureExpr::Call { func, args } => {
119            let mut count = replace_expr(func, target, var_name);
120            for arg in args {
121                count += replace_expr(arg, target, var_name);
122            }
123            count
124        }
125        PureExpr::MethodCall { receiver, args, .. } => {
126            let mut count = replace_expr(receiver, target, var_name);
127            for arg in args {
128                count += replace_expr(arg, target, var_name);
129            }
130            count
131        }
132        PureExpr::Field { expr: inner, .. } => replace_expr(inner, target, var_name),
133        PureExpr::Index { expr: e, index } => {
134            replace_expr(e, target, var_name) + replace_expr(index, target, var_name)
135        }
136        PureExpr::Block { block, .. } => replace_expr_in_block(block, target, var_name),
137        PureExpr::If {
138            cond,
139            then_branch,
140            else_branch,
141        } => {
142            let mut count = replace_expr(cond, target, var_name);
143            count += replace_expr_in_block(then_branch, target, var_name);
144            if let Some(else_expr) = else_branch {
145                count += replace_expr(else_expr, target, var_name);
146            }
147            count
148        }
149        PureExpr::Match { expr: e, arms } => {
150            let mut count = replace_expr(e, target, var_name);
151            for arm in arms {
152                count += replace_expr(&mut arm.body, target, var_name);
153                if let Some(guard) = &mut arm.guard {
154                    count += replace_expr(guard, target, var_name);
155                }
156            }
157            count
158        }
159        PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
160            replace_expr_in_block(block, target, var_name)
161        }
162        PureExpr::For { expr: e, body, .. } => {
163            replace_expr(e, target, var_name) + replace_expr_in_block(body, target, var_name)
164        }
165        PureExpr::Return(Some(inner))
166        | PureExpr::Break {
167            expr: Some(inner), ..
168        } => replace_expr(inner, target, var_name),
169        PureExpr::Closure { body, .. } => replace_expr(body, target, var_name),
170        PureExpr::Struct { fields, .. } => {
171            let mut count = 0;
172            for (_, field_expr) in fields {
173                count += replace_expr(field_expr, target, var_name);
174            }
175            count
176        }
177        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
178            let mut count = 0;
179            for e in exprs {
180                count += replace_expr(e, target, var_name);
181            }
182            count
183        }
184        PureExpr::Ref { expr: inner, .. } => replace_expr(inner, target, var_name),
185        PureExpr::Await(inner) | PureExpr::Try(inner) => replace_expr(inner, target, var_name),
186        PureExpr::Range { start, end, .. } => {
187            let mut count = 0;
188            if let Some(s) = start {
189                count += replace_expr(s, target, var_name);
190            }
191            if let Some(e) = end {
192                count += replace_expr(e, target, var_name);
193            }
194            count
195        }
196        PureExpr::Cast { expr: inner, .. } => replace_expr(inner, target, var_name),
197        PureExpr::Let { expr: inner, .. } => replace_expr(inner, target, var_name),
198        PureExpr::Async { body, .. } | PureExpr::Unsafe(body) => {
199            replace_expr_in_block(body, target, var_name)
200        }
201        PureExpr::Repeat { expr: e, len } => {
202            replace_expr(e, target, var_name) + replace_expr(len, target, var_name)
203        }
204        // Leaf nodes - no children to recurse into
205        PureExpr::Lit(_)
206        | PureExpr::Path(_)
207        | PureExpr::Macro { .. }
208        | PureExpr::Return(None)
209        | PureExpr::Break { expr: None, .. }
210        | PureExpr::Continue { .. }
211        | PureExpr::Other(_) => 0,
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::engine::ASTMutationEngine;
219    use ryo_analysis::testing::ContextBuilder;
220
221    #[test]
222    fn test_v2_introduce_variable() {
223        let mut ctx = ContextBuilder::new()
224            .with_file(
225                "src/lib.rs",
226                r#"
227fn compute() -> i32 {
228    let x = 1 + 2;
229    let y = 1 + 2;
230    x + y
231}
232"#,
233            )
234            .build();
235
236        // Create target expression: 1 + 2
237        let target = PureExpr::Binary {
238            op: "+".to_string(),
239            left: Box::new(PureExpr::Lit("1".to_string())),
240            right: Box::new(PureExpr::Lit("2".to_string())),
241        };
242
243        let mutation = IntroduceVariableMutation {
244            target_expr: target,
245            var_name: "sum".to_string(),
246            target_fn: None, // Note: target_fn requires SymbolId, not name
247            is_mut: false,
248        };
249
250        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
251
252        // Should have found and replaced the expression
253        // Note: The actual number depends on how the parser represents the code
254        println!("Result: {:?}", result.result);
255    }
256
257    #[test]
258    fn test_v2_introduce_variable_no_match() {
259        let mut ctx = ContextBuilder::new()
260            .with_file(
261                "src/lib.rs",
262                r#"
263fn simple() -> i32 {
264    42
265}
266"#,
267            )
268            .build();
269
270        let target = PureExpr::Binary {
271            op: "+".to_string(),
272            left: Box::new(PureExpr::Lit("1".to_string())),
273            right: Box::new(PureExpr::Lit("2".to_string())),
274        };
275
276        let mutation = IntroduceVariableMutation {
277            target_expr: target,
278            var_name: "sum".to_string(),
279            target_fn: None,
280            is_mut: false,
281        };
282
283        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
284
285        // No matching expression should be found
286        assert_eq!(result.result.changes, 0);
287    }
288}