Skip to main content

ryo_executor/engine/impls/
stmt_ops.rs

1//! V2 ASTRegApply implementations for statement/expression operations
2//!
3//! - ReplaceExpr: Replace expressions matching a pattern
4//! - ReplaceExprAt: Replace expression at specific position indices
5//! - WrapExpr: Wrap expressions with macro calls
6//! - RemoveStatement: Remove statements matching a pattern
7//! - InsertStatement: Insert statements at specific positions
8//! - ReplaceStatement: Replace statements matching a pattern
9//!
10//! # Implementation Strategy
11//!
12//! All operations work on function bodies in the ASTRegistry:
13//! 1. Find target function(s)
14//! 2. Traverse/modify statements or expressions
15//! 3. Update the AST in registry
16
17use ryo_analysis::SymbolKind;
18use ryo_mutations::basic::stmt::{
19    InsertPosition, InsertStatementMutation, RemoveStatementMutation, ReplaceExprAtMutation,
20    ReplaceExprMutation, ReplaceStatementMutation, WrapExprMutation,
21};
22use ryo_mutations::{Mutation, MutationResult};
23use ryo_source::pure::{MacroDelimiter, PureBlock, PureExpr, PureItem, PureStmt};
24
25use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
26
27// =============================================================================
28// ReplaceExprMutation
29// =============================================================================
30
31impl ASTRegApply for ReplaceExprMutation {
32    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
33        let fn_id = self.target_fn;
34
35        if !matches!(ctx.symbol_registry.kind(fn_id), Some(SymbolKind::Function)) {
36            return MutationResult {
37                mutation_type: self.mutation_type().to_string(),
38                changes: 0,
39                description: format!("Target function '{:?}' not found", fn_id),
40            };
41        }
42
43        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
44            let mut new_func = func.clone();
45            let replacements = replace_expr_in_block(
46                &mut new_func.body,
47                &self.old_expr,
48                &self.new_expr,
49                self.replace_all,
50            );
51
52            if replacements > 0 {
53                ctx.set_ast(fn_id, PureItem::Fn(new_func));
54                ctx.emit_modified(fn_id, ModificationType::BodyModified);
55                return MutationResult {
56                    mutation_type: self.mutation_type().to_string(),
57                    changes: replacements,
58                    description: format!("Replaced {} expression(s)", replacements),
59                };
60            }
61        }
62
63        MutationResult {
64            mutation_type: self.mutation_type().to_string(),
65            changes: 0,
66            description: "No matching expressions found".to_string(),
67        }
68    }
69}
70
71// =============================================================================
72// WrapExprMutation
73// =============================================================================
74
75impl ASTRegApply for WrapExprMutation {
76    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
77        let fn_id = self.target_fn;
78
79        if !matches!(ctx.symbol_registry.kind(fn_id), Some(SymbolKind::Function)) {
80            return MutationResult {
81                mutation_type: self.mutation_type().to_string(),
82                changes: 0,
83                description: format!("Target function '{:?}' not found", fn_id),
84            };
85        }
86
87        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
88            let mut new_func = func.clone();
89            let wraps = wrap_expr_in_block(
90                &mut new_func.body,
91                &self.target_expr,
92                &self.wrapper_macro,
93                self.wrap_all,
94            );
95
96            if wraps > 0 {
97                ctx.set_ast(fn_id, PureItem::Fn(new_func));
98                ctx.emit_modified(fn_id, ModificationType::BodyModified);
99                return MutationResult {
100                    mutation_type: self.mutation_type().to_string(),
101                    changes: wraps,
102                    description: format!(
103                        "Wrapped {} expression(s) with {}!()",
104                        wraps, self.wrapper_macro
105                    ),
106                };
107            }
108        }
109
110        MutationResult {
111            mutation_type: self.mutation_type().to_string(),
112            changes: 0,
113            description: "No matching expressions found".to_string(),
114        }
115    }
116}
117
118// =============================================================================
119// RemoveStatementMutation
120// =============================================================================
121
122impl ASTRegApply for RemoveStatementMutation {
123    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
124        let fn_id = self.target_fn;
125
126        if !matches!(ctx.symbol_registry.kind(fn_id), Some(SymbolKind::Function)) {
127            return MutationResult {
128                mutation_type: self.mutation_type().to_string(),
129                changes: 0,
130                description: format!("Target function '{:?}' not found", fn_id),
131            };
132        }
133
134        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
135            let mut new_func = func.clone();
136            let removed = remove_stmts_in_block(&mut new_func.body, &self.pattern, self.remove_all);
137
138            if removed > 0 {
139                ctx.set_ast(fn_id, PureItem::Fn(new_func));
140                ctx.emit_modified(fn_id, ModificationType::BodyModified);
141                return MutationResult {
142                    mutation_type: self.mutation_type().to_string(),
143                    changes: removed,
144                    description: format!("Removed {} statement(s)", removed),
145                };
146            }
147        }
148
149        MutationResult {
150            mutation_type: self.mutation_type().to_string(),
151            changes: 0,
152            description: "No matching statements found".to_string(),
153        }
154    }
155}
156
157// =============================================================================
158// InsertStatementMutation
159// =============================================================================
160
161impl ASTRegApply for InsertStatementMutation {
162    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
163        let fn_id = self.target_fn;
164
165        if !matches!(ctx.symbol_registry.kind(fn_id), Some(SymbolKind::Function)) {
166            return MutationResult {
167                mutation_type: self.mutation_type().to_string(),
168                changes: 0,
169                description: format!("Target function '{:?}' not found", fn_id),
170            };
171        }
172
173        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
174            let mut new_func = func.clone();
175
176            let inserted = match self.position {
177                InsertPosition::Start => {
178                    new_func.body.stmts.insert(0, self.stmt.clone());
179                    true
180                }
181                InsertPosition::End => {
182                    // Insert before return statement if present, otherwise at end
183                    let insert_idx = find_return_index(&new_func.body.stmts);
184                    new_func.body.stmts.insert(insert_idx, self.stmt.clone());
185                    true
186                }
187                InsertPosition::BeforePattern | InsertPosition::AfterPattern => {
188                    if let Some(ref reference_stmt) = self.reference_stmt {
189                        insert_relative_to_stmt(
190                            &mut new_func.body.stmts,
191                            &self.stmt,
192                            reference_stmt,
193                            self.position == InsertPosition::AfterPattern,
194                        )
195                    } else {
196                        false
197                    }
198                }
199            };
200
201            if inserted {
202                ctx.set_ast(fn_id, PureItem::Fn(new_func));
203                ctx.emit_modified(fn_id, ModificationType::BodyModified);
204                return MutationResult {
205                    mutation_type: self.mutation_type().to_string(),
206                    changes: 1,
207                    description: format!(
208                        "Inserted statement in '{}' at {:?}",
209                        self.target_fn, self.position
210                    ),
211                };
212            }
213        }
214
215        MutationResult {
216            mutation_type: self.mutation_type().to_string(),
217            changes: 0,
218            description: "Failed to insert statement".to_string(),
219        }
220    }
221}
222
223// =============================================================================
224// ReplaceStatementMutation
225// =============================================================================
226
227impl ASTRegApply for ReplaceStatementMutation {
228    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
229        let fn_id = self.target_fn;
230
231        if !matches!(ctx.symbol_registry.kind(fn_id), Some(SymbolKind::Function)) {
232            return MutationResult {
233                mutation_type: self.mutation_type().to_string(),
234                changes: 0,
235                description: format!("Target function '{:?}' not found", fn_id),
236            };
237        }
238
239        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
240            let mut new_func = func.clone();
241            let replaced =
242                replace_stmts_in_block(&mut new_func.body, &self.old_stmt, &self.new_stmt);
243
244            if replaced > 0 {
245                ctx.set_ast(fn_id, PureItem::Fn(new_func));
246                ctx.emit_modified(fn_id, ModificationType::BodyModified);
247                return MutationResult {
248                    mutation_type: self.mutation_type().to_string(),
249                    changes: replaced,
250                    description: format!("Replaced {} statement(s)", replaced),
251                };
252            }
253        }
254
255        MutationResult {
256            mutation_type: self.mutation_type().to_string(),
257            changes: 0,
258            description: "No matching statements found".to_string(),
259        }
260    }
261}
262
263// =============================================================================
264// Helper functions
265// =============================================================================
266
267/// Replace expressions in a block
268fn replace_expr_in_block(
269    block: &mut PureBlock,
270    old: &PureExpr,
271    new: &PureExpr,
272    replace_all: bool,
273) -> usize {
274    let mut count = 0;
275    for stmt in &mut block.stmts {
276        count += replace_expr_in_stmt(stmt, old, new, replace_all);
277        if !replace_all && count > 0 {
278            return count;
279        }
280    }
281    count
282}
283
284/// Wrap expressions in a block
285fn wrap_expr_in_block(
286    block: &mut PureBlock,
287    target: &PureExpr,
288    wrapper_macro: &str,
289    wrap_all: bool,
290) -> usize {
291    let mut count = 0;
292    for stmt in &mut block.stmts {
293        count += wrap_expr_in_stmt(stmt, target, wrapper_macro, wrap_all);
294        if !wrap_all && count > 0 {
295            return count;
296        }
297    }
298    count
299}
300
301fn wrap_expr_in_stmt(
302    stmt: &mut PureStmt,
303    target: &PureExpr,
304    wrapper_macro: &str,
305    wrap_all: bool,
306) -> usize {
307    match stmt {
308        PureStmt::Local { init, .. } => {
309            if let Some(expr) = init {
310                wrap_expr(expr, target, wrapper_macro, wrap_all)
311            } else {
312                0
313            }
314        }
315        PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
316            wrap_expr(expr, target, wrapper_macro, wrap_all)
317        }
318        PureStmt::Item(_) => 0,
319        // Verbatim is opaque raw bytes — no inner mutation target
320        // for stmt_ops to walk into (B-3-cont carrier).
321        PureStmt::Verbatim(_) => 0,
322    }
323}
324
325fn wrap_expr(expr: &mut PureExpr, target: &PureExpr, wrapper_macro: &str, wrap_all: bool) -> usize {
326    if expr == target {
327        let wrapped = PureExpr::Macro {
328            name: wrapper_macro.to_string(),
329            delimiter: MacroDelimiter::Paren,
330            tokens: format!("{:?}", expr),
331        };
332        *expr = wrapped;
333        return 1;
334    }
335
336    match expr {
337        PureExpr::Binary { left, right, .. } => {
338            let mut c = wrap_expr(left, target, wrapper_macro, wrap_all);
339            if wrap_all || c == 0 {
340                c += wrap_expr(right, target, wrapper_macro, wrap_all);
341            }
342            c
343        }
344        PureExpr::Unary { expr: inner, .. } => wrap_expr(inner, target, wrapper_macro, wrap_all),
345        PureExpr::Call { func, args } => {
346            let mut c = wrap_expr(func, target, wrapper_macro, wrap_all);
347            for arg in args {
348                if wrap_all || c == 0 {
349                    c += wrap_expr(arg, target, wrapper_macro, wrap_all);
350                }
351            }
352            c
353        }
354        PureExpr::MethodCall { receiver, args, .. } => {
355            let mut c = wrap_expr(receiver, target, wrapper_macro, wrap_all);
356            for arg in args {
357                if wrap_all || c == 0 {
358                    c += wrap_expr(arg, target, wrapper_macro, wrap_all);
359                }
360            }
361            c
362        }
363        PureExpr::Field { expr: inner, .. } => wrap_expr(inner, target, wrapper_macro, wrap_all),
364        PureExpr::Index { expr: e, index } => {
365            let mut c = wrap_expr(e, target, wrapper_macro, wrap_all);
366            if wrap_all || c == 0 {
367                c += wrap_expr(index, target, wrapper_macro, wrap_all);
368            }
369            c
370        }
371        PureExpr::Block { block: b, .. } => wrap_expr_in_block(b, target, wrapper_macro, wrap_all),
372        PureExpr::If {
373            cond,
374            then_branch,
375            else_branch,
376        } => {
377            let mut c = wrap_expr(cond, target, wrapper_macro, wrap_all);
378            if wrap_all || c == 0 {
379                c += wrap_expr_in_block(then_branch, target, wrapper_macro, wrap_all);
380            }
381            if let Some(else_expr) = else_branch {
382                if wrap_all || c == 0 {
383                    c += wrap_expr(else_expr, target, wrapper_macro, wrap_all);
384                }
385            }
386            c
387        }
388        PureExpr::Match { expr: e, arms } => {
389            let mut c = wrap_expr(e, target, wrapper_macro, wrap_all);
390            for arm in arms {
391                if wrap_all || c == 0 {
392                    c += wrap_expr(&mut arm.body, target, wrapper_macro, wrap_all);
393                }
394                if let Some(guard) = &mut arm.guard {
395                    if wrap_all || c == 0 {
396                        c += wrap_expr(guard, target, wrapper_macro, wrap_all);
397                    }
398                }
399            }
400            c
401        }
402        PureExpr::Loop { body: b, .. } => wrap_expr_in_block(b, target, wrapper_macro, wrap_all),
403        PureExpr::While { cond, body, .. } => {
404            let mut c = wrap_expr(cond, target, wrapper_macro, wrap_all);
405            if wrap_all || c == 0 {
406                c += wrap_expr_in_block(body, target, wrapper_macro, wrap_all);
407            }
408            c
409        }
410        PureExpr::For { expr: e, body, .. } => {
411            let mut c = wrap_expr(e, target, wrapper_macro, wrap_all);
412            if wrap_all || c == 0 {
413                c += wrap_expr_in_block(body, target, wrapper_macro, wrap_all);
414            }
415            c
416        }
417        PureExpr::Return(Some(inner))
418        | PureExpr::Break {
419            expr: Some(inner), ..
420        } => wrap_expr(inner, target, wrapper_macro, wrap_all),
421        PureExpr::Closure { body, .. } => wrap_expr(body, target, wrapper_macro, wrap_all),
422        PureExpr::Struct { fields, .. } => {
423            let mut c = 0;
424            for (_, field_expr) in fields {
425                if wrap_all || c == 0 {
426                    c += wrap_expr(field_expr, target, wrapper_macro, wrap_all);
427                }
428            }
429            c
430        }
431        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
432            let mut c = 0;
433            for e in exprs {
434                if wrap_all || c == 0 {
435                    c += wrap_expr(e, target, wrapper_macro, wrap_all);
436                }
437            }
438            c
439        }
440        PureExpr::Ref { expr: inner, .. } => wrap_expr(inner, target, wrapper_macro, wrap_all),
441        PureExpr::Await(inner) | PureExpr::Try(inner) => {
442            wrap_expr(inner, target, wrapper_macro, wrap_all)
443        }
444        PureExpr::Range { start, end, .. } => {
445            let mut c = 0;
446            if let Some(s) = start {
447                c += wrap_expr(s, target, wrapper_macro, wrap_all);
448            }
449            if let Some(e) = end {
450                if wrap_all || c == 0 {
451                    c += wrap_expr(e, target, wrapper_macro, wrap_all);
452                }
453            }
454            c
455        }
456        PureExpr::Cast { expr: inner, .. } => wrap_expr(inner, target, wrapper_macro, wrap_all),
457        PureExpr::Let { expr: inner, .. } => wrap_expr(inner, target, wrapper_macro, wrap_all),
458        PureExpr::Async { body, .. } | PureExpr::Unsafe(body) => {
459            wrap_expr_in_block(body, target, wrapper_macro, wrap_all)
460        }
461        PureExpr::Repeat { expr: e, len } => {
462            let mut c = wrap_expr(e, target, wrapper_macro, wrap_all);
463            if wrap_all || c == 0 {
464                c += wrap_expr(len, target, wrapper_macro, wrap_all);
465            }
466            c
467        }
468        PureExpr::Lit(_)
469        | PureExpr::Path(_)
470        | PureExpr::Macro { .. }
471        | PureExpr::Return(None)
472        | PureExpr::Break { expr: None, .. }
473        | PureExpr::Continue { .. }
474        | PureExpr::Other(_)
475        // Verbatim is opaque raw bytes — leaf for stmt_ops (B-3).
476        | PureExpr::Verbatim(_) => 0,
477    }
478}
479
480fn replace_expr_in_stmt(
481    stmt: &mut PureStmt,
482    old: &PureExpr,
483    new: &PureExpr,
484    replace_all: bool,
485) -> usize {
486    match stmt {
487        PureStmt::Local { init, .. } => {
488            if let Some(expr) = init {
489                replace_expr(expr, old, new, replace_all)
490            } else {
491                0
492            }
493        }
494        PureStmt::Semi(expr) | PureStmt::Expr(expr) => replace_expr(expr, old, new, replace_all),
495        PureStmt::Item(_) => 0,
496        // Verbatim is opaque raw bytes — no inner mutation target
497        // for stmt_ops to walk into (B-3-cont carrier).
498        PureStmt::Verbatim(_) => 0,
499    }
500}
501
502fn replace_expr(expr: &mut PureExpr, old: &PureExpr, new: &PureExpr, replace_all: bool) -> usize {
503    if expr == old {
504        *expr = new.clone();
505        return 1;
506    }
507
508    match expr {
509        PureExpr::Binary { left, right, .. } => {
510            let mut c = replace_expr(left, old, new, replace_all);
511            if replace_all || c == 0 {
512                c += replace_expr(right, old, new, replace_all);
513            }
514            c
515        }
516        PureExpr::Unary { expr: inner, .. } => replace_expr(inner, old, new, replace_all),
517        PureExpr::Call { func, args } => {
518            let mut c = replace_expr(func, old, new, replace_all);
519            for arg in args {
520                if replace_all || c == 0 {
521                    c += replace_expr(arg, old, new, replace_all);
522                }
523            }
524            c
525        }
526        PureExpr::MethodCall { receiver, args, .. } => {
527            let mut c = replace_expr(receiver, old, new, replace_all);
528            for arg in args {
529                if replace_all || c == 0 {
530                    c += replace_expr(arg, old, new, replace_all);
531                }
532            }
533            c
534        }
535        PureExpr::Field { expr: inner, .. } => replace_expr(inner, old, new, replace_all),
536        PureExpr::Index { expr: e, index } => {
537            let mut c = replace_expr(e, old, new, replace_all);
538            if replace_all || c == 0 {
539                c += replace_expr(index, old, new, replace_all);
540            }
541            c
542        }
543        PureExpr::Block { block: b, .. } => replace_expr_in_block(b, old, new, replace_all),
544        PureExpr::If {
545            cond,
546            then_branch,
547            else_branch,
548        } => {
549            let mut c = replace_expr(cond, old, new, replace_all);
550            if replace_all || c == 0 {
551                c += replace_expr_in_block(then_branch, old, new, replace_all);
552            }
553            if let Some(else_expr) = else_branch {
554                if replace_all || c == 0 {
555                    c += replace_expr(else_expr, old, new, replace_all);
556                }
557            }
558            c
559        }
560        PureExpr::Match { expr: e, arms } => {
561            let mut c = replace_expr(e, old, new, replace_all);
562            for arm in arms {
563                if replace_all || c == 0 {
564                    c += replace_expr(&mut arm.body, old, new, replace_all);
565                }
566                if let Some(guard) = &mut arm.guard {
567                    if replace_all || c == 0 {
568                        c += replace_expr(guard, old, new, replace_all);
569                    }
570                }
571            }
572            c
573        }
574        PureExpr::Loop { body: b, .. } => replace_expr_in_block(b, old, new, replace_all),
575        PureExpr::While { cond, body, .. } => {
576            let mut c = replace_expr(cond, old, new, replace_all);
577            if replace_all || c == 0 {
578                c += replace_expr_in_block(body, old, new, replace_all);
579            }
580            c
581        }
582        PureExpr::For { expr: e, body, .. } => {
583            let mut c = replace_expr(e, old, new, replace_all);
584            if replace_all || c == 0 {
585                c += replace_expr_in_block(body, old, new, replace_all);
586            }
587            c
588        }
589        PureExpr::Return(Some(inner))
590        | PureExpr::Break {
591            expr: Some(inner), ..
592        } => replace_expr(inner, old, new, replace_all),
593        PureExpr::Closure { body, .. } => replace_expr(body, old, new, replace_all),
594        PureExpr::Struct { fields, .. } => {
595            let mut c = 0;
596            for (_, field_expr) in fields {
597                if replace_all || c == 0 {
598                    c += replace_expr(field_expr, old, new, replace_all);
599                }
600            }
601            c
602        }
603        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
604            let mut c = 0;
605            for e in exprs {
606                if replace_all || c == 0 {
607                    c += replace_expr(e, old, new, replace_all);
608                }
609            }
610            c
611        }
612        PureExpr::Ref { expr: inner, .. } => replace_expr(inner, old, new, replace_all),
613        PureExpr::Await(inner) | PureExpr::Try(inner) => replace_expr(inner, old, new, replace_all),
614        PureExpr::Range { start, end, .. } => {
615            let mut c = 0;
616            if let Some(s) = start {
617                c += replace_expr(s, old, new, replace_all);
618            }
619            if let Some(e) = end {
620                if replace_all || c == 0 {
621                    c += replace_expr(e, old, new, replace_all);
622                }
623            }
624            c
625        }
626        PureExpr::Cast { expr: inner, .. } => replace_expr(inner, old, new, replace_all),
627        PureExpr::Let { expr: inner, .. } => replace_expr(inner, old, new, replace_all),
628        PureExpr::Async { body, .. } | PureExpr::Unsafe(body) => {
629            replace_expr_in_block(body, old, new, replace_all)
630        }
631        PureExpr::Repeat { expr: e, len } => {
632            let mut c = replace_expr(e, old, new, replace_all);
633            if replace_all || c == 0 {
634                c += replace_expr(len, old, new, replace_all);
635            }
636            c
637        }
638        PureExpr::Lit(_)
639        | PureExpr::Path(_)
640        | PureExpr::Macro { .. }
641        | PureExpr::Return(None)
642        | PureExpr::Break { expr: None, .. }
643        | PureExpr::Continue { .. }
644        | PureExpr::Other(_)
645        // Verbatim is opaque raw bytes — leaf for stmt_ops (B-3).
646        | PureExpr::Verbatim(_) => 0,
647    }
648}
649
650/// Check if two statements match structurally (using PartialEq).
651///
652/// Used by `InsertStatement` (BeforePattern/AfterPattern) for **exact** position matching.
653/// InsertStatement needs to identify a specific statement to insert next to,
654/// so structural equality is required (e.g., `let x = compute()` must NOT match `let x = other()`).
655///
656/// This is intentionally different from `stmt_matches_pattern` (used by RemoveStatement),
657/// which uses fuzzy string-based matching appropriate for "remove anything matching this pattern".
658fn stmt_matches(target: &PureStmt, stmt: &PureStmt) -> bool {
659    target == stmt
660}
661
662/// Check if a statement matches a pattern (fuzzy, string-based).
663///
664/// Used by `RemoveStatement` for flexible pattern matching.
665/// Also suitable for any operation where approximate matching is acceptable.
666///
667/// Pattern formats:
668/// - `macro_name!(..)` or `macro_name!` - matches macro calls by name
669/// - `let var_name = ...` - matches let statements by variable name
670/// - Other patterns - substring match against Debug format
671fn stmt_matches_pattern(stmt: &PureStmt, pattern: &str) -> bool {
672    // Check for macro pattern: "name!(..)` or "name!"
673    let macro_name = pattern
674        .strip_suffix("!(..)")
675        .or_else(|| pattern.strip_suffix('!'));
676
677    if let Some(name) = macro_name {
678        // Check if the statement is a macro call with this name
679        match stmt {
680            PureStmt::Semi(PureExpr::Macro { name: macro_n, .. })
681            | PureStmt::Expr(PureExpr::Macro { name: macro_n, .. }) => {
682                return macro_n == name;
683            }
684            _ => {}
685        }
686    }
687
688    // Check for let pattern: "let var_name = ..." or "let var_name"
689    if let Some(after_let) = pattern.strip_prefix("let ") {
690        // Extract variable name (before "=" if present)
691        let var_name = if let Some(eq_pos) = after_let.find(" = ") {
692            after_let[..eq_pos].trim()
693        } else if let Some(eq_pos) = after_let.find("=") {
694            after_let[..eq_pos].trim()
695        } else {
696            after_let.trim()
697        };
698
699        if let PureStmt::Local {
700            pattern: ryo_source::pure::PurePattern::Ident { name, .. },
701            ..
702        } = stmt
703        {
704            if name == var_name {
705                return true;
706            }
707        }
708    }
709
710    // Fallback: match against Debug format
711    let stmt_str = format!("{:?}", stmt);
712    stmt_str.contains(pattern)
713}
714
715/// Remove statements matching a pattern from a block
716fn remove_stmts_in_block(block: &mut PureBlock, pattern: &str, remove_all: bool) -> usize {
717    let initial_len = block.stmts.len();
718    let mut removed = 0;
719
720    block.stmts.retain(|stmt| {
721        if stmt_matches_pattern(stmt, pattern) {
722            if !remove_all && removed > 0 {
723                return true; // Keep this one, already removed one
724            }
725            removed += 1;
726            false // Remove
727        } else {
728            true // Keep
729        }
730    });
731
732    // Also recurse into nested blocks
733    for stmt in &mut block.stmts {
734        match stmt {
735            PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
736                removed += remove_stmts_in_expr(expr, pattern, remove_all);
737            }
738            _ => {}
739        }
740    }
741
742    removed.min(initial_len - block.stmts.len() + removed)
743}
744
745fn remove_stmts_in_expr(expr: &mut PureExpr, pattern: &str, remove_all: bool) -> usize {
746    match expr {
747        PureExpr::Block { block, .. } => remove_stmts_in_block(block, pattern, remove_all),
748        PureExpr::If {
749            then_branch,
750            else_branch,
751            ..
752        } => {
753            let mut c = remove_stmts_in_block(then_branch, pattern, remove_all);
754            if let Some(else_expr) = else_branch {
755                c += remove_stmts_in_expr(else_expr, pattern, remove_all);
756            }
757            c
758        }
759        PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
760            remove_stmts_in_block(block, pattern, remove_all)
761        }
762        PureExpr::For { body, .. } => remove_stmts_in_block(body, pattern, remove_all),
763        PureExpr::Match { arms, .. } => {
764            let mut c = 0;
765            for arm in arms {
766                c += remove_stmts_in_expr(&mut arm.body, pattern, remove_all);
767            }
768            c
769        }
770        PureExpr::Closure { body, .. } => remove_stmts_in_expr(body, pattern, remove_all),
771        PureExpr::Async { body, .. } | PureExpr::Unsafe(body) => {
772            remove_stmts_in_block(body, pattern, remove_all)
773        }
774        _ => 0,
775    }
776}
777
778/// Replace statements in a block
779fn replace_stmts_in_block(block: &mut PureBlock, old: &PureStmt, new: &PureStmt) -> usize {
780    let mut count = 0;
781    for stmt in &mut block.stmts {
782        if stmt == old {
783            *stmt = new.clone();
784            count += 1;
785        } else {
786            // Recurse into nested blocks
787            match stmt {
788                PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
789                    count += replace_stmts_in_expr(expr, old, new);
790                }
791                _ => {}
792            }
793        }
794    }
795    count
796}
797
798fn replace_stmts_in_expr(expr: &mut PureExpr, old: &PureStmt, new: &PureStmt) -> usize {
799    match expr {
800        PureExpr::Block { block, .. } => replace_stmts_in_block(block, old, new),
801        PureExpr::If {
802            then_branch,
803            else_branch,
804            ..
805        } => {
806            let mut c = replace_stmts_in_block(then_branch, old, new);
807            if let Some(else_expr) = else_branch {
808                c += replace_stmts_in_expr(else_expr, old, new);
809            }
810            c
811        }
812        PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
813            replace_stmts_in_block(block, old, new)
814        }
815        PureExpr::For { body, .. } => replace_stmts_in_block(body, old, new),
816        PureExpr::Match { arms, .. } => {
817            let mut c = 0;
818            for arm in arms {
819                c += replace_stmts_in_expr(&mut arm.body, old, new);
820            }
821            c
822        }
823        PureExpr::Closure { body, .. } => replace_stmts_in_expr(body, old, new),
824        PureExpr::Async { body, .. } | PureExpr::Unsafe(body) => {
825            replace_stmts_in_block(body, old, new)
826        }
827        _ => 0,
828    }
829}
830
831/// Find the index where return statement starts (for End insertion)
832fn find_return_index(stmts: &[PureStmt]) -> usize {
833    for (i, stmt) in stmts.iter().enumerate().rev() {
834        match stmt {
835            PureStmt::Expr(PureExpr::Return(_)) | PureStmt::Semi(PureExpr::Return(_)) => {
836                return i;
837            }
838            PureStmt::Expr(_) if i == stmts.len() - 1 => {
839                // Tail expression - insert before it
840                return i;
841            }
842            _ => {}
843        }
844    }
845    stmts.len() // Insert at end if no return found
846}
847
848/// Insert statement relative to a reference statement (exact structural match).
849///
850/// Uses `stmt_matches` (PartialEq) — NOT `stmt_matches_pattern`.
851/// InsertStatement は「この特定の文の隣に挿入」なので、正確な位置特定が必要。
852/// `reference_stmt` は converter が `syn::parse_str` → `to_pure()` で生成し、
853/// 元ソースも同一パイプラインを経由するため、同一コードからは同一 PureStmt になる。
854fn insert_relative_to_stmt(
855    stmts: &mut Vec<PureStmt>,
856    stmt: &PureStmt,
857    reference_stmt: &PureStmt,
858    after: bool,
859) -> bool {
860    for i in 0..stmts.len() {
861        if stmt_matches(reference_stmt, &stmts[i]) {
862            let insert_idx = if after { i + 1 } else { i };
863            stmts.insert(insert_idx, stmt.clone());
864            return true;
865        }
866    }
867    false
868}
869
870// =============================================================================
871// ReplaceExprAtMutation
872// =============================================================================
873
874impl ASTRegApply for ReplaceExprAtMutation {
875    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
876        let fn_id = self.target_fn;
877
878        if !matches!(ctx.symbol_registry.kind(fn_id), Some(SymbolKind::Function)) {
879            return MutationResult {
880                mutation_type: "ReplaceExprAt".to_string(),
881                changes: 0,
882                description: format!("Target function '{:?}' not found", fn_id),
883            };
884        }
885
886        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
887            let mut new_func = func.clone();
888
889            if self.body_indices.is_empty() {
890                return MutationResult {
891                    mutation_type: "ReplaceExprAt".to_string(),
892                    changes: 0,
893                    description: "Empty body indices".to_string(),
894                };
895            }
896
897            // First index is the statement index
898            let stmt_idx = self.body_indices[0];
899            if let Some(stmt) = new_func.body.stmts.get_mut(stmt_idx) {
900                let replaced = if self.body_indices.len() == 1 {
901                    replace_stmt_expr_at(stmt, &self.new_expr)
902                } else {
903                    replace_in_stmt_at_path(stmt, &self.body_indices[1..], &self.new_expr)
904                };
905
906                if replaced {
907                    ctx.set_ast(fn_id, PureItem::Fn(new_func));
908                    ctx.emit_modified(fn_id, ModificationType::BodyModified);
909                    return MutationResult {
910                        mutation_type: "ReplaceExprAt".to_string(),
911                        changes: 1,
912                        description: format!(
913                            "Replaced expression at {:?} in '{}'",
914                            self.body_indices, self.target_fn
915                        ),
916                    };
917                }
918            }
919        }
920
921        MutationResult {
922            mutation_type: "ReplaceExprAt".to_string(),
923            changes: 0,
924            description: "Failed to replace expression at position".to_string(),
925        }
926    }
927}
928
929fn replace_stmt_expr_at(stmt: &mut PureStmt, new_expr: &PureExpr) -> bool {
930    match stmt {
931        PureStmt::Local { init, .. } => {
932            if init.is_some() {
933                *init = Some(new_expr.clone());
934                true
935            } else {
936                false
937            }
938        }
939        PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
940            *expr = new_expr.clone();
941            true
942        }
943        PureStmt::Item(_) => false,
944        // Verbatim is opaque raw bytes — leaf for stmt_ops (B-3-cont).
945        PureStmt::Verbatim(_) => false,
946    }
947}
948
949fn replace_in_stmt_at_path(stmt: &mut PureStmt, path: &[usize], new_expr: &PureExpr) -> bool {
950    let expr = match stmt {
951        PureStmt::Local {
952            init: Some(expr), ..
953        } => expr,
954        PureStmt::Semi(expr) | PureStmt::Expr(expr) => expr,
955        _ => return false,
956    };
957
958    if path.len() == 1 {
959        // Replace child at index
960        replace_child_at(expr, path[0], new_expr)
961    } else {
962        // Navigate deeper
963        if let Some(child) = navigate_expr_mut(expr, &path[..path.len() - 1]) {
964            replace_child_at(child, path[path.len() - 1], new_expr)
965        } else {
966            false
967        }
968    }
969}
970
971fn navigate_expr_mut<'a>(expr: &'a mut PureExpr, path: &[usize]) -> Option<&'a mut PureExpr> {
972    if path.is_empty() {
973        return Some(expr);
974    }
975
976    let idx = path[0];
977    let child: Option<&'a mut PureExpr> = match expr {
978        PureExpr::Binary { left, right, .. } => match idx {
979            0 => Some(left.as_mut()),
980            1 => Some(right.as_mut()),
981            _ => None,
982        },
983        PureExpr::Unary { expr: inner, .. } => {
984            if idx == 0 {
985                Some(inner.as_mut())
986            } else {
987                None
988            }
989        }
990        PureExpr::Call { args, .. } => args.get_mut(idx),
991        PureExpr::MethodCall { receiver, args, .. } => {
992            if idx == 0 {
993                Some(receiver.as_mut())
994            } else {
995                args.get_mut(idx - 1)
996            }
997        }
998        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => exprs.get_mut(idx),
999        _ => None,
1000    };
1001
1002    child.and_then(|c| navigate_expr_mut(c, &path[1..]))
1003}
1004
1005fn replace_child_at(expr: &mut PureExpr, idx: usize, new_expr: &PureExpr) -> bool {
1006    match expr {
1007        PureExpr::Binary { left, right, .. } => match idx {
1008            0 => {
1009                **left = new_expr.clone();
1010                true
1011            }
1012            1 => {
1013                **right = new_expr.clone();
1014                true
1015            }
1016            _ => false,
1017        },
1018        PureExpr::Unary { expr: inner, .. } if idx == 0 => {
1019            **inner = new_expr.clone();
1020            true
1021        }
1022        PureExpr::Call { args, .. } => {
1023            if let Some(arg) = args.get_mut(idx) {
1024                *arg = new_expr.clone();
1025                true
1026            } else {
1027                false
1028            }
1029        }
1030        PureExpr::MethodCall { receiver, args, .. } => {
1031            if idx == 0 {
1032                **receiver = new_expr.clone();
1033                true
1034            } else if let Some(arg) = args.get_mut(idx - 1) {
1035                *arg = new_expr.clone();
1036                true
1037            } else {
1038                false
1039            }
1040        }
1041        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1042            if let Some(e) = exprs.get_mut(idx) {
1043                *e = new_expr.clone();
1044                true
1045            } else {
1046                false
1047            }
1048        }
1049        _ => false,
1050    }
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use super::*;
1056    use crate::engine::ASTMutationEngine;
1057    use ryo_analysis::testing::ContextBuilder;
1058
1059    #[test]
1060    fn test_v2_replace_expr() {
1061        let mut ctx = ContextBuilder::new()
1062            .with_file(
1063                "src/lib.rs",
1064                r#"
1065fn compute() -> i32 {
1066    1 + 2
1067}
1068"#,
1069            )
1070            .build();
1071
1072        // Find the function SymbolId
1073        let compute_id = ctx
1074            .registry
1075            .iter()
1076            .find(|(id, path)| {
1077                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1078                    && path.name() == "compute"
1079            })
1080            .map(|(id, _)| id)
1081            .expect("compute function not found");
1082
1083        let old = PureExpr::Binary {
1084            op: "+".to_string(),
1085            left: Box::new(PureExpr::Lit("1".to_string())),
1086            right: Box::new(PureExpr::Lit("2".to_string())),
1087        };
1088        let new = PureExpr::Lit("3".to_string());
1089
1090        let mutation = ReplaceExprMutation::new(old, new, compute_id);
1091
1092        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1093        println!("ReplaceExpr result: {:?}", result.result);
1094    }
1095
1096    #[test]
1097    fn test_v2_insert_statement_start() {
1098        let mut ctx = ContextBuilder::new()
1099            .with_file(
1100                "src/lib.rs",
1101                r#"
1102fn greet() {
1103    println!("World");
1104}
1105"#,
1106            )
1107            .build();
1108
1109        // Find the function SymbolId
1110        let greet_id = ctx
1111            .registry
1112            .iter()
1113            .find(|(id, path)| {
1114                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1115                    && path.name() == "greet"
1116            })
1117            .map(|(id, _)| id)
1118            .expect("greet function not found");
1119
1120        let stmt = PureStmt::Semi(PureExpr::Macro {
1121            name: "println".to_string(),
1122            delimiter: MacroDelimiter::Paren,
1123            tokens: "\"Hello\"".to_string(),
1124        });
1125
1126        let mutation = InsertStatementMutation {
1127            stmt,
1128            target_fn: greet_id,
1129            position: InsertPosition::Start,
1130            reference_stmt: None,
1131        };
1132
1133        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1134        assert_eq!(result.result.changes, 1);
1135    }
1136
1137    #[test]
1138    fn test_v2_insert_statement_after_pattern_let() {
1139        use ryo_source::pure::ToPure;
1140
1141        let mut ctx = ContextBuilder::new()
1142            .with_file(
1143                "src/lib.rs",
1144                r#"
1145fn process() {
1146    let x = 1;
1147    let y = 2;
1148}
1149"#,
1150            )
1151            .build();
1152
1153        let process_id = ctx
1154            .registry
1155            .iter()
1156            .find(|(id, path)| {
1157                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1158                    && path.name() == "process"
1159            })
1160            .map(|(id, _)| id)
1161            .expect("process function not found");
1162
1163        // Parse the statement to insert
1164        let new_stmt: syn::Stmt = syn::parse_str("let z = 3;").unwrap();
1165        // Parse the reference statement
1166        let ref_stmt: syn::Stmt = syn::parse_str("let x = 1;").unwrap();
1167
1168        // Debug: print the PureStmt representations
1169        let ref_pure = ref_stmt.to_pure();
1170        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(process_id) {
1171            eprintln!("=== AST body stmts ===");
1172            for (i, s) in func.body.stmts.iter().enumerate() {
1173                eprintln!("  [{}] {:?}", i, s);
1174            }
1175            eprintln!("=== reference_stmt ===");
1176            eprintln!("  {:?}", ref_pure);
1177            eprintln!("=== match result ===");
1178            for (i, s) in func.body.stmts.iter().enumerate() {
1179                eprintln!("  [{}] == ref? {}", i, &ref_pure == s);
1180            }
1181        }
1182
1183        let mutation = InsertStatementMutation {
1184            stmt: new_stmt.to_pure(),
1185            target_fn: process_id,
1186            position: InsertPosition::AfterPattern,
1187            reference_stmt: Some(ref_pure),
1188        };
1189
1190        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1191        assert_eq!(
1192            result.result.changes, 1,
1193            "AfterPattern should insert 1 statement, got description: {}",
1194            result.result.description
1195        );
1196    }
1197
1198    #[test]
1199    fn test_v2_insert_statement_after_pattern_macro() {
1200        use ryo_source::pure::ToPure;
1201
1202        let mut ctx = ContextBuilder::new()
1203            .with_file(
1204                "src/lib.rs",
1205                r#"
1206fn greet() {
1207    println!("hello");
1208    println!("world");
1209}
1210"#,
1211            )
1212            .build();
1213
1214        let greet_id = ctx
1215            .registry
1216            .iter()
1217            .find(|(id, path)| {
1218                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1219                    && path.name() == "greet"
1220            })
1221            .map(|(id, _)| id)
1222            .expect("greet function not found");
1223
1224        // Parse the statement to insert
1225        let new_stmt: syn::Stmt = syn::parse_str("println!(\"inserted\");").unwrap();
1226        // Parse the reference statement
1227        let ref_stmt: syn::Stmt = syn::parse_str("println!(\"hello\");").unwrap();
1228
1229        let ref_pure = ref_stmt.to_pure();
1230        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(greet_id) {
1231            eprintln!("=== AST body stmts (macro test) ===");
1232            for (i, s) in func.body.stmts.iter().enumerate() {
1233                eprintln!("  [{}] {:?}", i, s);
1234            }
1235            eprintln!("=== reference_stmt ===");
1236            eprintln!("  {:?}", ref_pure);
1237            eprintln!("=== match result ===");
1238            for (i, s) in func.body.stmts.iter().enumerate() {
1239                eprintln!("  [{}] == ref? {}", i, &ref_pure == s);
1240            }
1241        }
1242
1243        let mutation = InsertStatementMutation {
1244            stmt: new_stmt.to_pure(),
1245            target_fn: greet_id,
1246            position: InsertPosition::AfterPattern,
1247            reference_stmt: Some(ref_pure),
1248        };
1249
1250        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1251        assert_eq!(
1252            result.result.changes, 1,
1253            "AfterPattern should insert 1 statement, got description: {}",
1254            result.result.description
1255        );
1256    }
1257
1258    #[test]
1259    fn test_v2_insert_statement_after_pattern_method_chain() {
1260        use ryo_source::pure::ToPure;
1261
1262        let mut ctx = ContextBuilder::new()
1263            .with_file(
1264                "src/lib.rs",
1265                r#"
1266fn process(config: &mut Config) {
1267    config.set_timeout(Duration::from_secs(30));
1268    config.set_retries(3);
1269}
1270"#,
1271            )
1272            .build();
1273
1274        let process_id = ctx
1275            .registry
1276            .iter()
1277            .find(|(id, path)| {
1278                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1279                    && path.name() == "process"
1280            })
1281            .map(|(id, _)| id)
1282            .expect("process function not found");
1283
1284        // Parse the reference: method call with nested call arg
1285        let ref_stmt: syn::Stmt =
1286            syn::parse_str("config.set_timeout(Duration::from_secs(30));").unwrap();
1287        let new_stmt: syn::Stmt = syn::parse_str("config.enable_logging();").unwrap();
1288
1289        let ref_pure = ref_stmt.to_pure();
1290        if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(process_id) {
1291            eprintln!("=== AST body stmts (method chain test) ===");
1292            for (i, s) in func.body.stmts.iter().enumerate() {
1293                eprintln!("  [{}] {:?}", i, s);
1294            }
1295            eprintln!("=== reference_stmt ===");
1296            eprintln!("  {:?}", ref_pure);
1297            eprintln!("=== match ===");
1298            for (i, s) in func.body.stmts.iter().enumerate() {
1299                eprintln!("  [{}] == ref? {}", i, &ref_pure == s);
1300            }
1301        }
1302
1303        let mutation = InsertStatementMutation {
1304            stmt: new_stmt.to_pure(),
1305            target_fn: process_id,
1306            position: InsertPosition::AfterPattern,
1307            reference_stmt: Some(ref_pure),
1308        };
1309
1310        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1311        assert_eq!(
1312            result.result.changes, 1,
1313            "AfterPattern for method chain should work, got: {}",
1314            result.result.description
1315        );
1316    }
1317
1318    #[test]
1319    fn test_v2_remove_statement_no_match() {
1320        let mut ctx = ContextBuilder::new()
1321            .with_file(
1322                "src/lib.rs",
1323                r#"
1324fn simple() -> i32 {
1325    42
1326}
1327"#,
1328            )
1329            .build();
1330
1331        use ryo_analysis::SymbolKind;
1332        use ryo_source::pure::ToPure;
1333
1334        let simple_id = ctx
1335            .registry
1336            .iter()
1337            .find(|(id, path)| {
1338                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1339                    && path.name() == "simple"
1340            })
1341            .map(|(id, _)| id)
1342            .expect("simple function not found");
1343
1344        let target_stmt: syn::Stmt = syn::parse_str("nonexistent;").unwrap();
1345        let mutation = RemoveStatementMutation::new(
1346            target_stmt.to_pure(),
1347            "nonexistent;".to_string(),
1348            simple_id,
1349        );
1350
1351        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1352        assert_eq!(result.result.changes, 0);
1353    }
1354}