use tensor_wasm_jit::cache::KernelCache;
use tensor_wasm_jit::ir::{ElemType, GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
use tensor_wasm_jit::ptx_emit::emit;
use tensor_wasm_jit::rewrite::{rewrite_wasm, RewriteOptions};
fn ptxas_path() -> Option<String> {
std::env::var("TENSOR_WASM_PTXAS").ok()
}
fn run_ptxas_or_skip(ptx: &str, label: &str) {
let Some(ptxas) = ptxas_path() else {
eprintln!("TENSOR_WASM_PTXAS not set; skipping ptxas validation for {label}");
return;
};
let mut tmp = std::env::temp_dir();
tmp.push(format!("tensor_wasm_findings_{label}.ptx"));
std::fs::write(&tmp, ptx).expect("write tmp ptx");
let out = std::process::Command::new(&ptxas)
.arg("--gpu-name")
.arg("sm_80")
.arg("-o")
.arg(format!("{}.cubin", tmp.display()))
.arg(&tmp)
.output()
.expect("spawn ptxas");
assert!(
out.status.success(),
"ptxas rejected emitted PTX for {label}\nstderr:\n{}\nptx:\n{}",
String::from_utf8_lossy(&out.stderr),
ptx,
);
let _ = std::fs::remove_file(&tmp);
let _ = std::fs::remove_file(format!("{}.cubin", tmp.display()));
}
fn vec_kernel(elem: ElemType, lanes: u32, op: fn(ElemType, u32) -> TensorWasmOp) -> String {
let bp = TensorWasmKernelBlueprint::new("kernel")
.push(TensorWasmOp::LoadUnified { elem, lanes })
.push(TensorWasmOp::LoadUnified { elem, lanes })
.push(op(elem, lanes))
.push(TensorWasmOp::StoreUnified { elem, lanes })
.with_grid(GridHint {
total_threads: 256,
preferred_block_size: 128,
});
emit(&bp).expect("emit").text
}
#[test]
fn ptxas_validates_f32x4_add() {
let ptx = vec_kernel(ElemType::F32, 4, |elem, lanes| TensorWasmOp::VecAdd {
elem,
lanes,
});
assert!(ptx.contains("add.f32"));
run_ptxas_or_skip(&ptx, "f32x4_add");
}
#[test]
fn ptxas_validates_i32x4_add() {
let ptx = vec_kernel(ElemType::I32, 4, |elem, lanes| TensorWasmOp::VecAdd {
elem,
lanes,
});
assert!(ptx.contains("add.s32"));
run_ptxas_or_skip(&ptx, "i32x4_add");
}
#[test]
fn ptxas_validates_i32x4_mul() {
let ptx = vec_kernel(ElemType::I32, 4, |elem, lanes| TensorWasmOp::VecMul {
elem,
lanes,
});
assert!(ptx.contains("mul.lo.s32"));
run_ptxas_or_skip(&ptx, "i32x4_mul");
}
#[test]
fn integer_simd_emits_integer_ptx_not_float() {
let i32_ptx = vec_kernel(ElemType::I32, 4, |elem, lanes| TensorWasmOp::VecAdd {
elem,
lanes,
});
assert!(i32_ptx.contains("add.s32"), "i32x4.add must emit add.s32");
assert!(i32_ptx.contains("ld.global.lu.u32"));
assert!(i32_ptx.contains("st.global.cs.u32"));
assert!(
!i32_ptx.contains("add.f32"),
"i32x4.add must not lower to a float kernel:\n{i32_ptx}"
);
assert!(!i32_ptx.contains("ld.global.lu.f32"));
let f32_ptx = vec_kernel(ElemType::F32, 4, |elem, lanes| TensorWasmOp::VecAdd {
elem,
lanes,
});
assert!(f32_ptx.contains("add.f32"));
assert_ne!(
i32_ptx, f32_ptx,
"i32x4 and f32x4 kernels must be distinct, not the same float kernel"
);
}
#[test]
fn integer_and_float_blueprints_have_distinct_fingerprints() {
let i32_bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::I32,
lanes: 4,
});
let f32_bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4,
});
assert_ne!(i32_bp.fingerprint(), f32_bp.fingerprint());
}
struct XorShift(u64);
impl XorShift {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn fill(&mut self, buf: &mut [u8]) {
for chunk in buf.chunks_mut(8) {
let bytes = self.next_u64().to_le_bytes();
for (b, r) in chunk.iter_mut().zip(bytes.iter()) {
*b = *r;
}
}
}
}
#[test]
fn rewrite_does_not_panic_on_random_bytes() {
let cache = KernelCache::new();
let opts = RewriteOptions::default();
let mut rng = XorShift(0x9E37_79B9_7F4A_7C15);
for _case in 0..512u32 {
let len = (rng.next_u64() % 4096) as usize;
let mut bytes = vec![0u8; len];
rng.fill(&mut bytes);
let _ = rewrite_wasm(&bytes, &opts, &cache);
}
}
#[test]
fn rewrite_does_not_panic_on_wasm_prefixed_random_bytes() {
let cache = KernelCache::new();
let opts = RewriteOptions::default();
let mut rng = XorShift(0xD1B5_4A32_D192_ED03);
const HEADER: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
for _case in 0..512u32 {
let len = (rng.next_u64() % 2048) as usize;
let mut bytes = Vec::with_capacity(8 + len);
bytes.extend_from_slice(&HEADER);
let start = bytes.len();
bytes.resize(start + len, 0);
rng.fill(&mut bytes[start..]);
let _ = rewrite_wasm(&bytes, &opts, &cache);
}
}