vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Shared test helpers for integration tests.
//!
//! Each integration test file includes this module with `mod support;` and
//! uses the fabricators below to build synthetic `OpSpec` values without
//! depending on the real primitive registry.

#![allow(dead_code)]

use std::fs;
use std::ops::Deref;
use std::path::{Path, PathBuf};

use vyre_conform::enforce::category::Category;
use vyre_conform::enforce::CategoryFinding;
use vyre_conform::spec::law::AlgebraicLaw;
use vyre_conform::{types::DataType, types::OpSignature, OpSpec};

/// Noop CPU reference used by synthetic specs — returns a fixed-size zero
/// buffer so the test harness can still invoke the reference without
/// crashing on short inputs.
pub fn cpu_u32_zero(_input: &[u8]) -> Vec<u8> {
    vec![0; 4]
}

/// Minimal valid WGSL for a synthetic op — never dispatched, only parsed.
pub fn wgsl_noop() -> String {
    "fn vyre_op(index: u32, input_len: u32) -> u32 { return 0u; }".to_string()
}

/// Build a synthetic binary u32 op spec with the given id and law set.
pub fn synthetic_binary_spec(id: &'static str, laws: Vec<AlgebraicLaw>) -> OpSpec {
    let mut spec = vyre_conform::specs::primitive::xor::spec();
    spec.id = id;
    spec.category = Category::A {
        composition_of: vec!["primitive.bitwise.xor"],
    };
    spec.signature = OpSignature {
        inputs: vec![DataType::U32, DataType::U32],
        output: DataType::U32,
    };
    spec.cpu_fn = cpu_u32_zero;
    spec.wgsl_fn = wgsl_noop;
    spec.alt_wgsl_fns = Vec::new();
    spec.laws = laws;
    spec.equivalence_classes = Vec::new();
    spec.boundary_values = Vec::new();
    spec
}

/// Build a synthetic unary u32 op spec with the given id and law set.
pub fn synthetic_unary_spec(id: &'static str, laws: Vec<AlgebraicLaw>) -> OpSpec {
    let mut spec = synthetic_binary_spec(id, laws);
    spec.signature = OpSignature {
        inputs: vec![DataType::U32],
        output: DataType::U32,
    };
    spec
}

/// RAII temporary directory that deletes itself on drop.
pub struct TempDir {
    pub path: PathBuf,
}

impl Drop for TempDir {
    fn drop(&mut self) {
        if let Err(e) = fs::remove_dir_all(&self.path) {
            panic!("failed to remove temp dir {}: {}", self.path.display(), e);
        }
    }
}

impl Deref for TempDir {
    type Target = Path;
    fn deref(&self) -> &Self::Target {
        &self.path
    }
}

/// Return the one-based line number of the first line containing `needle`.
pub fn line_of(source: &str, needle: &str) -> usize {
    source
        .lines()
        .enumerate()
        .find(|(_, line)| line.contains(needle))
        .map(|(i, _)| i + 1)
        .unwrap_or_else(|| panic!("needle {needle:?} not found"))
}

/// Panic unless `findings` contains an entry whose message contains `needle`
/// and whose source line is within ±2 of `line`.
pub fn assert_located(findings: &[CategoryFinding], needle: &str, line: usize) {
    let f = findings
        .iter()
        .find(|f| f.message().contains(needle))
        .unwrap_or_else(|| panic!("no finding for {needle:?}; got {findings:?}"));
    let location = f.location();
    let loc = location
        .as_ref()
        .unwrap_or_else(|| panic!("finding has no location: {f:?}"));
    let delta = if loc.line >= line {
        loc.line - line
    } else {
        line - loc.line
    };
    assert!(
        delta <= 2,
        "expected finding around line {line} (±2), got {}: {}",
        loc.line,
        f.message()
    );
}