vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Arithmetic mutations.

use crate::adversarial::mutations::catalog::BinOpKind;
use syn::visit::Visit;

fn op_symbol(op: BinOpKind) -> &'static str {
    match op {
        BinOpKind::Add => "+",
        BinOpKind::Sub => "-",
        BinOpKind::Mul => "*",
        BinOpKind::Div => "/",
        BinOpKind::Shl => "<<",
        BinOpKind::Shr => ">>",
        BinOpKind::And => "&",
        BinOpKind::Or => "|",
        BinOpKind::Xor => "^",
        BinOpKind::Lt => "<",
        BinOpKind::Le => "<=",
        BinOpKind::Gt => ">",
        BinOpKind::Ge => ">=",
        BinOpKind::Eq => "==",
        BinOpKind::Ne => "!=",
    }
}

/// Swap an arithmetic operator.
#[inline]
pub fn apply_op_swap(source: &str, from: BinOpKind, to: BinOpKind) -> String {
    crate::adversarial::mutations::catalog::replace_operator(
        source,
        op_symbol(from),
        op_symbol(to),
        1,
    )
}

/// Replace `wrapping_add` with `saturating_add`.
#[inline]
pub fn apply_wrapping_to_saturating(source: &str) -> String {
    crate::adversarial::mutations::catalog::lexical::replace_code_word(
        source,
        "wrapping_add",
        "saturating_add",
        usize::MAX,
    )
}

/// Replace `wrapping_add` with `checked_add`.
#[inline]
pub fn apply_wrapping_to_checked(source: &str) -> String {
    crate::adversarial::mutations::catalog::lexical::replace_code_word(
        source,
        "wrapping_add",
        "checked_add",
        usize::MAX,
    )
}

/// Increment or decrement the first standalone integer literal.
#[inline]
pub fn apply_constant_increment(source: &str, by: i64) -> String {
    let mut result = String::with_capacity(source.len());
    let mut replaced = false;
    let mut i = 0;
    while i < source.len() {
        if !replaced
            && crate::adversarial::mutations::catalog::lexical::is_code_index(source, i)
            && source[i..]
                .chars()
                .next()
                .is_some_and(|c| c.is_ascii_digit())
        {
            let start = i;
            let mut end = i;
            while let Some(c) = source[end..].chars().next() {
                if c.is_ascii_digit() {
                    end += c.len_utf8();
                } else {
                    break;
                }
            }
            let next_char = source.get(end..).and_then(|s| s.chars().next());
            let is_standalone = next_char.is_none_or(|c| !c.is_alphanumeric() && c != '_');
            if is_standalone {
                let num_str = &source[start..end];
                if let Ok(n) = num_str.parse::<i64>() {
                    result.push_str(&n.saturating_add(by).to_string());
                    i = end;
                    replaced = true;
                    continue;
                }
            }
        }
        if let Some(c) = source[i..].chars().next() {
            result.push(c);
            i += c.len_utf8();
        } else {
            break;
        }
    }
    result
}

struct LitIntVisitor {
    target: Option<proc_macro2::Span>,
    value: u64,
}

struct AnyLitIntVisitor {
    target: Option<(proc_macro2::LineColumn, proc_macro2::LineColumn, u64)>,
}

impl<'ast> Visit<'ast> for AnyLitIntVisitor {
    fn visit_lit_int(&mut self, i: &'ast syn::LitInt) {
        if self.target.is_some() {
            return;
        }
        if let Ok(v) = i.base10_parse::<u64>() {
            self.target = Some((i.span().start(), i.span().end(), v));
        }
    }
}

struct AnyLitFloatVisitor {
    target: Option<(proc_macro2::LineColumn, proc_macro2::LineColumn, f32)>,
}

impl<'ast> Visit<'ast> for AnyLitFloatVisitor {
    fn visit_lit_float(&mut self, i: &'ast syn::LitFloat) {
        if self.target.is_some() {
            return;
        }
        if let Ok(v) = i.base10_parse::<f32>() {
            self.target = Some((i.span().start(), i.span().end(), v));
        }
    }
}

