weirflow 0.1.0

GPU-first dataflow analysis primitives for Vyre and Santh compiler pipelines.
Documentation
use super::U32_BITS_PER_WORD;

/// Require a bitset input to contain exactly the declared number of words.
///
/// # Errors
///
/// Returns an actionable ABI error when a production GPU wrapper receives a
/// short or trailing bitset. Silent truncation/padding changes dataflow facts.
#[inline]
pub(crate) fn require_bitset_words(
    stage: &str,
    values: &[u32],
    words: usize,
) -> Result<(), String> {
    if values.len() != words {
        return Err(format!(
            "{stage} bitset has {} u32 words, expected exactly {words}. Fix: pass the declared semantic bitset width; Weir GPU wrappers never silently pad or truncate facts.",
            values.len()
        ));
    }
    Ok(())
}

/// Number of `u32` words needed to represent `bits` domain facts.
#[inline]
#[must_use]
pub(crate) const fn bitset_words(bits: u32) -> u32 {
    let full_words = bits / U32_BITS_PER_WORD;
    if bits % U32_BITS_PER_WORD == 0 {
        full_words
    } else {
        full_words + 1
    }
}

/// Checked host capacity for a semantic bitset width.
#[inline]
pub(crate) fn bitset_word_capacity(stage: &str, bits: u32) -> Result<usize, String> {
    usize::try_from(bitset_words(bits)).map_err(|source| {
        format!(
            "{stage} bitset word count cannot fit usize: {source}. Fix: shard the dataflow domain before host scratch allocation."
        )
    })
}

/// Convert a u32 GPU/IR count to a host index with a uniform diagnostic.
#[inline]
pub(crate) fn u32_to_usize(value: u32, label: &str) -> Result<usize, String> {
    usize::try_from(value).map_err(|source| {
        format!(
            "{label} cannot fit host usize: {source}. Fix: shard the dataflow problem before GPU dispatch."
        )
    })
}

/// Require unused tail bits in the final bitset word to be zero.
///
/// # Errors
///
/// Returns an actionable ABI error when a caller sets bits beyond the declared
/// node/fact domain. Production GPU wrappers must reject those bits instead of
/// masking them silently because they represent out-of-domain dataflow facts.
#[inline]
pub(crate) fn require_bitset_tail_clear(
    stage: &str,
    values: &[u32],
    domain_bits: u32,
) -> Result<(), String> {
    if domain_bits == 0 {
        if values.iter().any(|word| *word != 0) {
            return Err(format!(
                "{stage} bitset sets bits outside the declared empty domain: values must be empty or all-zero. Fix: pass zero semantic words for empty domains before GPU dispatch."
            ));
        }
        return Ok(());
    }
    let tail = domain_bits % U32_BITS_PER_WORD;
    if tail == 0 || values.is_empty() {
        return Ok(());
    }
    let allowed = (1u32 << tail) - 1;
    let actual = *values.last().unwrap_or(&0);
    if actual & !allowed != 0 {
        return Err(format!(
            "{stage} seed bitset sets bits outside the declared domain of {domain_bits} bits: last word {actual:#010x}, allowed mask {allowed:#010x}. Fix: clear out-of-domain tail bits before GPU dispatch."
        ));
    }
    Ok(())
}

/// Require at least one fixpoint iteration budget before GPU setup.
///
/// # Errors
///
/// Returns an actionable boundary error for zero-iteration closure calls.
/// Production wrappers should fail before packing buffers or constructing
/// Programs when the caller supplied an impossible convergence budget.
#[inline]
pub(crate) fn require_positive_iterations(stage: &str, max_iterations: u32) -> Result<(), String> {
    if max_iterations == 0 {
        return Err(format!(
            "{stage} received max_iterations=0. Fix: pass a positive fixpoint iteration budget; Weir GPU wrappers reject impossible convergence budgets before dispatch setup."
        ));
    }
    Ok(())
}

/// Require a forward CSR graph to match its declared node and edge counts.
///
/// # Errors
///
/// Returns an actionable ABI error for missing sentinel offsets, non-monotonic
/// offsets, mismatched edge arrays, or targets outside `node_count`.
#[inline]
pub(crate) fn require_csr_shape(
    stage: &str,
    node_count: u32,
    edge_offsets: &[u32],
    edge_targets: &[u32],
    edge_kind_mask: &[u32],
) -> Result<(), String> {
    require_csr_offsets_targets(stage, node_count, edge_offsets, edge_targets)?;
    let nodes = usize::try_from(node_count).map_err(|_| {
        format!("{stage} node_count={node_count} cannot fit usize. Fix: shard the graph before dispatch.")
    })?;
    let edge_count = u32_to_usize(edge_offsets[nodes], "final CSR offset")?;
    if edge_kind_mask.len() != edge_count {
        return Err(format!(
            "{stage} declares {edge_count} edges but edge_kind_mask has {}. Fix: pass edge-kind arrays matching the final offset exactly.",
            edge_kind_mask.len()
        ));
    }
    Ok(())
}

/// Require generated CSR offsets and targets to match the declared graph domain.
///
/// # Errors
///
/// Returns an actionable ABI error for missing sentinel offsets, non-monotonic
/// offsets, mismatched target arrays, orphan prefix edges, or targets outside
/// `node_count`.
#[inline]
pub(crate) fn require_csr_offsets_targets(
    stage: &str,
    node_count: u32,
    edge_offsets: &[u32],
    edge_targets: &[u32],
) -> Result<(), String> {
    let nodes = usize::try_from(node_count).map_err(|_| {
        format!("{stage} node_count={node_count} cannot fit usize. Fix: shard the graph before dispatch.")
    })?;
    let expected_offsets = nodes.checked_add(1).ok_or_else(|| {
        format!("{stage} node_count={node_count} overflows when adding CSR sentinel offset.")
    })?;
    if edge_offsets.len() != expected_offsets {
        return Err(format!(
            "{stage} edge_offsets has {} entries, expected exactly node_count + 1 = {expected_offsets}. Fix: pass a complete CSR offset table.",
            edge_offsets.len()
        ));
    }
    let edge_count = u32_to_usize(edge_offsets[nodes], "final CSR offset")?;
    if edge_targets.len() != edge_count {
        return Err(format!(
            "{stage} declares {edge_count} edges but edge_targets has {}. Fix: pass edge arrays matching the final offset exactly.",
            edge_targets.len()
        ));
    }
    if edge_offsets[0] != 0 {
        return Err(format!(
            "{stage} edge_offsets[0] is {}, expected 0. Fix: rebuild CSR offsets from zero; Weir GPU wrappers reject orphan prefix edges.",
            edge_offsets[0]
        ));
    }
    for node in 0..nodes {
        let start = u32_to_usize(edge_offsets[node], "CSR row start offset")?;
        let end = u32_to_usize(edge_offsets[node + 1], "CSR row end offset")?;
        if start > end {
            return Err(format!(
                "{stage} edge_offsets is not monotonic at node {node}: start={start}, end={end}. Fix: rebuild CSR offsets."
            ));
        }
        if end > edge_count {
            return Err(format!(
                "{stage} edge_offsets[{}]={end} exceeds declared edge count {edge_count}. Fix: rebuild CSR offsets.",
                node + 1
            ));
        }
        for (edge, &target) in edge_targets.iter().enumerate().take(end).skip(start) {
            if target >= node_count {
                return Err(format!(
                    "{stage} edge {edge} targets node {target}, but node_count is {node_count}. Fix: reject or remap malformed graph edges before dispatch."
                ));
            }
        }
    }
    Ok(())
}