diff --git a/src/spv/lift.rs b/src/spv/lift.rs index 5bbb6551..257e9f1b 100644 --- a/src/spv/lift.rs +++ b/src/spv/lift.rs @@ -342,6 +342,13 @@ enum Merge { }, } +/// Relationship between loop body condition and loop-continue condition. +#[derive(Copy, Clone, PartialEq, Eq)] +enum CondRelation { + Same, + Not, +} + impl<'a> NeedsIdsCollector<'a> { fn alloc_ids( self, @@ -508,6 +515,324 @@ impl FuncAt<'_, Node> { } impl<'a> FuncLifting<'a> { + /// Recompute incoming edge counts for each CFG point in `blocks`. + /// + /// This has to run after control-flow rewrites and before dead-block + /// pruning, to avoid stale predecessor counts. + fn recompute_use_counts( + blocks: &FxIndexMap>, + use_counts: &mut FxHashMap, + ) { + use_counts.clear(); + use_counts.reserve(blocks.len()); + let all_edges = blocks.first().map(|(&entry_point, _)| entry_point).into_iter().chain( + blocks.values().flat_map(|block| { + block + .terminator + .merge + .iter() + .flat_map(|merge| { + let (a, b) = match merge { + Merge::Selection(a) => (a, None), + Merge::Loop { loop_merge: a, loop_continue: b } => (a, Some(b)), + }; + [a].into_iter().chain(b) + }) + .chain(&block.terminator.targets) + .copied() + }), + ); + for target in all_edges { + *use_counts.entry(target).or_default() += 1; + } + } + + /// Return `true` iff `point` is an empty pass-through block branching only + /// to `target`. + fn is_passthrough_branch_to( + blocks: &FxIndexMap>, + point: CfgPoint, + target: CfgPoint, + ) -> bool { + let Some(block) = blocks.get(&point) else { + return false; + }; + block.phis.is_empty() + && block.insts.is_empty() + && block.terminator.attrs == AttrSet::default() + && matches!(&*block.terminator.kind, cfg::ControlInstKind::Branch) + && block.terminator.inputs.is_empty() + && block.terminator.targets.as_slice() == [target] + && block.terminator.target_phi_values.keys().all(|&phi_target| phi_target == target) + && block.terminator.merge.is_none() + } + + fn is_const_opcode(cx: &Context, v: Value, opcode: spec::Opcode) -> bool { + match v { + Value::Const(c) => match &cx[c].kind { + ConstKind::SpvInst { spv_inst_and_const_inputs } => { + spv_inst_and_const_inputs.0.opcode == opcode + } + _ => false, + }, + _ => false, + } + } + + /// Determine whether `continue_cond` is equal to `body_cond` or its + /// logical negation. + fn continue_cond_relation( + cx: &Context, + func_def_body: &'a crate::FuncDefBody, + continue_cond: Value, + body_cond: Value, + ) -> Option { + if continue_cond == body_cond { + return Some(CondRelation::Same); + } + + let wk = &spec::Spec::get().well_known; + match continue_cond { + Value::NodeOutput { node, output_idx } => { + let node_def = func_def_body.at(node).def(); + let NodeKind::Select { kind: SelectionKind::BoolCond, scrutinee, cases } = + &node_def.kind + else { + return None; + }; + if *scrutinee != body_cond || cases.len() != 2 { + return None; + } + + let output_idx = output_idx as usize; + let true_case_outputs = &func_def_body.at(cases[0]).def().outputs; + let false_case_outputs = &func_def_body.at(cases[1]).def().outputs; + if output_idx >= true_case_outputs.len() || output_idx >= false_case_outputs.len() { + return None; + } + + let on_true = true_case_outputs[output_idx]; + let on_false = false_case_outputs[output_idx]; + if Self::is_const_opcode(cx, on_true, wk.OpConstantTrue) + && Self::is_const_opcode(cx, on_false, wk.OpConstantFalse) + { + Some(CondRelation::Same) + } else if Self::is_const_opcode(cx, on_true, wk.OpConstantFalse) + && Self::is_const_opcode(cx, on_false, wk.OpConstantTrue) + { + Some(CondRelation::Not) + } else { + None + } + } + + _ => None, + } + } + + /// Rewrite `loop_continue` into an unconditional backedge while preserving + /// only phi payloads for the loop header edge. + fn rewrite_continue_as_unconditional_backedge( + blocks: &mut FxIndexMap>, + loop_continue: CfgPoint, + ) { + let continue_block = blocks.get_mut(&loop_continue).unwrap(); + continue_block.terminator.kind = Cow::Owned(cfg::ControlInstKind::Branch); + continue_block.terminator.inputs = [].into_iter().collect(); + let header_point = continue_block.terminator.targets[0]; + continue_block.terminator.targets = [header_point].into_iter().collect(); + continue_block.terminator.target_phi_values.retain(|&target, _| target == header_point); + } + + /// Canonicalize strict loop shortcut patterns: + /// * loop header branches to a body select, + /// * one body arm is an empty pass-through to body merge, + /// * body merge branches to `loop_continue`, + /// * `loop_continue` conditionally branches to header/merge. + /// + /// Rewriting the pass-through arm directly to `loop_merge` avoids + /// preserving this fragile shape in lifted CFG. + fn canonicalize_loop_continue_shortcuts( + cx: &Context, + func_def_body: &'a crate::FuncDefBody, + blocks: &mut FxIndexMap>, + ) { + let mut loop_continue_shortcuts = + SmallVec::<[(CfgPoint, CfgPoint, CfgPoint, CfgPoint, bool); 4]>::new(); + for (&header_point, header_block) in &*blocks { + let header_term = &header_block.terminator; + let Some(Merge::Loop { loop_merge, loop_continue }) = header_term.merge else { + continue; + }; + if header_term.attrs != AttrSet::default() + || !matches!(&*header_term.kind, cfg::ControlInstKind::Branch) + || !header_term.inputs.is_empty() + || header_term.targets.len() != 1 + || !header_term.target_phi_values.is_empty() + { + continue; + } + + let body_point = header_term.targets[0]; + let Some(body_block) = blocks.get(&body_point) else { + continue; + }; + let body_term = &body_block.terminator; + let Some(Merge::Selection(body_merge)) = body_term.merge else { + continue; + }; + if body_term.attrs != AttrSet::default() + || !matches!( + &*body_term.kind, + cfg::ControlInstKind::SelectBranch(SelectionKind::BoolCond) + ) + || body_term.inputs.len() != 1 + || body_term.targets.len() != 2 + || !body_term.target_phi_values.is_empty() + { + continue; + } + + let Some(continue_block) = blocks.get(&loop_continue) else { + continue; + }; + if !continue_block.phis.is_empty() || !continue_block.insts.is_empty() { + continue; + } + let continue_term = &continue_block.terminator; + if continue_term.attrs != AttrSet::default() + || !matches!( + &*continue_term.kind, + cfg::ControlInstKind::SelectBranch(SelectionKind::BoolCond) + ) + || continue_term.inputs.len() != 1 + || continue_term.targets.as_slice() != [header_point, loop_merge] + || continue_term.target_phi_values.keys().any(|&target| target != header_point) + || continue_term.merge.is_some() + { + continue; + } + + let t0 = body_term.targets[0]; + let t1 = body_term.targets[1]; + let (_work_target, pass_target) = + if Self::is_passthrough_branch_to(blocks, t0, body_merge) { + (t1, t0) + } else if Self::is_passthrough_branch_to(blocks, t1, body_merge) { + (t0, t1) + } else { + continue; + }; + let Some(cond_relation) = Self::continue_cond_relation( + cx, + func_def_body, + continue_term.inputs[0], + body_term.inputs[0], + ) else { + continue; + }; + let continue_routes_work_to_header = match cond_relation { + CondRelation::Same => pass_target == t1, + CondRelation::Not => pass_target == t0, + }; + if !continue_routes_work_to_header { + continue; + } + + let body_merge_preds: SmallVec<[CfgPoint; 4]> = blocks + .iter() + .filter_map(|(&point, block)| { + block.terminator.targets.contains(&body_merge).then_some(point) + }) + .collect(); + if body_merge_preds.len() != 2 || !body_merge_preds.contains(&pass_target) { + continue; + } + let Some(other_body_merge_pred) = + body_merge_preds.into_iter().find(|&point| point != pass_target) + else { + continue; + }; + + let continue_pred_count = blocks + .values() + .filter(|block| block.terminator.targets.contains(&loop_continue)) + .count(); + if continue_pred_count != 1 { + continue; + } + + let Some(loop_merge_block) = blocks.get(&loop_merge) else { + continue; + }; + if !loop_merge_block.phis.is_empty() { + continue; + } + + let Some(body_merge_block) = blocks.get(&body_merge) else { + continue; + }; + let merge_term = &body_merge_block.terminator; + if merge_term.attrs != AttrSet::default() + || !matches!(&*merge_term.kind, cfg::ControlInstKind::Branch) + || !merge_term.inputs.is_empty() + || merge_term.targets.as_slice() != [loop_continue] + || !merge_term.target_phi_values.is_empty() + || merge_term.merge.is_some() + { + continue; + } + + let body_merge_phi_count = body_merge_block.phis.len(); + let payload_arity_to = |source: CfgPoint, target: CfgPoint| { + blocks + .get(&source) + .and_then(|block| block.terminator.target_phi_values.get(&target)) + .map_or(0, |values| values.len()) + }; + if payload_arity_to(pass_target, body_merge) != body_merge_phi_count + || payload_arity_to(other_body_merge_pred, body_merge) != body_merge_phi_count + { + continue; + } + + let header_phi_count = header_block.phis.len(); + let continue_payload_arity = + continue_term.target_phi_values.get(&header_point).map_or(0, |values| values.len()); + if continue_payload_arity != header_phi_count { + continue; + } + + loop_continue_shortcuts.push(( + body_point, + pass_target, + loop_merge, + loop_continue, + continue_routes_work_to_header, + )); + } + for ( + body_point, + pass_target, + loop_merge, + loop_continue, + rewrite_continue_to_unconditional_backedge, + ) in loop_continue_shortcuts + { + let body_block = blocks.get_mut(&body_point).unwrap(); + body_block.terminator.merge = None; + for target in &mut body_block.terminator.targets { + if *target == pass_target { + *target = loop_merge; + } + } + + if rewrite_continue_to_unconditional_backedge { + Self::rewrite_continue_as_unconditional_backedge(blocks, loop_continue); + } + } + } + fn from_func_decl( cx: &Context, func_decl: &'a FuncDecl, @@ -913,6 +1238,9 @@ impl<'a> FuncLifting<'a> { } } + Self::canonicalize_loop_continue_shortcuts(cx, func_def_body, &mut blocks); + Self::recompute_use_counts(&blocks, &mut use_counts); + // Remove now-unused blocks. blocks.retain(|point, _| use_counts.get(point).is_some_and(|&count| count > 0)); diff --git a/tests/data/basic.frag.glsl.dbg.spvbin b/tests/data/basic.frag.glsl.dbg.spvbin new file mode 100644 index 00000000..ecc9e6e2 Binary files /dev/null and b/tests/data/basic.frag.glsl.dbg.spvbin differ diff --git a/tests/data/loop-continue-shortcut-control-noloop.spvbin b/tests/data/loop-continue-shortcut-control-noloop.spvbin new file mode 100644 index 00000000..f6cc5205 Binary files /dev/null and b/tests/data/loop-continue-shortcut-control-noloop.spvbin differ diff --git a/tests/data/loop-continue-shortcut-control-posttest.spvbin b/tests/data/loop-continue-shortcut-control-posttest.spvbin new file mode 100644 index 00000000..06f735a7 Binary files /dev/null and b/tests/data/loop-continue-shortcut-control-posttest.spvbin differ diff --git a/tests/data/loop-continue-shortcut-nested-len.repro.spvbin b/tests/data/loop-continue-shortcut-nested-len.repro.spvbin new file mode 100644 index 00000000..f6cea38b Binary files /dev/null and b/tests/data/loop-continue-shortcut-nested-len.repro.spvbin differ diff --git a/tests/data/loop-continue-shortcut-nested.repro.spvbin b/tests/data/loop-continue-shortcut-nested.repro.spvbin new file mode 100644 index 00000000..4c995a72 Binary files /dev/null and b/tests/data/loop-continue-shortcut-nested.repro.spvbin differ diff --git a/tests/data/loop-continue-shortcut-pretest-len.repro.spvbin b/tests/data/loop-continue-shortcut-pretest-len.repro.spvbin new file mode 100644 index 00000000..e29272cb Binary files /dev/null and b/tests/data/loop-continue-shortcut-pretest-len.repro.spvbin differ diff --git a/tests/data/loop-continue-shortcut.repro.spvbin b/tests/data/loop-continue-shortcut.repro.spvbin new file mode 100644 index 00000000..a4a8ccfd Binary files /dev/null and b/tests/data/loop-continue-shortcut.repro.spvbin differ diff --git a/tests/regression_loop_continue_shortcut.rs b/tests/regression_loop_continue_shortcut.rs new file mode 100644 index 00000000..293058bd --- /dev/null +++ b/tests/regression_loop_continue_shortcut.rs @@ -0,0 +1,454 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use spirt::spv; + +struct Block { + label: spv::Id, + insts: Vec, +} + +struct LiftedFixture { + blocks: Vec, + undef_ids: std::collections::BTreeSet, +} + +fn lifted_fixture_from_spv_fixture(spv_bytes: &[u8]) -> LiftedFixture { + let cx = Rc::new(spirt::Context::new()); + let mut module = spirt::Module::lower_from_spv_bytes(cx, spv_bytes.to_vec()).unwrap(); + + spirt::passes::link::minimize_exports(&mut module, |export_key| { + matches!(export_key, spirt::ExportKey::SpvEntryPoint { .. }) + }); + spirt::passes::legalize::structurize_func_cfgs(&mut module); + spirt::passes::link::resolve_imports(&mut module); + + let emitted = module.lift_to_spv_module_emitter().unwrap(); + let spv_bytes = emitted.words.iter().flat_map(|word| word.to_le_bytes()).collect::>(); + let parser = spv::read::ModuleParser::read_from_spv_bytes(spv_bytes).unwrap(); + + let wk = &spv::spec::Spec::get().well_known; + + let mut in_first_function = false; + let mut blocks = Vec::new(); + let mut current: Option = None; + let mut undef_ids = std::collections::BTreeSet::new(); + + for inst in parser { + let inst = inst.unwrap(); + + if inst.opcode == wk.OpUndef { + if let Some(result_id) = inst.result_id { + undef_ids.insert(result_id); + } + } + + if inst.opcode == wk.OpFunction { + if in_first_function { + continue; + } + in_first_function = true; + continue; + } + if !in_first_function { + continue; + } + if inst.opcode == wk.OpFunctionEnd { + if let Some(block) = current.take() { + blocks.push(block); + } + break; + } + if inst.opcode == wk.OpLabel { + if let Some(block) = current.take() { + blocks.push(block); + } + current = Some(Block { label: inst.result_id.unwrap(), insts: Vec::new() }); + continue; + } + + if let Some(block) = &mut current { + block.insts.push(inst); + } + } + + LiftedFixture { blocks, undef_ids } +} + +fn block_is_trivial_branch_to(wk: &spv::spec::WellKnown, block: &Block, target: spv::Id) -> bool { + matches!(block.insts.as_slice(), [terminator] if terminator.opcode == wk.OpBranch && terminator.ids.as_slice() == [target]) +} + +fn block_terminator(block: &Block) -> Option<&spv::InstWithIds> { + block.insts.last() +} + +fn find_bad_loop_continue_shortcut_shape(blocks: &[Block]) -> bool { + let wk = &spv::spec::Spec::get().well_known; + + let by_label = blocks.iter().map(|b| (b.label, b)).collect::>(); + + let loop_merge_and_continue = |block: &Block| { + block.insts.iter().find(|inst| inst.opcode == wk.OpLoopMerge).and_then(|inst| { + match inst.ids.as_slice() { + [loop_merge, loop_continue] => Some((*loop_merge, *loop_continue)), + _ => None, + } + }) + }; + + let selection_merge = |block: &Block| { + block.insts.iter().find(|inst| inst.opcode == wk.OpSelectionMerge).and_then(|inst| { + match inst.ids.as_slice() { + [merge] => Some(*merge), + _ => None, + } + }) + }; + + for header in blocks { + let Some((loop_merge, loop_continue)) = loop_merge_and_continue(header) else { + continue; + }; + + let Some(header_term) = block_terminator(header) else { + continue; + }; + if !(header_term.opcode == wk.OpBranch && header_term.ids.len() == 1) { + continue; + } + let body = header_term.ids[0]; + + let Some(body_block) = by_label.get(&body) else { + continue; + }; + let Some(body_merge) = selection_merge(body_block) else { + continue; + }; + let Some(body_term) = block_terminator(body_block) else { + continue; + }; + if !(body_term.opcode == wk.OpBranchConditional && body_term.ids.len() == 3) { + continue; + } + + let body_cond = body_term.ids[0]; + let body_t0 = body_term.ids[1]; + let body_t1 = body_term.ids[2]; + + let pass_target = if by_label + .get(&body_t0) + .is_some_and(|b| block_is_trivial_branch_to(wk, b, body_merge)) + { + body_t0 + } else if by_label + .get(&body_t1) + .is_some_and(|b| block_is_trivial_branch_to(wk, b, body_merge)) + { + body_t1 + } else { + continue; + }; + + let Some(body_merge_block) = by_label.get(&body_merge) else { + continue; + }; + let Some(body_merge_term) = block_terminator(body_merge_block) else { + continue; + }; + if !(body_merge_term.opcode == wk.OpBranch + && body_merge_term.ids.as_slice() == [loop_continue]) + { + continue; + } + + let Some(loop_continue_block) = by_label.get(&loop_continue) else { + continue; + }; + let Some(loop_continue_term) = block_terminator(loop_continue_block) else { + continue; + }; + if !(loop_continue_term.opcode == wk.OpBranchConditional + && loop_continue_term.ids.len() == 3 + && loop_continue_term.ids[0] == body_cond) + { + continue; + } + + let continue_targets = [loop_continue_term.ids[1], loop_continue_term.ids[2]]; + if !continue_targets.contains(&header.label) || !continue_targets.contains(&loop_merge) { + continue; + } + + eprintln!( + "found shortcut shape: header=%{:?} body=%{:?} pass=%{:?} body_merge=%{:?} continue=%{:?} merge=%{:?}", + header.label, body, pass_target, body_merge, loop_continue, loop_merge, + ); + return true; + } + + false +} + +fn find_loop_carried_undef_from_shortcut( + blocks: &[Block], + undef_ids: &std::collections::BTreeSet, +) -> bool { + let wk = &spv::spec::Spec::get().well_known; + + let by_label = blocks.iter().map(|b| (b.label, b)).collect::>(); + + let loop_merge_and_continue = |block: &Block| { + block.insts.iter().find(|inst| inst.opcode == wk.OpLoopMerge).and_then(|inst| { + match inst.ids.as_slice() { + [loop_merge, loop_continue] => Some((*loop_merge, *loop_continue)), + _ => None, + } + }) + }; + + let selection_merge = |block: &Block| { + block.insts.iter().find(|inst| inst.opcode == wk.OpSelectionMerge).and_then(|inst| { + match inst.ids.as_slice() { + [merge] => Some(*merge), + _ => None, + } + }) + }; + + for header in blocks { + let Some((loop_merge, loop_continue)) = loop_merge_and_continue(header) else { + continue; + }; + + let Some(header_term) = block_terminator(header) else { + continue; + }; + if !(header_term.opcode == wk.OpBranch && header_term.ids.len() == 1) { + continue; + } + let body = header_term.ids[0]; + + let Some(body_block) = by_label.get(&body) else { + continue; + }; + let Some(body_merge) = selection_merge(body_block) else { + continue; + }; + let Some(body_term) = block_terminator(body_block) else { + continue; + }; + if !(body_term.opcode == wk.OpBranchConditional && body_term.ids.len() == 3) { + continue; + } + + let body_cond = body_term.ids[0]; + let body_t0 = body_term.ids[1]; + let body_t1 = body_term.ids[2]; + + let (pass_target, _work_target) = if by_label + .get(&body_t0) + .is_some_and(|b| block_is_trivial_branch_to(wk, b, body_merge)) + { + (body_t0, body_t1) + } else if by_label + .get(&body_t1) + .is_some_and(|b| block_is_trivial_branch_to(wk, b, body_merge)) + { + (body_t1, body_t0) + } else { + continue; + }; + + let Some(body_merge_block) = by_label.get(&body_merge) else { + continue; + }; + let Some(body_merge_term) = block_terminator(body_merge_block) else { + continue; + }; + if !(body_merge_term.opcode == wk.OpBranch + && body_merge_term.ids.as_slice() == [loop_continue]) + { + continue; + } + + let Some(loop_continue_block) = by_label.get(&loop_continue) else { + continue; + }; + let Some(loop_continue_term) = block_terminator(loop_continue_block) else { + continue; + }; + if !(loop_continue_term.opcode == wk.OpBranchConditional + && loop_continue_term.ids.len() == 3 + && loop_continue_term.ids[0] == body_cond) + { + continue; + } + let continue_targets = [loop_continue_term.ids[1], loop_continue_term.ids[2]]; + if !continue_targets.contains(&header.label) || !continue_targets.contains(&loop_merge) { + continue; + } + + let mut backedge_values = std::collections::BTreeSet::new(); + for inst in &header.insts { + if inst.opcode != wk.OpPhi { + continue; + } + for pair in inst.ids.chunks_exact(2) { + let incoming_value = pair[0]; + let incoming_pred = pair[1]; + if incoming_pred == loop_continue { + backedge_values.insert(incoming_value); + } + } + } + + for inst in &body_merge_block.insts { + if inst.opcode != wk.OpPhi { + continue; + } + let Some(phi_result) = inst.result_id else { + continue; + }; + if !backedge_values.contains(&phi_result) { + continue; + } + + for pair in inst.ids.chunks_exact(2) { + let incoming_value = pair[0]; + let incoming_pred = pair[1]; + if incoming_pred != pass_target { + continue; + } + + if undef_ids.contains(&incoming_value) { + eprintln!( + "found loop-carried undef via shortcut: header=%{:?} body=%{:?} pass=%{:?} body_merge=%{:?} continue=%{:?} phi=%{:?}", + header.label, body, pass_target, body_merge, loop_continue, phi_result + ); + return true; + } + } + } + } + + false +} + +#[test] +fn no_loop_continue_shortcut_shape_after_lift() { + let fixtures: [(&str, &[u8]); 4] = [ + ( + "loop-continue-shortcut.repro", + include_bytes!("data/loop-continue-shortcut.repro.spvbin"), + ), + ( + "loop-continue-shortcut-pretest-len.repro", + include_bytes!("data/loop-continue-shortcut-pretest-len.repro.spvbin"), + ), + ( + "loop-continue-shortcut-nested.repro", + include_bytes!("data/loop-continue-shortcut-nested.repro.spvbin"), + ), + ( + "loop-continue-shortcut-nested-len.repro", + include_bytes!("data/loop-continue-shortcut-nested-len.repro.spvbin"), + ), + ]; + + let mut offenders = Vec::new(); + for (name, spv_bytes) in fixtures { + let fixture = lifted_fixture_from_spv_fixture(spv_bytes); + if find_bad_loop_continue_shortcut_shape(&fixture.blocks) { + offenders.push(name); + } + } + + assert!( + offenders.is_empty(), + "found loop-continue shortcut shape that can mis-handle loop edge values in fixtures: {offenders:?}" + ); +} + +#[test] +fn detector_does_not_trigger_on_non_loop_fixture() { + let fixtures: [(&str, &[u8]); 3] = [ + ("basic.frag.glsl.dbg", include_bytes!("data/basic.frag.glsl.dbg.spvbin")), + ( + "loop-continue-shortcut-control-posttest", + include_bytes!("data/loop-continue-shortcut-control-posttest.spvbin"), + ), + ( + "loop-continue-shortcut-control-noloop", + include_bytes!("data/loop-continue-shortcut-control-noloop.spvbin"), + ), + ]; + + let mut offenders = Vec::new(); + for (name, spv_bytes) in fixtures { + let fixture = lifted_fixture_from_spv_fixture(spv_bytes); + if find_bad_loop_continue_shortcut_shape(&fixture.blocks) { + offenders.push(name); + } + } + + assert!(offenders.is_empty(), "detector matched control fixtures unexpectedly: {offenders:?}"); +} + +#[test] +fn no_loop_carried_undef_from_shortcut_after_lift() { + let repro_fixtures: [(&str, &[u8]); 4] = [ + ( + "loop-continue-shortcut.repro", + include_bytes!("data/loop-continue-shortcut.repro.spvbin"), + ), + ( + "loop-continue-shortcut-pretest-len.repro", + include_bytes!("data/loop-continue-shortcut-pretest-len.repro.spvbin"), + ), + ( + "loop-continue-shortcut-nested.repro", + include_bytes!("data/loop-continue-shortcut-nested.repro.spvbin"), + ), + ( + "loop-continue-shortcut-nested-len.repro", + include_bytes!("data/loop-continue-shortcut-nested-len.repro.spvbin"), + ), + ]; + + let mut offenders = Vec::new(); + for (name, spv_bytes) in repro_fixtures { + let fixture = lifted_fixture_from_spv_fixture(spv_bytes); + if find_loop_carried_undef_from_shortcut(&fixture.blocks, &fixture.undef_ids) { + offenders.push(name); + } + } + + assert!( + offenders.is_empty(), + "found loop-carried undef values through shortcut fixtures: {offenders:?}" + ); + + let control_fixtures: [(&str, &[u8]); 3] = [ + ("basic.frag.glsl.dbg", include_bytes!("data/basic.frag.glsl.dbg.spvbin")), + ( + "loop-continue-shortcut-control-posttest", + include_bytes!("data/loop-continue-shortcut-control-posttest.spvbin"), + ), + ( + "loop-continue-shortcut-control-noloop", + include_bytes!("data/loop-continue-shortcut-control-noloop.spvbin"), + ), + ]; + let mut false_positives = Vec::new(); + for (name, spv_bytes) in control_fixtures { + let fixture = lifted_fixture_from_spv_fixture(spv_bytes); + if find_loop_carried_undef_from_shortcut(&fixture.blocks, &fixture.undef_ids) { + false_positives.push(name); + } + } + assert!( + false_positives.is_empty(), + "semantic detector matched control fixtures unexpectedly: {false_positives:?}" + ); +}