use crate::{KernelBody, KernelDescriptor, KernelOpKind};
use rustc_hash::FxHashSet;
#[must_use]
pub fn drop_unused_bindings(desc: &KernelDescriptor) -> KernelDescriptor {
let referenced = collect_referenced_slots(desc);
let mut out = desc.clone();
out.bindings.slots.retain(|s| {
referenced.contains(&s.slot)
|| matches!(
s.memory_class,
crate::MemoryClass::Global | crate::MemoryClass::Constant
)
});
out
}
fn collect_referenced_slots(desc: &KernelDescriptor) -> FxHashSet<u32> {
let mut acc =
FxHashSet::with_capacity_and_hasher(desc.bindings.slots.len(), Default::default());
walk(&desc.body, &mut acc);
acc
}
fn walk(body: &KernelBody, acc: &mut FxHashSet<u32>) {
for op in &body.ops {
if let Some(slot) = slot_operand(&op.kind, &op.operands) {
acc.insert(slot);
}
}
for child in &body.child_bodies {
walk(child, acc);
}
}
fn slot_operand(kind: &KernelOpKind, operands: &[u32]) -> Option<u32> {
use KernelOpKind::*;
match kind {
LoadGlobal
| LoadShared
| LoadConstant
| StoreGlobal
| StoreShared
| Atomic { .. }
| AsyncLoad { .. }
| AsyncStore { .. }
| BufferLength => operands.first().copied(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelBody, KernelDescriptor,
KernelOp, KernelOpKind, LiteralValue, MemoryClass,
};
use vyre_foundation::ir::DataType;
fn slot(id: u32, name: &str) -> BindingSlot {
BindingSlot {
slot: id,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadWrite,
name: name.into(),
}
}
fn input_slot(id: u32, name: &str) -> BindingSlot {
BindingSlot {
slot: id,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadOnly,
name: name.into(),
}
}
fn shared_slot(id: u32, name: &str) -> BindingSlot {
BindingSlot {
slot: id,
element_type: DataType::U32,
element_count: Some(16),
memory_class: MemoryClass::Shared,
visibility: BindingVisibility::ReadWrite,
name: name.into(),
}
}
#[test]
fn no_bindings_no_op() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
let out = drop_unused_bindings(&desc);
assert!(out.bindings.slots.is_empty());
}
#[test]
fn all_referenced_unchanged() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![slot(0, "buf0"), slot(1, "buf1")],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 1], result: None,
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![1, 0, 1], result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let out = drop_unused_bindings(&desc);
assert_eq!(out.bindings.slots.len(), 2);
}
#[test]
fn declared_bindings_retained_even_when_unreferenced() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![
slot(0, "buf0"),
input_slot(1, "buf1_unused"),
slot(2, "buf2"),
],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 1],
result: None,
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![2, 0, 1],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
},
};
let out = drop_unused_bindings(&desc);
assert_eq!(out.bindings.slots.len(), 3);
assert!(out.bindings.slots.iter().any(|s| s.slot == 0));
assert!(out.bindings.slots.iter().any(|s| s.slot == 1));
assert!(out.bindings.slots.iter().any(|s| s.slot == 2));
}
#[test]
fn child_body_references_keep_slot() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![input_slot(0, "buf0"), slot(7, "buf7_in_child")],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::StructuredIfThen,
operands: vec![0, 0],
result: None,
},
],
child_bodies: vec![KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![7, 0, 0], result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(99)],
}],
literals: vec![LiteralValue::U32(1)],
},
};
let out = drop_unused_bindings(&desc);
assert!(out.bindings.slots.iter().any(|s| s.slot == 7));
assert!(out.bindings.slots.iter().any(|s| s.slot == 0));
}
#[test]
fn idempotent() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![slot(0, "a"), input_slot(1, "b_unused")],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::StoreGlobal,
operands: vec![0, 0, 0],
result: None,
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(1)],
},
};
let once = drop_unused_bindings(&desc);
let twice = drop_unused_bindings(&once);
assert_eq!(once.bindings.slots, twice.bindings.slots);
assert_eq!(once.bindings.slots.len(), 2);
}
#[test]
fn loadglobal_keeps_its_slot() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![slot(3, "buf3"), input_slot(99, "unused")],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::LoadGlobal,
operands: vec![3, 0],
result: Some(1),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(0)],
},
};
let out = drop_unused_bindings(&desc);
assert!(out.bindings.slots.iter().any(|s| s.slot == 3));
assert!(out.bindings.slots.iter().any(|s| s.slot == 99));
}
#[test]
fn writeonly_and_readwrite_outputs_retained_when_unreferenced() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![
input_slot(0, "in_unused"),
BindingSlot {
slot: 1,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::WriteOnly,
name: "out_writeonly".into(),
},
input_slot(2, "in_unused2"),
BindingSlot {
slot: 3,
element_type: DataType::U32,
element_count: None,
memory_class: MemoryClass::Global,
visibility: BindingVisibility::ReadWrite,
name: "out_readwrite".into(),
},
],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
let out = drop_unused_bindings(&desc);
assert!(
out.bindings.slots.iter().any(|s| s.slot == 1),
"WriteOnly output must survive even when unreferenced"
);
assert!(
out.bindings.slots.iter().any(|s| s.slot == 3),
"ReadWrite output must survive even when unreferenced"
);
assert!(
out.bindings.slots.iter().any(|s| s.slot == 0),
"ReadOnly input is part of the dispatch contract; survives"
);
assert!(
out.bindings.slots.iter().any(|s| s.slot == 2),
"ReadOnly input is part of the dispatch contract; survives"
);
}
#[test]
fn unreferenced_backend_local_bindings_are_dropped() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout {
slots: vec![input_slot(0, "host_input"), shared_slot(9, "unused_shared")],
},
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![],
literals: vec![],
},
};
let out = drop_unused_bindings(&desc);
assert!(out.bindings.slots.iter().any(|slot| slot.slot == 0));
assert!(!out.bindings.slots.iter().any(|slot| slot.slot == 9));
}
}