weirflow 0.1.0

GPU-first dataflow analysis primitives for Vyre and Santh compiler pipelines.
Documentation
#[allow(deprecated)]
use super::*;
use vyre::ir::Node;

fn write_words(out: &mut Vec<u8>, words: &[u32]) {
    out.clear();
    for word in words {
        out.extend_from_slice(&word.to_le_bytes());
    }
}

fn read_word(bytes: &[u8], index: usize) -> u32 {
    let start = index * 4;
    let mut word = [0; 4];
    word.copy_from_slice(&bytes[start..start + 4]);
    u32::from_le_bytes(word)
}

#[test]
fn callgraph_build_emits_program_with_op_id() {
    let p = callgraph_build_with_count("d", "i", "pts", "out", 64);
    // The Program is wrapped in a Region tagged with OP_ID; the body
    // must contain at least one Node::Region whose generator is "weir::callgraph".
    let body = p.entry();
    let has_region = body.iter().any(|n| {
        matches!(
            n,
            Node::Region { generator, .. } if generator.as_str() == OP_ID
        )
    });
    assert!(
        has_region,
        "weir::callgraph: Program body must contain a Region tagged `{OP_ID}`"
    );
}

#[test]
fn callgraph_build_is_deterministic() {
    let p1 = callgraph_build_with_count("d", "i", "pts", "out", 64);
    let p2 = callgraph_build_with_count("d", "i", "pts", "out", 64);
    assert_eq!(
        p1.entry().len(),
        p2.entry().len(),
        "weir::callgraph: build must be deterministic across calls"
    );
}

#[test]
fn callgraph_build_declares_four_buffers() {
    // direct_edges_in, indirect_sites_in, pts_closure_in, callgraph_out
    let p = callgraph_build_with_count("d", "i", "pts", "out", 64);
    assert_eq!(
        p.buffers().len(),
        4,
        "weir::callgraph: Program must declare exactly 4 buffers (direct, indirect, pts, out)"
    );
}

#[test]
fn callgraph_soundness_is_mayover() {
    use super::super::soundness::SoundnessTagged;
    assert_eq!(
        Callgraph.soundness(),
        super::super::soundness::Soundness::MayOver,
        "weir::callgraph: indirect-call resolution is over-approximate (MayOver)"
    );
}

#[test]
fn callgraph_build_rejects_out_of_domain_input_tail_bits() {
    let err = callgraph_build_borrowed_via(
        &|_, _, _| unreachable!("validation must reject before dispatch"),
        &[0b1000],
        &[0],
        &[0],
        3,
    )
    .expect_err("tail bit outside node_count must be rejected");
    assert!(
        err.contains("outside the declared domain"),
        "unexpected diagnostic: {err}"
    );
}

#[test]
fn callgraph_build_rejects_out_of_domain_output_tail_bits() {
    let err = callgraph_build_borrowed_via(
        &|_, _, _| Ok(vec![0b1000u32.to_le_bytes().to_vec()]),
        &[0],
        &[0],
        &[0],
        3,
    )
    .expect_err("backend output tail bit outside node_count must be rejected");
    assert!(
        err.contains("outside the declared domain"),
        "unexpected diagnostic: {err}"
    );
}

#[test]
fn callgraph_build_into_reuses_caller_output_slot() {
    use std::cell::Cell;

    let mut outputs = vec![Vec::with_capacity(16)];
    let outputs_addr = outputs.as_ptr() as usize;
    let slot_addr = outputs[0].as_ptr() as usize;
    let observed = Cell::new(false);

    let out = callgraph_build_borrowed_into_via(
        &|_, inputs, grid, outputs| {
            assert_eq!(grid, Some([1, 1, 1]));
            assert_eq!(inputs.len(), 4);
            assert_eq!(outputs.len(), 1);
            assert_eq!(outputs.as_ptr() as usize, outputs_addr);
            assert_eq!(outputs[0].as_ptr() as usize, slot_addr);

            let direct = read_word(inputs[0], 0);
            let indirect = read_word(inputs[1], 0);
            let pts = read_word(inputs[2], 0);
            write_words(&mut outputs[0], &[direct | (indirect & pts)]);
            assert_eq!(outputs[0].as_ptr() as usize, slot_addr);
            observed.set(true);
            Ok(())
        },
        &[0b001],
        &[0b010],
        &[0b010],
        3,
        &mut outputs,
    )
    .expect("caller-owned output scratch should decode");

    assert!(observed.get());
    assert_eq!(out, vec![0b011]);
    assert_eq!(outputs.len(), 1);
    assert_eq!(outputs.as_ptr() as usize, outputs_addr);
    assert_eq!(outputs[0].as_ptr() as usize, slot_addr);
}

