use super::*;
mod access_provenance;
pub(in crate::passes) use access_provenance::*;
mod private_memory;
pub(in crate::passes) use private_memory::*;
mod access_chain;
pub(in crate::passes) use access_chain::*;
mod index_remap;
pub(in crate::passes) use index_remap::*;
mod vector_subword;
pub(in crate::passes) use vector_subword::*;
mod scalar_store;
pub(in crate::passes) use scalar_store::*;
mod workgroup;
pub(in crate::passes) use workgroup::*;
mod byte_aggregate;
pub(in crate::passes) use byte_aggregate::*;
mod dynamic_reinterpret;
pub(in crate::passes) use dynamic_reinterpret::*;
mod raw_byte;
pub(in crate::passes) use raw_byte::*;
#[cfg(test)]
mod byte_reinterpret_tests {
use super::*;
use crate::spirv_module::{Block, Function};
fn install_entry(ctx: &mut Ctx, mut body: Vec<Instruction>) {
body.push(Instruction::new(Op::Return, None, None, vec![]));
let label = ctx.module.fresh_id();
let func_id = ctx.module.fresh_id();
ctx.module.functions.push(Function {
def: Some(Instruction::new(Op::Function, None, Some(func_id), vec![])),
end: Some(Instruction::new(Op::FunctionEnd, None, None, vec![])),
parameters: vec![],
blocks: vec![Block {
label: Some(Instruction::new(Op::Label, None, Some(label), vec![])),
instructions: body,
}],
});
}
fn storage_buffer_var(ctx: &mut Ctx, ptr_ty: Word) -> Word {
let id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::Variable,
Some(ptr_ty),
Some(id),
vec![Operand::StorageClass(StorageClass::StorageBuffer)],
));
id
}
fn only_inst(ctx: &Ctx) -> &Instruction {
&ctx.module.functions[0].blocks[0].instructions[0]
}
#[test]
fn strided_descent_flips_overindexing_chain_to_ptr_access_chain() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(float);
let ptr_rt = ctx.ty_ptr(StorageClass::StorageBuffer, rt);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_rt);
let i = ctx.const_uint(0);
let j = ctx.const_uint(1);
let chain_id = ctx.module.fresh_id();
let ops = vec![Operand::IdRef(base), Operand::IdRef(i), Operand::IdRef(j)];
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
ops.clone(),
)],
);
rewrite_strided_descent_access_chains(&mut ctx, 0);
let inst = only_inst(&ctx);
assert_eq!(inst.class.opcode, Op::PtrAccessChain, "opcode flipped");
assert_eq!(inst.result_type, Some(ptr_float), "result type preserved");
assert_eq!(inst.result_id, Some(chain_id), "result id preserved");
assert_eq!(
inst.operands, ops,
"operands (base + both indices) unchanged"
);
}
#[test]
fn strided_descent_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(float);
let ptr_rt = ctx.ty_ptr(StorageClass::StorageBuffer, rt);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_rt);
let i = ctx.const_uint(0);
let j = ctx.const_uint(1);
let chain_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(i), Operand::IdRef(j)],
)],
);
rewrite_strided_descent_access_chains(&mut ctx, 0);
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_strided_descent_access_chains(&mut ctx, 0);
let after_second = &ctx.module.functions[0].blocks[0].instructions;
assert_eq!(
&after_first, after_second,
"second application leaves the block byte-identical"
);
}
#[test]
fn strided_descent_leaves_valid_chain_untouched() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let inner = ctx.ty_runtime_array(float);
let outer = ctx.ty_runtime_array(inner);
let ptr_outer = ctx.ty_ptr(StorageClass::StorageBuffer, outer);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_outer);
let i = ctx.const_uint(0);
let j = ctx.const_uint(1);
let chain_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(i), Operand::IdRef(j)],
)],
);
rewrite_strided_descent_access_chains(&mut ctx, 0);
assert_eq!(
only_inst(&ctx).class.opcode,
Op::InBoundsAccessChain,
"a cleanly-walking chain is not rewritten"
);
}
fn find_inst(ctx: &Ctx, id: Word) -> &Instruction {
ctx.module.functions[0].blocks[0]
.instructions
.iter()
.find(|i| i.result_id == Some(id))
.expect("instruction with the given result id")
}
#[test]
fn dynamic_struct_index_reinterpret_descends_member0_and_bitcasts_load() {
let mut ctx = Ctx::new(Module::new());
let uint = ctx.ty_uint();
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(uint);
let struct_id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeStruct,
None,
Some(struct_id),
vec![Operand::IdRef(rt)],
));
let ptr_struct = ctx.ty_ptr(StorageClass::StorageBuffer, struct_id);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_struct);
let dyn_idx = ctx.module.fresh_id();
let chain_id = ctx.module.fresh_id();
let val_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![
Instruction::new(Op::Undef, Some(uint), Some(dyn_idx), vec![]),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(dyn_idx)],
),
Instruction::new(
Op::Load,
Some(float),
Some(val_id),
vec![Operand::IdRef(chain_id)],
),
],
);
rewrite_dynamic_struct_index_reinterpret(&mut ctx, 0).unwrap();
let chain = find_inst(&ctx, chain_id);
assert_eq!(chain.class.opcode, Op::InBoundsAccessChain);
assert_eq!(chain.operands.len(), 3, "member-0 index inserted");
assert_eq!(chain.operands[0], Operand::IdRef(base));
let Operand::IdRef(member0) = chain.operands[1] else {
panic!("member index is not an id");
};
assert_eq!(
const_u32(&ctx, member0),
Some(0),
"inserted index is constant 0"
);
let Some(Operand::IdRef(chain_pointee)) =
type_def_of(&ctx, chain.result_type.unwrap()).and_then(|d| d.operands.get(1).cloned())
else {
panic!("chain result type is not a pointer");
};
assert_eq!(
chain_pointee, uint,
"chain retyped to the uint element pointer"
);
let cast = find_inst(&ctx, val_id);
assert_eq!(cast.class.opcode, Op::Bitcast, "reinterpret load → bitcast");
assert_eq!(cast.result_type, Some(float));
let Operand::IdRef(load_id) = cast.operands[0] else {
panic!("bitcast source is not an id");
};
assert_eq!(
find_inst(&ctx, load_id).result_type,
Some(uint),
"the split load reads the uint element"
);
}
#[test]
fn dynamic_struct_index_reinterpret_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
let uint = ctx.ty_uint();
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(uint);
let struct_id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeStruct,
None,
Some(struct_id),
vec![Operand::IdRef(rt)],
));
let ptr_struct = ctx.ty_ptr(StorageClass::StorageBuffer, struct_id);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_struct);
let dyn_idx = ctx.module.fresh_id();
let chain_id = ctx.module.fresh_id();
let val_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![
Instruction::new(Op::Undef, Some(uint), Some(dyn_idx), vec![]),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(dyn_idx)],
),
Instruction::new(
Op::Load,
Some(float),
Some(val_id),
vec![Operand::IdRef(chain_id)],
),
],
);
rewrite_dynamic_struct_index_reinterpret(&mut ctx, 0).unwrap();
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_dynamic_struct_index_reinterpret(&mut ctx, 0).unwrap();
assert_eq!(
after_first, ctx.module.functions[0].blocks[0].instructions,
"second application is a no-op"
);
}
#[test]
fn dynamic_struct_index_reinterpret_skips_constant_index() {
let mut ctx = Ctx::new(Module::new());
let uint = ctx.ty_uint();
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(uint);
let struct_id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeStruct,
None,
Some(struct_id),
vec![Operand::IdRef(rt)],
));
let ptr_struct = ctx.ty_ptr(StorageClass::StorageBuffer, struct_id);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_struct);
let const_idx = ctx.const_uint(3);
let chain_id = ctx.module.fresh_id();
let val_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(const_idx)],
),
Instruction::new(
Op::Load,
Some(float),
Some(val_id),
vec![Operand::IdRef(chain_id)],
),
],
);
rewrite_dynamic_struct_index_reinterpret(&mut ctx, 0).unwrap();
let chain = find_inst(&ctx, chain_id);
assert_eq!(
chain.operands.len(),
2,
"constant-index chain is left untouched"
);
}
#[test]
fn dynamic_struct_index_vector_reinterpret_replays_scalar_lanes() {
let mut ctx = Ctx::new(Module::new());
let uint = ctx.ty_uint();
let v2uint = ctx.ty_vec_uint(2);
let runtime = ctx.ty_runtime_array(uint);
let block = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeStruct,
None,
Some(block),
vec![Operand::IdRef(runtime)],
));
let ptr_block = ctx.ty_ptr(StorageClass::StorageBuffer, block);
let ptr_v2uint = ctx.ty_ptr(StorageClass::StorageBuffer, v2uint);
let base = storage_buffer_var(&mut ctx, ptr_block);
let dyn_idx = ctx.module.fresh_id();
let chain = ctx.module.fresh_id();
let value = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![
Instruction::new(Op::Undef, Some(uint), Some(dyn_idx), vec![]),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_v2uint),
Some(chain),
vec![Operand::IdRef(base), Operand::IdRef(dyn_idx)],
),
Instruction::new(
Op::Load,
Some(v2uint),
Some(value),
vec![Operand::IdRef(chain)],
),
],
);
rewrite_dynamic_struct_index_vector_reinterpret(&mut ctx, 0).unwrap();
let insts = &ctx.module.functions[0].blocks[0].instructions;
assert!(
!insts.iter().any(|inst| inst.result_id == Some(chain)),
"the illegal vector pointer must be eliminated"
);
let mul = insts
.iter()
.find(|inst| inst.class.opcode == Op::IMul)
.expect("vector index is scaled by lane count");
assert_eq!(mul.operands.first(), Some(&Operand::IdRef(dyn_idx)));
let Operand::IdRef(two) = mul.operands.get(1).expect("lane multiplier") else {
panic!("lane multiplier is not an id")
};
assert_eq!(const_u32(&ctx, *two), Some(2));
let chains: Vec<&Instruction> = insts
.iter()
.filter(|inst| inst.class.opcode == Op::InBoundsAccessChain)
.collect();
assert_eq!(chains.len(), 2, "one scalar pointer per vector lane");
for chain in &chains {
assert_eq!(chain.operands.first(), Some(&Operand::IdRef(base)));
let Operand::IdRef(member0) = chain.operands.get(1).expect("member-0 index") else {
panic!("member index is not an id")
};
assert_eq!(const_u32(&ctx, *member0), Some(0));
}
let rebuilt = find_inst(&ctx, value);
assert_eq!(rebuilt.class.opcode, Op::CompositeConstruct);
assert_eq!(rebuilt.result_type, Some(v2uint));
assert_eq!(rebuilt.operands.len(), 2);
}
struct ChainedFixture {
buf: Word,
dyn_f: Word,
dyn_u: Word,
out_id: Word,
val_id: Word,
uint: Word,
float: Word,
}
fn build_chained_reinterpret(ctx: &mut Ctx) -> ChainedFixture {
let uint = ctx.ty_uint();
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(uint);
let struct_id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeStruct,
None,
Some(struct_id),
vec![Operand::IdRef(rt)],
));
let ptr_struct = ctx.ty_ptr(StorageClass::StorageBuffer, struct_id);
let ptr_uint = ctx.ty_ptr(StorageClass::StorageBuffer, uint);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let buf = storage_buffer_var(ctx, ptr_struct);
let zero = ctx.const_uint(0);
let dyn_f = ctx.module.fresh_id();
let dyn_u = ctx.module.fresh_id();
let inner_id = ctx.module.fresh_id();
let out_id = ctx.module.fresh_id();
let val_id = ctx.module.fresh_id();
install_entry(
ctx,
vec![
Instruction::new(Op::Undef, Some(uint), Some(dyn_f), vec![]),
Instruction::new(Op::Undef, Some(uint), Some(dyn_u), vec![]),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_uint),
Some(inner_id),
vec![
Operand::IdRef(buf),
Operand::IdRef(zero),
Operand::IdRef(dyn_f),
],
),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(out_id),
vec![
Operand::IdRef(inner_id),
Operand::IdRef(zero),
Operand::IdRef(dyn_u),
],
),
Instruction::new(
Op::Load,
Some(float),
Some(val_id),
vec![Operand::IdRef(out_id)],
),
],
);
ChainedFixture {
buf,
dyn_f,
dyn_u,
out_id,
val_id,
uint,
float,
}
}
#[test]
fn chained_element_reinterpret_reroots_with_summed_index() {
let mut ctx = Ctx::new(Module::new());
let fx = build_chained_reinterpret(&mut ctx);
rewrite_chained_element_reinterpret(&mut ctx, 0).unwrap();
let out = find_inst(&ctx, fx.out_id);
assert_eq!(out.class.opcode, Op::InBoundsAccessChain);
assert_eq!(
out.operands.len(),
3,
"outer chain keeps member-0 + summed index"
);
assert_eq!(
out.operands[0],
Operand::IdRef(fx.buf),
"re-rooted onto the buffer"
);
let Operand::IdRef(member0) = out.operands[1] else {
panic!("member index is not an id");
};
assert_eq!(const_u32(&ctx, member0), Some(0));
let Operand::IdRef(sum) = out.operands[2] else {
panic!("summed index is not an id");
};
let sum_inst = find_inst(&ctx, sum);
assert_eq!(
sum_inst.class.opcode,
Op::IAdd,
"index is the sum of the two dynamic indices"
);
assert_eq!(sum_inst.result_type, Some(fx.uint));
assert!(
sum_inst.operands.contains(&Operand::IdRef(fx.dyn_f))
&& sum_inst.operands.contains(&Operand::IdRef(fx.dyn_u)),
"sum adds dynF and dynU"
);
let cast = find_inst(&ctx, fx.val_id);
assert_eq!(cast.class.opcode, Op::Bitcast, "reinterpret load → bitcast");
assert_eq!(cast.result_type, Some(fx.float));
let Operand::IdRef(load_id) = cast.operands[0] else {
panic!("bitcast source is not an id");
};
assert_eq!(
find_inst(&ctx, load_id).result_type,
Some(fx.uint),
"split load reads uint"
);
}
#[test]
fn chained_element_reinterpret_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
build_chained_reinterpret(&mut ctx);
rewrite_chained_element_reinterpret(&mut ctx, 0).unwrap();
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_chained_element_reinterpret(&mut ctx, 0).unwrap();
assert_eq!(
after_first, ctx.module.functions[0].blocks[0].instructions,
"second application is a no-op"
);
}
fn ty_int16(ctx: &mut Ctx) -> Word {
let id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeInt,
None,
Some(id),
vec![Operand::LiteralBit32(16), Operand::LiteralBit32(0)],
));
id
}
struct ByteBufFixture {
out_id: Word,
val_id: Word,
u16: Word,
u32: Word,
}
fn build_byte_buffer_widen(ctx: &mut Ctx) -> ByteBufFixture {
let u16 = ty_int16(ctx);
let u32 = ctx.ty_uint();
let rt = ctx.ty_runtime_array(u16);
let struct_id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeStruct,
None,
Some(struct_id),
vec![Operand::IdRef(rt)],
));
let ptr_struct = ctx.ty_ptr(StorageClass::StorageBuffer, struct_id);
let ptr_u16 = ctx.ty_ptr(StorageClass::StorageBuffer, u16);
let ptr_u32 = ctx.ty_ptr(StorageClass::StorageBuffer, u32);
let buf = storage_buffer_var(ctx, ptr_struct);
let zero = ctx.const_uint(0);
let byte_idx = ctx.module.fresh_id();
let k = ctx.module.fresh_id();
let inner_id = ctx.module.fresh_id();
let out_id = ctx.module.fresh_id();
let val_id = ctx.module.fresh_id();
install_entry(
ctx,
vec![
Instruction::new(Op::Undef, Some(u32), Some(byte_idx), vec![]),
Instruction::new(Op::Undef, Some(u32), Some(k), vec![]),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_u16),
Some(inner_id),
vec![
Operand::IdRef(buf),
Operand::IdRef(zero),
Operand::IdRef(byte_idx),
],
),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_u32),
Some(out_id),
vec![Operand::IdRef(inner_id), Operand::IdRef(k)],
),
Instruction::new(
Op::Load,
Some(u32),
Some(val_id),
vec![Operand::IdRef(out_id)],
),
],
);
ByteBufFixture {
out_id,
val_id,
u16,
u32,
}
}
#[test]
fn byte_buffer_chained_reinterpret_expands_widening_load_into_slots() {
let mut ctx = Ctx::new(Module::new());
let fx = build_byte_buffer_widen(&mut ctx);
rewrite_byte_buffer_chained_reinterpret(&mut ctx, 0).unwrap();
let block = &ctx.module.functions[0].blocks[0].instructions;
let val = find_inst(&ctx, fx.val_id);
assert_eq!(
val.class.opcode,
Op::BitwiseOr,
"little-endian OR-assembled result"
);
assert_eq!(val.result_type, Some(fx.u32));
let slot_loads = block
.iter()
.filter(|i| i.class.opcode == Op::Load && i.result_type == Some(fx.u16))
.count();
assert_eq!(slot_loads, 2, "two little-endian narrow slot loads");
assert!(
block.iter().any(|i| i.class.opcode == Op::IMul),
"out_idx * ratio"
);
assert!(
block.iter().any(|i| i.class.opcode == Op::IAdd),
"byteIdx + out_idx*ratio"
);
assert!(
block.iter().all(|i| i.result_id != Some(fx.out_id)),
"outer widening chain replaced"
);
}
#[test]
fn byte_buffer_chained_reinterpret_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
build_byte_buffer_widen(&mut ctx);
rewrite_byte_buffer_chained_reinterpret(&mut ctx, 0).unwrap();
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_byte_buffer_chained_reinterpret(&mut ctx, 0).unwrap();
assert_eq!(
after_first, ctx.module.functions[0].blocks[0].instructions,
"second application is a no-op"
);
}
fn ty_uint8(ctx: &mut Ctx) -> Word {
let id = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::TypeInt,
None,
Some(id),
vec![
Operand::LiteralBit32(RAW_BYTE_POINTER_ELEMENT_BITS),
Operand::LiteralBit32(0),
],
));
id
}
struct RawByteWideFixture {
chain_id: Word,
load_id: Word,
byte: Word,
v4float: Word,
base_phi: Word,
index: Word,
ulong: Word,
}
fn build_raw_byte_phi_wide_load(ctx: &mut Ctx) -> RawByteWideFixture {
let byte = ty_uint8(ctx);
let ulong = ctx.ty_ulong();
let v4float = ctx.ty_vecf(4);
let ptr_byte = ctx.ty_ptr(StorageClass::StorageBuffer, byte);
let ptr_v4float = ctx.ty_ptr(StorageClass::StorageBuffer, v4float);
let left = ctx.module.fresh_id();
let right = ctx.module.fresh_id();
let base_phi = ctx.module.fresh_id();
let pred_left = ctx.module.fresh_id();
let pred_right = ctx.module.fresh_id();
let index = ctx.module.fresh_id();
let chain_id = ctx.module.fresh_id();
let load_id = ctx.module.fresh_id();
install_entry(
ctx,
vec![
Instruction::new(Op::Undef, Some(ptr_byte), Some(left), vec![]),
Instruction::new(Op::Undef, Some(ptr_byte), Some(right), vec![]),
Instruction::new(
Op::Phi,
Some(ptr_byte),
Some(base_phi),
vec![
Operand::IdRef(left),
Operand::IdRef(pred_left),
Operand::IdRef(right),
Operand::IdRef(pred_right),
],
),
Instruction::new(Op::Undef, Some(ulong), Some(index), vec![]),
Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_v4float),
Some(chain_id),
vec![Operand::IdRef(base_phi), Operand::IdRef(index)],
),
Instruction::new(
Op::Load,
Some(v4float),
Some(load_id),
vec![Operand::IdRef(chain_id)],
),
],
);
RawByteWideFixture {
chain_id,
load_id,
byte,
v4float,
base_phi,
index,
ulong,
}
}
#[test]
fn raw_byte_pointer_wide_load_replays_vector_through_phi() {
let mut ctx = Ctx::new(Module::new());
let fx = build_raw_byte_phi_wide_load(&mut ctx);
rewrite_raw_byte_pointer_wide_loads(&mut ctx, 0);
let block = &ctx.module.functions[0].blocks[0].instructions;
assert!(
block.iter().all(|inst| inst.result_id != Some(fx.chain_id)),
"the invalid wide pointer is removed"
);
let result = find_inst(&ctx, fx.load_id);
assert_eq!(result.class.opcode, Op::CompositeConstruct);
assert_eq!(result.result_type, Some(fx.v4float));
assert_eq!(
block
.iter()
.filter(|inst| inst.class.opcode == Op::PtrAccessChain)
.count(),
16,
"four 32-bit lanes replay through sixteen byte pointers"
);
assert_eq!(
block
.iter()
.filter(|inst| inst.class.opcode == Op::Load && inst.result_type == Some(fx.byte))
.count(),
16,
"one byte load per byte of the float4"
);
assert!(
block.iter().any(|inst| {
inst.class.opcode == Op::IMul
&& inst.result_type == Some(fx.ulong)
&& inst.operands.contains(&Operand::IdRef(fx.index))
}),
"the original 64-bit GEP index scales the whole float4"
);
assert!(
block
.iter()
.filter(|inst| inst.class.opcode == Op::PtrAccessChain)
.all(|inst| { inst.operands.first() == Some(&Operand::IdRef(fx.base_phi)) }),
"all byte accesses preserve the selected/phi raw base"
);
}
#[test]
fn raw_byte_pointer_wide_load_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
build_raw_byte_phi_wide_load(&mut ctx);
rewrite_raw_byte_pointer_wide_loads(&mut ctx, 0);
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_raw_byte_pointer_wide_loads(&mut ctx, 0);
assert_eq!(after_first, ctx.module.functions[0].blocks[0].instructions);
}
#[test]
fn raw_byte_pointer_wide_load_declines_pointer_escape() {
let mut ctx = Ctx::new(Module::new());
let fx = build_raw_byte_phi_wide_load(&mut ctx);
let pointer_ty = find_inst(&ctx, fx.chain_id)
.result_type
.expect("fixture chain has a pointer result type");
let escape = ctx.module.fresh_id();
let block = &mut ctx.module.functions[0].blocks[0].instructions;
let ret = block.pop().expect("fixture ends in return");
block.push(Instruction::new(
Op::CopyObject,
Some(pointer_ty),
Some(escape),
vec![Operand::IdRef(fx.chain_id)],
));
block.push(ret);
rewrite_raw_byte_pointer_wide_loads(&mut ctx, 0);
assert_eq!(
find_inst(&ctx, fx.chain_id).class.opcode,
Op::InBoundsAccessChain,
"a pointer escape disqualifies the entire byte replay"
);
assert_eq!(
find_inst(&ctx, fx.load_id).class.opcode,
Op::Load,
"the original load remains paired with the untouched pointer"
);
}
fn build_samewidth_reinterpret_load(ctx: &mut Ctx) -> (Word, Word, Word, Word) {
let float = ctx.ty_float();
let uint = ctx.ty_uint();
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let ptr = storage_buffer_var(ctx, ptr_float);
let val_id = ctx.module.fresh_id();
install_entry(
ctx,
vec![Instruction::new(
Op::Load,
Some(uint),
Some(val_id),
vec![Operand::IdRef(ptr)],
)],
);
(ptr, val_id, uint, float)
}
#[test]
fn reinterpret_scalar_load_samewidth_becomes_typed_load_plus_bitcast() {
let mut ctx = Ctx::new(Module::new());
let (_ptr, val_id, uint, float) = build_samewidth_reinterpret_load(&mut ctx);
rewrite_reinterpret_scalar_loads(&mut ctx, 0);
let cast = find_inst(&ctx, val_id);
assert_eq!(
cast.class.opcode,
Op::Bitcast,
"same-width reinterpret → bitcast"
);
assert_eq!(
cast.result_type,
Some(uint),
"result keeps the loaded (uint) type"
);
let Operand::IdRef(lo) = cast.operands[0] else {
panic!("bitcast source is not an id");
};
let lo_inst = find_inst(&ctx, lo);
assert_eq!(
lo_inst.class.opcode,
Op::Load,
"slot is loaded in its declared type"
);
assert_eq!(
lo_inst.result_type,
Some(float),
"the declared-type load reads float"
);
}
#[test]
fn reinterpret_scalar_load_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
build_samewidth_reinterpret_load(&mut ctx);
rewrite_reinterpret_scalar_loads(&mut ctx, 0);
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_reinterpret_scalar_loads(&mut ctx, 0);
assert_eq!(
after_first, ctx.module.functions[0].blocks[0].instructions,
"second application is a no-op"
);
}
#[test]
fn scalar_pointer_arithmetic_flips_samewidth_chain_to_ptr_access_chain() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_float);
let idx = ctx.const_uint(3);
let chain_id = ctx.module.fresh_id();
let ops = vec![Operand::IdRef(base), Operand::IdRef(idx)];
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
ops.clone(),
)],
);
rewrite_scalar_pointer_arithmetic_access_chains(&mut ctx, 0);
let inst = only_inst(&ctx);
assert_eq!(inst.class.opcode, Op::PtrAccessChain, "opcode flipped");
assert_eq!(inst.result_type, Some(ptr_float), "result type preserved");
assert_eq!(inst.result_id, Some(chain_id), "result id preserved");
assert_eq!(inst.operands, ops, "base + index operands unchanged");
}
#[test]
fn scalar_pointer_arithmetic_is_idempotent() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_float);
let idx = ctx.const_uint(3);
let chain_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(idx)],
)],
);
rewrite_scalar_pointer_arithmetic_access_chains(&mut ctx, 0);
let after_first = ctx.module.functions[0].blocks[0].instructions.clone();
rewrite_scalar_pointer_arithmetic_access_chains(&mut ctx, 0);
assert_eq!(
after_first, ctx.module.functions[0].blocks[0].instructions,
"second application leaves the block byte-identical"
);
}
#[test]
fn scalar_pointer_arithmetic_leaves_composite_descent_untouched() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let rt = ctx.ty_runtime_array(float);
let ptr_rt = ctx.ty_ptr(StorageClass::StorageBuffer, rt);
let ptr_float = ctx.ty_ptr(StorageClass::StorageBuffer, float);
let base = storage_buffer_var(&mut ctx, ptr_rt);
let idx = ctx.const_uint(0);
let chain_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(idx)],
)],
);
rewrite_scalar_pointer_arithmetic_access_chains(&mut ctx, 0);
assert_eq!(
only_inst(&ctx).class.opcode,
Op::InBoundsAccessChain,
"an aggregate-indexing chain (base type != result type) is not rewritten"
);
}
#[test]
fn scalar_pointer_arithmetic_declines_private_storage() {
let mut ctx = Ctx::new(Module::new());
let float = ctx.ty_float();
let ptr_float_priv = ctx.ty_ptr(StorageClass::Private, float);
let base = ctx.module.fresh_id();
ctx.new_globals.push(Instruction::new(
Op::Variable,
Some(ptr_float_priv),
Some(base),
vec![Operand::StorageClass(StorageClass::Private)],
));
let idx = ctx.const_uint(3);
let chain_id = ctx.module.fresh_id();
install_entry(
&mut ctx,
vec![Instruction::new(
Op::InBoundsAccessChain,
Some(ptr_float_priv),
Some(chain_id),
vec![Operand::IdRef(base), Operand::IdRef(idx)],
)],
);
rewrite_scalar_pointer_arithmetic_access_chains(&mut ctx, 0);
assert_eq!(
only_inst(&ctx).class.opcode,
Op::InBoundsAccessChain,
"a Private-storage scalar chain is not turned into OpPtrAccessChain"
);
}
}