Skip to content

Fix invalid SPIRV from continue inside switch inside for loop#10301

Merged
jkwak-work merged 1 commit intoshader-slang:masterfrom
jkwak-work:fix/switch-without-case
Mar 12, 2026
Merged

Fix invalid SPIRV from continue inside switch inside for loop#10301
jkwak-work merged 1 commit intoshader-slang:masterfrom
jkwak-work:fix/switch-without-case

Conversation

@jkwak-work
Copy link
Collaborator

@jkwak-work jkwak-work commented Feb 28, 2026

For a for-loop with a switch containing a continue, the IR has this shape:

  loop(target=%body, break=%after, continue=%incr)
  %body:
    switch(x, break=%post_switch, ...)
      case 0: unconditionalBranch(%incr)        <- 'continue'
      case 1: unconditionalBranch(%post_switch) <- 'break'
  %post_switch:
    unconditionalBranch(%incr)   <- normal post-switch flow
  %incr: i++; unconditionalBranch(%body)  <- back-edge
  %after: ...                              <- loop break

The continue inside the switch branches to %incr (the loop's continueBlock).
Because continueBlock (%incr) != targetBlock (%body) in a for-loop, this is a
branch that exits the switch region to reach an exit block of the enclosing loop
region -- a multi-level branch that must be transformed for valid SPIRV.

The bug: populateExitBlocks() stored continueBlock but did not add it to
region->exitBlocks, so mapExitBlockToRegion never mapped %incr to the loop
region. gatherInfo() therefore never flagged the branch from case 0 to %incr as
a multi-level branch, needsContinueElimination stayed false, and the raw IR
branch reached the SPIRV emitter -- producing unstructured control flow.

The fix: add continueBlock to region->exitBlocks whenever continueBlock !=
targetBlock (i.e. for for-loops). This makes %incr visible in
mapExitBlockToRegion, gatherInfo() detects the multi-level branch, and
eliminateContinueBlocksInFunc() is called to wrap the loop body in an inner
breakable region. After that transformation the continue becomes a break from
the inner region -- a valid SPIRV structured exit to its merge block -- and the
outer loop handles the actual iteration.

Also fix a stack leak: after processing a loop region, pop continueBlock from
the global exitBlocks stack unconditionally. For for-loops it is already in
info.exitBlocks and removed by the existing loop; for while-loops (where
continueBlock == targetBlock and is not in info.exitBlocks) it was pushed to
the global stack but never popped, which could affect sibling constructs.

Fixes #9585, #10198.

@jkwak-work jkwak-work self-assigned this Feb 28, 2026
@jkwak-work jkwak-work added the pr: non-breaking PRs without breaking changes label Feb 28, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 434d7737-f3d5-4aa3-80ab-edce55b5d8fb

📥 Commits

Reviewing files that changed from the base of the PR and between 695944c and a90cb7c.

📒 Files selected for processing (2)
  • source/slang/slang-ir-eliminate-multilevel-break.cpp
  • tests/bugs/nested-switch-continue-in-loop.slang
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/bugs/nested-switch-continue-in-loop.slang
  • source/slang/slang-ir-eliminate-multilevel-break.cpp

📝 Walkthrough

Walkthrough

Handle continue targets when eliminating multi-level breaks: loop headers are captured as IRLoop, continue blocks are recognized and treated as exit points when distinct from loop targets, and continue blocks are removed from exit-block sets after region processing to preserve structured control flow.

Changes

Cohort / File(s) Summary
IR Control Flow Fix
source/slang/slang-ir-eliminate-multilevel-break.cpp
Capture loop headers as IRLoop, obtain continueBlock via getContinueBlock(), assert non-null, add it to exitBlocks when it differs from the loop target; on region pop, remove info.continueBlock from exitBlocks to keep exit set consistent.
Regression Test
tests/bugs/nested-switch-continue-in-loop.slang
Add a test exercising continue inside a switch within for/while loops; writes results to a buffer and asserts expected outputs (3, 36, 39) to validate correct control-flow handling across backends.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through IR blocks, a twist in the loop,
A switch made me skip—what a puzzling loop-de-loop!
I nudged the continue, marked its exit with care,
Now structured flow's tidy, no SPIR‑V nightmare.
The rabbit applauds: small hops, big repair. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main fix: addressing invalid SPIRV generation when continue appears inside a switch within a for loop.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the bug, root cause, and the specific fixes applied to handle multi-level branches in nested control structures.
Linked Issues check ✅ Passed The changes fully address the linked issue #9585 by implementing IR transformation to detect and handle multi-level branches from continue in switch-within-loop structures, and fix a stack leak affecting while-loops.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the nested switch-continue-in-loop issue and its related stack leak; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

github-actions[bot]

This comment was marked as outdated.

@jkwak-work jkwak-work force-pushed the fix/switch-without-case branch from 6f5c55f to 2188a47 Compare March 11, 2026 00:44
github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

@jkwak-work jkwak-work requested a review from jhelferty-nv March 11, 2026 01:03
@jkwak-work jkwak-work marked this pull request as ready for review March 11, 2026 01:03
@jkwak-work jkwak-work requested a review from a team as a code owner March 11, 2026 01:03
@jkwak-work jkwak-work requested review from bmillsNV and removed request for a team March 11, 2026 01:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 282e06c7-f085-4f90-8559-0e44e6d10e10

📥 Commits

Reviewing files that changed from the base of the PR and between 38b5b83 and 695944c.

📒 Files selected for processing (2)
  • source/slang/slang-ir-eliminate-multilevel-break.cpp
  • tests/bugs/nested-switch-continue-in-loop.slang

github-actions[bot]

This comment was marked as outdated.

@github-actions github-actions bot dismissed their stale review March 11, 2026 01:13

Automated review dismissed: bot reviews must use COMMENT only, not APPROVE or REQUEST_CHANGES.

Copy link
Contributor

@jhelferty-nv jhelferty-nv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@jkwak-work jkwak-work added this pull request to the merge queue Mar 11, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 11, 2026
@jkwak-work jkwak-work added this pull request to the merge queue Mar 11, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 11, 2026
@jkwak-work jkwak-work enabled auto-merge March 12, 2026 14:01
github-actions[bot]

This comment was marked as outdated.

…hader-slang#9585, shader-slang#10198)

For a for-loop with a switch containing a `continue`, the IR has this shape:

  loop(target=%body, break=%after, continue=%incr)
  %body:
    switch(x, break=%post_switch, ...)
      case 0: unconditionalBranch(%incr)        <- 'continue'
      case 1: unconditionalBranch(%post_switch) <- 'break'
  %post_switch:
    unconditionalBranch(%incr)   <- normal post-switch flow
  %incr: i++; unconditionalBranch(%body)  <- back-edge
  %after: ...                              <- loop break

The `continue` inside the switch branches to %incr (the loop's continueBlock).
Because continueBlock (%incr) != targetBlock (%body) in a for-loop, this is a
branch that exits the switch region to reach an exit block of the enclosing loop
region -- a multi-level branch that must be transformed for valid SPIRV.

The bug: populateExitBlocks() stored continueBlock but did not add it to
region->exitBlocks, so mapExitBlockToRegion never mapped %incr to the loop
region. gatherInfo() therefore never flagged the branch from case 0 to %incr as
a multi-level branch, needsContinueElimination stayed false, and the raw IR
branch reached the SPIRV emitter -- producing unstructured control flow.

The fix: add continueBlock to region->exitBlocks whenever continueBlock !=
targetBlock (i.e. for for-loops). This makes %incr visible in
mapExitBlockToRegion, gatherInfo() detects the multi-level branch, and
eliminateContinueBlocksInFunc() is called to wrap the loop body in an inner
breakable region. After that transformation the `continue` becomes a break from
the inner region -- a valid SPIRV structured exit to its merge block -- and the
outer loop handles the actual iteration.

Also fix a stack leak: after processing a loop region, pop continueBlock from
the global exitBlocks stack unconditionally. For for-loops it is already in
info.exitBlocks and removed by the existing loop; for while-loops (where
continueBlock == targetBlock and is not in info.exitBlocks) it was pushed to
the global stack but never popped, which could affect sibling constructs.

Fixes shader-slang#9585, shader-slang#10198.
@jkwak-work jkwak-work force-pushed the fix/switch-without-case branch from 73b06c8 to a90cb7c Compare March 12, 2026 17:38
@jkwak-work
Copy link
Collaborator Author

Having a trouble with CI.
I force-push-ed to see if it helps.

@jkwak-work jkwak-work disabled auto-merge March 12, 2026 19:36
@jkwak-work jkwak-work merged commit 775387b into shader-slang:master Mar 12, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: non-breaking PRs without breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

For loop continue inside a switch breaks structured control flow

2 participants