Skip to content

Conversation

@shi-eric
Copy link
Contributor

@shi-eric shi-eric commented Jan 11, 2026

When a global Python variable had the same name as a kernel for-loop variable, wp.static() incorrectly used the global value. The fix tracks loop variables during AST traversal and defers wp.static() evaluation when the expression references a loop variable.

Fixes GH-1139

Description

Before your PR is "Ready for review"

  • All commits are signed-off to indicate that your contribution adheres to the Developer Certificate of Origin requirements
  • Necessary tests have been added
  • Documentation is up-to-date
  • Auto-generated files modified by compiling Warp and building the documentation have been updated (e.g. __init__.pyi, docs/api_reference/, docs/language_reference/)
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • Bug Fixes

    • wp.static() no longer captures global variables instead of loop variables; loop-local variables in for-loops are now respected, including in nested and shadowed cases.
  • Tests

    • Added tests validating wp.static() with simple loops, expressions, nested loops, and reused loop-variable names to ensure correct behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Tracks loop-local variable names during codegen so wp.static() inside for-loops no longer captures shadowed globals; adds tests exercising simple, expression, nested, and shadowed-variable loop scenarios and a changelog entry.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added entry noting fix for wp.static() capturing globals instead of loop variables inside kernels.
Code Generator
warp/_src/codegen.py
Enhanced StaticExpressionReplacer with per-instance loop_vars tracking and visit_For push/pop logic; when a static expression references a tracked loop variable, evaluation is deferred by setting has_unresolved_static_expressions, preventing premature capture of shadowed globals.
Tests
warp/tests/test_static.py
Added kernels static_loop_var_kernel, static_loop_var_in_expr_kernel, static_nested_loop_kernel, static_nested_loop_same_var_kernel and test test_static_loop_variable_not_shadowed_by_global; registered test to validate loop-variable capture, expressions, nesting, and shadowing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main fix: preventing wp.static() from capturing global variables when kernel loop variables share the same name.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3db7004 and 11764fd.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/tests/test_static.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-13T06:46:05.869Z
Learnt from: shi-eric
Repo: NVIDIA/warp PR: 1167
File: warp/tests/test_static.py:776-781
Timestamp: 2026-01-13T06:46:05.869Z
Learning: In the NVIDIA/warp repository, allow star imports like 'from warp.tests.unittest_utils import *' in test files as dictated by the project's Ruff configuration. Do not flag F405 in test files provided that pre-commit checks pass. Apply this guidance to test files under warp/tests (e.g., warp/tests/**/*.py). Ensure pre-commit and Ruff configuration are the source of truth for these allowances.

Applied to files:

  • warp/tests/test_static.py
🪛 Ruff (0.14.11)
warp/tests/test_static.py

710-710: Unused function argument: test

(ARG001)


776-776: add_function_test may be undefined, or defined from star imports

(F405)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: ci / build-docs
  • GitHub Check: ci / test-warp (ubuntu-24.04, ubuntu-x86_64)
  • GitHub Check: ci / test-warp (windows-2025, windows)
  • GitHub Check: ci / test-warp (ubuntu-24.04-arm, ubuntu-aarch64)
  • GitHub Check: ci / test-warp (macos-latest, macos)
  • GitHub Check: Greptile Review
🔇 Additional comments (6)
warp/_src/codegen.py (2)

3741-3758: LGTM! Clean implementation of loop variable tracking.

The counter-based approach correctly handles nested loops that reuse the same variable name. The increment-on-entry, decrement-on-exit pattern with cleanup when count reaches 0 ensures proper scoping.

Minor observation: The tuple unpacking case (for x, y in ...) is intentionally not tracked, as noted in the comment. This is a reasonable trade-off for Warp kernels.


3760-3770: LGTM! Correct deferral logic for loop variable references.

The implementation properly:

  1. Extracts the expression from positional or keyword arguments
  2. Walks the AST to find all referenced Name nodes
  3. Checks intersection with tracked loop variables
  4. Defers evaluation by marking has_unresolved_static_expressions and returning early

This ensures that wp.static() expressions referencing loop variables are evaluated at codegen time when the loop constant is available, rather than at declaration time when only the global would be visible.

warp/tests/test_static.py (4)

662-664: LGTM! Clear test setup for shadowing scenario.

