vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Conformance pipeline: orchestration, execution, reporting.

use crate::proof::comparator::ComparatorKind;
use crate::spec::types::{ChainSpec, OpSpec, Strictness};

mod proof;
mod suite;

/// Backend glue — `VyreBackend` impls and dispatch helpers.
pub mod backend;
/// Conformance certificate generation and rendering.
pub mod certify;
/// Execution primitives for conformance dispatch.
pub mod execution;
/// Community TOML rule loading.
pub mod loader;
/// Notification dispatch.
pub mod notify;
/// Progress reporters.
pub mod reporter;
/// Constant-memory streaming conformance runner.
#[allow(missing_docs)]
pub mod streaming;

pub use suite::ConformanceSuite;

use crate::spec::minimums::{MIN_BOUNDARY_VALUES, MIN_EQUIVALENCE_CLASSES};

/// Validate that an op spec meets minimum coverage requirements.
#[inline]
pub fn validate_minimum_coverage(op: &crate::OpSpec) -> Result<(), String> {
    // P1.20-F14: error messages were a single long line with wide internal
    // padding ("...minimum is 4.              Add boundary values..."). Use
    // explicit newlines so the rendered output is readable.
    if op.boundary_values.len() < MIN_BOUNDARY_VALUES {
        return Err(format!(
            "Fix: op '{}' has {} boundary values, minimum is {}.\n\
             Add boundary values covering zero, one, max, and at least one\n\
             domain-specific edge case.",
            op.id,
            op.boundary_values.len(),
            MIN_BOUNDARY_VALUES
        ));
    }
    if op.equivalence_classes.len() < MIN_EQUIVALENCE_CLASSES {
        return Err(format!(
            "Fix: op '{}' has {} equivalence classes, minimum is {}.\n\
             Add at least one equivalence class describing the input domain.",
            op.id,
            op.equivalence_classes.len(),
            MIN_EQUIVALENCE_CLASSES
        ));
    }
    Ok(())
}

/// Construct the conformance workgroup-size schedule for an op.
///
/// P1.20-F16: previously this function silently dropped `Some(0)` and
/// returned `[1, 64]` — a contributor-supplied 0 was invisible. The trust
/// boundary for spec.toml now rejects 0 at load time (P1.20-F28), but the
/// in-process call sites that hand-build a `preferred` (e.g., the chain
/// helpers below) need the same guard. Returning `Result` makes the rejection
/// explicit instead of swallowed.
#[inline]
pub(crate) fn workgroup_sizes(preferred: Option<u32>) -> Result<Vec<u32>, String> {
    let mut sizes = vec![1u32, 64];
    if let Some(size) = preferred {
        if size == 0 {
            return Err(
                "Fix: workgroup_size cannot be 0. Use Some(n) with n in 1..=1024 \
                 or None to accept the default schedule."
                    .to_string(),
            );
        }
        if size > 1024 {
            return Err(format!(
                "Fix: workgroup_size {size} exceeds 1024. Cap to the device max."
            ));
        }
        sizes.push(size);
    }
    sizes.sort_unstable();
    sizes.dedup();
    Ok(sizes)
}

fn chain_workgroup_sizes(chain: &ChainSpec) -> Result<Vec<u32>, String> {
    let mut sizes = workgroup_sizes(None)?;
    for spec in &chain.specs {
        if let Some(size) = spec.workgroup_size {
            // Reject 0 / >1024 here too. F16: silent drop of 0 is gone.
            for added in workgroup_sizes(Some(size))? {
                sizes.push(added);
            }
        }
    }
    sizes.sort_unstable();
    sizes.dedup();
    Ok(sizes)
}

