1use ryo_source::pure::{PureBlock, PureClosureParam, PureExpr, PurePattern, PureStmt, PureType};
17
18use crate::Mutation;
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum LoopPattern {
23 MapCollect {
25 iter_expr: PureExpr,
26 var_name: String,
27 target_var: String,
28 transform: PureExpr,
29 },
30 FilterCollect {
32 iter_expr: PureExpr,
33 var_name: String,
34 target_var: String,
35 condition: PureExpr,
36 },
37 FilterMapCollect {
39 iter_expr: PureExpr,
40 var_name: String,
41 target_var: String,
42 condition: PureExpr,
43 transform: PureExpr,
44 },
45 ForEach {
47 iter_expr: PureExpr,
48 var_name: String,
49 body: PureBlock,
50 },
51}
52
53#[derive(Debug, Clone)]
55pub struct BlockLoopPattern {
56 pub vec_decl_idx: usize,
58 pub for_loop_idx: usize,
60 pub target_var: String,
62 pub loop_pattern: LoopPattern,
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct LoopToIteratorMutation {
69 pub target_var: Option<String>,
71 pub aggressive: bool,
73}
74
75impl LoopToIteratorMutation {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn with_target(mut self, var: impl Into<String>) -> Self {
82 self.target_var = Some(var.into());
83 self
84 }
85
86 pub fn aggressive(mut self) -> Self {
88 self.aggressive = true;
89 self
90 }
91
92 fn detect_pattern(
94 var_pattern: &PurePattern,
95 iter_expr: &PureExpr,
96 body: &PureBlock,
97 ) -> Option<LoopPattern> {
98 let var_name = match var_pattern {
99 PurePattern::Ident { name, .. } => name.clone(),
100 _ => return None, };
102
103 if body.stmts.len() == 1 {
105 if let Some(pattern) =
106 Self::detect_single_stmt_pattern(&var_name, iter_expr, &body.stmts[0])
107 {
108 return Some(pattern);
109 }
110 }
111
112 if body.stmts.len() == 1 {
114 if let PureStmt::Semi(PureExpr::If {
115 cond,
116 then_branch,
117 else_branch: None,
118 })
119 | PureStmt::Expr(PureExpr::If {
120 cond,
121 then_branch,
122 else_branch: None,
123 }) = &body.stmts[0]
124 {
125 if then_branch.stmts.len() == 1 {
126 if let Some((target_var, pushed_expr)) =
127 Self::extract_push(&then_branch.stmts[0])
128 {
129 if Self::is_simple_var(&pushed_expr, &var_name) {
131 return Some(LoopPattern::FilterCollect {
132 iter_expr: iter_expr.clone(),
133 var_name,
134 target_var,
135 condition: *cond.clone(),
136 });
137 } else {
138 return Some(LoopPattern::FilterMapCollect {
139 iter_expr: iter_expr.clone(),
140 var_name,
141 target_var,
142 condition: *cond.clone(),
143 transform: pushed_expr,
144 });
145 }
146 }
147 }
148 }
149 }
150
151 Some(LoopPattern::ForEach {
153 iter_expr: iter_expr.clone(),
154 var_name,
155 body: body.clone(),
156 })
157 }
158
159 fn detect_single_stmt_pattern(
161 var_name: &str,
162 iter_expr: &PureExpr,
163 stmt: &PureStmt,
164 ) -> Option<LoopPattern> {
165 if let Some((target_var, pushed_expr)) = Self::extract_push(stmt) {
167 if Self::is_simple_var(&pushed_expr, var_name) {
168 return Some(LoopPattern::MapCollect {
170 iter_expr: iter_expr.clone(),
171 var_name: var_name.to_string(),
172 target_var,
173 transform: pushed_expr,
174 });
175 } else {
176 return Some(LoopPattern::MapCollect {
178 iter_expr: iter_expr.clone(),
179 var_name: var_name.to_string(),
180 target_var,
181 transform: pushed_expr,
182 });
183 }
184 }
185 None
186 }
187
188 fn extract_push(stmt: &PureStmt) -> Option<(String, PureExpr)> {
190 let expr = match stmt {
191 PureStmt::Semi(e) | PureStmt::Expr(e) => e,
192 _ => return None,
193 };
194
195 if let PureExpr::MethodCall {
197 receiver,
198 method,
199 args,
200 ..
201 } = expr
202 {
203 if method == "push" && args.len() == 1 {
204 if let PureExpr::Path(target_var) = receiver.as_ref() {
205 return Some((target_var.clone(), args[0].clone()));
206 }
207 }
208 }
209 None
210 }
211
212 fn is_simple_var(expr: &PureExpr, var_name: &str) -> bool {
214 matches!(expr, PureExpr::Path(name) if name == var_name)
215 }
216
217 fn pattern_to_iter_expr(pattern: &LoopPattern) -> PureExpr {
219 match pattern {
220 LoopPattern::MapCollect {
221 iter_expr,
222 var_name,
223 transform,
224 ..
225 } => {
226 let is_identity = Self::is_simple_var(transform, var_name);
227
228 if is_identity {
229 PureExpr::MethodCall {
231 receiver: Box::new(PureExpr::MethodCall {
232 receiver: Box::new(iter_expr.clone()),
233 method: "into_iter".to_string(),
234 turbofish: None,
235 args: vec![],
236 }),
237 method: "collect".to_string(),
238 turbofish: None,
239 args: vec![],
240 }
241 } else {
242 PureExpr::MethodCall {
244 receiver: Box::new(PureExpr::MethodCall {
245 receiver: Box::new(PureExpr::MethodCall {
246 receiver: Box::new(iter_expr.clone()),
247 method: "into_iter".to_string(),
248 turbofish: None,
249 args: vec![],
250 }),
251 method: "map".to_string(),
252 turbofish: None,
253 args: vec![PureExpr::Closure {
254 is_async: false,
255 is_move: false,
256 params: vec![PureClosureParam::untyped(PurePattern::Ident {
257 name: var_name.clone(),
258 is_mut: false,
259 })],
260 ret: None,
261 body: Box::new(transform.clone()),
262 }],
263 }),
264 method: "collect".to_string(),
265 turbofish: None,
266 args: vec![],
267 }
268 }
269 }
270 LoopPattern::FilterCollect {
271 iter_expr,
272 var_name,
273 condition,
274 ..
275 } => {
276 PureExpr::MethodCall {
280 receiver: Box::new(PureExpr::MethodCall {
281 receiver: Box::new(PureExpr::MethodCall {
282 receiver: Box::new(iter_expr.clone()),
283 method: "into_iter".to_string(),
284 turbofish: None,
285 args: vec![],
286 }),
287 method: "filter".to_string(),
288 turbofish: None,
289 args: vec![PureExpr::Closure {
290 is_async: false,
291 is_move: false,
292 params: vec![PureClosureParam::untyped(PurePattern::Ref {
293 is_mut: false,
294 pattern: Box::new(PurePattern::Ident {
295 name: var_name.clone(),
296 is_mut: false,
297 }),
298 })],
299 ret: None,
300 body: Box::new(condition.clone()),
301 }],
302 }),
303 method: "collect".to_string(),
304 turbofish: None,
305 args: vec![],
306 }
307 }
308 LoopPattern::FilterMapCollect {
309 iter_expr,
310 var_name,
311 condition,
312 transform,
313 ..
314 } => {
315 PureExpr::MethodCall {
319 receiver: Box::new(PureExpr::MethodCall {
320 receiver: Box::new(PureExpr::MethodCall {
321 receiver: Box::new(PureExpr::MethodCall {
322 receiver: Box::new(iter_expr.clone()),
323 method: "into_iter".to_string(),
324 turbofish: None,
325 args: vec![],
326 }),
327 method: "filter".to_string(),
328 turbofish: None,
329 args: vec![PureExpr::Closure {
330 is_async: false,
331 is_move: false,
332 params: vec![PureClosureParam::untyped(PurePattern::Ref {
333 is_mut: false,
334 pattern: Box::new(PurePattern::Ident {
335 name: var_name.clone(),
336 is_mut: false,
337 }),
338 })],
339 ret: None,
340 body: Box::new(condition.clone()),
341 }],
342 }),
343 method: "map".to_string(),
344 turbofish: None,
345 args: vec![PureExpr::Closure {
346 is_async: false,
347 is_move: false,
348 params: vec![PureClosureParam::untyped(PurePattern::Ident {
349 name: var_name.clone(),
350 is_mut: false,
351 })],
352 ret: None,
353 body: Box::new(transform.clone()),
354 }],
355 }),
356 method: "collect".to_string(),
357 turbofish: None,
358 args: vec![],
359 }
360 }
361 LoopPattern::ForEach {
362 iter_expr,
363 var_name,
364 body,
365 } => {
366 PureExpr::MethodCall {
368 receiver: Box::new(PureExpr::MethodCall {
369 receiver: Box::new(iter_expr.clone()),
370 method: "into_iter".to_string(),
371 turbofish: None,
372 args: vec![],
373 }),
374 method: "for_each".to_string(),
375 turbofish: None,
376 args: vec![PureExpr::Closure {
377 is_async: false,
378 is_move: false,
379 params: vec![PureClosureParam::untyped(PurePattern::Ident {
380 name: var_name.clone(),
381 is_mut: false,
382 })],
383 ret: None,
384 body: Box::new(PureExpr::Block {
385 label: None,
386 block: body.clone(),
387 }),
388 }],
389 }
390 }
391 }
392 }
393
394 fn detect_block_patterns(stmts: &[PureStmt]) -> Vec<BlockLoopPattern> {
396 let mut patterns = Vec::new();
397
398 for (i, stmt) in stmts.iter().enumerate() {
399 if let Some((var_name, is_vec_init)) = Self::extract_vec_init(stmt) {
401 if !is_vec_init {
402 continue;
403 }
404
405 for (j, jstmt) in stmts.iter().enumerate().skip(i + 1) {
407 if let Some(loop_pattern) = Self::extract_for_loop_push(jstmt, &var_name) {
408 patterns.push(BlockLoopPattern {
409 vec_decl_idx: i,
410 for_loop_idx: j,
411 target_var: var_name.clone(),
412 loop_pattern,
413 });
414 break; }
416
417 if Self::stmt_uses_var(jstmt, &var_name) {
419 break;
420 }
421 }
422 }
423 }
424
425 patterns
426 }
427
428 fn extract_vec_init(stmt: &PureStmt) -> Option<(String, bool)> {
430 if let PureStmt::Local {
431 pattern: PurePattern::Ident { name, is_mut: true },
432 init: Some(init_expr),
433 ..
434 } = stmt
435 {
436 let is_vec_init = Self::is_vec_new_call(init_expr) || Self::is_vec_macro(init_expr);
437 return Some((name.clone(), is_vec_init));
438 }
439 None
440 }
441
442 fn is_vec_new_call(expr: &PureExpr) -> bool {
444 if let PureExpr::Call { func, args } = expr {
445 if args.is_empty() {
446 if let PureExpr::Path(path) = func.as_ref() {
448 return path == "Vec::new" || path.ends_with("::Vec::new");
449 }
450 }
451 }
452 false
453 }
454
455 fn is_vec_macro(expr: &PureExpr) -> bool {
457 if let PureExpr::Macro { name, .. } = expr {
458 return name == "vec" || name.ends_with("::vec");
459 }
460 false
461 }
462
463 fn extract_for_loop_push(stmt: &PureStmt, target_var: &str) -> Option<LoopPattern> {
465 let for_expr = match stmt {
466 PureStmt::Semi(e) | PureStmt::Expr(e) => e,
467 _ => return None,
468 };
469
470 if let PureExpr::For {
471 pat,
472 expr: iter_expr,
473 body,
474 ..
475 } = for_expr
476 {
477 let pattern = Self::detect_pattern(pat, iter_expr, body)?;
478
479 let pattern_target = match &pattern {
481 LoopPattern::MapCollect { target_var, .. } => target_var,
482 LoopPattern::FilterCollect { target_var, .. } => target_var,
483 LoopPattern::FilterMapCollect { target_var, .. } => target_var,
484 LoopPattern::ForEach { .. } => return None, };
486
487 if pattern_target == target_var {
488 return Some(pattern);
489 }
490 }
491 None
492 }
493
494 fn stmt_uses_var(stmt: &PureStmt, var_name: &str) -> bool {
496 match stmt {
497 PureStmt::Semi(expr) | PureStmt::Expr(expr) => Self::expr_uses_var(expr, var_name),
498 PureStmt::Local { init: Some(e), .. } => Self::expr_uses_var(e, var_name),
499 _ => false,
500 }
501 }
502
503 fn expr_uses_var(expr: &PureExpr, var_name: &str) -> bool {
505 match expr {
506 PureExpr::Path(name) => name == var_name,
507 PureExpr::MethodCall { receiver, args, .. } => {
508 Self::expr_uses_var(receiver, var_name)
509 || args.iter().any(|a| Self::expr_uses_var(a, var_name))
510 }
511 PureExpr::Call { func, args } => {
512 Self::expr_uses_var(func, var_name)
513 || args.iter().any(|a| Self::expr_uses_var(a, var_name))
514 }
515 PureExpr::Binary { left, right, .. } => {
516 Self::expr_uses_var(left, var_name) || Self::expr_uses_var(right, var_name)
517 }
518 PureExpr::If {
519 cond,
520 then_branch,
521 else_branch,
522 } => {
523 Self::expr_uses_var(cond, var_name)
524 || then_branch
525 .stmts
526 .iter()
527 .any(|s| Self::stmt_uses_var(s, var_name))
528 || else_branch
529 .as_ref()
530 .map(|e| Self::expr_uses_var(e, var_name))
531 .unwrap_or(false)
532 }
533 PureExpr::For { .. } => false, _ => false,
535 }
536 }
537
538 fn block_pattern_to_let_stmt(pattern: &BlockLoopPattern) -> PureStmt {
540 let iter_expr = Self::pattern_to_iter_expr(&pattern.loop_pattern);
541
542 PureStmt::Local {
543 pattern: PurePattern::Ident {
544 name: pattern.target_var.clone(),
545 is_mut: false, },
547 ty: Some(PureType::Other("Vec<_>".to_string())), init: Some(iter_expr),
549 }
550 }
551
552 pub fn transform_block(&self, block: &mut PureBlock) -> usize {
554 let mut changes = 0;
555
556 let block_patterns = Self::detect_block_patterns(&block.stmts);
558
559 if !block_patterns.is_empty() {
560 let mut indices_to_remove = Vec::new();
562
563 for pattern in block_patterns.iter().rev() {
564 let new_stmt = Self::block_pattern_to_let_stmt(pattern);
566 block.stmts[pattern.vec_decl_idx] = new_stmt;
567
568 indices_to_remove.push(pattern.for_loop_idx);
570 changes += 1;
571 }
572
573 indices_to_remove.sort();
575 indices_to_remove.reverse();
576 for idx in indices_to_remove {
577 block.stmts.remove(idx);
578 }
579 }
580
581 for stmt in &mut block.stmts {
583 changes += self.transform_stmt(stmt);
584 }
585
586 changes
587 }
588
589 fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
591 match stmt {
592 PureStmt::Semi(expr) | PureStmt::Expr(expr) => self.transform_expr(expr),
593 PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
594 _ => 0,
595 }
596 }
597
598 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
600 let mut changes = 0;
601
602 match expr {
603 PureExpr::For {
604 pat,
605 expr: iter_expr,
606 body,
607 ..
608 } => {
609 if let Some(ref target) = self.target_var {
611 if let PurePattern::Ident { name, .. } = pat {
612 if name != target {
613 changes += self.transform_block(body);
615 return changes;
616 }
617 }
618 }
619
620 if self.aggressive {
622 if let Some(pattern) = Self::detect_pattern(pat, iter_expr, body) {
623 if matches!(pattern, LoopPattern::ForEach { .. }) {
624 *expr = Self::pattern_to_iter_expr(&pattern);
625 changes += 1;
626 }
627 }
628 } else {
629 changes += self.transform_block(body);
631 }
632 }
633 PureExpr::Block { block, .. } => {
634 changes += self.transform_block(block);
635 }
636 PureExpr::If {
637 cond,
638 then_branch,
639 else_branch,
640 } => {
641 changes += self.transform_expr(cond);
642 changes += self.transform_block(then_branch);
643 if let Some(else_expr) = else_branch {
644 changes += self.transform_expr(else_expr);
645 }
646 }
647 PureExpr::Match { expr: e, arms } => {
648 changes += self.transform_expr(e);
649 for arm in arms {
650 changes += self.transform_expr(&mut arm.body);
651 }
652 }
653 PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
654 changes += self.transform_block(block);
655 }
656 PureExpr::Closure { body, .. } => {
657 changes += self.transform_expr(body);
658 }
659 _ => {}
660 }
661
662 changes
663 }
664}
665
666impl Mutation for LoopToIteratorMutation {
667 fn describe(&self) -> String {
668 "Convert for loops to iterator chains".to_string()
669 }
670
671 fn mutation_type(&self) -> &'static str {
672 "LoopToIterator"
673 }
674
675 fn box_clone(&self) -> Box<dyn Mutation> {
676 Box::new(self.clone())
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn test_detect_map_pattern() {
686 let pat = PurePattern::Ident {
687 name: "x".to_string(),
688 is_mut: false,
689 };
690 let iter = PureExpr::Path("items".to_string());
691 let body = PureBlock {
692 stmts: vec![PureStmt::Semi(PureExpr::MethodCall {
693 receiver: Box::new(PureExpr::Path("result".to_string())),
694 method: "push".to_string(),
695 turbofish: None,
696 args: vec![PureExpr::Binary {
697 op: "*".to_string(),
698 left: Box::new(PureExpr::Path("x".to_string())),
699 right: Box::new(PureExpr::Lit("2".to_string())),
700 }],
701 })],
702 };
703
704 let pattern = LoopToIteratorMutation::detect_pattern(&pat, &iter, &body);
705 assert!(matches!(pattern, Some(LoopPattern::MapCollect { .. })));
706 }
707
708 #[test]
709 fn test_detect_filter_pattern() {
710 let pat = PurePattern::Ident {
711 name: "x".to_string(),
712 is_mut: false,
713 };
714 let iter = PureExpr::Path("items".to_string());
715 let body = PureBlock {
716 stmts: vec![PureStmt::Semi(PureExpr::If {
717 cond: Box::new(PureExpr::Binary {
718 op: ">".to_string(),
719 left: Box::new(PureExpr::Path("x".to_string())),
720 right: Box::new(PureExpr::Lit("0".to_string())),
721 }),
722 then_branch: PureBlock {
723 stmts: vec![PureStmt::Semi(PureExpr::MethodCall {
724 receiver: Box::new(PureExpr::Path("result".to_string())),
725 method: "push".to_string(),
726 turbofish: None,
727 args: vec![PureExpr::Path("x".to_string())],
728 })],
729 },
730 else_branch: None,
731 })],
732 };
733
734 let pattern = LoopToIteratorMutation::detect_pattern(&pat, &iter, &body);
735 assert!(matches!(pattern, Some(LoopPattern::FilterCollect { .. })));
736 }
737}