vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
use super::{chain_comparator, chain_version, workgroup_sizes, ConformanceSuite};
use crate::proof::comparator::ComparatorKind;
use crate::spec::op_registry;
use crate::spec::types::ChainSpec;
use crate::spec::types::Convention;
use crate::spec::types::{ConstructionTime, ProofToken, ProofTokenError};

fn manual_token() -> Result<ProofToken, ProofTokenError> {
    ProofToken::from_specs(&[], ConstructionTime::Manual)
}

#[test]
fn workgroup_sizes_default_contains_1_and_64() -> Result<(), String> {
    let sizes = workgroup_sizes(None)?;
    assert!(sizes.contains(&1));
    assert!(sizes.contains(&64));
    Ok(())
}

#[test]
fn workgroup_sizes_with_preferred() -> Result<(), String> {
    let sizes = workgroup_sizes(Some(128))?;
    assert!(sizes.contains(&1));
    assert!(sizes.contains(&64));
    assert!(sizes.contains(&128));
    Ok(())
}

#[test]
fn workgroup_sizes_preferred_zero_rejected() {
    // P1.20-F16: zero used to be silently dropped; now an explicit Err.
    let err = workgroup_sizes(Some(0)).unwrap_err();
    assert!(err.contains("cannot be 0"), "got: {err}");
}

#[test]
fn workgroup_sizes_preferred_too_large_rejected() {
    // P1.20-F16 companion: > 1024 also rejected.
    let err = workgroup_sizes(Some(2048)).unwrap_err();
    assert!(err.contains("exceeds 1024"), "got: {err}");
}

#[test]
fn workgroup_sizes_deduplication() -> Result<(), String> {
    let sizes = workgroup_sizes(Some(64))?;
    assert_eq!(sizes.iter().filter(|&&s| s == 64).count(), 1);
    Ok(())
}

#[test]
fn chain_version_max_of_specs() -> Result<(), ProofTokenError> {
    let chain = ChainSpec {
        id: "test.chain".to_string(),
        ops: vec!["a", "b"],
        signature: crate::spec::primitive::binary_u32_sig(),
        specs: vec![crate::spec::primitive::xor::spec(), {
            let mut s = crate::spec::primitive::and::spec();
            s.version = 3;
            s
        }],
        cpu_chain: None,
        proof_token: manual_token()?,
    };
    assert_eq!(chain_version(&chain), 3);
    Ok(())
}

#[test]
#[should_panic(expected = "empty chain")]
fn chain_version_empty_chain() {
    let chain = ChainSpec {
        id: "empty".to_string(),
        ops: vec![],
        signature: crate::spec::primitive::unary_u32_sig(),
        specs: vec![],
        cpu_chain: None,
        proof_token: manual_token().expect("test token should build"),
    };
    assert_eq!(chain_version(&chain), 0);
}

#[test]
fn chain_comparator_uses_last_spec() -> Result<(), ProofTokenError> {
    let chain = ChainSpec {
        id: "test".to_string(),
        ops: vec!["a"],
        signature: crate::spec::primitive::unary_u32_sig(),
        specs: vec![{
            let mut s = crate::spec::primitive::xor::spec();
            s.comparator = ComparatorKind::UnorderedMatch;
            s
        }],
        cpu_chain: None,
        proof_token: manual_token()?,
    };
    assert_eq!(chain_comparator(&chain), ComparatorKind::UnorderedMatch);
    Ok(())
}

#[test]
fn chain_comparator_empty_defaults_to_exact() -> Result<(), ProofTokenError> {
    let chain = ChainSpec {
        id: "empty".to_string(),
        ops: vec![],
        signature: crate::spec::primitive::unary_u32_sig(),
        specs: vec![],
        cpu_chain: None,
        proof_token: manual_token()?,
    };
    assert_eq!(chain_comparator(&chain), ComparatorKind::ExactMatch);
    Ok(())
}

#[test]
fn suite_default_has_generators_and_reporters() {
    let suite = ConformanceSuite::new();
    assert!(!suite.generators.is_empty());
    assert!(!suite.reporters.is_empty());
}

#[test]
fn registry_has_required_primitives() {
    // Tripwire: the registered primitive count must keep growing toward the
    // 500-opcode target. Shrinkage indicates an accidental removal and is a
    // release-blocker; absolute equality is not asserted because the catalog
    // is explicitly scaling.
    const MINIMUM_EXPECTED_PRIMITIVES: usize = 35;
    let specs = op_registry::all_specs();
    let count = specs
        .iter()
        .filter(|s| s.id.starts_with("primitive."))
        .count();
    assert!(
        count >= MINIMUM_EXPECTED_PRIMITIVES,
        "expected at least {MINIMUM_EXPECTED_PRIMITIVES} primitive ops, got {count}"
    );
}

#[test]
fn all_ops_have_version_gte_1() {
    for op in op_registry::all_specs() {
        assert!(op.version >= 1, "op {} has version 0", op.id);
    }
}

#[test]
fn all_ops_have_non_empty_wgsl() {
    for op in op_registry::all_specs() {
        let wgsl = (op.wgsl_fn)();
        assert!(
            !wgsl.is_empty(),
            "op {} produced empty WGSL fragment",
            op.id
        );
        assert!(
            wgsl.contains("fn "),
            "op {} WGSL does not contain any function definition",
            op.id
        );
    }
}

#[test]
fn all_ops_have_default_convention() {
    for op in op_registry::all_specs() {
        if op.id.starts_with("primitive.") {
            assert_eq!(
                op.convention,
                Convention::default(),
                "primitive op {} should use default convention",
                op.id
            );
        }
    }
}

#[test]
fn all_cpu_fns_handle_empty_input_gracefully() {
    for op in op_registry::all_specs() {
        let result = (op.cpu_fn)(&[]);
        // Bytes-output ops may return empty on empty input.
        // Some ops (decode, match, string) legitimately return empty on
        // empty input. Only require non-empty output from ops whose min
        // input is > 0 (they have typed scalar inputs that produce typed output).
        if op.signature.min_input_bytes() > 0 {
            assert!(
                !result.is_empty(),
                "op {} returned empty on empty input",
                op.id
            );
        }
    }
}

#[test]
fn all_cpu_fns_return_correct_output_size() {
    use crate::spec::types::DataType;
    for op in op_registry::all_specs() {
        if !op.id.starts_with("primitive.") {
            continue;
        }
        let min_bytes = op.signature.min_input_bytes();
        let input = vec![0_u8; min_bytes.max(8)];
        let result = (op.cpu_fn)(&input);
        // Fixed-width scalar outputs must return exactly the scalar's byte
        // count. Bytes-typed outputs are variable-length and only have to
        // contain at least their declared minimum byte count.
        match op.signature.output {
            DataType::Bytes => assert!(
                result.len() >= op.signature.output.min_bytes(),
                "op {} returned {} bytes, minimum {} for output type {:?}",
                op.id,
                result.len(),
                op.signature.output.min_bytes(),
                op.signature.output
            ),
            _ => assert_eq!(
                result.len(),
                op.signature.output.min_bytes(),
                "op {} returned {} bytes, expected {} for output type {:?}",
                op.id,
                result.len(),
                op.signature.output.min_bytes(),
                op.signature.output
            ),
        }
    }
}