weirflow 0.1.0

GPU-first dataflow analysis primitives for Vyre and Santh compiler pipelines.
Documentation
//! Shared dispatch-decode ABI schema for Weir GPU boundaries.
//!
//! Pack, unpack, validation, fuzz, and release evidence all need the same
//! byte-width and exact-length contract. This module owns that schema while
//! the sibling modules keep doing one kind of work each.

use crate::error_format::ErrorFormatScratch;

pub(crate) const U32_BYTES: usize = std::mem::size_of::<u32>();
pub(crate) const U32_BITS_PER_WORD: u32 = u32::BITS;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) enum DispatchDecodeDirection {
    Input,
    Output,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct DispatchU32Schema<'a> {
    pub name: &'a str,
    pub words: usize,
    pub direction: DispatchDecodeDirection,
}

impl<'a> DispatchU32Schema<'a> {
    #[inline]
    #[must_use]
    pub(crate) const fn input(name: &'a str, words: usize) -> Self {
        Self {
            name,
            words,
            direction: DispatchDecodeDirection::Input,
        }
    }

    #[inline]
    #[must_use]
    pub(crate) const fn output(name: &'a str, words: usize) -> Self {
        Self {
            name,
            words,
            direction: DispatchDecodeDirection::Output,
        }
    }

    #[inline]
    #[must_use]
    pub(crate) const fn scalar_output(name: &'a str) -> Self {
        Self::output(name, 1)
    }

    #[inline]
    pub(crate) fn required_bytes(self) -> Result<usize, String> {
        byte_len_for_u32_words(self.words, self.name)
    }

    #[inline]
    pub(crate) fn require_input_words(self, got: usize) -> Result<(), String> {
        if got != self.words {
            return Err(input_word_count_mismatch(self.name, got, self.words));
        }
        Ok(())
    }

    #[inline]
    pub(crate) fn require_output_bytes(self, got: usize) -> Result<(), String> {
        let required = self.required_bytes()?;
        if got != required {
            return Err(output_byte_len_mismatch(self.name, got, required));
        }
        Ok(())
    }

    #[inline]
    pub(crate) fn require_scalar_output_bytes(self, got: usize) -> Result<(), String> {
        if got != U32_BYTES {
            return Err(scalar_byte_len_mismatch(self.name, got));
        }
        Ok(())
    }
}

#[inline]
pub(crate) fn byte_len_for_u32_words(words: usize, caller: &str) -> Result<usize, String> {
    words.checked_mul(U32_BYTES).ok_or_else(|| {
        // Byte-length overflow is a cold malformed-dimension path.
        byte_len_overflow(caller, words)
    })
}

#[cold]
fn input_word_count_mismatch(name: &str, got: usize, expected: usize) -> String {
    format!(
        "{name} input has {got} u32 words, expected exactly {expected}. Fix: pass the declared semantic input width; Weir GPU wrappers never silently pad or truncate inputs."
    )
}

#[cold]
fn output_byte_len_mismatch(name: &str, got: usize, required: usize) -> String {
    let mut scratch = ErrorFormatScratch::default();
    crate::error_format::write_dispatch_byte_len_mismatch(&mut scratch, name, got, required)
}

#[cold]
fn scalar_byte_len_mismatch(name: &str, got: usize) -> String {
    let mut scratch = ErrorFormatScratch::default();
    crate::error_format::write_scalar_byte_len_mismatch(&mut scratch, name, got)
}

#[cold]
fn byte_len_overflow(caller: &str, words: usize) -> String {
    format!(
        "{caller} word count {words} overflows host byte indexing. Fix: split the Weir GPU dispatch buffer before packing or decoding."
    )
}

#[cfg(test)]
mod tests {
    use super::{byte_len_for_u32_words, DispatchU32Schema, U32_BITS_PER_WORD, U32_BYTES};

    #[test]
    fn u32_schema_reports_canonical_widths() {
        assert_eq!(U32_BYTES, 4);
        assert_eq!(U32_BITS_PER_WORD, 32);
        assert_eq!(
            DispatchU32Schema::output("schema-test", 3)
                .required_bytes()
                .expect("three u32 words should fit bytes"),
            12
        );
    }

    #[test]
    fn u32_schema_rejects_malformed_input_word_count() {
        let err = DispatchU32Schema::input("schema input", 2)
            .require_input_words(3)
            .expect_err("wrong input word count must fail closed");
        assert!(
            err.contains("schema input input has 3 u32 words")
                && err.contains("expected exactly 2")
                && err.contains("never silently pad or truncate inputs"),
            "unexpected diagnostic: {err}"
        );
    }

    #[test]
    fn u32_schema_rejects_malformed_output_byte_count() {
        let err = DispatchU32Schema::output("schema output", 2)
            .require_output_bytes(7)
            .expect_err("wrong output byte count must fail closed");
        assert!(
            err.contains("schema output output has malformed byte length")
                && err.contains("got 7 bytes")
                && err.contains("expected exactly 8")
                && err.contains("no trailing bytes"),
            "unexpected diagnostic: {err}"
        );
    }

    #[test]
    fn u32_schema_rejects_malformed_scalar_byte_count() {
        let err = DispatchU32Schema::scalar_output("schema scalar")
            .require_scalar_output_bytes(8)
            .expect_err("wrong scalar byte count must fail closed");
        assert!(
            err.contains("schema scalar output has malformed byte length")
                && err.contains("expected exactly 4")
                && err.contains("scalar u32 output size"),
            "unexpected diagnostic: {err}"
        );
    }

    #[test]
    fn u32_schema_checked_byte_length_overflow_is_actionable() {
        let err = byte_len_for_u32_words(usize::MAX, "schema overflow")
            .expect_err("overflowing byte length must fail closed");
        assert!(
            err.contains("schema overflow word count")
                && err.contains("overflows host byte indexing")
                && err.contains("split the Weir GPU dispatch buffer"),
            "unexpected diagnostic: {err}"
        );
    }
}