@@ -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
6678fn 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]
182339fn 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