Skip to content

[python] Reject kernels whose only return is inside a dynamic-bound loop - #5370

Open
udsy19 wants to merge 1 commit into
NVIDIA:mainfrom
udsy19:fix/kernel-return-validation-ignores-zero-trip-loops
Open

udsy19 wants to merge 1 commit into
NVIDIA:mainfrom
udsy19:fix/kernel-return-validation-ignores-zero-trip-loops

Conversation

@udsy19

@udsy19 udsy19 commented Sep 3, 2026

Copy link
Copy Markdown

Fixes #5369

Bug

ValidateReturnStatements.all_paths_return (python/cudaq/kernel/analysis.py:191-206) treats a
for/while loop as guaranteeing a return whenever its body alone always returns, regardless of
whether the loop ever executes:

if isinstance(stmt, (ast.For, ast.While)):
    if all_paths_return(stmt.body) or all_paths_return(
            stmt.orelse):
        return True

For a loop with a runtime-dependent trip count (for i in range(n):, while i < n:), this is
unsound: a zero-trip loop falls through with no return statement having run, and
cudaq.kernel functions with return type annotations must have a return statement. never fires.
The kernel compiles and the backend's cc.UndefOp fallback silently manufactures the returned
value instead of erroring. The sibling ast.If check just above requires both branches to
return (and); this is the same soundness rule, just not applied to loops.

Fix

Only tighten the two shapes that are provably unsound from the syntax alone: a while whose
condition is not a statically-true comparison, and a for over range(...) whose bound is not a
compile-time constant with a non-empty span. Any other iterable (a plain variable, a list literal,
a qvector, ...) keeps the existing lenient treatment, since it is a compile-time unknown either way
and this file's own test suite already relies on that leniency for unrelated reasons in exactly
that case. The pre-existing while <var> <op> <literal>: idiom used throughout this file's own
tests (i = 0 then while i < 6:) is preserved by tracking compile-time-constant integer
variables assigned earlier in the same statement list.

Verification

Not a regression from a deliberate choice: introduced with the validator itself in cb16d1e59
(#3148, "Handling return from loop variations in a kernel") with no comment addressing the
and/or asymmetry with the If case just above — confirmed via git log -S. Unchanged since:
git log <wheel-sha>..main -- python/cudaq/kernel/analysis.py is empty, and the file is
byte-identical between the 0.15.1 wheel and current main.

Negative control, both directions, run directly against the installed wheel (this box has no
local LLVM/MLIR build; analysis.py is pure Python with no compiled-extension dependency, so
this file is not subject to that limitation — the worktree's file was copied into the wheel's
site-packages before each run):

without the fix: 2 failed
  test_return_only_inside_for_loop_over_dynamic_range_is_rejected: Failed: DID NOT RAISE RuntimeError
  test_return_only_inside_while_loop_over_dynamic_condition_is_rejected: Failed: DID NOT RAISE RuntimeError
with the fix:     3 passed

Also ran the full existing test_run_kernel.py suite with the fix applied: 80 passed, 4 skipped,
no regressions — including every test_return_from_*/test_return_with_* case that already
exercises this validator (54 tests), specifically
test_return_from_if_and_else_loop_with_true_condition_in_for_loop and
..._in_while_loop, whose range(6) / i = 0; while i < 6: bodies are exactly the shapes this
fix must keep accepting.

Small diff; touches only the loop-soundness branch of one validator and adds three regression
tests.

Signed-off-by: Udaya Tejas udayatejas2004@gmail.com

`ValidateReturnStatements.all_paths_return` treats a `for`/`while` loop
as guaranteeing a return whenever its body alone always returns,
regardless of whether the loop is ever entered:

    if isinstance(stmt, (ast.For, ast.While)):
        if all_paths_return(stmt.body) or all_paths_return(stmt.orelse):
            return True

For a loop with a runtime-dependent trip count (`for i in range(n):`,
`while i < n:`) this is unsound: if the loop executes zero times,
control falls off the end of the function with no return statement
having run, and the compiler never raises "functions with return type
annotations must have a return statement." Reproduced end to end: a
kernel `def k(n: int) -> int: for i in range(n): return 1` compiles
successfully and, called with `n=0`, returns whatever the backend's
`cc.UndefOp` fallback happens to produce instead of erroring.

Only tighten the two shapes that are provably unsound from the syntax
alone: a `while` whose condition is not a statically-true comparison,
and a `for` over `range(...)` whose bound is not a compile-time
constant with a non-empty span. Iteration over anything else (a plain
variable, a list literal, a qvector, ...) keeps the existing lenient
treatment, since this file's own test suite already relies on it for
unrelated reasons in exactly that case, and there is no static
argument for or against it either way.

The pre-existing `while <var> <op> <literal>:` idiom used throughout
this file's own test suite (`i = 0` then `while i < 6:`) is preserved
by tracking compile-time-constant integer variables assigned earlier
in the same statement list.

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

copy-pr-bot Bot commented Sep 3, 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 Sep 3, 2026
@atgeller

atgeller commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

In the C++ bridge, a loop that will definitely execute will still produce a warning for a missing return. This PR should probably go the easier route of rejecting programs without a return after the loop and not look too closely at the loop condition. @schweitzpgi, does that sound reasonable to you?

#include <cudaq.h>

int __qpu__ helper() {
  for(int i = 0; i < 10; i++) {
    return i;
  }
}

int main() {
  int x = helper(-10);
  return x;
}

Produces

nvq++ test.cpp
test.cpp:7:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
    7 | }
      | ^
1 warning generated.

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.

[python] Kernel with a return only inside a dynamic-bound loop compiles without a return-type check

2 participants