vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
use crate::generate::generators;
use crate::OpSpec;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use super::util::*;

static GOLDEN_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

/// Freeze goldens for every spec in `specs`.
///
/// Generates 50 deterministic inputs per op, runs the CPU reference, and writes
/// JSON files to `out_dir/<sanitized_op_id>/v<version>/<sha256_hex>.json`.
///
/// Idempotent: if a file already exists for a given (op_id, version, input_hash),
/// it is NOT overwritten.
#[inline]
pub fn freeze_goldens(specs: &[OpSpec], out_dir: &Path) -> io::Result<usize> {
    let mut frozen = 0;
    for spec in specs {
        let seed = fnv1a_u64(spec.id.as_bytes());
        let inputs = generate_50_inputs(spec, seed);
        let op_dir = out_dir
            .join(sanitize(spec.id))
            .join(format!("v{}", spec.version));
        fs::create_dir_all(&op_dir)?;

        for input in inputs {
            let hash = sha256_hex(&input);
            let path = op_dir.join(format!("{hash}.json"));
            if path.exists() {
                continue;
            }
            let output = (spec.cpu_fn)(&input);
            let golden = Golden {
                op_id: spec.id.to_string(),
                spec_version: spec.version,
                input,
                output,
            };
            let json = serde_json::to_string_pretty(&golden).map_err(|e| {
                io::Error::new(io::ErrorKind::InvalidData, format!("JSON serialize: {e}"))
            })?;
            atomic_write_new(&path, json.as_bytes())?;
            frozen += 1;
        }
    }
    Ok(frozen)
}

fn atomic_write_new(path: &Path, bytes: &[u8]) -> io::Result<()> {
    let tmp = temp_path(path);
    let mut file = fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&tmp)?;
    if let Err(err) = file
        .write_all(bytes)
        .and_then(|()| file.sync_all())
        .and_then(|()| fs::hard_link(&tmp, path))
        .and_then(|()| fs::remove_file(&tmp))
    {
        let _ = fs::remove_file(&tmp);
        if err.kind() == io::ErrorKind::AlreadyExists {
            return Ok(());
        }
        return Err(err);
    }
    Ok(())
}

fn temp_path(path: &Path) -> PathBuf {
    let pid = std::process::id();
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_nanos());
    let counter = GOLDEN_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("golden");
    path.with_file_name(format!("{file_name}.tmp.{pid}.{nanos}.{counter}"))
}