1use std::collections::HashMap;
2
3use eyre::Result;
4use solar::ast::yul;
5
6use super::{MutationContext, Mutator};
7use crate::mutation::mutant::{Mutant, MutationType};
8
9pub struct AssemblyMutator {
10 opcode_mutations: HashMap<&'static str, Vec<&'static str>>,
11}
12
13impl Default for AssemblyMutator {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl AssemblyMutator {
20 pub fn new() -> Self {
21 let mut opcode_mutations: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
22
23 opcode_mutations.insert("add", vec!["sub", "mul"]);
25 opcode_mutations.insert("sub", vec!["add", "mul", "div"]);
26 opcode_mutations.insert("mul", vec!["add", "div"]);
27 opcode_mutations.insert("div", vec!["mul", "sub", "mod"]);
28 opcode_mutations.insert("sdiv", vec!["smod", "mul"]);
29 opcode_mutations.insert("mod", vec!["div", "mul"]);
30 opcode_mutations.insert("smod", vec!["sdiv", "mod"]);
31 opcode_mutations.insert("exp", vec!["mul", "add"]);
32 opcode_mutations.insert("addmod", vec!["mulmod"]);
33 opcode_mutations.insert("mulmod", vec!["addmod"]);
34
35 opcode_mutations.insert("lt", vec!["gt", "eq", "slt"]);
37 opcode_mutations.insert("gt", vec!["lt", "eq", "sgt"]);
38 opcode_mutations.insert("slt", vec!["sgt", "lt"]);
39 opcode_mutations.insert("sgt", vec!["slt", "gt"]);
40 opcode_mutations.insert("eq", vec!["lt", "gt"]);
41
42 opcode_mutations.insert("and", vec!["or", "xor"]);
44 opcode_mutations.insert("or", vec!["and", "xor"]);
45 opcode_mutations.insert("xor", vec!["and", "or"]);
46
47 opcode_mutations.insert("shl", vec!["shr", "sar"]);
49 opcode_mutations.insert("shr", vec!["shl", "sar"]);
50 opcode_mutations.insert("sar", vec!["shr", "shl"]);
51
52 Self { opcode_mutations }
53 }
54
55 pub fn get_mutations(&self, opcode: &str) -> Option<&[&'static str]> {
56 self.opcode_mutations.get(opcode).map(|v| v.as_slice())
57 }
58}
59
60impl Mutator for AssemblyMutator {
61 fn generate_mutants(&self, context: &MutationContext<'_>) -> Result<Vec<Mutant>> {
62 let yul_expr = context.yul_expr.ok_or_else(|| eyre::eyre!("No Yul expression"))?;
63
64 let call = match &yul_expr.kind {
65 yul::ExprKind::Call(call) => call,
66 _ => return Ok(vec![]),
67 };
68
69 let opcode_name = call.name.as_str();
70
71 let alternatives = match self.get_mutations(opcode_name) {
72 Some(alts) => alts,
73 None => return Ok(vec![]),
74 };
75
76 let original = context.original_text();
77 if original.is_empty() {
78 return Ok(vec![]);
79 }
80
81 let expected_len = (context.span.hi().0 - context.span.lo().0) as usize;
82 if original.len() != expected_len {
83 return Ok(vec![]);
84 }
85
86 let source_line = context.source_line();
87 let line_number = context.line_number();
88 let column_number = context.column_number();
89
90 let name_span = call.name.span;
91
92 let mutants = alternatives
93 .iter()
94 .filter_map(|&new_opcode| {
95 let mutated =
96 replace_at_span(&original, context.span, name_span, opcode_name, new_opcode)?;
97 Some(Mutant {
98 span: context.span,
99 mutation: MutationType::YulOpcode {
100 original_opcode: opcode_name.to_string(),
101 new_opcode: new_opcode.to_string(),
102 mutated_expr: mutated,
103 },
104 path: context.path.clone(),
105 original: original.clone(),
106 source_line: source_line.clone(),
107 line_number,
108 column_number,
109 })
110 })
111 .collect();
112
113 Ok(mutants)
114 }
115
116 fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
117 if let Some(yul_expr) = ctxt.yul_expr
118 && let yul::ExprKind::Call(call) = &yul_expr.kind
119 {
120 return self.opcode_mutations.contains_key(call.name.as_str());
121 }
122 false
123 }
124}
125
126fn replace_at_span(
127 original: &str,
128 outer_span: solar::ast::Span,
129 target_span: solar::ast::Span,
130 expected_opcode: &str,
131 replacement: &str,
132) -> Option<String> {
133 let outer_lo = outer_span.lo().0 as usize;
134 let target_lo = target_span.lo().0 as usize;
135 let target_hi = target_span.hi().0 as usize;
136
137 let rel_lo = target_lo.checked_sub(outer_lo)?;
138 let rel_hi = target_hi.checked_sub(outer_lo)?;
139
140 if rel_lo > rel_hi || rel_hi > original.len() {
141 return None;
142 }
143
144 let prefix = original.get(..rel_lo)?;
145 let replaced = original.get(rel_lo..rel_hi)?;
146 let suffix = original.get(rel_hi..)?;
147
148 if replaced != expected_opcode {
149 return None;
150 }
151
152 Some(format!("{prefix}{replacement}{suffix}"))
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_opcode_mutations_exist() {
161 let mutator = AssemblyMutator::new();
162
163 assert!(mutator.get_mutations("add").unwrap().contains(&"sub"));
164 assert!(mutator.get_mutations("mul").unwrap().contains(&"div"));
165
166 assert!(mutator.get_mutations("lt").unwrap().contains(&"gt"));
167 assert!(mutator.get_mutations("slt").unwrap().contains(&"sgt"));
168
169 assert!(mutator.get_mutations("and").unwrap().contains(&"or"));
170 assert!(mutator.get_mutations("shl").unwrap().contains(&"shr"));
171 }
172
173 #[test]
174 fn test_no_cross_family_mutations() {
175 let mutator = AssemblyMutator::new();
176 let add_alts = mutator.get_mutations("add").unwrap();
177 assert!(!add_alts.contains(&"xor"), "add should not mutate to xor (cross-family)");
178 assert!(!add_alts.contains(&"and"), "add should not mutate to and (cross-family)");
179
180 let mul_alts = mutator.get_mutations("mul").unwrap();
181 assert!(!mul_alts.contains(&"and"), "mul should not mutate to and (cross-family)");
182 }
183
184 #[test]
185 fn test_no_iszero_not_mapping() {
186 let mutator = AssemblyMutator::new();
187 assert!(mutator.get_mutations("iszero").is_none(), "iszero should not be mutated");
188 assert!(mutator.get_mutations("not").is_none(), "not should not be mutated");
189 }
190
191 #[test]
192 fn test_no_mload_sload_mapping() {
193 let mutator = AssemblyMutator::new();
194 assert!(mutator.get_mutations("mload").is_none());
195 assert!(mutator.get_mutations("sload").is_none());
196 }
197
198 #[test]
199 fn test_replace_at_span_valid() {
200 use solar::interface::BytePos;
201 let original = "add(a, b)";
202 let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
203 let target = solar::ast::Span::new(BytePos(10), BytePos(13));
204 let result = replace_at_span(original, outer, target, "add", "sub");
205 assert_eq!(result, Some("sub(a, b)".to_string()));
206 }
207
208 #[test]
209 fn test_replace_at_span_target_outside_outer() {
210 use solar::interface::BytePos;
211 let original = "add(a, b)";
212 let outer = solar::ast::Span::new(BytePos(20), BytePos(29));
213 let target = solar::ast::Span::new(BytePos(10), BytePos(13));
214 assert!(replace_at_span(original, outer, target, "add", "sub").is_none());
215 }
216
217 #[test]
218 fn test_replace_at_span_target_exceeds_length() {
219 use solar::interface::BytePos;
220 let original = "add(a, b)";
221 let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
222 let target = solar::ast::Span::new(BytePos(10), BytePos(30));
223 assert!(replace_at_span(original, outer, target, "add", "sub").is_none());
224 }
225
226 #[test]
227 fn test_replace_at_span_opcode_mismatch() {
228 use solar::interface::BytePos;
229 let original = "mul(a, b)";
230 let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
231 let target = solar::ast::Span::new(BytePos(10), BytePos(13));
232 assert!(replace_at_span(original, outer, target, "add", "sub").is_none());
233 }
234
235 #[test]
236 fn test_replace_at_span_empty_original() {
237 use solar::interface::BytePos;
238 let outer = solar::ast::Span::new(BytePos(10), BytePos(19));
239 let target = solar::ast::Span::new(BytePos(10), BytePos(13));
240 assert!(replace_at_span("", outer, target, "add", "sub").is_none());
241 }
242}