Skip to content

[python] Pass the loop-carried arguments to a bare break or continue - #5177

Open
udsy19 wants to merge 1 commit into
NVIDIA:mainfrom
udsy19:fix/for-break-continue-arity
Open

[python] Pass the loop-carried arguments to a bare break or continue#5177
udsy19 wants to merge 1 commit into
NVIDIA:mainfrom
udsy19:fix/for-break-continue-arity

Conversation

@udsy19

@udsy19 udsy19 commented Aug 18, 2026

Copy link
Copy Markdown

A break or continue written directly in a for body, rather than nested inside an if, emitted
its cc.break / cc.continue terminator with an empty operand list, so the kernel failed to
compile:

@cudaq.kernel
def bare_cont() -> int:
    r = 0
    for j in range(3):
        r = r + 1
        continue
    return r
error: 'cc.loop' op along control flow edge from Operation cc.continue to Region #2:
       region branch point has 0 operands, but region successor needs 1 inputs

RuntimeError: pass pipeline failed
RuntimeError: could not compile code for 'bare_cont..'.

The same happens for break, in nested loops, when iterating a qvector, and in purely quantum
kernels with no classical return value.

What this changes

Every terminator in a cc.loop body region has to forward that region's block arguments.
createForLoop establishes the invariant itself — the implicit fall-through terminator it appends
uses bodyBlock.arguments, and that is exactly the tuple pushForBodyStack records. The
isInIfStmtBlock() branch of visit_Break / visit_Continue already forwards it; only the other
branch hardcoded []. Hoisting the binding above the if and using it in both branches is a
net-zero-line change:

        # Get the innermost enclosing `for` or `while` loop
        inArgs = [b for b in self.inForBodyStack[-1]]
        if self.isInIfStmtBlock():
            cc.UnwindContinueOp(inArgs)
        else:
            cc.ContinueOp(inArgs)

The resulting terminator has the same shape python/tests/mlir/ast_break.py already asserts for the
if-nested path.

while loops are unaffected either way: visit_While builds the loop with no block arguments, so
the forwarded list is empty. I confirmed the printed module for a while loop with a bare
continue and with a bare break is byte-identical before and after this change, as is the module
for each of the two kernels the existing tests in these files already cover.

This is the same class of arity mismatch as #1682, fixed in #1693 — that fix corrected
inForBodyStack[0]inForBodyStack[-1] in the isInIfStmtBlock() branch and left the other
branch emitting [].

Testing

python/tests/mlir/ast_break.py gains test_bare_break and python/tests/mlir/ast_continue.py
gains test_bare_continue, each a for loop whose break / continue is not nested in an if,
with a CHECK block in the files' existing style asserting the terminator now carries the
loop-carried operands. Both tests also invoke the kernel, exercising the JIT path that raised the
RuntimeError. Both fail before this change and pass after it.

There was no coverage of this branch before: both files place the statement under an if, and an
AST scan of python/ finds no @cudaq.kernel anywhere in the tree with a break or continue
that is not nested in an if.

Validation

The change was applied to the ast_bridge.py of an installed cudaq 0.15.1 wheel
(cuda_quantum_cu13, Python 3.13, macOS 26.5.1 arm64), exercised end to end, and the wheel restored
afterwards. Before, all eight for-loop forms failed to compile; after, every one matches CPython
(3, 1, 6, 2, 5, and the expected measurement distributions), and every case that worked
before is unchanged.

Fixes #5176

A `break` or `continue` written directly in a `for` body, rather than
nested inside an `if`, emitted its `cc.break` / `cc.continue` terminator
with an empty operand list. Every terminator in a `cc.loop` body region
has to forward that region's block arguments, so the kernel failed to
compile:

    error: 'cc.loop' op along control flow edge from Operation
    cc.continue to Region NVIDIA#2: region branch point has 0 operands, but
    region successor needs 1 inputs

    RuntimeError: could not compile code for '...'

`createForLoop` establishes the invariant itself: the implicit
fall-through terminator it appends uses `bodyBlock.arguments`, and that
is exactly the tuple `pushForBodyStack` records. The `isInIfStmtBlock()`
branch of `visit_Break` / `visit_Continue` already forwards it; only the
other branch hardcoded `[]`. Hoist the binding above the `if` and use it
in both branches.

`while` loops are unaffected either way -- `visit_While` builds the loop
with no block arguments, so the forwarded list is empty and the emitted
IR is unchanged.

Signed-off-by: Udaya Tejas <udayatejas2004@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added python-lang Anything related to the Python CUDA Quantum language implementation python bridge Involves the python bridge to quake labels Aug 18, 2026

@schweitzpgi schweitzpgi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, that's a bug in the bridge. In fact, we should get rid of the premature optimizations in the bridge of it trying to thread variables around as values. The design is to have the compiler do that itself so it's done correctly.

@udsy19

udsy19 commented Aug 19, 2026

Copy link
Copy Markdown
Author

Thanks @schweitzpgi — and agreed on the larger point about the bridge threading values around. This PR does not touch that design; it only passes the loop-carried arguments that cc.BreakOp and cc.ContinueOp already require, so the emitted IR verifies instead of failing with "region branch point has 0 operands, but region successor needs 1 inputs".

CI has never run on it. Could you post /ok to test 031c1dce3ac2f94bb40dffbdaa6e174213d6f964 when you get a chance? And if you would rather fold this into a broader bridge cleanup, that is fine by me — happy to close it in favour of that.


Comment on lines +109 to +122
# CHECK-DAG: %[[VAL_44:.*]] = cc.undef i64
# CHECK-DAG: %[[VAL_45:.*]] = quake.alloca !quake.veq<4>
# CHECK: %[[VAL_46:.*]]:2 = cc.loop while ((%[[VAL_47:.*]] = %[[VAL_42]], %[[VAL_48:.*]] = %[[VAL_44]]) -> (i64, i64)) {
# CHECK: %[[VAL_49:.*]] = arith.cmpi slt, %[[VAL_47]], %[[VAL_43]] : i64
# CHECK: cc.condition %[[VAL_49]](%[[VAL_47]], %[[VAL_48]] : i64, i64)
# CHECK: } do {
# CHECK: ^bb0(%[[VAL_50:.*]]: i64, %[[VAL_51:.*]]: i64):
# CHECK: %[[VAL_52:.*]] = quake.extract_ref %[[VAL_45]]{{\[}}%[[VAL_50]]] : (!quake.veq<4>, i64) -> !quake.ref
# CHECK: quake.ry (%[[VAL_40]]) %[[VAL_52]] : (f64, !quake.ref) -> ()
# CHECK: cc.continue %[[VAL_50]], %[[VAL_50]] : i64, i64
# CHECK: } step {
# CHECK: ^bb0(%[[VAL_53:.*]]: i64, %[[VAL_54:.*]]: i64):
# CHECK: %[[VAL_55:.*]] = arith.addi %[[VAL_53]], %[[VAL_41]] : i64
# CHECK: cc.continue %[[VAL_55]], %[[VAL_54]] : i64, i64

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The CHECK lines expect the loop to carry two i64 values plus a cc.undef i64. The kernel in the test has no break, so the loop carries only the induction variable. The real IR is a single argument loop with no cc.undef.

Running the file's own RUN line fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python bridge Involves the python bridge to quake python-lang Anything related to the Python CUDA Quantum language implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bare break/continue in a for loop emits wrong-arity IR and fails the verifier

3 participants