#[test]
fn callgraph_build_with_scratch_reuses_staging_and_output_slots() {
    let mut scratch = CallgraphBuildScratch::default();
    let dispatch = |_: &vyre::ir::Program,
                    inputs: &[&[u8]],
                    _: Option<[u32; 3]>|
     -> Result<Vec<Vec<u8>>, String> {
        let direct = read_word(inputs[0], 0);
        let indirect = read_word(inputs[1], 0);
        let pts = read_word(inputs[2], 0);
        let mut out = Vec::with_capacity(64);
        write_words(&mut out, &[direct | (indirect & pts)]);
        Ok(vec![out])
    };

    let first = callgraph_build_borrowed_with_scratch_via(
        &dispatch,
        &[0b001],
        &[0b010],
        &[0b010],
        3,
        &mut scratch,
    )
    .expect("first scratch-backed callgraph build should decode");
    assert_eq!(first, vec![0b011]);
    let direct_capacity = scratch.direct_edges_bytes.capacity();
    let indirect_capacity = scratch.indirect_sites_bytes.capacity();
    let pts_capacity = scratch.pts_closure_bytes.capacity();
    let out_capacity = scratch.out_bytes.capacity();
    let outputs_addr = scratch.outputs.as_ptr() as usize;
    let slot_addr = scratch.outputs[0].as_ptr() as usize;

    let second = callgraph_build_borrowed_with_scratch_via(
        &dispatch,
        &[0b100],
        &[0b010],
        &[0b000],
        3,
        &mut scratch,
    )
    .expect("second scratch-backed callgraph build should decode");

    assert_eq!(second, vec![0b100]);
    assert_eq!(scratch.direct_edges_bytes.capacity(), direct_capacity);
    assert_eq!(scratch.indirect_sites_bytes.capacity(), indirect_capacity);
    assert_eq!(scratch.pts_closure_bytes.capacity(), pts_capacity);
    assert_eq!(scratch.out_bytes.capacity(), out_capacity);
    assert_eq!(scratch.outputs.as_ptr() as usize, outputs_addr);
    assert_eq!(scratch.outputs[0].as_ptr() as usize, slot_addr);
}

#[test]
fn callgraph_build_into_result_with_scratch_reuses_decoded_result() {
    let mut scratch = CallgraphBuildScratch::default();
    let mut result = Vec::with_capacity(4);
    let result_addr = result.as_ptr() as usize;
    let dispatch = |_: &vyre::ir::Program,
                    inputs: &[&[u8]],
                    _: Option<[u32; 3]>|
     -> Result<Vec<Vec<u8>>, String> {
        let direct = read_word(inputs[0], 0);
        let indirect = read_word(inputs[1], 0);
        let pts = read_word(inputs[2], 0);
        let mut out = Vec::with_capacity(64);
        write_words(&mut out, &[direct | (indirect & pts)]);
        Ok(vec![out])
    };

    callgraph_build_borrowed_into_result_with_scratch_via(
        &dispatch,
        &[0b001],
        &[0b010],
        &[0b010],
        3,
        &mut scratch,
        &mut result,
    )
    .expect("first decoded-result scratch callgraph build should decode");
    assert_eq!(result, vec![0b011]);
    assert_eq!(result.as_ptr() as usize, result_addr);

    callgraph_build_borrowed_into_result_with_scratch_via(
        &dispatch,
        &[0b100],
        &[0b010],
        &[0b000],
        3,
        &mut scratch,
        &mut result,
    )
    .expect("second decoded-result scratch callgraph build should decode");
    assert_eq!(result, vec![0b100]);
    assert_eq!(result.as_ptr() as usize, result_addr);
}