use rustc_hash::FxHashSet as HashSet;
use vyre_foundation::ir::BinOp;
use super::literal::ResultAllocator;
use crate::{KernelBody, KernelDescriptor, KernelOp, KernelOpKind, LiteralValue};
#[must_use]
pub fn apply_tail_mask(desc: &KernelDescriptor, logical_element_count: u32) -> KernelDescriptor {
if logical_element_count == 0 {
return desc.clone();
}
let mut out = desc.clone();
let mut allocator = ResultAllocator::for_body_tree(&out.body);
let inherited = HashSet::default();
apply_to_body(
&mut out.body,
logical_element_count,
&mut allocator,
&inherited,
);
out
}
fn apply_to_body(
body: &mut KernelBody,
logical_element_count: u32,
allocator: &mut ResultAllocator,
inherited_tainted_ids: &HashSet<u32>,
) {
let mut tainted_ids = inherited_tainted_ids.clone();
let original_child_count = body.child_bodies.len();
let original_ops = std::mem::take(&mut body.ops);
let mut rewritten = Vec::with_capacity(original_ops.len());
for op in original_ops {
let taint_kind = op.kind.clone();
let taint_operands = op.operands.clone();
let taint_result = op.result;
if let Some(index_id) = tail_mask_index(&op, &tainted_ids) {
let limit_id = allocator.push_literal(
&mut rewritten,
&mut body.literals,
LiteralValue::U32(logical_element_count),
);
let cond_id = allocator.push_result(
&mut rewritten,
KernelOpKind::BinOpKind(BinOp::Lt),
vec![index_id, limit_id],
);
let child_idx = body.child_bodies.len() as u32;
body.child_bodies.push(KernelBody {
ops: vec![op],
child_bodies: Vec::new(),
literals: Vec::new(),
});
rewritten.push(KernelOp {
kind: KernelOpKind::StructuredIfThen,
operands: vec![cond_id, child_idx],
result: None,
});
} else {
rewritten.push(op);
}
mark_tainted_from_parts(&taint_kind, &taint_operands, taint_result, &mut tainted_ids);
}
body.ops = rewritten;
for child in body.child_bodies[..original_child_count].iter_mut() {
apply_to_body(child, logical_element_count, allocator, &tainted_ids);
}
}
fn tail_mask_index(op: &KernelOp, tainted_ids: &HashSet<u32>) -> Option<u32> {
let index_operand = masked_index_operand(op)?;
let index_id = op.operands.get(index_operand).copied()?;
tainted_ids.contains(&index_id).then_some(index_id)
}
fn masked_index_operand(op: &KernelOp) -> Option<usize> {
match op.kind {
KernelOpKind::LoadGlobal
| KernelOpKind::LoadShared
| KernelOpKind::LoadConstant
| KernelOpKind::StoreGlobal
| KernelOpKind::StoreShared
| KernelOpKind::Atomic { .. } => Some(1),
KernelOpKind::AsyncLoad { .. } | KernelOpKind::AsyncStore { .. } => Some(2),
_ => None,
}
}
fn mark_tainted_from_parts(
kind: &KernelOpKind,
operands: &[u32],
result: Option<u32>,
tainted_ids: &mut HashSet<u32>,
) {
let is_lane_x = matches!(kind, KernelOpKind::GlobalInvocationId)
&& operands.first().copied().unwrap_or(0) == 0;
let flows_from_tainted = operands.iter().any(|operand| tainted_ids.contains(operand));
if is_lane_x || flows_from_tainted {
if let Some(result) = result {
tainted_ids.insert(result);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelDescriptor, MemoryClass,
};
use vyre_foundation::ir::DataType;
fn desc_with_store_index(index_kind: KernelOpKind) -> KernelDescriptor {
KernelDescriptor {
id: "tail-mask-test".to_string(),
bindings: BindingLayout {
slots: vec![BindingSlot {
slot: 0,
element_type: DataType::U32,
element_count: Some(100),
memory_class: MemoryClass::Global,
visibility: BindingVisibility::WriteOnly,
name: "out".to_string(),
}],
},
dispatch: Dispatch::new(64, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: index_kind,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 1],
result: None,
},
],
child_bodies: Vec::new(),
literals: vec![LiteralValue::U32(7)],
},
}
}
#[test]
fn masks_global_lane_store() {
let desc = desc_with_store_index(KernelOpKind::GlobalInvocationId);
let out = apply_tail_mask(&desc, 100);
crate::verify::verify(&out)
.expect("Fix: tail-mask rewrite must preserve descriptor invariants");
assert_eq!(out.body.child_bodies.len(), 1);
assert!(matches!(
out.body.ops.last().map(|op| &op.kind),
Some(KernelOpKind::StructuredIfThen)
));
assert!(matches!(
out.body.ops[out.body.ops.len() - 2].kind,
KernelOpKind::BinOpKind(BinOp::Lt)
));
assert_eq!(out.body.literals.last(), Some(&LiteralValue::U32(100)));
assert!(matches!(
out.body.child_bodies[0].ops[0].kind,
KernelOpKind::StoreGlobal
));
}
#[test]
fn leaves_non_global_lane_store_unmasked() {
let desc = desc_with_store_index(KernelOpKind::LocalInvocationId);
let out = apply_tail_mask(&desc, 100);
assert_eq!(out, desc);
}
#[test]
fn zero_logical_count_is_noop() {
let desc = desc_with_store_index(KernelOpKind::GlobalInvocationId);
let out = apply_tail_mask(&desc, 0);
assert_eq!(out, desc);
}
#[test]
fn generated_result_ids_follow_existing_ids() {
let desc = desc_with_store_index(KernelOpKind::GlobalInvocationId);
let out = apply_tail_mask(&desc, 100);
let lit = &out.body.ops[out.body.ops.len() - 3];
let cond = &out.body.ops[out.body.ops.len() - 2];
assert_eq!(lit.result, Some(2));
assert_eq!(cond.result, Some(3));
}
#[test]
fn wrapper_child_is_not_re_tail_masked() {
let desc = desc_with_store_index(KernelOpKind::GlobalInvocationId);
let out = apply_tail_mask(&desc, 100);
assert_eq!(
out.body.child_bodies.len(),
1,
"exactly one wrapper child must be appended; recursion must not produce nested wrappers"
);
let wrapper = &out.body.child_bodies[0];
assert!(
wrapper.child_bodies.is_empty(),
"wrapper body must contain no nested wrapper child - re-masking would have appended one"
);
assert_eq!(
wrapper.ops.len(),
1,
"wrapper body must contain exactly the one moved StoreGlobal op"
);
assert!(matches!(wrapper.ops[0].kind, KernelOpKind::StoreGlobal));
}
}