vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Source rewriting for concrete meta-mutations.

use std::path::{Path, PathBuf};

use syn::spanned::Spanned;
use syn::visit::Visit;

use super::{MetaMutation, MetaMutationError, MetaOperator};

/// Apply a meta-mutation to the sources under `vyre_conform_root`,
/// returning the edited file paths so the caller can snapshot and restore
/// them.
///
/// # Errors
///
/// Returns [`MetaMutationError::TargetMissing`] if the source cannot be
/// modified at the specified location.
#[inline]
pub fn apply(
    vyre_conform_root: &Path,
    mutation: MetaMutation,
) -> Result<Vec<PathBuf>, MetaMutationError> {
    match mutation {
        MetaMutation::Syntactic {
            component,
            operator,
            line,
            column,
        } => apply_syntactic(vyre_conform_root, component, operator, line, column),
        _ => Err(MetaMutationError::NotYetWired),
    }
}

fn apply_syntactic(
    vyre_conform_root: &Path,
    component: &str,
    operator: MetaOperator,
    line: usize,
    column: usize,
) -> Result<Vec<PathBuf>, MetaMutationError> {
    let relative = component.replace("::", "/");
    let mut source_path = vyre_conform_root.to_path_buf();
    source_path.push("src");
    source_path.push(relative);
    source_path.set_extension("rs");

    let source =
        std::fs::read_to_string(&source_path).map_err(|e| MetaMutationError::TargetMissing {
            message: format!("Fix: could not read {}: {}", source_path.display(), e),
        })?;

    let new_source = match operator {
        MetaOperator::SwapComparison => apply_swap_comparison(&source, line, column)?,
        MetaOperator::OffByOne => apply_off_by_one(&source, line, column)?,
        _ => apply_line_replacement(&source, component, operator, line)?,
    };
    std::fs::write(&source_path, new_source).map_err(|e| MetaMutationError::TargetMissing {
        message: format!("Fix: could not write {}: {}", source_path.display(), e),
    })?;

    Ok(vec![source_path])
}

fn apply_line_replacement(
    source: &str,
    component: &str,
    operator: MetaOperator,
    line: usize,
) -> Result<String, MetaMutationError> {
    let mut lines: Vec<String> = source.lines().map(|s| s.to_string()).collect();
    if line == 0 || line > lines.len() {
        return Err(MetaMutationError::TargetMissing {
            message: format!("Fix: line {} out of bounds for {}", line, component),
        });
    }

    let line_content = &lines[line - 1];
    let (original, replacement) = find_replacement(line_content, operator)?;
    let new_line = line_content.replacen(original, replacement, 1);
    lines[line - 1] = new_line;
    Ok(lines.join("\n"))
}

fn find_replacement(
    line: &str,
    operator: MetaOperator,
) -> Result<(&'static str, &'static str), MetaMutationError> {
    match operator {
        MetaOperator::SwapComparison | MetaOperator::OffByOne => {
            unreachable!("handled by syn-based apply path")
        }
        MetaOperator::WeakenBound => {
            if line.contains("<") && !line.contains("<=") {
                Ok(("<", "<="))
            } else if line.contains(">") && !line.contains(">=") {
                Ok((">", ">="))
            } else {
                Err(MetaMutationError::TargetMissing {
                    message: "Fix: no strict inequality found on line".into(),
                })
            }
        }
        MetaOperator::DropAssertion => {
            if line.contains("assert!") {
                Ok(("assert!", "// assert!"))
            } else if line.contains("assert_eq!") {
                Ok(("assert_eq!", "// assert_eq!"))
            } else {
                Err(MetaMutationError::TargetMissing {
                    message: "Fix: no assertion found on line".into(),
                })
            }
        }
        MetaOperator::SwallowError => {
            if line.contains("?") {
                Ok(("?", "/* ? */.unwrap_or_default()"))
            } else {
                Err(MetaMutationError::TargetMissing {
                    message: "Fix: no ? operator found on line".into(),
                })
            }
        }
    }
}

