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 use solar::{ast::Span, interface::BytePos};
159
160 #[test]
161 fn test_opcode_mutations_exist() {
162 let mutator = AssemblyMutator::new();
163
164 assert!(mutator.get_mutations("add").unwrap().contains(&"sub"));
165 assert!(mutator.get_mutations("mul").unwrap().contains(&"div"));
166
167 assert!(mutator.get_mutations("lt").unwrap().contains(&"gt"));
168 assert!(mutator.get_mutations("slt").unwrap().contains(&"sgt"));
169
170 assert!(mutator.get_mutations("and").unwrap().contains(&"or"));
171 assert!(mutator.get_mutations("shl").unwrap().contains(&"shr"));
172 }
173
174 #[test]
175 fn test_no_cross_family_mutations() {
176 let mutator = AssemblyMutator::new();
177 let add_alts = mutator.get_mutations("add").unwrap();
178 assert!(!add_alts.contains(&"xor"), "add should not mutate to xor (cross-family)");
179 assert!(!add_alts.contains(&"and"), "add should not mutate to and (cross-family)");
180
181 let mul_alts = mutator.get_mutations("mul").unwrap();
182 assert!(!mul_alts.contains(&"and"), "mul should not mutate to and (cross-family)");
183 }
184
185 #[test]
186 fn test_no_iszero_not_mapping() {
187 let mutator = AssemblyMutator::new();
188 assert!(mutator.get_mutations("iszero").is_none(), "iszero should not be mutated");
189 assert!(mutator.get_mutations("not").is_none(), "not should not be mutated");
190 }
191
192 #[test]
193 fn test_no_mload_sload_mapping() {
194 let mutator = AssemblyMutator::new();
195 assert!(mutator.get_mutations("mload").is_none());
196 assert!(mutator.get_mutations("sload").is_none());
197 }
198
199 #[test]
200 fn test_replace_at_span() {
201 for (case, original, outer, target, expected) in [
202 ("valid", "add(a, b)", (10, 19), (10, 13), Some("sub(a, b)")),
203 ("target outside outer", "add(a, b)", (20, 29), (10, 13), None),
204 ("target exceeds length", "add(a, b)", (10, 19), (10, 30), None),
205 ("opcode mismatch", "mul(a, b)", (10, 19), (10, 13), None),
206 ("empty original", "", (10, 19), (10, 13), None),
207 ] {
208 let outer = Span::new(BytePos(outer.0), BytePos(outer.1));
209 let target = Span::new(BytePos(target.0), BytePos(target.1));
210 assert_eq!(
211 replace_at_span(original, outer, target, "add", "sub").as_deref(),
212 expected,
213 "{case}",
214 );
215 }
216 }
217}