The global variables with distinctive values (999, 888) make it easy to detect if the wrong value is captured.


667-707: Comprehensive test coverage for loop variable handling.

The four test kernels effectively cover:

  1. Simple loop variable capture
  2. Loop variable in arithmetic expressions
  3. Nested loops with different variable names
  4. Nested loops reusing the same variable name (tests the counter-based tracking)

The last test case (static_nested_loop_same_var_kernel) is particularly valuable as it validates the counter-based loop variable tracking correctly handles the edge case where inner and outer loops use the same variable name.


710-743: Well-structured test function with clear assertions.

The test uses np.testing.assert_array_equal with descriptive err_msg values, which is consistent with how array-based assertions are done elsewhere in Warp tests.

Note: The static analysis flags test as unused (ARG001). This is intentional as the function uses np.testing for array comparisons rather than test.assertEqual. This pattern is consistent with other test functions in this file.


776-781: Test registration looks correct.

The F405 warning about add_function_test is a false positive—it comes from the star import which is allowed per project configuration for test files.


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.

Copy link

@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: 0

🧹 Nitpick comments (2)
warp/_src/codegen.py (1)

3744-3753: Loop variable tracking logic is correct.

The implementation properly adds and removes loop variables when entering/exiting For node scope. The use of discard() instead of remove() is a good defensive choice.

Optional: Consider handling tuple unpacking in loop targets.

While the comment notes that tuple unpacking is rare, the current implementation silently ignores patterns like for x, y in .... Consider either:

  1. Extracting all names from tuple targets and tracking them
  2. Raising a warning if tuple unpacking is used with wp.static()

This could prevent subtle bugs if users do use tuple unpacking.

warp/tests/test_static.py (1)

691-716: Test logic is correct, but parameter may be unused.

The test function correctly validates that wp.static() captures loop variables instead of shadowed globals. The expected outputs are accurate:

  • Simple loop: [0, 1, 2] from iteration values
  • Expression: [1, 3, 5] from i*2+1
  • Nested: [0, 1, 10, 11] from i*10+j

Consider addressing the unused test parameter.

Static analysis indicates the test parameter is unused. If it's required by the test framework interface, consider prefixing it with an underscore (_test) to indicate it's intentionally unused. Otherwise, you may be able to use it for assertions instead of relying on NumPy's assert_array_equal.

♻️ Optional: Use test parameter for assertions

If the test framework provides assertion methods, you could use them instead:

-        np.testing.assert_array_equal(results.numpy(), np.array([0, 1, 2]), err_msg="Simple loop variable test failed")
+        test.assertTrue(np.array_equal(results.numpy(), np.array([0, 1, 2])), "Simple loop variable test failed")

Or if not needed, rename to _test to indicate it's intentionally unused.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb3e96a and b7e5a81.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/tests/test_static.py
🧰 Additional context used
🪛 Ruff (0.14.10)
warp/tests/test_static.py

691-691: Unused function argument: test

(ARG001)


748-748: add_function_test may be undefined, or defined from star imports

(F405)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: ci / test-warp (ubuntu-24.04, ubuntu-x86_64)
  • GitHub Check: ci / test-warp (macos-latest, macos)
  • GitHub Check: ci / test-warp (windows-2025, windows)
  • GitHub Check: ci / test-warp (ubuntu-24.04-arm, ubuntu-aarch64)
  • GitHub Check: ci / build-docs
  • GitHub Check: cppcheck
🔇 Additional comments (6)
CHANGELOG.md (1)

18-19: Changelog entry looks good.

The fix is documented clearly and accurately. The entry follows the established format, includes the GitHub issue reference, and provides sufficient context for users encountering this issue.

warp/_src/codegen.py (2)

3739-3742: LGTM!

The initialization of loop_vars as a set is appropriate for tracking unique loop variable names. The comment clearly explains the purpose of preventing wp.static() from capturing shadowed global variables.


3758-3765: Correct implementation of loop variable detection.

The code properly:

  1. Walks the AST of the static expression to find all referenced names
  2. Checks intersection with tracked loop variables
  3. Defers evaluation by setting the has_unresolved_static_expressions flag
  4. Continues traversal to allow later codegen-time evaluation

This ensures that loop constants are available when the static expression is eventually evaluated.

