1use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
18use ryo_symbol::SymbolId;
19
20use crate::Mutation;
21
22#[derive(Debug, Clone, Default)]
34pub struct AssignOpMutation {
35 pub target_fn: Option<SymbolId>,
37}
38
39impl AssignOpMutation {
40 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn in_function(mut self, id: SymbolId) -> Self {
46 self.target_fn = Some(id);
47 self
48 }
49
50 fn compound_op(op: &str) -> Option<&'static str> {
52 match op {
53 "+" => Some("+="),
54 "-" => Some("-="),
55 "*" => Some("*="),
56 "/" => Some("/="),
57 "%" => Some("%="),
58 "&" => Some("&="),
59 "|" => Some("|="),
60 "^" => Some("^="),
61 "<<" => Some("<<="),
62 ">>" => Some(">>="),
63 _ => None,
64 }
65 }
66
67 fn expr_eq(a: &PureExpr, b: &PureExpr) -> bool {
69 match (a, b) {
70 (PureExpr::Path(pa), PureExpr::Path(pb)) => pa == pb,
71 (
72 PureExpr::Field {
73 expr: ea,
74 field: fa,
75 },
76 PureExpr::Field {
77 expr: eb,
78 field: fb,
79 },
80 ) => fa == fb && Self::expr_eq(ea, eb),
81 (
82 PureExpr::Index {
83 expr: ea,
84 index: ia,
85 },
86 PureExpr::Index {
87 expr: eb,
88 index: ib,
89 },
90 ) => Self::expr_eq(ea, eb) && Self::expr_eq(ia, ib),
91 (PureExpr::Unary { op: opa, expr: ea }, PureExpr::Unary { op: opb, expr: eb }) => {
93 opa == opb && Self::expr_eq(ea, eb)
94 }
95 (
97 PureExpr::Ref {
98 is_mut: ma,
99 expr: ea,
100 },
101 PureExpr::Ref {
102 is_mut: mb,
103 expr: eb,
104 },
105 ) => ma == mb && Self::expr_eq(ea, eb),
106 _ => false,
107 }
108 }
109
110 pub fn transform_block(&self, block: &mut PureBlock) -> usize {
112 let mut changes = 0;
113
114 for stmt in &mut block.stmts {
115 changes += self.transform_stmt(stmt);
116 }
117
118 changes
119 }
120
121 fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
123 match stmt {
124 PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
125 if let PureExpr::Binary { op, left, right } = expr {
127 if op == "=" {
128 if let PureExpr::Binary {
130 op: bin_op,
131 left: bin_left,
132 right: bin_right,
133 } = right.as_ref()
134 {
135 if Self::expr_eq(left, bin_left) {
136 if let Some(compound) = Self::compound_op(bin_op) {
137 let target = std::mem::replace(
139 left.as_mut(),
140 PureExpr::Path("__placeholder".to_string()),
141 );
142 let rhs = bin_right.as_ref().clone();
143
144 *expr = PureExpr::Binary {
145 op: compound.to_string(),
146 left: Box::new(target),
147 right: Box::new(rhs),
148 };
149
150 return 1;
151 }
152 }
153 }
154 }
155 }
156
157 self.transform_expr(expr)
159 }
160 PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
161 _ => 0,
162 }
163 }
164
165 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
167 let mut changes = 0;
168
169 match expr {
170 PureExpr::Block { block, .. } => {
171 changes += self.transform_block(block);
172 }
173 PureExpr::If {
174 cond,
175 then_branch,
176 else_branch,
177 } => {
178 changes += self.transform_expr(cond);
179 changes += self.transform_block(then_branch);
180 if let Some(else_expr) = else_branch {
181 changes += self.transform_expr(else_expr);
182 }
183 }
184 PureExpr::Match { expr: e, arms } => {
185 changes += self.transform_expr(e);
186 for arm in arms {
187 changes += self.transform_expr(&mut arm.body);
188 }
189 }
190 PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
191 changes += self.transform_block(block);
192 }
193 PureExpr::For { body, .. } => {
194 changes += self.transform_block(body);
195 }
196 PureExpr::Closure { body, .. } => {
197 changes += self.transform_expr(body);
198 }
199 _ => {}
200 }
201
202 changes
203 }
204}
205
206impl Mutation for AssignOpMutation {
207 fn describe(&self) -> String {
208 "Convert assignments to compound operators (a = a + b → a += b)".to_string()
209 }
210
211 fn mutation_type(&self) -> &'static str {
212 "AssignOp"
213 }
214
215 fn box_clone(&self) -> Box<dyn Mutation> {
216 Box::new(self.clone())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_compound_op() {
226 assert_eq!(AssignOpMutation::compound_op("+"), Some("+="));
227 assert_eq!(AssignOpMutation::compound_op("-"), Some("-="));
228 assert_eq!(AssignOpMutation::compound_op("*"), Some("*="));
229 assert_eq!(AssignOpMutation::compound_op("/"), Some("/="));
230 assert_eq!(AssignOpMutation::compound_op("&&"), None);
231 }
232
233 #[test]
234 fn test_expr_eq() {
235 let a = PureExpr::Path("x".to_string());
236 let b = PureExpr::Path("x".to_string());
237 let c = PureExpr::Path("y".to_string());
238
239 assert!(AssignOpMutation::expr_eq(&a, &b));
240 assert!(!AssignOpMutation::expr_eq(&a, &c));
241 }
242
243 #[test]
244 fn test_expr_eq_unary() {
245 let a = PureExpr::Unary {
247 op: "*".to_string(),
248 expr: Box::new(PureExpr::Path("x".to_string())),
249 };
250 let b = PureExpr::Unary {
251 op: "*".to_string(),
252 expr: Box::new(PureExpr::Path("x".to_string())),
253 };
254 let c = PureExpr::Unary {
255 op: "*".to_string(),
256 expr: Box::new(PureExpr::Path("y".to_string())),
257 };
258
259 assert!(AssignOpMutation::expr_eq(&a, &b));
260 assert!(!AssignOpMutation::expr_eq(&a, &c));
261 }
262
263 #[test]
264 fn test_expr_eq_field() {
265 let a = PureExpr::Field {
266 expr: Box::new(PureExpr::Path("self".to_string())),
267 field: "count".to_string(),
268 };
269 let b = PureExpr::Field {
270 expr: Box::new(PureExpr::Path("self".to_string())),
271 field: "count".to_string(),
272 };
273 let c = PureExpr::Field {
274 expr: Box::new(PureExpr::Path("self".to_string())),
275 field: "other".to_string(),
276 };
277
278 assert!(AssignOpMutation::expr_eq(&a, &b));
279 assert!(!AssignOpMutation::expr_eq(&a, &c));
280 }
281}