Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- Add a `name` argument to `@wp.kernel` to override the kernel's generated key.
([GH-1561](https://github.com/NVIDIA/warp/issues/1561)).
- Add mipmap (texture level-of-detail) support to `wp.Texture1D`, `wp.Texture2D`, and `wp.Texture3D` via the new
`num_mip_levels` and `mip_filter_mode` constructor parameters, and allow `wp.texture_sample()` to accept an optional
trailing `lod` argument for controlling sampled detail level ([GH-1409](https://github.com/NVIDIA/warp/issues/1409)).
Expand Down
8 changes: 8 additions & 0 deletions warp/_src/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5131,6 +5131,14 @@ def make_full_qualified_name(func: str | Callable) -> str:
return re.sub("[^0-9a-zA-Z_]+", "", func.replace(".", "__"))


# matches a valid C identifier: a letter or underscore followed by letters, digits, or underscores
_C_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


def is_valid_c_identifier(name: str) -> bool:
return _C_IDENTIFIER_RE.match(name) is not None
Comment on lines +5138 to +5139

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Reserved C/C++ keywords pass validation

is_valid_c_identifier only checks the structural regex pattern — it accepts any syntactically valid identifier, including C reserved words (int, float, void, if, for, while, return, struct, etc.) and CUDA built-ins (threadIdx, blockDim, etc.). A user who calls @wp.kernel(name="int") will pass this check but still get a cryptic compiler error, which is the exact failure mode this check was added to prevent.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +5135 to +5139

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Are reserved C/C++ keywords (for example for, while, class, template) legal identifiers for function names?

💡 Result:

No, reserved C/C++ keywords (such as for, while, class, and template) are not legal identifiers for function names [1][2][3]. In C and C++, keywords are reserved by the language specification to provide specific syntactic and semantic meaning to the compiler [1][4]. Because these terms are essential to the language's core structures, they cannot be redefined, overloaded, or used as names for identifiers such as functions, variables, classes, or templates [1][2][5]. The only exception to this rule is the use of these tokens within an attribute-token context (introduced in C++11), where they are not treated as reserved keywords [1][4]. Attempting to use a keyword as a function name will result in a compilation error [3]. Furthermore, beyond explicit keywords, certain other identifiers are reserved for use by the implementation (such as those containing a double underscore or beginning with an underscore followed by an uppercase letter), and using these as function names can lead to undefined behavior [1][2][6].

Citations:


🏁 Script executed:

# Check the current code at the specified location
head -5140 warp/_src/codegen.py | tail -15

Repository: NVIDIA/warp

Length of output: 572


🏁 Script executed:

# Search for any existing reserved keyword handling in codegen.py
rg -n "keyword|reserved|RESERVED" warp/_src/codegen.py | head -20

Repository: NVIDIA/warp

Length of output: 883


🏁 Script executed:

# Check if this validator is used for kernel names or function validation
rg -n "is_valid_c_identifier\|_C_IDENTIFIER_RE" warp/_src/codegen.py

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# The is_valid_c_identifier might not be in use yet. Let's check if it's called anywhere
rg "is_valid_c_identifier" warp/_src/

Repository: NVIDIA/warp

Length of output: 209


🏁 Script executed:

# Check if there's any validation of kernel names in the kernel decorator or related code
rg -n "def kernel\|`@kernel`" warp/_src/codegen.py | head -10

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Search for where kernel names are processed or validated
rg -n "name=" warp/_src/codegen.py | grep -i "kernel\|func" | head -15

Repository: NVIDIA/warp

Length of output: 952


🏁 Script executed:

# Check where is_valid_c_identifier is called in context.py and what it validates
rg -B 5 -A 5 "is_valid_c_identifier" warp/_src/context.py

Repository: NVIDIA/warp

Length of output: 492


🏁 Script executed:

# Also check the kernel decorator implementation to understand the kernel name validation flow
fd "kernel.py\|__init__.py" warp/_src/ --type f | head -5

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Check if there's any existing keyword filtering or handling anywhere in warp codebase
rg -i "keyword|reserved" warp/_src/ --type py | grep -v "\.pyc" | head -20

Repository: NVIDIA/warp

Length of output: 2137


🏁 Script executed:

# Look at the make_full_qualified_name function to understand how kernel names are generated
rg -B 3 -A 8 "make_full_qualified_name" warp/_src/codegen.py

Repository: NVIDIA/warp

Length of output: 473


🏁 Script executed:

# Check C/C++ codegen targets to understand which keywords Warp needs to avoid
rg -n "cuda\|c_\+\+\|CPP\|GLSL" warp/_src/codegen.py | grep -i "target\|backend\|language" | head -10

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Check if Warp targets CUDA or other languages beyond C/C++
rg -n "class.*CodeGen\|def.*codegen" warp/_src/codegen.py | head -10

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Verify if CUDA has additional reserved keywords beyond C++
web_search
CUDA C/C++ reserved keywords additional to C++ standard

Repository: NVIDIA/warp

Length of output: 149


🏁 Script executed:

# Check if there are existing tests for identifier validation
fd "test.*codegen\|test.*kernel" warp/tests/ --type f

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Find test files related to codegen or kernel
find warp/tests -name "*test*codegen*" -o -name "*test*kernel*" | head -10

Repository: NVIDIA/warp

Length of output: 294


🏁 Script executed:

# Check if there are existing tests that might validate identifier handling
rg "is_valid_c_identifier\|_C_IDENTIFIER_RE" warp/tests/

Repository: NVIDIA/warp

Length of output: 37


🌐 Web query:

CUDA C reserved keywords beyond C standard library

💡 Result:

In CUDA C/C++ (as implemented by nvcc), the main “reserved keywords beyond the C standard library” are the CUDA language extension keywords/specifiers—i.e., identifiers starting with double underscores that nvcc recognizes and gives special meaning. Key CUDA-reserved specifiers (language extensions) documented by NVIDIA include: - host / device / global (execution space specifiers for where code runs, or to declare kernels) [1] - restrict (pointer restrict qualifier) [2][1] - launch_bounds (function qualifier for launch bounds on global functions) [2][1] - grid_constant (qualifier for parameters) [2][1] - noinline, forceinline, inline_hint (inlining control qualifiers) [2][1] - cluster_dims (cluster dimensions qualifier) [2][1] - device, constant, shared, managed (memory-space/storage specifiers for variables) [1] Additionally, NVIDIA’s CUDA programming guide documents that CUDA source files use these extensions and that any source file containing CUDA extensions must be compiled with nvcc. [3]

Citations:


🏁 Script executed:

# Check test_codegen.py for any identifier validation tests
rg -n "is_valid_c_identifier\|C_IDENTIFIER" warp/tests/test_codegen.py

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Look at test_codegen.py to understand what's already tested
head -100 warp/tests/test_codegen.py

Repository: NVIDIA/warp

Length of output: 1991


🏁 Script executed:

# Check if there are any actual kernel name validations in tests
rg -B 3 -A 3 "name.*=.*`@wp`\.kernel\|`@wp`\.kernel.*name" warp/tests/ | head -30

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Let's check the context where the validator is used to understand the full picture
rg -B 10 -A 5 "is_valid_c_identifier" warp/_src/context.py

Repository: NVIDIA/warp

Length of output: 582


🏁 Script executed:

# Verify there are no existing keyword checks elsewhere in the kernel path
rg -n "keyword\|reserved.*keyword" warp/_src/context.py

Repository: NVIDIA/warp

Length of output: 37


🏁 Script executed:

# Check what happens when a keyword is used as kernel name currently
rg -n "ValueError.*valid.*C.*identifier" warp/_src/

Repository: NVIDIA/warp

Length of output: 192


Reject reserved C/C++ keywords in the identifier validator

Line 5138 currently accepts keyword tokens like for/class because it only checks identifier shape. Those are invalid function identifiers in generated C/CUDA code and will fail at compile time, defeating the purpose of early validation.

Proposed fix
 _C_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+_C_RESERVED_KEYWORDS = {
+    # include the full C/C++ keyword set used by Warp codegen targets
+    "auto", "break", "case", "char", "const", "continue", "default", "do",
+    "double", "else", "enum", "extern", "float", "for", "goto", "if", "int",
+    "long", "register", "return", "short", "signed", "sizeof", "static",
+    "struct", "switch", "typedef", "union", "unsigned", "void", "volatile",
+    "while", "class", "namespace", "template", "typename",
+}
 
 
 def is_valid_c_identifier(name: str) -> bool:
-    return _C_IDENTIFIER_RE.match(name) is not None
+    return _C_IDENTIFIER_RE.fullmatch(name) is not None and name not in _C_RESERVED_KEYWORDS
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@warp/_src/codegen.py` around lines 5135 - 5139, The is_valid_c_identifier
function currently only validates the syntactic shape of identifiers using regex
pattern matching, but fails to reject reserved C/C++ keywords like "for",
"class", "int", and "return" which are syntactically valid identifiers but
cannot be used as function identifiers in generated C/CUDA code. Add a check
within is_valid_c_identifier to verify that the name is not a C/C++ reserved
keyword by comparing it against a comprehensive set of reserved keywords, and
return False if the name matches any keyword before returning the regex match
result.



def codegen_struct(struct, device="cpu", indent_size=4):
name = struct.native_name

Expand Down
42 changes: 39 additions & 3 deletions warp/_src/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,13 @@ def __init__(self, func, key=None, module=None, options=None, code_transformers=
else:
self.module = module

# Keep track of the qualified name of the Python function used to create
# this kernel, so it can still be identified even when the kernel is given
# a custom name (key).
self.qualname = warp._src.codegen.make_full_qualified_name(func)

if key is None:
self.key = warp._src.codegen.make_full_qualified_name(func)
self.key = self.qualname
else:
self.key = key

Expand Down Expand Up @@ -1371,6 +1376,7 @@ def kernel2(
def kernel(
f: Callable | None = None,
*,
name: str | None = None,
enable_backward: bool | None = None,
launch_bounds: tuple[int, ...] | int | None = None,
module: Module | Literal["unique"] | str | None = None,
Expand Down Expand Up @@ -1419,6 +1425,9 @@ def my_kernel_fast(a: wp.array[float], b: wp.array[float]):

Args:
f: The function to be registered as a kernel.
name: Overrides the kernel's key. If ``None``, the function's
qualified name will be used as the key. The name must be a valid
C identifier; raises a ``ValueError`` otherwise.
enable_backward: If False, the backward pass will not be
generated.
launch_bounds: CUDA ``__launch_bounds__`` attribute for the
Expand Down Expand Up @@ -1448,6 +1457,13 @@ def my_kernel_fast(a: wp.array[float], b: wp.array[float]):
def wrapper(f, *args, **kwargs):
kernel_options = {}

if name is None:
key = warp._src.codegen.make_full_qualified_name(f)
else:
if not warp._src.codegen.is_valid_c_identifier(name):
raise ValueError(f"@wp.kernel for '{f.__name__}': name '{name}' is not a valid C identifier.")
key = name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please fix decorator-form @wp.overload with renamed generic kernels.

This branch registers a kernel only under the explicit name= key, but the documented overload-stub path still resolves the kernel from the stub's Python qualified name via Module._find_kernel(). That means this now fails:

from typing import Any

import warp as wp


@wp.kernel(name="custom_scale")
def scale(x: wp.array[Any], s: Any):
    i = wp.tid()
    x[i] = s * x[i]


@wp.overload
def scale(x: wp.array[wp.float32], s: wp.float32):
    ...

Observed on this branch:

RuntimeError: Failed to find a kernel named 'scale' in module __main__

Direct overload registration still works:

wp.overload(scale, [wp.array[wp.float32], wp.float32])

so the regression is specifically in the decorator-form lookup. I think the fix should preserve lookup by the original Python qualified name, or otherwise teach _find_kernel() / @wp.overload how to find kernels whose key was overridden by @wp.kernel(name=...).

Please also add a regression test covering @wp.kernel(name="...") plus decorator-form @wp.overload.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed, thanks for spotting this issue.

From looking at the code, there are a couple ways to address this. I took the liberty of implementing one of them.

For completeness, these are the options considered:

  1. Key self.kernels by kernel.qualname instead of kernel.key. This makes the _find_kernel() implementation pretty simple, but messes with the AOT and causes collisions deeper in the code. Not really a valid solution.
  2. Store the kernel.qualname next to kernel.key. If _find_kernel fails to find a kernel from the func's qualname, assume it was renamed. Then iterate over the kernels in the module, and compare the qualnames. This is the one implemented.
  3. Keep another dict self.kernels_by_qualname next to self.kernels in the Module class. _find_kernel() uses this other dict. Drawback is that we need to keep these two data structures in sync.

Open to iterating over this.


if enable_backward is not None:
kernel_options["enable_backward"] = enable_backward

Expand Down Expand Up @@ -1493,7 +1509,7 @@ def wrapper(f, *args, **kwargs):
# Create the kernel object and register it with the module
k = Kernel(
func=f,
key=warp._src.codegen.make_full_qualified_name(f),
key=key,
module=m,
options=kernel_options,
)
Expand Down Expand Up @@ -2997,7 +3013,27 @@ def _detach_references(self):
# find kernel corresponding to a Python function
def _find_kernel(self, func):
qualname = warp._src.codegen.make_full_qualified_name(func)
return self.kernels.get(qualname)

# A kernel's key matches the qualified name unless the user specifies a
# custom name for the kernel.
# First, check the fast path: kernels registered under their ``qualname``.
kernel = self.kernels.get(qualname)
if kernel is not None:
return kernel

# Otherwise, the kernel was renamed. Fall back to matching via the
# ``qualname`` attribute.
# In this case, several kernels may share the same qualified name (e.g.
# kernels generated in a closure). We take the most recently registered
# kernel as the intended target. This assumes, that the ``@wp.overload``
# is defined just after the overloaded kernel, ``self.kernels`` preserves
# insertion order and that the ``self.kernels`` keys aren't overwritten.
match = None
for kernel in self.kernels.values():
if kernel.qualname == qualname:
match = kernel

return match

# collect all referenced functions / structs
# given the AST of a function or kernel
Expand Down
8 changes: 8 additions & 0 deletions warp/tests/test_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,14 @@ def test_line_directive_preserves_normalized_path(self):

self.assertEqual(directive, '#line 1 "C:/warp/kernels/example.py"')

def test_is_valid_c_identifier(self):
for name in ("foo", "_foo", "Foo123", "_", "a1_b2"):
with self.subTest(name=name):
self.assertTrue(codegen.is_valid_c_identifier(name))
for name in ("", "1leading_digit", "has space", "has-dash", "a.b", "a+b", "naïve"):
with self.subTest(name=name):
self.assertFalse(codegen.is_valid_c_identifier(name))

def test_extract_function_source_slow_path_when_fast_returns_none(self):
"""When ``_try_extract_function_source`` returns ``None`` (e.g. for an
``exec``-defined function with no linecache entry), ``extract_function_source``
Expand Down
31 changes: 31 additions & 0 deletions warp/tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,37 @@ def test_context_type_str(self):
self.assertEqual(wp._src.context.type_str(tuple[int, float]), "tuple[int, float]")
self.assertEqual(wp._src.context.type_str(tuple[int, ...]), "tuple[int, ...]")

def test_kernel_name_override(self):
"""@wp.kernel(name=...) should override the kernel's key."""

@wp.kernel(name="custom_kernel_name")
def some_kernel(x: wp.array(dtype=float)):
i = wp.tid()
x[i] = x[i]

self.assertEqual(some_kernel.key, "custom_kernel_name")

def test_kernel_name_default(self):
"""Omitting name should fall back to the qualified name."""

@wp.kernel
def default_named_kernel(x: wp.array(dtype=float)):
i = wp.tid()
x[i] = x[i]

self.assertTrue(default_named_kernel.key.endswith("default_named_kernel"))

def test_kernel_name_invalid(self):
"""A name that is not a valid C identifier should raise ValueError."""
with self.assertRaises(ValueError) as cm:

@wp.kernel(name="has space")
def bad_kernel(x: wp.array(dtype=float)):
i = wp.tid()
x[i] = x[i]

self.assertIn("not a valid C identifier", str(cm.exception))


if __name__ == "__main__":
unittest.main(verbosity=2)
45 changes: 45 additions & 0 deletions warp/tests/test_generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,47 @@ def nested_overload_stub_kernel(x: float): ...
test.assertEqual(len(nested_overload_stub_kernel.overloads), 1)


def test_overload_with_renamed_kernel(test, device):
@wp.kernel(name="renamed_overload_kernel")
def scale(x: Any):
return

@wp.overload
def scale(x: float): ...

@wp.overload
def scale(x: int): ...

test.assertEqual(scale.key, "renamed_overload_kernel")
test.assertEqual(len(scale.overloads), 2)


def test_overload_renamed_kernel_closure(test, device):
# A closure that yields one renamed kernel per call produces several kernels
# sharing the same qualified name. Decorator-form @wp.overload is defined
# inline next to each kernel, so it must bind to that call's kernel.
def make(suffix):
@wp.kernel(name=f"closure_overload_kernel_{suffix}")
def scale(x: Any):
return

@wp.overload
def scale(x: float): ...

@wp.overload
def scale(x: int): ...

return scale

first = make("a")
second = make("b")

test.assertEqual(first.key, "closure_overload_kernel_a")
test.assertEqual(second.key, "closure_overload_kernel_b")
test.assertEqual(len(first.overloads), 2)
test.assertEqual(len(second.overloads), 2)


@wp.kernel
def overload_stub_return_annotation_kernel(x: Any):
return
Expand Down Expand Up @@ -622,6 +663,10 @@ class TestGenerics(unittest.TestCase):
add_function_test(TestGenerics, "test_generic_conditional_setter", test_generic_conditional_setter, devices=devices)
add_function_test(TestGenerics, "test_generic_transform_kernel", test_generic_transform_kernel, devices=devices)
add_function_test(TestGenerics, "test_nested_overload_stub", test_nested_overload_stub, devices=devices)
add_function_test(TestGenerics, "test_overload_with_renamed_kernel", test_overload_with_renamed_kernel, devices=devices)
add_function_test(
TestGenerics, "test_overload_renamed_kernel_closure", test_overload_renamed_kernel_closure, devices=devices
)
add_function_test(
TestGenerics, "test_overload_stub_return_annotation", test_overload_stub_return_annotation, devices=devices
)
Expand Down