/// Highest spec version in the chain, or 0 for an empty chain.
///
/// P1.20-F18: previously this used `.unwrap_or_default()` which silently
/// produced `chain_version = 0` for an empty `chain.specs`. The `0` then
/// surfaced in the certificate as if a real op had been certified at v0.
/// Public API kept its `u32` return for backward compat, but a debug_assert
/// catches the empty case in test builds and the doc comment now warns
/// callers explicitly. Library callers that want to reject empty chains
/// should check `chain.specs.is_empty()` themselves before calling this.
#[inline]
pub(crate) fn chain_version(chain: &ChainSpec) -> u32 {
    debug_assert!(
        !chain.specs.is_empty(),
        "chain_version called on empty chain '{}' — caller should reject \
         empty chains explicitly (P1.20-F18)",
        chain.id
    );
    chain
        .specs
        .iter()
        .map(|spec| spec.version)
        .max()
        .unwrap_or(0)
}

#[inline]
pub(crate) fn chain_comparator(chain: &ChainSpec) -> ComparatorKind {
    chain
        .specs
        .last()
        .map_or(ComparatorKind::ExactMatch, |spec| spec.comparator)
}

/// Certificate track selected from an op's declared strictness and output family.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CertificateTrack {
    /// Bit-exact integer and byte-oriented operations.
    Integer,
    /// Bit-exact floating-point operations.
    Float,
    /// Tolerance-verified approximate operations.
    Approximate,
}

/// Route a spec into the certificate track it is allowed to occupy.
#[inline]
pub(crate) fn certificate_track_for_op(op: &OpSpec) -> CertificateTrack {
    match op.strictness {
        Strictness::Approximate { .. } => CertificateTrack::Approximate,
        Strictness::Strict if op.signature.output.is_float_family() => CertificateTrack::Float,
        Strictness::Strict => CertificateTrack::Integer,
    }
}

/// Curated minimum naga capability set vyre's IR is allowed to require.
///
/// P1.20-F19: the previous `Capabilities::all()` accepted ANY capability
/// (subgroup ops, ray tracing, mesh shaders, atomic float, etc.) at
/// validation time, even though the kernel would fail on devices without
/// them. The result was the T26 "subgroup-intrinsic-assumption" attack
/// surface: a contributor's kernel could pass `validate_wgsl_syntax` here
/// and then fail on every consumer's GPU.
///
/// The set below names ONLY the capabilities vyre's emitted lowerings
/// actually use. To opt a new capability in, add it here AND update
/// `coordination/wgsl-capability-allowlist.md` (and run cycle audit).
fn vyre_minimum_capabilities() -> naga::valid::Capabilities {
    use naga::valid::Capabilities;
    // Empty bitset = the wgpu DownlevelDefault baseline; any feature beyond
    // that must be opted in here.
    Capabilities::empty()
}

/// Validate that an op's WGSL fragment produces parseable WGSL when wrapped.
#[inline]
pub fn validate_wgsl_syntax(op: &crate::OpSpec) -> Result<(), String> {
    let wgsl_source = (op.wgsl_fn)();
    let config = crate::pipeline::backend::ConformDispatchConfig::default();
    let wrapped = crate::pipeline::backend::wrap_shader(&wgsl_source, &config);
    let result = naga::front::wgsl::parse_str(&wrapped);
    match result {
        Ok(module) => {
            let info = naga::valid::Validator::new(
                naga::valid::ValidationFlags::all(),
                vyre_minimum_capabilities(),
            )
            .validate(&module);
            match info {
                Ok(_) => Ok(()),
                Err(e) => Err(format!(
                    "Fix: op '{}' WGSL fails naga validation: {e}\n\
                     The shader parses but has semantic errors, OR uses a \
                     naga capability outside vyre's minimum allowlist (see \
                     vyre_minimum_capabilities). If a new capability is \
                     required, opt it in there + update \
                     coordination/wgsl-capability-allowlist.md.",
                    op.id
                )),
            }
        }
        Err(e) => Err(format!(
            "Fix: op '{}' WGSL fails naga parsing: {e}\n\
             The shader source is syntactically invalid.",
            op.id
        )),
    }
}

#[cfg(test)]
mod tests;