fn apply_swap_comparison(
    source: &str,
    target_line: usize,
    target_column: usize,
) -> Result<String, MetaMutationError> {
    let syntax = syn::parse_file(source).map_err(|e| MetaMutationError::TargetMissing {
        message: format!("Fix: failed to parse source: {}", e),
    })?;

    struct Visitor {
        target_line: usize,
        target_column: usize,
        match_span: Option<proc_macro2::Span>,
        replacement: Option<&'static str>,
    }

    impl<'ast> Visit<'ast> for Visitor {
        fn visit_expr_binary(&mut self, i: &'ast syn::ExprBinary) {
            match i.op {
                syn::BinOp::Eq(_) | syn::BinOp::Ne(_) | syn::BinOp::Lt(_) | syn::BinOp::Gt(_) => {
                    let span = i.op.span();
                    let start = span.start();
                    if start.line == self.target_line && start.column == self.target_column {
                        self.match_span = Some(span);
                        self.replacement = Some(match i.op {
                            syn::BinOp::Eq(_) => "!=",
                            syn::BinOp::Ne(_) => "==",
                            syn::BinOp::Lt(_) => ">",
                            syn::BinOp::Gt(_) => "<",
                            _ => unreachable!(),
                        });
                    }
                }
                _ => {}
            }
            syn::visit::visit_expr_binary(self, i);
        }
    }

    let mut visitor = Visitor {
        target_line,
        target_column,
        match_span: None,
        replacement: None,
    };
    visitor.visit_file(&syntax);

    if let (Some(span), Some(replacement)) = (visitor.match_span, visitor.replacement) {
        replace_at_span(source, span, replacement)
    } else {
        Err(MetaMutationError::TargetMissing {
            message: "Fix: no comparison operator found at target location".into(),
        })
    }
}

fn apply_off_by_one(
    source: &str,
    target_line: usize,
    target_column: usize,
) -> Result<String, MetaMutationError> {
    let syntax = syn::parse_file(source).map_err(|e| MetaMutationError::TargetMissing {
        message: format!("Fix: failed to parse source: {}", e),
    })?;

    struct Visitor {
        target_line: usize,
        target_column: usize,
        match_span: Option<proc_macro2::Span>,
        replacement: Option<&'static str>,
    }

    impl<'ast> Visit<'ast> for Visitor {
        fn visit_lit_int(&mut self, i: &'ast syn::LitInt) {
            if let Ok(val) = i.base10_parse::<u64>() {
                if val == 0 || val == 1 {
                    let span = i.span();
                    let start = span.start();
                    if start.line == self.target_line && start.column == self.target_column {
                        self.match_span = Some(span);
                        self.replacement = Some(if val == 0 { "1" } else { "0" });
                    }
                }
            }
            syn::visit::visit_lit_int(self, i);
        }
    }

    let mut visitor = Visitor {
        target_line,
        target_column,
        match_span: None,
        replacement: None,
    };
    visitor.visit_file(&syntax);

    if let (Some(span), Some(replacement)) = (visitor.match_span, visitor.replacement) {
        replace_at_span(source, span, replacement)
    } else {
        Err(MetaMutationError::TargetMissing {
            message: "Fix: no 0 or 1 literal found at target location".into(),
        })
    }
}

fn replace_at_span(
    source: &str,
    span: proc_macro2::Span,
    replacement: &str,
) -> Result<String, MetaMutationError> {
    let start = span.start();
    let end = span.end();
    let byte_start =
        byte_offset_for_line_column(source, start.line, start.column).ok_or_else(|| {
            MetaMutationError::TargetMissing {
                message: "Fix: could not map span start to byte offset".into(),
            }
        })?;
    let byte_end = byte_offset_for_line_column(source, end.line, end.column).ok_or_else(|| {
        MetaMutationError::TargetMissing {
            message: "Fix: could not map span end to byte offset".into(),
        }
    })?;

    let mut result =
        String::with_capacity(source.len() - (byte_end - byte_start) + replacement.len());
    result.push_str(&source[..byte_start]);
    result.push_str(replacement);
    result.push_str(&source[byte_end..]);
    Ok(result)
}

fn byte_offset_for_line_column(
    source: &str,
    target_line: usize,
    target_column: usize,
) -> Option<usize> {
    let mut line = 1;
    let mut column = 1;
    for (byte_offset, ch) in source.char_indices() {
        if line == target_line && column == target_column {
            return Some(byte_offset);
        }
        if ch == '\n' {
            line += 1;
            column = 1;
        } else {
            column += ch.len_utf8();
        }
    }
    if line == target_line && column == target_column {
        return Some(source.len());
    }
    None
}