Skip to content

Commit ac46bd7

Browse files
author
niklas
committed
tests: add semantic loop-carried undef regression detector
1 parent a870372 commit ac46bd7

1 file changed

Lines changed: 221 additions & 6 deletions

File tree

tests/regression_loop_continue_shortcut.rs

Lines changed: 221 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ struct Block {
88
insts: Vec<spv::InstWithIds>,
99
}
1010

11-
fn lifted_blocks_from_spv_fixture(spv_bytes: &[u8]) -> Vec<Block> {
11+
struct LiftedFixture {
12+
blocks: Vec<Block>,
13+
undef_ids: std::collections::BTreeSet<spv::Id>,
14+
}
15+
16+
fn lifted_fixture_from_spv_fixture(spv_bytes: &[u8]) -> LiftedFixture {
1217
let cx = Rc::new(spirt::Context::new());
1318
let mut module = spirt::Module::lower_from_spv_bytes(cx, spv_bytes.to_vec()).unwrap();
1419

@@ -27,10 +32,17 @@ fn lifted_blocks_from_spv_fixture(spv_bytes: &[u8]) -> Vec<Block> {
2732
let mut in_first_function = false;
2833
let mut blocks = Vec::new();
2934
let mut current: Option<Block> = None;
35+
let mut undef_ids = std::collections::BTreeSet::new();
3036

3137
for inst in parser {
3238
let inst = inst.unwrap();
3339

40+
if inst.opcode == wk.OpUndef {
41+
if let Some(result_id) = inst.result_id {
42+
undef_ids.insert(result_id);
43+
}
44+
}
45+
3446
if inst.opcode == wk.OpFunction {
3547
if in_first_function {
3648
continue;
@@ -60,7 +72,7 @@ fn lifted_blocks_from_spv_fixture(spv_bytes: &[u8]) -> Vec<Block> {
6072
}
6173
}
6274

63-
blocks
75+
LiftedFixture { blocks, undef_ids }
6476
}
6577

6678
fn block_is_trivial_branch_to(wk: &spv::spec::WellKnown, block: &Block, target: spv::Id) -> bool {
@@ -178,6 +190,151 @@ fn find_bad_loop_continue_shortcut_shape(blocks: &[Block]) -> bool {
178190
false
179191
}
180192

193+
fn find_loop_carried_undef_from_shortcut(
194+
blocks: &[Block],
195+
undef_ids: &std::collections::BTreeSet<spv::Id>,
196+
) -> bool {
197+
let wk = &spv::spec::Spec::get().well_known;
198+
199+
let by_label = blocks.iter().map(|b| (b.label, b)).collect::<HashMap<_, _>>();
200+
201+
let loop_merge_and_continue = |block: &Block| {
202+
block.insts.iter().find(|inst| inst.opcode == wk.OpLoopMerge).and_then(|inst| {
203+
match inst.ids.as_slice() {
204+
[loop_merge, loop_continue] => Some((*loop_merge, *loop_continue)),
205+
_ => None,
206+
}
207+
})
208+
};
209+
210+
let selection_merge = |block: &Block| {
211+
block.insts.iter().find(|inst| inst.opcode == wk.OpSelectionMerge).and_then(|inst| {
212+
match inst.ids.as_slice() {
213+
[merge] => Some(*merge),
214+
_ => None,
215+
}
216+
})
217+
};
218+
219+
for header in blocks {
220+
let Some((loop_merge, loop_continue)) = loop_merge_and_continue(header) else {
221+
continue;
222+
};
223+
224+
let Some(header_term) = block_terminator(header) else {
225+
continue;
226+
};
227+
if !(header_term.opcode == wk.OpBranch && header_term.ids.len() == 1) {
228+
continue;
229+
}
230+
let body = header_term.ids[0];
231+
232+
let Some(body_block) = by_label.get(&body) else {
233+
continue;
234+
};
235+
let Some(body_merge) = selection_merge(body_block) else {
236+
continue;
237+
};
238+
let Some(body_term) = block_terminator(body_block) else {
239+
continue;
240+
};
241+
if !(body_term.opcode == wk.OpBranchConditional && body_term.ids.len() == 3) {
242+
continue;
243+
}
244+
245+
let body_cond = body_term.ids[0];
246+
let body_t0 = body_term.ids[1];
247+
let body_t1 = body_term.ids[2];
248+
249+
let (pass_target, _work_target) = if by_label
250+
.get(&body_t0)
251+
.is_some_and(|b| block_is_trivial_branch_to(wk, b, body_merge))
252+
{
253+
(body_t0, body_t1)
254+
} else if by_label
255+
.get(&body_t1)
256+
.is_some_and(|b| block_is_trivial_branch_to(wk, b, body_merge))
257+
{
258+
(body_t1, body_t0)
259+
} else {
260+
continue;
261+
};
262+
263+
let Some(body_merge_block) = by_label.get(&body_merge) else {
264+
continue;
265+
};
266+
let Some(body_merge_term) = block_terminator(body_merge_block) else {
267+
continue;
268+
};
269+
if !(body_merge_term.opcode == wk.OpBranch
270+
&& body_merge_term.ids.as_slice() == [loop_continue])
271+
{
272+
continue;
273+
}
274+
275+
let Some(loop_continue_block) = by_label.get(&loop_continue) else {
276+
continue;
277+
};
278+
let Some(loop_continue_term) = block_terminator(loop_continue_block) else {
279+
continue;
280+
};
281+
if !(loop_continue_term.opcode == wk.OpBranchConditional
282+
&& loop_continue_term.ids.len() == 3
283+
&& loop_continue_term.ids[0] == body_cond)
284+
{
285+
continue;
286+
}
287+
let continue_targets = [loop_continue_term.ids[1], loop_continue_term.ids[2]];
288+
if !continue_targets.contains(&header.label) || !continue_targets.contains(&loop_merge) {
289+
continue;
290+
}
291+
292+
let mut backedge_values = std::collections::BTreeSet::new();
293+
for inst in &header.insts {
294+
if inst.opcode != wk.OpPhi {
295+
continue;
296+
}
297+
for pair in inst.ids.chunks_exact(2) {
298+
let incoming_value = pair[0];
299+
let incoming_pred = pair[1];
300+
if incoming_pred == loop_continue {
301+
backedge_values.insert(incoming_value);
302+
}
303+
}
304+
}
305+
306+
for inst in &body_merge_block.insts {
307+
if inst.opcode != wk.OpPhi {
308+
continue;
309+
}
310+
let Some(phi_result) = inst.result_id else {
311+
continue;
312+
};
313+
if !backedge_values.contains(&phi_result) {
314+
continue;
315+
}
316+
317+
for pair in inst.ids.chunks_exact(2) {
318+
let incoming_value = pair[0];
319+
let incoming_pred = pair[1];
320+
if incoming_pred != pass_target {
321+
continue;
322+
}
323+
324+
if undef_ids.contains(&incoming_value) {
325+
eprintln!(
326+
"found loop-carried undef via shortcut: header=%{:?} body=%{:?} pass=%{:?} body_merge=%{:?} continue=%{:?} phi=%{:?}",
327+
header.label, body, pass_target, body_merge, loop_continue, phi_result
328+
);
329+
return true;
330+
}
331+
}
332+
}
333+
}
334+
335+
false
336+
}
337+
181338
#[test]
182339
fn no_loop_continue_shortcut_shape_after_lift() {
183340
let fixtures: [(&str, &[u8]); 4] = [
@@ -201,8 +358,8 @@ fn no_loop_continue_shortcut_shape_after_lift() {
201358

202359
let mut offenders = Vec::new();
203360
for (name, spv_bytes) in fixtures {
204-
let blocks = lifted_blocks_from_spv_fixture(spv_bytes);
205-
if find_bad_loop_continue_shortcut_shape(&blocks) {
361+
let fixture = lifted_fixture_from_spv_fixture(spv_bytes);
362+
if find_bad_loop_continue_shortcut_shape(&fixture.blocks) {
206363
offenders.push(name);
207364
}
208365
}
@@ -229,11 +386,69 @@ fn detector_does_not_trigger_on_non_loop_fixture() {
229386

230387
let mut offenders = Vec::new();
231388
for (name, spv_bytes) in fixtures {
232-
let blocks = lifted_blocks_from_spv_fixture(spv_bytes);
233-
if find_bad_loop_continue_shortcut_shape(&blocks) {
389+
let fixture = lifted_fixture_from_spv_fixture(spv_bytes);
390+
if find_bad_loop_continue_shortcut_shape(&fixture.blocks) {
234391
offenders.push(name);
235392
}
236393
}
237394

238395
assert!(offenders.is_empty(), "detector matched control fixtures unexpectedly: {offenders:?}");
239396
}
397+
398+
#[test]
399+
fn no_loop_carried_undef_from_shortcut_after_lift() {
400+
let repro_fixtures: [(&str, &[u8]); 4] = [
401+
(
402+
"loop-continue-shortcut.repro",
403+
include_bytes!("data/loop-continue-shortcut.repro.spvbin"),
404+
),
405+
(
406+
"loop-continue-shortcut-pretest-len.repro",
407+
include_bytes!("data/loop-continue-shortcut-pretest-len.repro.spvbin"),
408+
),
409+
(
410+
"loop-continue-shortcut-nested.repro",
411+
include_bytes!("data/loop-continue-shortcut-nested.repro.spvbin"),
412+
),
413+
(
414+
"loop-continue-shortcut-nested-len.repro",
415+
include_bytes!("data/loop-continue-shortcut-nested-len.repro.spvbin"),
416+
),
417+
];
418+
419+
let mut offenders = Vec::new();
420+
for (name, spv_bytes) in repro_fixtures {
421+
let fixture = lifted_fixture_from_spv_fixture(spv_bytes);
422+
if find_loop_carried_undef_from_shortcut(&fixture.blocks, &fixture.undef_ids) {
423+
offenders.push(name);
424+
}
425+
}
426+
427+
assert!(
428+
offenders.is_empty(),
429+
"found loop-carried undef values through shortcut fixtures: {offenders:?}"
430+
);
431+
432+
let control_fixtures: [(&str, &[u8]); 3] = [
433+
("basic.frag.glsl.dbg", include_bytes!("data/basic.frag.glsl.dbg.spvbin")),
434+
(
435+
"loop-continue-shortcut-control-posttest",
436+
include_bytes!("data/loop-continue-shortcut-control-posttest.spvbin"),
437+
),
438+
(
439+
"loop-continue-shortcut-control-noloop",
440+
include_bytes!("data/loop-continue-shortcut-control-noloop.spvbin"),
441+
),
442+
];
443+
let mut false_positives = Vec::new();
444+
for (name, spv_bytes) in control_fixtures {
445+
let fixture = lifted_fixture_from_spv_fixture(spv_bytes);
446+
if find_loop_carried_undef_from_shortcut(&fixture.blocks, &fixture.undef_ids) {
447+
false_positives.push(name);
448+
}
449+
}
450+
assert!(
451+
false_positives.is_empty(),
452+
"semantic detector matched control fixtures unexpectedly: {false_positives:?}"
453+
);
454+
}

0 commit comments

Comments
 (0)