Skip to main content

forge/mutation/
mutant.rs

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