Skip to main content

forge/mutation/
mutant.rs

1use std::{fmt::Display, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4use solar::{
5    interface::BytePos,
6    parse::ast::{BinOpKind, LitKind, Span, StrKind, UnOpKind},
7};
8
9use super::visitor::AssignVarTypes;
10
11/// Wraps an unary operator mutated, to easily store pre/post-fix op swaps
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct UnaryOpMutated {
14    /// String containing the whole new expression (operator and its target)
15    /// eg `a++`
16    new_expression: String,
17
18    /// The underlying operator used by this mutant
19    #[serde(serialize_with = "serialize_unop_kind", deserialize_with = "deserialize_unop_kind")]
20    pub resulting_op_kind: UnOpKind,
21}
22
23// Custom serialization for UnOpKind
24fn serialize_unop_kind<S>(value: &UnOpKind, serializer: S) -> Result<S::Ok, S::Error>
25where
26    S: serde::Serializer,
27{
28    let s = format!("{value:?}");
29    serializer.serialize_str(&s)
30}
31
32fn deserialize_unop_kind<'de, D>(deserializer: D) -> Result<UnOpKind, D::Error>
33where
34    D: serde::Deserializer<'de>,
35{
36    let s = String::deserialize(deserializer)?;
37    match s.as_str() {
38        "PreInc" => Ok(UnOpKind::PreInc),
39        "PostInc" => Ok(UnOpKind::PostInc),
40        "PreDec" => Ok(UnOpKind::PreDec),
41        "PostDec" => Ok(UnOpKind::PostDec),
42        "Not" => Ok(UnOpKind::Not),
43        "BitNot" => Ok(UnOpKind::BitNot),
44        "Neg" => Ok(UnOpKind::Neg),
45        other => Err(serde::de::Error::custom(format!("Unknown UnOpKind: {other}"))),
46    }
47}
48
49impl UnaryOpMutated {
50    pub const fn new(new_expression: String, resulting_op_kind: UnOpKind) -> Self {
51        Self { new_expression, resulting_op_kind }
52    }
53}
54
55impl Display for UnaryOpMutated {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}", self.new_expression)
58    }
59}
60
61// Custom serialization for BinOpKind
62fn serialize_binop<S>(value: &BinOpKind, serializer: S) -> Result<S::Ok, S::Error>
63where
64    S: serde::Serializer,
65{
66    let s = format!("{value:?}");
67    serializer.serialize_str(&s)
68}
69
70fn deserialize_binop<'de, D>(deserializer: D) -> Result<BinOpKind, D::Error>
71where
72    D: serde::Deserializer<'de>,
73{
74    let s = String::deserialize(deserializer)?;
75    match s.as_str() {
76        "Add" => Ok(BinOpKind::Add),
77        "Sub" => Ok(BinOpKind::Sub),
78        "Mul" => Ok(BinOpKind::Mul),
79        "Div" => Ok(BinOpKind::Div),
80        "And" => Ok(BinOpKind::And),
81        "Or" => Ok(BinOpKind::Or),
82        "Eq" => Ok(BinOpKind::Eq),
83        "Ne" => Ok(BinOpKind::Ne),
84        "Lt" => Ok(BinOpKind::Lt),
85        "Le" => Ok(BinOpKind::Le),
86        "Gt" => Ok(BinOpKind::Gt),
87        "Ge" => Ok(BinOpKind::Ge),
88        "BitAnd" => Ok(BinOpKind::BitAnd),
89        "BitOr" => Ok(BinOpKind::BitOr),
90        "BitXor" => Ok(BinOpKind::BitXor),
91        "Shl" => Ok(BinOpKind::Shl),
92        "Shr" => Ok(BinOpKind::Shr),
93        "Sar" => Ok(BinOpKind::Sar),
94        "Pow" => Ok(BinOpKind::Pow),
95        "Rem" => Ok(BinOpKind::Rem),
96        other => Err(serde::de::Error::custom(format!("Unknown BinOpKind: {other}"))),
97    }
98}
99
100// @todo add a mutation from universalmutator: line swap (swap two lines of code, as it
101// could theoretically uncover untested reentrancies
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub enum OwnedStrKind {
104    Str,
105    Unicode,
106    Hex,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub enum OwnedLiteral {
111    Str {
112        kind: OwnedStrKind,
113        text: String,
114    },
115    Number(alloy_primitives::U256),
116    Rational(String),
117    Address(String),
118    Bool(bool),
119    Err(String),
120    /// Signed-negation of a numeric literal (e.g. `-123`). We cannot represent
121    /// negative values inside `Number(U256)` (the cast wraps via two's
122    /// complement and renders as a huge unsigned literal), so we carry the
123    /// negation textually and render it as `-{val}`.
124    NegatedNumber(alloy_primitives::U256),
125}
126
127impl From<&LitKind<'_>> for OwnedLiteral {
128    fn from(lit_kind: &LitKind<'_>) -> Self {
129        match lit_kind {
130            LitKind::Bool(b) => Self::Bool(*b),
131            LitKind::Number(n) => Self::Number(*n),
132            LitKind::Rational(r) => Self::Rational(r.to_string()),
133            LitKind::Address(addr) => Self::Address(addr.to_string()),
134            LitKind::Str(sk, bytesym, _extras) => {
135                let text = String::from_utf8_lossy(bytesym.as_byte_str()).into_owned();
136                let kind = match sk {
137                    StrKind::Str => OwnedStrKind::Str,
138                    StrKind::Unicode => OwnedStrKind::Unicode,
139                    StrKind::Hex => OwnedStrKind::Hex,
140                };
141                Self::Str { kind, text }
142            }
143            LitKind::Err(_) => Self::Err("parse_error".to_string()),
144        }
145    }
146}
147
148impl Display for OwnedLiteral {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            Self::Bool(val) => write!(f, "{val}"),
152            Self::Number(val) => write!(f, "{val}"),
153            Self::NegatedNumber(val) => write!(f, "-{val}"),
154            Self::Rational(s) => write!(f, "{s}"),
155            Self::Address(s) => write!(f, "{s}"),
156            Self::Str { kind, text } => match kind {
157                OwnedStrKind::Str => write!(f, "\"{text}\""),
158                OwnedStrKind::Unicode => write!(f, "unicode\"{text}\""),
159                OwnedStrKind::Hex => write!(f, "hex\"{text}\""),
160            },
161            Self::Err(s) => write!(f, "{s}"),
162        }
163    }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub enum MutationType {
168    // Note: Solar's LitKind::Number(U256) doesn't differentiate int vs uint - it only stores the
169    // numeric value without signedness info. For now we generate mutations for both and let solc
170    // filter out invalid ones (e.g., -x on uint). Future improvement: track variable types in a
171    // symbol table to avoid generating invalid mutants.
172    /// For an initializer x, of type
173    /// bool: replace x with !x
174    /// uint: replace x with 0
175    /// int: replace x with 0; replace x with -x (temp: this is mutated for uint as well)
176    ///
177    /// For a binary op y: apply BinaryOp(y)
178    Assignment(AssignVarTypes),
179
180    /// For a binary op y in BinOpKind ("+", "-", ">=", etc)
181    /// replace y with each non-y in op (legacy, kept for cache compatibility)
182    #[serde(serialize_with = "serialize_binop", deserialize_with = "deserialize_binop")]
183    BinaryOp(BinOpKind),
184
185    /// Binary operator mutation with full expression context
186    /// Stores both the new operator and the full mutated expression for display
187    BinaryOpExpr {
188        #[serde(serialize_with = "serialize_binop", deserialize_with = "deserialize_binop")]
189        new_op: BinOpKind,
190        mutated_expr: String,
191    },
192
193    /// For a delete expr x `delete foo`, replace x with `assert(true)`
194    DeleteExpression,
195
196    /// replace "delegatecall" with "call"
197    ElimDelegate,
198
199    /// Gambit doesn't implement nor define it?
200    FunctionCall,
201
202    // /// For a if(x) condition x:
203    // /// replace x with true; replace x with false
204    // This mutation is not used anymore, as we mutate the condition as an expression,
205    // which will creates true/false mutant as well as more complex conditions (eg if(foo++ >
206    // --bar) ) IfStatementMutation,
207    /// For a require(x) condition:
208    /// replace x with true; replace x with false
209    // Same as for IfStatementMutation, the expression inside the require is mutated as an
210    // expression to handle increment etc
211    Require,
212
213    /// For require(condition)/assert(condition), mutate the condition:
214    /// - require(x) -> require(true) (always passes - security critical!)
215    /// - require(x) -> require(false) (always fails)
216    /// - require(x) -> require(!x) (inverted condition)
217    RequireCondition {
218        /// The mutated full call expression
219        mutated_call: String,
220    },
221
222    // @todo review if needed -> this might creates *a lot* of combinations for super-polyadic fn
223    // tho       only swapping same type (to avoid obvious compilation failure), but should
224    // take into account       implicit casting too...
225    /// For 2 args of the same type x,y in a function args:
226    /// swap(x, y)
227    SwapArgumentsFunction,
228
229    // @todo same remark as above, might end up in a space too big to explore + filtering out
230    // based on type
231    /// For an expr taking 2 expression x, y (x+y, x-y, x = x + ...):
232    /// swap(x, y)
233    SwapArgumentsOperator,
234
235    /// For an unary operator x in UnOpKind (eg "++", "--", "~", "!"):
236    /// replace x with all other operator in op
237    /// Pre or post- are different UnOp
238    UnaryOperator(UnaryOpMutated),
239
240    YulOpcode {
241        original_opcode: String,
242        new_opcode: String,
243        mutated_expr: String,
244    },
245}
246
247impl Display for MutationType {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        match self {
250            Self::Assignment(kind) => match kind {
251                AssignVarTypes::Literal(lit) => write!(f, "{lit}"),
252                AssignVarTypes::Identifier(ident) => write!(f, "{ident}"),
253            },
254            Self::BinaryOp(kind) => write!(f, "{}", kind.to_str()),
255            Self::BinaryOpExpr { mutated_expr, .. } => write!(f, "{mutated_expr}"),
256            Self::DeleteExpression => write!(f, "assert(true)"),
257            Self::ElimDelegate => write!(f, "call"),
258            Self::UnaryOperator(mutated) => write!(f, "{mutated}"),
259            Self::RequireCondition { mutated_call } => write!(f, "{mutated_call}"),
260
261            Self::YulOpcode { mutated_expr, .. } => write!(f, "{mutated_expr}"),
262
263            Self::FunctionCall
264            | Self::Require
265            | Self::SwapArgumentsFunction
266            | Self::SwapArgumentsOperator => write!(f, ""),
267        }
268    }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub enum MutationResult {
273    Dead,
274    Alive,
275    Invalid,
276    Skipped,
277    /// The mutant's compile-and-test run exceeded the configured timeout.
278    /// Treated as unresolved: not counted toward survived or killed.
279    TimedOut,
280}
281
282impl MutationResult {
283    /// Short uppercase label used in progress / reporter output.
284    pub const fn label(&self) -> &'static str {
285        match self {
286            Self::Dead => "KILLED",
287            Self::Alive => "SURVIVED",
288            Self::Invalid => "INVALID",
289            Self::Skipped => "SKIPPED",
290            Self::TimedOut => "TIMED OUT",
291        }
292    }
293}
294
295/// A given mutation
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct Mutant {
298    /// The path to the project root where this mutant (tries to) live
299    pub path: PathBuf,
300    #[serde(serialize_with = "serialize_span", deserialize_with = "deserialize_span")]
301    pub span: Span,
302    pub mutation: MutationType,
303    /// The original source text that will be replaced by this mutation (full expression)
304    #[serde(default)]
305    pub original: String,
306    /// The full source line for context (e.g., "uint256 x = a * b;")
307    #[serde(default)]
308    pub source_line: String,
309    /// Line number in the source file (1-indexed)
310    #[serde(default)]
311    pub line_number: usize,
312    /// Column number in the source file (1-indexed)
313    #[serde(default)]
314    pub column_number: usize,
315}
316
317// Custom serialization for Span (since solar::parse::ast::Span doesn't implement Serialize)
318fn serialize_span<S>(span: &Span, serializer: S) -> Result<S::Ok, S::Error>
319where
320    S: serde::Serializer,
321{
322    use serde::Serialize;
323    #[derive(Serialize)]
324    struct SpanHelper {
325        lo: u32,
326        hi: u32,
327    }
328    SpanHelper { lo: span.lo().0, hi: span.hi().0 }.serialize(serializer)
329}
330
331fn deserialize_span<'de, D>(deserializer: D) -> Result<Span, D::Error>
332where
333    D: serde::Deserializer<'de>,
334{
335    use serde::Deserialize;
336    #[derive(Deserialize)]
337    struct SpanHelper {
338        lo: u32,
339        hi: u32,
340    }
341    let helper = SpanHelper::deserialize(deserializer)?;
342    Ok(Span::new(BytePos(helper.lo), BytePos(helper.hi)))
343}
344
345impl Mutant {
346    /// Returns a relative path string.
347    ///
348    /// Walks ancestor components looking for a well-known directory root
349    /// (`src`, `test`, `lib`, `contracts`) so the output is cross-platform and
350    /// does not rely on OS-specific path separators.
351    pub fn relative_path(&self) -> String {
352        let components: Vec<_> = self.path.components().collect();
353        for (i, comp) in components.iter().enumerate() {
354            if let std::path::Component::Normal(name) = comp {
355                let s = name.to_string_lossy();
356                if matches!(s.as_ref(), "src" | "test" | "script" | "lib" | "contracts") {
357                    let parts: Vec<_> = components[i..]
358                        .iter()
359                        .filter_map(|c| match c {
360                            std::path::Component::Normal(s) => Some(s.to_string_lossy()),
361                            _ => None,
362                        })
363                        .collect();
364                    return parts.join("/");
365                }
366            }
367        }
368        self.path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown").to_string()
369    }
370
371    /// Returns a concise one-line description of the mutation (full original code)
372    pub fn short_description(&self) -> String {
373        let original = if self.original.is_empty() {
374            "<unknown>".to_string()
375        } else {
376            self.original.trim().to_string()
377        };
378        let mutated = self.mutation.to_string();
379
380        format!("`{}` → `{}`", original, mutated.trim())
381    }
382}
383
384impl Display for Mutant {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        if self.line_number > 0 {
387            write!(f, "{}:{}: {}", self.relative_path(), self.line_number, self.short_description())
388        } else {
389            write!(f, "{}: {}", self.relative_path(), self.short_description())
390        }
391    }
392}