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, Lit, 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    BigNumber(String),
121    Address(String),
122    Bool(bool),
123    Err(String),
124    /// Signed-negation of a numeric literal (e.g. `-123`). We cannot represent
125    /// negative values inside `Number(U256)` (the cast wraps via two's
126    /// complement and renders as a huge unsigned literal), so we carry the
127    /// negation textually and render it as `-{val}`.
128    NegatedNumber(alloy_primitives::U256),
129}
130
131impl From<&Lit<'_>> for OwnedLiteral {
132    fn from(lit: &Lit<'_>) -> Self {
133        match &lit.kind {
134            LitKind::Bool(b) => Self::Bool(*b),
135            LitKind::Number(n) => Self::Number(*n),
136            LitKind::Rational(..) => Self::Rational(lit.symbol.to_string()),
137            LitKind::BigNumber(..) => Self::BigNumber(lit.symbol.to_string()),
138            LitKind::Address(addr) => Self::Address(addr.to_string()),
139            LitKind::Str(sk, bytesym, _extras) => {
140                let text = String::from_utf8_lossy(bytesym.as_byte_str()).into_owned();
141                let kind = match sk {
142                    StrKind::Str => OwnedStrKind::Str,
143                    StrKind::Unicode => OwnedStrKind::Unicode,
144                    StrKind::Hex => OwnedStrKind::Hex,
145                };
146                Self::Str { kind, text }
147            }
148            LitKind::Err(_) => Self::Err("parse_error".to_string()),
149        }
150    }
151}
152
153impl Display for OwnedLiteral {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            Self::Bool(val) => write!(f, "{val}"),
157            Self::Number(val) => write!(f, "{val}"),
158            Self::NegatedNumber(val) => write!(f, "-{val}"),
159            Self::Rational(s) | Self::BigNumber(s) => write!(f, "{s}"),
160            Self::Address(s) => write!(f, "{s}"),
161            Self::Str { kind, text } => match kind {
162                OwnedStrKind::Str => write!(f, "\"{text}\""),
163                OwnedStrKind::Unicode => write!(f, "unicode\"{text}\""),
164                OwnedStrKind::Hex => write!(f, "hex\"{text}\""),
165            },
166            Self::Err(s) => write!(f, "{s}"),
167        }
168    }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub enum MutationType {
173    // Note: Solar's LitKind::Number(U256) doesn't differentiate int vs uint - it only stores the
174    // numeric value without signedness info. For now we generate mutations for both and let solc
175    // filter out invalid ones (e.g., -x on uint). Future improvement: track variable types in a
176    // symbol table to avoid generating invalid mutants.
177    /// For an initializer x, of type
178    /// bool: replace x with !x
179    /// uint: replace x with 0
180    /// int: replace x with 0; replace x with -x (temp: this is mutated for uint as well)
181    ///
182    /// For a binary op y: apply BinaryOp(y)
183    Assignment(AssignVarTypes),
184
185    /// For a binary op y in BinOpKind ("+", "-", ">=", etc)
186    /// replace y with each non-y in op (legacy, kept for cache compatibility)
187    #[serde(serialize_with = "serialize_binop", deserialize_with = "deserialize_binop")]
188    BinaryOp(BinOpKind),
189
190    /// Binary operator mutation with full expression context
191    /// Stores both the new operator and the full mutated expression for display
192    BinaryOpExpr {
193        #[serde(serialize_with = "serialize_binop", deserialize_with = "deserialize_binop")]
194        new_op: BinOpKind,
195        mutated_expr: String,
196    },
197
198    /// For a delete expr x `delete foo`, replace x with `assert(true)`
199    DeleteExpression,
200
201    /// replace "delegatecall" with "call"
202    ElimDelegate,
203
204    /// Gambit doesn't implement nor define it?
205    FunctionCall,
206
207    /// Replace a function body with a no-op or constant return.
208    FunctionBody(String),
209
210    // /// For a if(x) condition x:
211    // /// replace x with true; replace x with false
212    // This mutation is not used anymore, as we mutate the condition as an expression,
213    // which will creates true/false mutant as well as more complex conditions (eg if(foo++ >
214    // --bar) ) IfStatementMutation,
215    /// For a require(x) condition:
216    /// replace x with true; replace x with false
217    // Same as for IfStatementMutation, the expression inside the require is mutated as an
218    // expression to handle increment etc
219    Require,
220
221    /// For require(condition)/assert(condition), mutate the condition:
222    /// - require(x) -> require(true) (always passes - security critical!)
223    /// - require(x) -> require(false) (always fails)
224    /// - require(x) -> require(!x) (inverted condition)
225    RequireCondition {
226        /// The mutated full call expression
227        mutated_call: String,
228    },
229
230    // @todo review if needed -> this might creates *a lot* of combinations for super-polyadic fn
231    // tho       only swapping same type (to avoid obvious compilation failure), but should
232    // take into account       implicit casting too...
233    /// For 2 args of the same type x,y in a function args:
234    /// swap(x, y)
235    SwapArgumentsFunction,
236
237    // @todo same remark as above, might end up in a space too big to explore + filtering out
238    // based on type
239    /// For an expr taking 2 expression x, y (x+y, x-y, x = x + ...):
240    /// swap(x, y)
241    SwapArgumentsOperator,
242
243    /// For an unary operator x in UnOpKind (eg "++", "--", "~", "!"):
244    /// replace x with all other operator in op
245    /// Pre or post- are different UnOp
246    UnaryOperator(UnaryOpMutated),
247
248    YulOpcode {
249        original_opcode: String,
250        new_opcode: String,
251        mutated_expr: String,
252    },
253}
254
255impl Display for MutationType {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        match self {
258            Self::Assignment(kind) => match kind {
259                AssignVarTypes::Literal(lit) => write!(f, "{lit}"),
260                AssignVarTypes::Identifier(ident) => write!(f, "{ident}"),
261                AssignVarTypes::NegatedIdentifier(ident) => write!(f, "-{ident}"),
262            },
263            Self::BinaryOp(kind) => write!(f, "{}", kind.to_str()),
264            Self::BinaryOpExpr { mutated_expr, .. } => write!(f, "{mutated_expr}"),
265            Self::DeleteExpression => write!(f, "assert(true)"),
266            Self::ElimDelegate => write!(f, "call"),
267            Self::UnaryOperator(mutated) => write!(f, "{mutated}"),
268            Self::RequireCondition { mutated_call } => write!(f, "{mutated_call}"),
269            Self::FunctionBody(body) => write!(f, "{body}"),
270
271            Self::YulOpcode { mutated_expr, .. } => write!(f, "{mutated_expr}"),
272
273            Self::FunctionCall
274            | Self::Require
275            | Self::SwapArgumentsFunction
276            | Self::SwapArgumentsOperator => write!(f, ""),
277        }
278    }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub enum MutationResult {
283    Dead,
284    Alive,
285    Invalid,
286    Skipped,
287    /// The mutant's compile-and-test run exceeded the configured timeout.
288    /// Treated as unresolved: not counted toward survived or killed.
289    TimedOut,
290}
291
292impl MutationResult {
293    /// Short uppercase label used in progress / reporter output.
294    pub const fn label(&self) -> &'static str {
295        match self {
296            Self::Dead => "KILLED",
297            Self::Alive => "SURVIVED",
298            Self::Invalid => "INVALID",
299            Self::Skipped => "SKIPPED",
300            Self::TimedOut => "TIMED OUT",
301        }
302    }
303}
304
305/// A given mutation
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct Mutant {
308    /// The source path relative to the project root, or the absolute path if the source is
309    /// outside the root.
310    pub path: PathBuf,
311    #[serde(serialize_with = "serialize_span", deserialize_with = "deserialize_span")]
312    pub span: Span,
313    pub mutation: MutationType,
314    /// The original source text that will be replaced by this mutation (full expression)
315    #[serde(default)]
316    pub original: String,
317    /// The full source line for context (e.g., "uint256 x = a * b;")
318    #[serde(default)]
319    pub source_line: String,
320    /// Line number in the source file (1-indexed)
321    #[serde(default)]
322    pub line_number: usize,
323    /// Column number in the source file (1-indexed)
324    #[serde(default)]
325    pub column_number: usize,
326}
327
328// Custom serialization for Span (since solar::parse::ast::Span doesn't implement Serialize)
329fn serialize_span<S>(span: &Span, serializer: S) -> Result<S::Ok, S::Error>
330where
331    S: serde::Serializer,
332{
333    use serde::Serialize;
334    #[derive(Serialize)]
335    struct SpanHelper {
336        lo: u32,
337        hi: u32,
338    }
339    SpanHelper { lo: span.lo().0, hi: span.hi().0 }.serialize(serializer)
340}
341
342fn deserialize_span<'de, D>(deserializer: D) -> Result<Span, D::Error>
343where
344    D: serde::Deserializer<'de>,
345{
346    use serde::Deserialize;
347    #[derive(Deserialize)]
348    struct SpanHelper {
349        lo: u32,
350        hi: u32,
351    }
352    let helper = SpanHelper::deserialize(deserializer)?;
353    Ok(Span::new(BytePos(helper.lo), BytePos(helper.hi)))
354}
355
356impl Mutant {
357    /// Returns the mutant path with `/` separators.
358    ///
359    /// Mutant paths are relative to the project root, so this returns the full path. If the
360    /// source is outside the project root, the path is absolute: this then starts at the first
361    /// well-known directory (`src`, `test`, `script`, `lib`, `contracts`), or uses the file name.
362    pub fn relative_path(&self) -> String {
363        let components = self
364            .path
365            .components()
366            .filter_map(|c| match c {
367                Component::Normal(s) => Some(s.to_string_lossy()),
368                _ => None,
369            })
370            .collect::<Vec<_>>();
371        let start = if self.path.is_relative() {
372            0
373        } else {
374            components
375                .iter()
376                .position(|s| matches!(s.as_ref(), "src" | "test" | "script" | "lib" | "contracts"))
377                .unwrap_or(components.len().saturating_sub(1))
378        };
379        components[start..].join("/")
380    }
381
382    /// Returns a concise one-line description of the mutation (full original code)
383    pub fn short_description(&self) -> String {
384        let original = if self.original.is_empty() {
385            "<unknown>".to_string()
386        } else {
387            self.original.trim().to_string()
388        };
389        let mutated = self.mutation.to_string();
390
391        format!("`{}` → `{}`", original, mutated.trim())
392    }
393}
394
395impl Display for Mutant {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        if self.line_number > 0 {
398            write!(f, "{}:{}: {}", self.relative_path(), self.line_number, self.short_description())
399        } else {
400            write!(f, "{}: {}", self.relative_path(), self.short_description())
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use solar::{
409        ast::LitUint,
410        interface::{Session, Symbol},
411    };
412
413    #[test]
414    fn preserve_numeric_literal_spelling() {
415        Session::builder().with_stderr_emitter().build().enter(|| {
416            let cases = [
417                (
418                    "1.25e-2",
419                    LitKind::Rational(LitUint::from_be_bytes(&[1]), LitUint::from_be_bytes(&[80])),
420                ),
421                ("1e100", LitKind::BigNumber(LitUint::from_be_bytes(&[1; 42]))),
422            ];
423            for (source, kind) in cases {
424                let lit = Lit { span: Span::DUMMY, symbol: Symbol::intern(source), kind };
425                assert_eq!(OwnedLiteral::from(&lit).to_string(), source);
426            }
427        });
428    }
429}