vyre-wgpu 0.1.0

wgpu backend for vyre IR — implements VyreBackend, owns GPU runtime, buffer pool, pipeline cache
Documentation
//! DFA scan pipeline compilation and transition-table validation.
//!
//! Lowers the `dfa_scan` op IR to WGSL, compiles the compute pipeline
//! against a given [`wgpu::Device`], and verifies that input DFA
//! tables carry the expected sentinel + byte-class shape before the
//! kernel runs. Every stage surfaces an actionable [`Error::Dfa`] on
//! failure so the caller can decide whether to reject the build.

use crate::engine::dfa::buffers::{buffer_bytes, gpu_err, match_buffer_size};
use crate::engine::dfa::{BYTE_CLASSES, MAX_DFA_MATCHES, SENTINEL_NO_ACCEPT};
use vyre::error::{Error, Result};

pub(crate) fn compile_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
    vyre::ops::registry::gate::verify_certificate(
        vyre::ops::match_ops::dfa_scan::DfaScan::SPEC.id(),
    )
    .map_err(|source| Error::Dfa {
        message: source.to_string(),
    })?;

    let wgsl = vyre::lower::wgsl::lower(&vyre::ops::match_ops::dfa_scan::DfaScan::program())
        .map_err(|source| Error::Dfa {
            message: format!("failed to lower DFA scan IR to WGSL: {source}. Fix: reject this runtime build or repair the DFA scan IR composition."),
        })?;
    crate::runtime::compile_compute_pipeline(device, "vyre dfa pipeline", &wgsl, "main")
        .map_err(|source| Error::Dfa {
            message: format!(
                "failed to compile DFA scan pipeline: {source}. Fix: repair the DFA scan WGSL or GPU runtime configuration."
            ),
        })
}

pub(crate) fn validate_compile_inputs(
    device: &wgpu::Device,
    transitions: &[u32],
    state_count: usize,
    accept_states: &[(u32, u32)],
    output_links: &[u32],
    pattern_lengths: &[u32],
    max_matches: u32,
) -> Result<()> {
    if state_count == 0 {
        return gpu_err(
            "DFA state_count is zero. Fix: compile a table with at least the start state.",
        );
    }
    if u32::try_from(state_count).is_err() {
        return gpu_err(&format!(
            "DFA state_count {state_count} exceeds u32::MAX. Fix: split the automaton or reduce states."
        ));
    }
    if max_matches == 0 {
        return gpu_err("DFA max_matches is zero. Fix: use a positive match capacity.");
    }
    if max_matches > MAX_DFA_MATCHES {
        return gpu_err(&format!(
            "DFA max_matches {max_matches} exceeds {MAX_DFA_MATCHES}. Fix: lower max_matches or shard the scan."
        ));
    }
    let expected = state_count
        .checked_mul(BYTE_CLASSES)
        .ok_or_else(|| Error::Dfa {
            message: format!(
                "DFA state_count {state_count} * 256 overflows usize. Fix: reduce state_count."
            ),
        })?;
    if transitions.len() != expected {
        return gpu_err(&format!(
            "DFA transition table has {} entries but state_count * 256 is {expected}. Fix: pass a full byte-indexed transition table.",
            transitions.len()
        ));
    }
    if output_links.len() != state_count {
        return gpu_err(&format!(
            "DFA output_links has {} entries but state_count is {state_count}. Fix: pass one output link per state.",
            output_links.len()
        ));
    }

    for (index, &target) in transitions.iter().enumerate() {
        let target_index = usize::try_from(target).map_err(|source| Error::Dfa {
            message: format!(
                "DFA transition {index} target {target} cannot fit usize: {source}. Fix: reject this transition table on this platform."
            ),
        })?;
        if target_index >= state_count {
            return gpu_err(&format!(
                "DFA transition {index} targets state {target}, outside 0..{state_count}. Fix: ensure every transition target is valid."
            ));
        }
    }

    let mut seen_accept = vec![false; state_count];
    for &(state, pattern_id) in accept_states {
        let state_index = usize::try_from(state).map_err(|source| Error::Dfa {
            message: format!(
                "DFA accept state {state} cannot fit usize: {source}. Fix: reject this accept table on this platform."
            ),
        })?;
        if state_index >= state_count {
            return gpu_err(&format!(
                "DFA accept state {state} is outside 0..{state_count}. Fix: remove or correct the accept entry."
            ));
        }
        if seen_accept[state_index] {
            return gpu_err(&format!(
                "DFA accept state {state} appears more than once. Fix: choose one direct pattern id per state and use output_links for suffix accepts."
            ));
        }
        seen_accept[state_index] = true;
        let pattern_index = usize::try_from(pattern_id).map_err(|source| Error::Dfa {
            message: format!(
                "DFA accept pattern {pattern_id} cannot fit usize: {source}. Fix: reject this accept table on this platform."
            ),
        })?;
        match pattern_lengths.get(pattern_index) {
            Some(0) => {
                return gpu_err(&format!(
                    "DFA accept pattern {pattern_id} has zero length. Fix: exclude empty patterns from GPU DFA scans."
                ));
            }
            Some(_) => {}
            None => {
                return gpu_err(&format!(
                    "DFA accept pattern {pattern_id} has no pattern length. Fix: provide pattern_lengths[{pattern_id}]."
                ));
            }
        }
    }

    validate_output_links(state_count, output_links)?;
    let limit = u64::from(device.limits().max_storage_buffer_binding_size);
    for (label, bytes) in [
        ("transition", buffer_bytes(transitions.len())?),
        ("accept", buffer_bytes(state_count)?),
        ("output link", buffer_bytes(state_count)?),
        ("pattern length", buffer_bytes(pattern_lengths.len())?),
        ("match", match_buffer_size(max_matches)?),
    ] {
        if bytes > limit {
            return gpu_err(&format!(
                "DFA {label} buffer is {bytes} bytes but adapter max_storage_buffer_binding_size is {limit}. Fix: reduce DFA size or use a larger adapter."
            ));
        }
    }
    Ok(())
}