warp/tests/test_static.py (3)

662-665: LGTM!

The global test variables are well-named and their purpose is clearly documented. The values (999, 888) are deliberately different from loop iteration values to make it obvious if wp.static() incorrectly captures the globals instead of loop variables.


667-689: Comprehensive test coverage for loop variable handling.

The three test kernels effectively cover:

  1. Simple loop variable capture
  2. Loop variables in complex expressions
  3. Nested loops with multiple loop variables

Each kernel has a clear docstring explaining its purpose, and the test cases progressively increase in complexity. This provides good validation that wp.static() correctly handles loop variables in various scenarios.


748-753: LGTM!

The test registration follows the same pattern as other tests in the file and correctly:

  1. Associates the test with the TestStatic class
  2. Uses a descriptive test name
  3. Passes the test function and devices parameter

The static analysis warning about add_function_test being undefined is a false positive—it's imported via the star import from warp.tests.unittest_utils at line 24.

@greptile-apps
Copy link

greptile-apps bot commented Jan 11, 2026

Greptile Summary

This PR fixes a bug where wp.static() incorrectly captured global Python variables instead of kernel loop variables when they had the same name. The fix introduces loop variable tracking during AST traversal, deferring wp.static() evaluation until codegen time when loop constants are available.

Key Changes:

  • Added loop_vars dictionary to track enclosing loop variables using a counter-based approach for nested loops
  • Modified visit_For to track loop variables during AST traversal
  • Enhanced visit_Call to detect when wp.static() references loop variables and defer evaluation
  • Added comprehensive test coverage for simple loops, arithmetic expressions, nested loops, and variable shadowing

Technical Correctness:
The implementation correctly handles Python scoping semantics. During loop unrolling at codegen time, loop variables exist as constants in adj.symbols, and the deferred wp.static() evaluation correctly finds these constants through the evaluation context built from adj.symbols. For edge cases like wp.static() in iterator expressions, the evaluation happens before the loop variable is added to symbols, correctly falling back to the global scope.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The fix is well-designed, correctly implements deferred evaluation for wp.static() when loop variables are referenced, and includes comprehensive test coverage. The implementation properly handles nested loops and variable shadowing using a counter-based tracking mechanism. Previous concerns about iterator expression scoping have been addressed by the developer and are correct given that evaluation happens at codegen time with proper symbol table context.
  • No files require special attention

Important Files Changed

Filename Overview
warp/_src/codegen.py Adds loop variable tracking to StaticExpressionReplacer to correctly defer wp.static() evaluation when referencing loop variables, preventing incorrect global variable capture
warp/tests/test_static.py Adds comprehensive test coverage for wp.static() loop variable handling including simple loops, expressions, nested loops, and variable shadowing scenarios

Sequence Diagram

