use crate::spirv_module::Instruction;
use crate::spirv_module::Module;
use crate::spirv_module::Operand;
use spirv::{Op, StorageClass, Word};
use std::collections::HashMap;
pub(super) fn rewrite_logical_pointer_phis(module: &mut Module) -> bool {
rewrite_pointer_phis(module, false)
}
pub(super) fn rewrite_variable_pointer_phis(module: &mut Module) -> bool {
rewrite_pointer_phis(module, true)
}
pub(super) fn rewrite_integer_width_phis(module: &mut Module) -> bool {
let mut int_types: HashMap<Word, (u32, u32)> = HashMap::new();
let mut global_value_type: HashMap<Word, Word> = HashMap::new();
for i in &module.types_global_values {
if i.class.opcode == Op::TypeInt {
if let (Some(rid), Some(Operand::LiteralBit32(w)), Some(Operand::LiteralBit32(s))) =
(i.result_id, i.operands.first(), i.operands.get(1))
{
int_types.insert(rid, (*w, *s));
}
}
if let (Some(rid), Some(rty)) = (i.result_id, i.result_type) {
global_value_type.insert(rid, rty);
}
}
if int_types.is_empty() {
return false;
}
let mut next_id = module.header.as_ref().map(|h| h.bound).unwrap_or(0);
let mut fresh = || {
let id = next_id;
next_id += 1;
id
};
let mut any = false;
for function in &mut module.functions {
let mut value_type: HashMap<Word, Word> = global_value_type.clone();
for block in &function.blocks {
for inst in &block.instructions {
if let (Some(rid), Some(rty)) = (inst.result_id, inst.result_type) {
value_type.insert(rid, rty);
}
}
}
let mut label_to_block: HashMap<Word, usize> = HashMap::new();
for (bi, block) in function.blocks.iter().enumerate() {
if let Some(rid) = block.label.as_ref().and_then(|l| l.result_id) {
label_to_block.insert(rid, bi);
}
}
struct Conv {
phi_block: usize,
phi_result: Word,
value_pos: usize, pred_label: Word,
value: Word,
target_ty: Word,
}
let mut convs: Vec<Conv> = Vec::new();
for (bi, block) in function.blocks.iter().enumerate() {
for inst in &block.instructions {
if inst.class.opcode != Op::Phi {
continue;
}
let Some(rty) = inst.result_type else {
continue;
};
let Some(&(rwidth, rsign)) = int_types.get(&rty) else {
continue; };
let Some(phi_result) = inst.result_id else {
continue;
};
let mut pos = 0;
while pos + 1 < inst.operands.len() {
if let (Operand::IdRef(v), Operand::IdRef(b)) =
(&inst.operands[pos], &inst.operands[pos + 1])
{
if let Some(vty) = value_type.get(v) {
if let Some(&(vwidth, _)) = int_types.get(vty) {
if vwidth > rwidth && rsign == 0 && label_to_block.contains_key(b) {
convs.push(Conv {
phi_block: bi,
phi_result,
value_pos: pos,
pred_label: *b,
value: *v,
target_ty: rty,
});
}
}
}
}
pos += 2;
}
}
}
if convs.is_empty() {
continue;
}
let mut conv_id: HashMap<(Word, Word, Word), Word> = HashMap::new();
for c in &convs {
conv_id
.entry((c.pred_label, c.value, c.target_ty))
.or_insert_with(&mut fresh);
}
let mut inserted: std::collections::HashSet<(Word, Word, Word)> = Default::default();
for c in &convs {
let key = (c.pred_label, c.value, c.target_ty);
if !inserted.insert(key) {
continue;
}
let new_id = conv_id[&key];
let pred_idx = label_to_block[&c.pred_label];
let block = &mut function.blocks[pred_idx];
let mut insert_at = block.instructions.len().saturating_sub(1); if insert_at > 0
&& matches!(
block.instructions[insert_at - 1].class.opcode,
Op::SelectionMerge | Op::LoopMerge
)
{
insert_at -= 1; }
block.instructions.insert(
insert_at,
Instruction::new(
Op::UConvert,
Some(c.target_ty),
Some(new_id),
vec![Operand::IdRef(c.value)],
),
);
any = true;
}
for c in &convs {
let new_id = conv_id[&(c.pred_label, c.value, c.target_ty)];
let block = &mut function.blocks[c.phi_block];
if let Some(inst) = block
.instructions
.iter_mut()
.find(|i| i.result_id == Some(c.phi_result))
{
if let Some(op) = inst.operands.get_mut(c.value_pos) {
*op = Operand::IdRef(new_id);
}
}
}
}
if any {
if let Some(header) = module.header.as_mut() {
header.bound = next_id;
}
}
any
}
fn rewrite_pointer_phis(module: &mut Module, include_variable_pointer_classes: bool) -> bool {
let type_defs: HashMap<Word, Instruction> = module
.types_global_values
.iter()
.filter_map(|i| i.result_id.map(|id| (id, i.clone())))
.collect();
let global_vars: std::collections::HashSet<Word> = module
.types_global_values
.iter()
.filter(|i| i.class.opcode == Op::Variable)
.filter_map(|i| i.result_id)
.collect();
let ptr_storage = |ptr_ty: Word| -> Option<StorageClass> {
let inst = type_defs.get(&ptr_ty)?;
if inst.class.opcode != Op::TypePointer {
return None;
}
match inst.operands.first()? {
Operand::StorageClass(s) => Some(*s),
_ => None,
}
};
let mut next_id = module.header.as_ref().map(|h| h.bound).unwrap_or(0);
let mut fresh = || {
let id = next_id;
next_id += 1;
id
};
let mut any = false;
for function in &mut module.functions {
let mut eligible_bases = global_vars.clone();
if let Some(entry) = function.blocks.first() {
for inst in &entry.instructions {
if inst.class.opcode == Op::Variable {
if let Some(rid) = inst.result_id {
eligible_bases.insert(rid);
}
}
}
}
let mut value_def: HashMap<Word, Instruction> = HashMap::new();
let mut value_type: HashMap<Word, Word> = HashMap::new();
for block in &function.blocks {
for inst in &block.instructions {
if let Some(rid) = inst.result_id {
value_def.insert(rid, inst.clone());
if let Some(rty) = inst.result_type {
value_type.insert(rid, rty);
}
}
}
}
let const_type = |id: Word| -> Option<Word> {
value_type
.get(&id)
.copied()
.or_else(|| type_defs.get(&id).and_then(|i| i.result_type))
};
enum IndexSrc {
Reuse(Word),
Phi(Word, Vec<(Word, Word)>),
}
struct Plan {
block: usize,
phi_result: Word,
ptr_ty: Word,
base: Word,
index_srcs: Vec<IndexSrc>,
}
let mut plans: Vec<Plan> = Vec::new();
for (bi, block) in function.blocks.iter().enumerate() {
for inst in &block.instructions {
if inst.class.opcode != Op::Phi {
continue;
}
let Some(ptr_ty) = inst.result_type else {
continue;
};
let Some(storage) = ptr_storage(ptr_ty) else {
continue; };
if !include_variable_pointer_classes
&& matches!(
storage,
StorageClass::StorageBuffer | StorageClass::Workgroup
)
{
continue; }
let Some(phi_result) = inst.result_id else {
continue;
};
let arms: Vec<(Word, Word)> = inst
.operands
.chunks(2)
.filter_map(|c| match (c.first(), c.get(1)) {
(Some(Operand::IdRef(v)), Some(Operand::IdRef(b))) => Some((*v, *b)),
_ => None,
})
.collect();
if arms.len() < 2 || arms.len() * 2 != inst.operands.len() {
continue;
}
let mut base: Option<Word> = None;
let mut arity: Option<usize> = None;
let mut arm_defs: Vec<(Instruction, Word)> = Vec::new(); let mut ok = true;
for (v, b) in &arms {
let Some(def) = value_def.get(v) else {
ok = false;
break;
};
if !matches!(def.class.opcode, Op::AccessChain | Op::InBoundsAccessChain) {
ok = false;
break;
}
let Some(Operand::IdRef(b0)) = def.operands.first() else {
ok = false;
break;
};
if !eligible_bases.contains(b0) {
ok = false;
break;
}
let k = def.operands.len() - 1;
match (base, arity) {
(None, None) => {
base = Some(*b0);
arity = Some(k);
}
(Some(pb), Some(pk)) if pb == *b0 && pk == k => {}
_ => {
ok = false;
break;
}
}
arm_defs.push((def.clone(), *b));
}
if !ok {
continue;
}
let (Some(base), Some(k)) = (base, arity) else {
continue;
};
if k == 0 {
continue;
}
let mut index_srcs: Vec<IndexSrc> = Vec::new();
let mut positions_ok = true;
for j in 0..k {
let mut entries: Vec<(Word, Word)> = Vec::new();
let mut idx_ty: Option<Word> = None;
for (def, blk) in &arm_defs {
let Some(Operand::IdRef(idx)) = def.operands.get(1 + j) else {
positions_ok = false;
break;
};
if idx_ty.is_none() {
idx_ty = const_type(*idx);
}
entries.push((*idx, *blk));
}
if !positions_ok {
break;
}
if entries.iter().all(|(v, _)| *v == entries[0].0) {
index_srcs.push(IndexSrc::Reuse(entries[0].0));
} else {
let Some(idx_ty) = idx_ty else {
positions_ok = false;
break;
};
index_srcs.push(IndexSrc::Phi(idx_ty, entries));
}
}
if !positions_ok {
continue;
}
plans.push(Plan {
block: bi,
phi_result,
ptr_ty,
base,
index_srcs,
});
}
}
if plans.is_empty() {
continue;
}
let mut remap: HashMap<Word, Word> = HashMap::new();
for plan in &plans {
let block = &mut function.blocks[plan.block];
let mut new_phis: Vec<Instruction> = Vec::new();
let mut chain_args: Vec<Operand> = vec![Operand::IdRef(plan.base)];
for src in &plan.index_srcs {
match src {
IndexSrc::Reuse(id) => chain_args.push(Operand::IdRef(*id)),
IndexSrc::Phi(idx_ty, entries) => {
let idx_phi_id = fresh();
let mut ops: Vec<Operand> = Vec::with_capacity(entries.len() * 2);
for (val, blk) in entries {
ops.push(Operand::IdRef(*val));
ops.push(Operand::IdRef(*blk));
}
new_phis.push(Instruction::new(
Op::Phi,
Some(*idx_ty),
Some(idx_phi_id),
ops,
));
chain_args.push(Operand::IdRef(idx_phi_id));
}
}
}
let new_ptr_id = fresh();
let access = Instruction::new(
Op::InBoundsAccessChain,
Some(plan.ptr_ty),
Some(new_ptr_id),
chain_args,
);
block
.instructions
.retain(|i| i.result_id != Some(plan.phi_result));
for (k, p) in new_phis.into_iter().enumerate() {
block.instructions.insert(k, p);
}
let after_phis = block
.instructions
.iter()
.position(|i| i.class.opcode != Op::Phi)
.unwrap_or(block.instructions.len());
block.instructions.insert(after_phis, access);
remap.insert(plan.phi_result, new_ptr_id);
any = true;
}
if !remap.is_empty() {
for block in &mut function.blocks {
for inst in &mut block.instructions {
for op in &mut inst.operands {
if let Operand::IdRef(id) = op {
if let Some(new) = remap.get(id) {
*id = *new;
}
}
}
}
}
}
}
if any {
if let Some(header) = module.header.as_mut() {
header.bound = next_id;
}
}
any
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spirv_module::{Block, Function, ModuleHeader};
fn inst(op: Op, ty: Option<Word>, res: Option<Word>, ops: Vec<Operand>) -> Instruction {
Instruction::new(op, ty, res, ops)
}
#[test]
fn narrows_ulong_incoming_of_uint_phi_with_uconvert_in_predecessor() {
let mut m = Module::new();
m.header = Some(ModuleHeader::new(50));
m.types_global_values = vec![
inst(
Op::TypeInt,
None,
Some(1),
vec![Operand::LiteralBit32(32), Operand::LiteralBit32(0)],
),
inst(
Op::TypeInt,
None,
Some(2),
vec![Operand::LiteralBit32(64), Operand::LiteralBit32(0)],
),
inst(
Op::Constant,
Some(1),
Some(10),
vec![Operand::LiteralBit32(1)],
),
inst(
Op::Constant,
Some(2),
Some(11),
vec![Operand::LiteralBit32(1)],
),
];
let entry = Block {
label: Some(inst(Op::Label, None, Some(40), vec![])),
instructions: vec![inst(Op::Branch, None, None, vec![Operand::IdRef(41)])],
};
let header = Block {
label: Some(inst(Op::Label, None, Some(41), vec![])),
instructions: vec![
inst(
Op::Phi,
Some(1),
Some(30),
vec![
Operand::IdRef(10),
Operand::IdRef(40),
Operand::IdRef(20),
Operand::IdRef(42),
],
),
inst(Op::Branch, None, None, vec![Operand::IdRef(42)]),
],
};
let latch = Block {
label: Some(inst(Op::Label, None, Some(42), vec![])),
instructions: vec![
inst(
Op::IAdd,
Some(2),
Some(20),
vec![Operand::IdRef(11), Operand::IdRef(11)],
),
inst(Op::Branch, None, None, vec![Operand::IdRef(41)]),
],
};
let mut func = Function::new();
func.blocks = vec![entry, header, latch];
m.functions = vec![func];
assert!(rewrite_integer_width_phis(&mut m));
let latch = &m.functions[0].blocks[2];
let conv = latch
.instructions
.iter()
.find(|i| i.class.opcode == Op::UConvert)
.expect("uconvert inserted in predecessor");
assert_eq!(conv.result_type, Some(1)); assert_eq!(conv.operands[0], Operand::IdRef(20)); assert_eq!(latch.instructions.last().unwrap().class.opcode, Op::Branch);
let header = &m.functions[0].blocks[1];
let phi = header
.instructions
.iter()
.find(|i| i.class.opcode == Op::Phi)
.unwrap();
assert_eq!(phi.operands[2], Operand::IdRef(conv.result_id.unwrap()));
assert_eq!(phi.operands[3], Operand::IdRef(42));
assert_eq!(phi.operands[0], Operand::IdRef(10));
}
#[test]
fn rewrites_struct_alloca_pointer_phi_reusing_constant_first_index() {
let mut m = Module::new();
m.header = Some(ModuleHeader::new(60));
m.types_global_values = vec![
inst(
Op::TypeInt,
None,
Some(1),
vec![Operand::LiteralBit32(32), Operand::LiteralBit32(0)],
),
inst(
Op::TypeFloat,
None,
Some(2),
vec![Operand::LiteralBit32(32)],
),
inst(Op::TypeStruct, None, Some(3), vec![Operand::IdRef(2)]),
inst(
Op::TypePointer,
None,
Some(4),
vec![
Operand::StorageClass(StorageClass::Function),
Operand::IdRef(3),
],
),
inst(
Op::TypePointer,
None,
Some(5),
vec![
Operand::StorageClass(StorageClass::Function),
Operand::IdRef(2),
],
),
inst(
Op::Constant,
Some(1),
Some(10),
vec![Operand::LiteralBit32(0)],
),
inst(
Op::Constant,
Some(1),
Some(11),
vec![Operand::LiteralBit32(1)],
),
];
let entry = Block {
label: Some(inst(Op::Label, None, Some(19), vec![])),
instructions: vec![inst(
Op::Variable,
Some(4),
Some(20),
vec![Operand::StorageClass(StorageClass::Function)],
)],
};
let arm_a = Block {
label: Some(inst(Op::Label, None, Some(29), vec![])),
instructions: vec![inst(
Op::InBoundsAccessChain,
Some(5),
Some(30),
vec![Operand::IdRef(20), Operand::IdRef(10), Operand::IdRef(11)],
)],
};
let arm_b = Block {
label: Some(inst(Op::Label, None, Some(32), vec![])),
instructions: vec![
inst(Op::Undef, Some(1), Some(40), vec![]),
inst(
Op::InBoundsAccessChain,
Some(5),
Some(31),
vec![Operand::IdRef(20), Operand::IdRef(10), Operand::IdRef(40)],
),
],
};
let header = Block {
label: Some(inst(Op::Label, None, Some(49), vec![])),
instructions: vec![
inst(
Op::Phi,
Some(5),
Some(50),
vec![
Operand::IdRef(30),
Operand::IdRef(29),
Operand::IdRef(31),
Operand::IdRef(32),
],
),
inst(Op::Load, Some(2), Some(51), vec![Operand::IdRef(50)]),
],
};
let mut func = Function::new();
func.blocks = vec![entry, arm_a, arm_b, header];
m.functions = vec![func];
assert!(rewrite_logical_pointer_phis(&mut m));
let hdr = &m.functions[0].blocks[3];
assert!(!hdr
.instructions
.iter()
.any(|i| i.class.opcode == Op::Phi && i.result_type == Some(5)));
let int_phi = hdr
.instructions
.iter()
.find(|i| i.class.opcode == Op::Phi && i.result_type == Some(1));
let int_phi = int_phi.expect("differing index should be phi'd");
let int_phi_id = int_phi.result_id.unwrap();
let chain = hdr
.instructions
.iter()
.find(|i| i.class.opcode == Op::InBoundsAccessChain)
.expect("rematerialized access chain");
assert_eq!(chain.operands[0], Operand::IdRef(20)); assert_eq!(chain.operands[1], Operand::IdRef(10)); assert_eq!(chain.operands[2], Operand::IdRef(int_phi_id)); let load = hdr
.instructions
.iter()
.find(|i| i.class.opcode == Op::Load)
.unwrap();
assert_eq!(load.operands[0], Operand::IdRef(chain.result_id.unwrap()));
}
}