impl<'ast> Visit<'ast> for LitIntVisitor {
    fn visit_lit_int(&mut self, i: &'ast syn::LitInt) {
        if self.target.is_some() {
            return;
        }
        if let Ok(v) = i.base10_parse::<u64>() {
            if v == self.value {
                self.target = Some(i.span());
            }
        }
    }
}

fn replace_first_lit_int(source: &str, match_value: u64, replacement: &str) -> String {
    let syntax = match syn::parse_file(source) {
        Ok(s) => s,
        Err(_) => return source.to_string(),
    };
    let mut visitor = LitIntVisitor {
        target: None,
        value: match_value,
    };
    visitor.visit_file(&syntax);
    if let Some(span) = visitor.target {
        let start = crate::adversarial::mutations::catalog::lexical::line_column_to_byte_offset(
            source,
            span.start().line,
            span.start().column,
        );
        let end = crate::adversarial::mutations::catalog::lexical::line_column_to_byte_offset(
            source,
            span.end().line,
            span.end().column,
        );
        let mut result = String::with_capacity(source.len() - (end - start) + replacement.len());
        result.push_str(&source[..start]);
        result.push_str(replacement);
        result.push_str(&source[end..]);
        result
    } else {
        source.to_string()
    }
}

/// Replace constant `0` with `1`.
#[inline]
pub fn apply_constant_zero_to_one(source: &str) -> String {
    replace_first_lit_int(source, 0, "1")
}

/// Replace constant `1` with `0`.
#[inline]
pub fn apply_constant_one_to_zero(source: &str) -> String {
    replace_first_lit_int(source, 1, "0")
}

/// Replace `u32::MAX` with `0`.
#[inline]
pub fn apply_constant_max_to_zero(source: &str) -> String {
    crate::adversarial::mutations::catalog::lexical::replace_code(source, "u32::MAX", "0", 1)
}

/// Flip one bit in the first integer literal.
#[inline]
pub fn apply_integer_constant_bit_flip(source: &str, bit: u8) -> String {
    let syntax = match syn::parse_file(source) {
        Ok(s) => s,
        Err(_) => return source.to_string(),
    };
    let mut visitor = AnyLitIntVisitor { target: None };
    visitor.visit_file(&syntax);
    let Some((start_lc, end_lc, value)) = visitor.target else {
        return source.to_string();
    };
    let start = crate::adversarial::mutations::catalog::lexical::line_column_to_byte_offset(
        source,
        start_lc.line,
        start_lc.column,
    );
    let end = crate::adversarial::mutations::catalog::lexical::line_column_to_byte_offset(
        source,
        end_lc.line,
        end_lc.column,
    );
    let mask = 1u64.checked_shl(u32::from(bit % 64)).unwrap_or(0);
    let replacement = (value ^ mask).to_string();
    let mut result = String::with_capacity(source.len() - (end - start) + replacement.len());
    result.push_str(&source[..start]);
    result.push_str(&replacement);
    result.push_str(&source[end..]);
    result
}

/// Flip one representation bit in the first f32 literal.
#[inline]
pub fn apply_float_constant_bit_flip(source: &str, bit: u8) -> String {
    let syntax = match syn::parse_file(source) {
        Ok(s) => s,
        Err(_) => return source.to_string(),
    };
    let mut visitor = AnyLitFloatVisitor { target: None };
    visitor.visit_file(&syntax);
    let Some((start_lc, end_lc, value)) = visitor.target else {
        return source.to_string();
    };
    let start = crate::adversarial::mutations::catalog::lexical::line_column_to_byte_offset(
        source,
        start_lc.line,
        start_lc.column,
    );
    let end = crate::adversarial::mutations::catalog::lexical::line_column_to_byte_offset(
        source,
        end_lc.line,
        end_lc.column,
    );
    let mask = 1u32.checked_shl(u32::from(bit % 32)).unwrap_or(0);
    let replacement = format!("f32::from_bits(0x{:08x})", value.to_bits() ^ mask);
    let mut result = String::with_capacity(source.len() - (end - start) + replacement.len());
    result.push_str(&source[..start]);
    result.push_str(&replacement);
    result.push_str(&source[end..]);
    result
}