sequenceDiagram
    participant User as User Code
    participant AST as AST Transformer
    participant Adj as Adjoint
    participant Eval as Static Evaluator
    participant Codegen as Code Generator

    User->>AST: Define kernel with wp.static(loop_var)
    AST->>AST: visit_For(): Track loop variable in loop_vars
    AST->>AST: visit_Call(): Detect wp.static() referencing loop_var
    AST->>Adj: Set has_unresolved_static_expressions = True
    AST->>AST: Defer evaluation (don't replace with constant)
    
    Note over AST,Adj: wp.static() left as call node in AST
    
    Adj->>Codegen: emit_For(): Begin loop unrolling
    loop For each iteration i
        Codegen->>Adj: Add loop constant to symbols[loop_var]
        Codegen->>Eval: Evaluate wp.static(loop_var) with loop constant
        Eval->>Eval: Build evaluation context from adj.symbols
        Eval->>Eval: Find loop_var constant in symbols
        Eval-->>Codegen: Return correct loop iteration value
        Codegen->>Codegen: Generate code for loop body with constant
    end
    
    Note over Codegen: Loop unrolled with correct constants
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 3744 to 3758
def visit_For(self, node):
# Track loop variable while visiting loop body (simple names only;
# tuple unpacking like `for x, y in ...` is rare in Warp kernels)
var_name = node.target.id if isinstance(node.target, ast.Name) else None
if var_name:
self.loop_vars.add(var_name)
result = self.generic_visit(node)
if var_name:
self.loop_vars.discard(var_name) # Remove when exiting loop scope
return result
Copy link

Choose a reason for hiding this comment

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

The loop variable is added to self.loop_vars before visiting child nodes, which incorrectly treats it as "in scope" when visiting the iterator expression (node.iter). In Python semantics, the iterator expression is evaluated before the loop variable is bound.

This causes incorrect behavior when wp.static() references a variable with the same name as the loop variable in the iterator expression:

n = 5
for n in range(wp.static(n)):  # wp.static(n) should use global n=5, not defer
    pass

The current implementation would defer wp.static(n) because n is in loop_vars, but it should evaluate to 5 using the global scope.

To fix this, only track the loop variable when visiting the loop body and orelse clause:

Suggested change
def visit_For(self, node):
# Track loop variable while visiting loop body (simple names only;
# tuple unpacking like `for x, y in ...` is rare in Warp kernels)
var_name = node.target.id if isinstance(node.target, ast.Name) else None
if var_name:
self.loop_vars.add(var_name)
result = self.generic_visit(node)
if var_name:
self.loop_vars.discard(var_name) # Remove when exiting loop scope
return result
def visit_For(self, node):
# Track loop variable while visiting loop body (simple names only;
# tuple unpacking like `for x, y in ...` is rare in Warp kernels)
var_name = node.target.id if isinstance(node.target, ast.Name) else None
# Visit the iterator expression first, without the loop variable in scope
self.visit(node.iter)
# Now add loop variable to scope and visit the body
if var_name:
self.loop_vars.add(var_name)
for stmt in node.body:
self.visit(stmt)
for stmt in node.orelse:
self.visit(stmt)
if var_name:
self.loop_vars.discard(var_name)
return node

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@greptileai Your observation about Python semantics is technically correct—the iterator expression is evaluated before the loop variable is bound. However, we verified this edge case works correctly in practice. When wp.static(n) in the iterator is deferred, the subsequent evaluation at codegen time still finds the global n in the evaluation context, producing the correct result. No code change is needed.

@shi-eric shi-eric self-assigned this Jan 11, 2026
@shi-eric shi-eric force-pushed the ershi/fix-static-loop-var branch from b7e5a81 to 3db7004 Compare January 11, 2026 03:07
Copy link

@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: 5

🤖 Fix all issues with AI agents
In @warp/tests/test_static.py:
- Around line 776-781: Ruff flags F405 for the bare usage of add_function_test
due to star-imports; fix by adding a local noqa to that call site — append a
trailing comment "# noqa: F405" to the add_function_test invocation for
test_static_loop_variable_not_shadowed_by_global so the single-line suppression
silences the warning without changing imports.
- Around line 674-680: The triple-quoted docstring inside the @wp.kernel
function static_loop_var_in_expr_kernel can end up as a runtime string literal
inside the kernel body and interfere with static-analysis; remove or move that
docstring out of the kernel (e.g., convert it to a regular comment above the
function or place the descriptive string at module level) so the kernel body
contains no top-level string literal, keeping the loop and the use of
wp.static(_global_test_idx * 2 + 1) unchanged.
- Around line 682-689: The kernel static_nested_loop_kernel contains a Python
docstring inside the kernel body which can be misinterpreted by the compiler;
remove the triple-quoted docstring from inside the kernel (or convert it to a
regular comment) and place any description outside the kernel function so the
loop variables (_global_test_idx, _global_test_j), idx calculation, and
wp.static usage remain unchanged and the test still expects [0, 1, 10, 11].
- Around line 710-743: The test function
test_static_loop_variable_not_shadowed_by_global has an unused parameter `test`
causing Ruff ARG001—rename it to `_test` or `_` (or add `# noqa: ARG001`) to
silence the linter; additionally, replace uses of
`np.testing.assert_array_equal(...)` with the file's existing
`assert_np_equal(...)` helper for consistency (update the four assertions: the
ones checking results, results2, results3, results4) while preserving the same
expected arrays and messages.
- Around line 667-672: The kernel function static_loop_var_kernel currently
contains a leading docstring inside the @wp.kernel function which can become an
ast.Expr and break Warp codegen; remove the triple-quoted string literal from
inside the function and instead place the explanatory text as a regular comment
immediately above the def line (or convert it to inline comments within the
body), ensuring the function body only contains executable statements that use
results and wp.static(_global_test_idx) with the loop variable _global_test_idx.
🧹 Nitpick comments (1)
warp/tests/test_static.py (1)

662-665: Make the “shadowed global” intent clearer and safer from collisions.

These globals are fine for reproducing the bug, but consider making them even more unique (e.g., include static_loop_shadow_) to reduce any chance of name collision with other tests/modules.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7e5a81 and 3db7004.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/tests/test_static.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • warp/_src/codegen.py
🧰 Additional context used
🪛 Ruff (0.14.10)
warp/tests/test_static.py

710-710: Unused function argument: test

(ARG001)


776-776: add_function_test may be undefined, or defined from star imports

(F405)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / build-warp (windows-2025, windows, ["cudart", "nvcc", "nvrtc_dev", "nvjitlink"])
🔇 Additional comments (1)
warp/tests/test_static.py (1)

691-708: Test correctly asserts the intended behavior; docstring usage is valid.

The test properly validates that wp.static() captures the loop variable's final value (2) after the inner loop completes, not the global value (999). The assertion results == [2, 2] is correct: the outer loop executes twice, and each iteration wp.static(_global_test_idx) captures 2, matching Python's variable shadowing semantics.

Regarding docstrings in kernels: docstrings are explicitly allowed in Warp kernels per the framework documentation ("Only pass, ellipsis (...), comments, or docstrings are allowed"). This kernel follows the same pattern as other kernels in the same test file (static_loop_var_kernel, static_nested_loop_kernel, etc.), all with docstrings, making the approach consistent and correct.

Likely an incorrect or invalid review comment.

Comment on lines +667 to +672
@wp.kernel
def static_loop_var_kernel(results: wp.array(dtype=int)):
"""Kernel where wp.static() should capture the loop variable, not the global."""
for _global_test_idx in range(3):
results[_global_test_idx] = wp.static(_global_test_idx)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential Warp compile hazard: docstring inside @wp.kernel.

If Warp’s AST/codegen doesn’t explicitly ignore function docstrings, the leading string literal becomes an ast.Expr statement and can break compilation. Safer to replace kernel docstrings with comments above the function.

Proposed fix
 @wp.kernel
 def static_loop_var_kernel(results: wp.array(dtype=int)):
-    """Kernel where wp.static() should capture the loop variable, not the global."""
+    # wp.static() should capture the loop variable, not the global.
     for _global_test_idx in range(3):
         results[_global_test_idx] = wp.static(_global_test_idx)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@wp.kernel
def static_loop_var_kernel(results: wp.array(dtype=int)):
"""Kernel where wp.static() should capture the loop variable, not the global."""
for _global_test_idx in range(3):
results[_global_test_idx] = wp.static(_global_test_idx)
@wp.kernel
def static_loop_var_kernel(results: wp.array(dtype=int)):
# wp.static() should capture the loop variable, not the global.
for _global_test_idx in range(3):
results[_global_test_idx] = wp.static(_global_test_idx)
🤖 Prompt for AI Agents
In @warp/tests/test_static.py around lines 667 - 672, The kernel function
static_loop_var_kernel currently contains a leading docstring inside the
@wp.kernel function which can become an ast.Expr and break Warp codegen; remove
the triple-quoted string literal from inside the function and instead place the
explanatory text as a regular comment immediately above the def line (or convert
it to inline comments within the body), ensuring the function body only contains
executable statements that use results and wp.static(_global_test_idx) with the
loop variable _global_test_idx.

Comment on lines +674 to +680
@wp.kernel
def static_loop_var_in_expr_kernel(results: wp.array(dtype=int)):
"""Kernel where loop variable is used in an arithmetic expression."""
for _global_test_idx in range(3):
# Even in complex expressions, the loop variable should be used
results[_global_test_idx] = wp.static(_global_test_idx * 2 + 1)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same docstring-in-kernel risk as above.

Also, nice coverage for “loop var used in expression” (this matches the reported bug shape well).

Proposed fix
 @wp.kernel
 def static_loop_var_in_expr_kernel(results: wp.array(dtype=int)):
-    """Kernel where loop variable is used in an arithmetic expression."""
+    # Loop variable is used in an arithmetic expression.
     for _global_test_idx in range(3):
         # Even in complex expressions, the loop variable should be used
         results[_global_test_idx] = wp.static(_global_test_idx * 2 + 1)
🤖 Prompt for AI Agents
In @warp/tests/test_static.py around lines 674 - 680, The triple-quoted
docstring inside the @wp.kernel function static_loop_var_in_expr_kernel can end
up as a runtime string literal inside the kernel body and interfere with
static-analysis; remove or move that docstring out of the kernel (e.g., convert
it to a regular comment above the function or place the descriptive string at
module level) so the kernel body contains no top-level string literal, keeping
the loop and the use of wp.static(_global_test_idx * 2 + 1) unchanged.

Comment on lines +682 to +689
@wp.kernel
def static_nested_loop_kernel(results: wp.array(dtype=int)):
"""Kernel with nested loops - both loop variables should be protected."""
for _global_test_idx in range(2):
for _global_test_j in range(2):
idx = _global_test_idx * 2 + _global_test_j
results[idx] = wp.static(_global_test_idx * 10 + _global_test_j)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same docstring-in-kernel risk; nested-loop coverage looks solid.

The idx mapping and expected values in the test look consistent ([0, 1, 10, 11]).

Proposed fix
 @wp.kernel
 def static_nested_loop_kernel(results: wp.array(dtype=int)):
-    """Kernel with nested loops - both loop variables should be protected."""
+    # Nested loops: both loop variables should be protected from global capture.
     for _global_test_idx in range(2):
         for _global_test_j in range(2):
             idx = _global_test_idx * 2 + _global_test_j
             results[idx] = wp.static(_global_test_idx * 10 + _global_test_j)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@wp.kernel
def static_nested_loop_kernel(results: wp.array(dtype=int)):
"""Kernel with nested loops - both loop variables should be protected."""
for _global_test_idx in range(2):
for _global_test_j in range(2):
idx = _global_test_idx * 2 + _global_test_j
results[idx] = wp.static(_global_test_idx * 10 + _global_test_j)
@wp.kernel
def static_nested_loop_kernel(results: wp.array(dtype=int)):
# Nested loops: both loop variables should be protected from global capture.
for _global_test_idx in range(2):
for _global_test_j in range(2):
idx = _global_test_idx * 2 + _global_test_j
results[idx] = wp.static(_global_test_idx * 10 + _global_test_j)
🤖 Prompt for AI Agents
In @warp/tests/test_static.py around lines 682 - 689, The kernel
static_nested_loop_kernel contains a Python docstring inside the kernel body
which can be misinterpreted by the compiler; remove the triple-quoted docstring
from inside the kernel (or convert it to a regular comment) and place any
description outside the kernel function so the loop variables (_global_test_idx,
_global_test_j), idx calculation, and wp.static usage remain unchanged and the
test still expects [0, 1, 10, 11].

Comment on lines +710 to +743
def test_static_loop_variable_not_shadowed_by_global(test, device):
"""Test that wp.static() inside a for loop correctly captures the loop variable.
When a global Python variable exists with the same name as a kernel loop variable,
wp.static() should use the loop variable's compile-time constant value (0, 1, 2, ...),
not the unrelated global variable. This prevents confusing behavior where the
presence of a global variable silently changes the kernel's output.
"""
with wp.ScopedDevice(device):
# Test 1: Simple loop variable
results = wp.zeros(3, dtype=int)
wp.launch(static_loop_var_kernel, dim=1, inputs=[results])
np.testing.assert_array_equal(results.numpy(), np.array([0, 1, 2]), err_msg="Simple loop variable test failed")

# Test 2: Loop variable in arithmetic expression
results2 = wp.zeros(3, dtype=int)
wp.launch(static_loop_var_in_expr_kernel, dim=1, inputs=[results2])
np.testing.assert_array_equal(
results2.numpy(), np.array([1, 3, 5]), err_msg="Loop variable in expression test failed"
)

# Test 3: Nested loops - both variables protected
results3 = wp.zeros(4, dtype=int)
wp.launch(static_nested_loop_kernel, dim=1, inputs=[results3])
np.testing.assert_array_equal(results3.numpy(), np.array([0, 1, 10, 11]), err_msg="Nested loop test failed")

# Test 4: Nested loops reusing the same variable name
# Tests counter-based tracking: global should not be captured even with shadowing
results4 = wp.zeros(2, dtype=int)
wp.launch(static_nested_loop_same_var_kernel, dim=1, inputs=[results4])
# Per Python semantics: inner loop shadows outer, final value (2) persists
np.testing.assert_array_equal(
results4.numpy(), np.array([2, 2]), err_msg="Nested loop with same variable test failed"
)
Copy link

@coderabbitai coderabbitai bot Jan 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix Ruff ARG001 (test unused) and consider using existing assert_np_equal helper.

Ruff flags test as unused here (ARG001). Either rename it to _test / _ or add a # noqa: ARG001. Also, for consistency with the rest of this file, you may want to use assert_np_equal(...) instead of np.testing.assert_array_equal(...).

Proposed fix (minimal)
-def test_static_loop_variable_not_shadowed_by_global(test, device):
+def test_static_loop_variable_not_shadowed_by_global(_test, device):
     """Test that wp.static() inside a for loop correctly captures the loop variable.
@@
     with wp.ScopedDevice(device):
@@
-        np.testing.assert_array_equal(results.numpy(), np.array([0, 1, 2]), err_msg="Simple loop variable test failed")
+        assert_np_equal(results.numpy(), np.array([0, 1, 2]), tol=0.0)
@@
-        np.testing.assert_array_equal(
-            results2.numpy(), np.array([1, 3, 5]), err_msg="Loop variable in expression test failed"
-        )
+        assert_np_equal(results2.numpy(), np.array([1, 3, 5]), tol=0.0)
@@
-        np.testing.assert_array_equal(results3.numpy(), np.array([0, 1, 10, 11]), err_msg="Nested loop test failed")
+        assert_np_equal(results3.numpy(), np.array([0, 1, 10, 11]), tol=0.0)
@@
-        np.testing.assert_array_equal(
-            results4.numpy(), np.array([2, 2]), err_msg="Nested loop with same variable test failed"
-        )
+        assert_np_equal(results4.numpy(), np.array([2, 2]), tol=0.0)
🧰 Tools
🪛 Ruff (0.14.10)

710-710: Unused function argument: test

(ARG001)

🤖 Prompt for AI Agents
In @warp/tests/test_static.py around lines 710 - 743, The test function
test_static_loop_variable_not_shadowed_by_global has an unused parameter `test`
causing Ruff ARG001—rename it to `_test` or `_` (or add `# noqa: ARG001`) to
silence the linter; additionally, replace uses of
`np.testing.assert_array_equal(...)` with the file's existing
`assert_np_equal(...)` helper for consistency (update the four assertions: the
ones checking results, results2, results3, results4) while preserving the same
expected arrays and messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the pre-commit checks passed on this MR (which it did) then there are no issues with the current set of Ruff rules used in this project.

Copy link

Choose a reason for hiding this comment

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

@shi-eric, you're absolutely right—if the pre-commit checks passed, the code is compliant with the project's configured Ruff rules. I apologize for the confusion. The project's Ruff configuration likely doesn't include the ARG001 rule or has it configured differently than I indicated. Thank you for clarifying!

Comment on lines +776 to +781
add_function_test(
TestStatic,
"test_static_loop_variable_not_shadowed_by_global",
test_static_loop_variable_not_shadowed_by_global,
devices=devices,
)
Copy link

@coderabbitai coderabbitai bot Jan 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Address Ruff F405 for add_function_test on this new call site.

Since this file relies on from warp.tests.unittest_utils import *, Ruff flags this new usage (F405). Easiest is a local # noqa: F405 on the new call (or switch to explicit import, but that’s broader than this change).

Proposed fix
-add_function_test(
+add_function_test(  # noqa: F405
     TestStatic,
     "test_static_loop_variable_not_shadowed_by_global",
     test_static_loop_variable_not_shadowed_by_global,
     devices=devices,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
add_function_test(
TestStatic,
"test_static_loop_variable_not_shadowed_by_global",
test_static_loop_variable_not_shadowed_by_global,
devices=devices,
)
add_function_test( # noqa: F405
TestStatic,
"test_static_loop_variable_not_shadowed_by_global",
test_static_loop_variable_not_shadowed_by_global,
devices=devices,
)
🧰 Tools
🪛 Ruff (0.14.10)

776-776: add_function_test may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
In @warp/tests/test_static.py around lines 776 - 781, Ruff flags F405 for the
bare usage of add_function_test due to star-imports; fix by adding a local noqa
to that call site — append a trailing comment "# noqa: F405" to the
add_function_test invocation for
test_static_loop_variable_not_shadowed_by_global so the single-line suppression
silences the warning without changing imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the pre-commit checks passed on this MR (which it did) then there are no issues with the current set of Ruff rules used in this project.

Copy link

Choose a reason for hiding this comment

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

@shi-eric, thank you for the clarification! You're absolutely right—if the pre-commit checks passed with your project's Ruff configuration, then the F405 warning isn't applicable here. I appreciate you pointing that out, and I'll note that your project's linting rules are already properly configured for this codebase.


✏️ Learnings added
Learnt from: shi-eric
Repo: NVIDIA/warp PR: 1167
File: warp/tests/test_static.py:776-781
Timestamp: 2026-01-13T06:46:05.869Z
Learning: In the NVIDIA/warp repository, star imports like `from warp.tests.unittest_utils import *` in test files are acceptable per the project's Ruff configuration, and F405 warnings should not be flagged if pre-commit checks pass.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +3745 to +3758
def visit_For(self, node):
# Track loop variable while visiting loop body (simple names only;
# tuple unpacking like `for x, y in ...` is rare in Warp kernels)
var_name = node.target.id if isinstance(node.target, ast.Name) else None
if var_name:
self.loop_vars[var_name] = self.loop_vars.get(var_name, 0) + 1
result = self.generic_visit(node)
if var_name:
self.loop_vars[var_name] -= 1
if self.loop_vars[var_name] == 0:
del self.loop_vars[var_name]
return result
Copy link

Choose a reason for hiding this comment

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

The loop variable is added to self.loop_vars before generic_visit(node) is called. This means when the iterator expression (node.iter) is visited, the loop variable is already tracked, which violates Python scoping rules.

In Python, the loop variable is NOT in scope when evaluating the iterator:

i = 10
for i in range(wp.static(i)):  # wp.static(i) should use outer i=10, not defer
    pass

With the current implementation, wp.static(i) in the iterator would be incorrectly deferred because i is already in self.loop_vars.

Fix: Only visit the loop body with the variable tracked, not the entire For node. Replace generic_visit(node) with explicit visiting:

Suggested change
def visit_For(self, node):
# Track loop variable while visiting loop body (simple names only;
# tuple unpacking like `for x, y in ...` is rare in Warp kernels)
var_name = node.target.id if isinstance(node.target, ast.Name) else None
if var_name:
self.loop_vars[var_name] = self.loop_vars.get(var_name, 0) + 1
result = self.generic_visit(node)
if var_name:
self.loop_vars[var_name] -= 1
if self.loop_vars[var_name] == 0:
del self.loop_vars[var_name]
return result
def visit_For(self, node):
# Track loop variable while visiting loop body (simple names only;
# tuple unpacking like `for x, y in ...` is rare in Warp kernels)
var_name = node.target.id if isinstance(node.target, ast.Name) else None
# Visit target and iterator BEFORE adding loop var to tracking
node.target = self.visit(node.target)
node.iter = self.visit(node.iter)
# NOW add loop variable for body traversal
if var_name:
self.loop_vars[var_name] = self.loop_vars.get(var_name, 0) + 1
# Visit body with loop variable tracked
node.body = [self.visit(stmt) for stmt in node.body]
node.orelse = [self.visit(stmt) for stmt in node.orelse]
# Clean up after visiting body
if var_name:
self.loop_vars[var_name] -= 1
if self.loop_vars[var_name] == 0:
del self.loop_vars[var_name]
return node

When a global Python variable had the same name as a kernel for-loop
variable, wp.static() incorrectly used the global value. The fix tracks
loop variables during AST traversal and defers wp.static() evaluation
when the expression references a loop variable.

Fixes NVIDIAGH-1139

Signed-off-by: Eric Shi <[email protected]>
@shi-eric shi-eric force-pushed the ershi/fix-static-loop-var branch from 3db7004 to 11764fd Compare January 13, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Inconsistent capture of python loop variables during loop-unrolling

1 participant