pub fn validate_output_links(state_count: usize, output_links: &[u32]) -> Result<()> {
    for (state, &target) in output_links.iter().enumerate() {
        let target_index = if target == SENTINEL_NO_ACCEPT {
            None
        } else {
            Some(usize::try_from(target).map_err(|source| Error::Dfa {
                message: format!(
                    "DFA output link target {target} cannot fit usize: {source}. Fix: reject this output-link table on this platform."
                ),
            })?)
        };
        if target_index.is_some_and(|index| index >= state_count) {
            return gpu_err(&format!(
                "DFA output link from state {state} targets state {target}, outside 0..{state_count}. Fix: use a valid state or 0xFFFF_FFFF."
            ));
        }
        let mut seen = vec![false; state_count];
        let mut next = target;
        while next != SENTINEL_NO_ACCEPT {
            let index = usize::try_from(next).map_err(|source| Error::Dfa {
                message: format!(
                    "DFA output link state {next} cannot fit usize: {source}. Fix: reject this output-link chain on this platform."
                ),
            })?;
            if seen[index] {
                return gpu_err(&format!(
                    "DFA output link chain from state {state} cycles at state {next}. Fix: emit acyclic output links."
                ));
            }
            seen[index] = true;
            next = output_links[index];
        }
    }
    Ok(())
}

pub(crate) fn build_accept_map(
    state_count: usize,
    accept_states: &[(u32, u32)],
) -> Result<Vec<u32>> {
    let mut accept_map = vec![SENTINEL_NO_ACCEPT; state_count];
    for &(state, pattern_id) in accept_states {
        let index = usize::try_from(state).map_err(|source| Error::Dfa {
            message: format!(
                "DFA accept state {state} cannot fit usize while building accept map: {source}. Fix: reject this malformed accept table."
            ),
        })?;
        let slot = accept_map.get_mut(index).ok_or_else(|| Error::Dfa {
            message: format!(
                "DFA accept state {state} is outside 0..{state_count} while building accept map. Fix: reject this malformed accept table."
            ),
        })?;
        *slot = pattern_id;
    }
    Ok(accept_map)
}