Skip to content

Add support for indexed arrays in structs and related kernels#1328

Open
FabienPean-Virtonomy wants to merge 1 commit intoNVIDIA:mainfrom
Virtonomy:FabienPean/indexedarray_in_struct
Open

Add support for indexed arrays in structs and related kernels#1328
FabienPean-Virtonomy wants to merge 1 commit intoNVIDIA:mainfrom
Virtonomy:FabienPean/indexedarray_in_struct

Conversation

@FabienPean-Virtonomy
Copy link
Copy Markdown

@FabienPean-Virtonomy FabienPean-Virtonomy commented Mar 30, 2026

Description

This PR adds support for indexedarray within struct.

Closes #1327

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

Test plan

Tests were added in warp/tests/test_indexedarray.py

New feature / enhancement

This now works

import warp as wp

@wp.struct
class Foo:
    arr: wp.indexedarray(dtype=wp.float32)

Summary by CodeRabbit

  • Bug Fixes

    • Enabled proper support for indexedarray fields inside structs: assignment, device transfers (including cross-device behavior), and NumPy structured-value handling; works for nested structs and arrays-of-structs.
  • Tests

    • Added tests covering indexedarrays in structs, nested structs, struct arrays, NumPy conversion, and device-transfer scenarios.
  • Documentation

    • Changelog updated to note indexedarray support across struct operations.

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot bot commented Mar 30, 2026

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Adds support for using wp.indexedarray as a field type in wp.struct: struct construction, field setters, device transfers, NumPy structured dtype/value serialization, and native atomic overloads; tests added for kernel access, nested structs, struct arrays, NumPy conversion, and device transfer.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added an [Unreleased] entry documenting support for wp.indexedarray fields in structs.
Struct marshalling & codegen
warp/_src/codegen.py
Recognize indexedarray field types in constructors/setters; added set_indexedarray_value() setter (accepts None or validated indexedarray), include indexedarray_t in ctypes _fields_, preserve indexedarray across StructInstance.to(device), and serialize indexedarray in Struct.numpy_dtype(), StructInstance.numpy_value(), and Struct.from_ptr().
Type system & serialization
warp/_src/types.py
Added indexedarray_t._numpy_dtype_, indexedarray_t.numpy_dtype() classmethod, and indexedarray_t.numpy_value() instance method to produce NumPy structured-dtype metadata and values (data, fixed-length indices as u64, shape list).
Native kernel support
warp/native/array.h
Added a CUDA-callable atomic_add overload for indexedarray_t<T>* / indexedarray_t<T> to support atomic-add resolution when indexed arrays are embedded in structs (no-op stub).
Tests
warp/tests/test_indexedarray.py
Added IndexedArrayStruct and NestedIndexedArrayStruct; kernels accessing indexedarray fields (direct, nested, array-of-structs) with updates and atomics; tests for kernel behavior, nested/array-of-structs, NumPy dtype/value, and CUDA .to(device) transfer.

Sequence Diagram

sequenceDiagram
    participant User
    participant StructDef as Struct Definition
    participant Setter as Field Setter / Constructor
    participant Marshalling as Marshalling Layer
    participant Device as Device / NumPy Backend
    participant Native as Native Kernel Layer

    User->>StructDef: define `@wp.struct` with `indexedarray` field
    StructDef->>Setter: call _make_struct_field_constructor / _make_struct_field_setter
    Setter->>Setter: recognize `indexedarray`, create constructor/setter stubs

    User->>Marshalling: instance.to(device)
    Marshalling->>Device: call member.value.to(device)
    Device-->>Marshalling: return device-bound array/array-like
    Marshalling->>Marshalling: wrap/ensure `indexedarray(...)` if needed

    User->>Marshalling: instance.numpy_value() / Struct.numpy_dtype()
    Marshalling->>Device: call indexedarray.numpy_value() / indexedarray_t.numpy_dtype()
    Device-->>Marshalling: return NumPy-compatible structured value/dtype
    Marshalling-->>User: structured numpy representation

    User->>Native: launch kernel accessing indexedarray in struct
    Native->>Native: atomic_add overload resolves for indexedarray
    Native-->>User: kernel executes and updates indexedarray
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the primary change: adding support for indexed arrays in structs.
Linked Issues check ✅ Passed The PR fully addresses all objectives from #1327: struct field support, assignment, device transfer, kernel usage, and NumPy interoperability are all implemented.
Out of Scope Changes check ✅ Passed All changes are directly related to supporting indexedarray fields in structs; no unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from a9690f0 to 06e268d Compare March 30, 2026 12:19
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR extends Warp's @wp.struct support to accept wp.indexedarray fields, closing issue GH-1327. All the plumbing required for struct integration is added symmetrically with the existing array field support: ctype field registration, Python field setter (with dtype validation), field constructor (returning None from the raw ctype), device-transfer via StructInstance.to(), and NumPy structured-array interop via numpy_dtype() / numpy_value(). A no-op atomic_add stub is also added in array.h so that the backward pass can atomically accumulate a struct that contains an indexedarray field.

Key observations:

  • The device-transfer implementation in StructInstance.to() correctly distinguishes the same-device case (value.to(device) returns self, an indexedarray) from the cross-device case (returns a plain warp.array) via isinstance(cloned, indexedarray), avoiding the identity-comparison pitfall noted in earlier review threads.
  • Cross-device transfer silently drops the index arrays (consistent with how warp.clone behaves on any indexedarray), leaving the field as a contiguous indexedarray with all-None indices on the target device. The test asserts this behaviour but a user-visible note in the docs/changelog would set accurate expectations.
  • set_indexedarray_value validates dtype but not ndim, which is consistent with set_array_value; a mismatched-dimension assignment would only surface later at kernel launch.
  • The same-device path through StructInstance.to() (where the isinstance branch is taken) has no dedicated test case.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/documentation suggestions with no blocking correctness issues.

The implementation correctly handles all struct code paths for indexedarray fields, is symmetric with the existing array field support, and includes solid test coverage across the main scenarios. Previous review-thread concerns (identity vs equality device check, unreachable else branch) are either resolved or were not actual bugs in the submitted code. The three open findings are all P2: a missing same-device test, a missing ndim guard in the setter, and undocumented indices-lost-on-cross-device-transfer semantics — none of these block merge.

warp/_src/codegen.py (setter ndim validation, device-transfer semantics) and warp/tests/test_indexedarray.py (same-device transfer coverage).

Important Files Changed

Filename Overview
warp/_src/codegen.py Adds indexedarray handling across all struct code paths: ctype field registration, field setter, constructor, to() device transfer, numpy_dtype(), numpy_value(), and from_ptr(). The device-transfer implementation correctly uses isinstance(cloned, indexedarray) to distinguish same-device (returns self) vs cross-device (returns a plain array) cases. Minor: set_indexedarray_value does not validate ndim; cross-device transfer semantics (indices are lost) could benefit from a comment.
warp/_src/types.py Adds numpy_dtype() classmethod and numpy_value() instance method to indexedarray_t, and defines _numpy_dtype_ using correct field offsets. Format strings ({ARRAY_MAX_DIMS}u8 for void-pointer indices, {ARRAY_MAX_DIMS}i4 for shape) match the ctypes layout. Consistent with the existing array_t pattern.
warp/native/array.h Adds a no-op atomic_add(indexedarray_t<T>*, indexedarray_t<T>) stub, symmetric with the existing array_t stub, needed for the adjoint backward pass when a struct containing an indexedarray field is atomically accumulated.
warp/tests/test_indexedarray.py Adds five new test functions covering: basic struct assignment and kernel launch, nested structs, struct arrays, numpy dtype/value round-trip, and cross-device (CPU→CUDA) transfer. The same-device transfer path (which takes the isinstance(cloned, indexedarray) True branch) is not directly tested; the CUDA transfer test correctly asserts that indices become None after cross-device clone.
CHANGELOG.md Changelog entry added for indexedarray struct support, correctly linked to issue GH-1327.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["StructInstance.to(device)"] --> B{"field type?"}
    B -- "array" --> C["value.to(device) → warp.array\nsetattr(dst, name, result)"]
    B -- "indexedarray" --> D["cloned = value.to(device)"]
    D --> E{"isinstance cloned indexedarray?"}
    E -- "True: same device, .to returned self" --> F["setattr dst name cloned\noriginal indexedarray preserved"]
    E -- "False: cross-device, clone returned warp.array" --> G["indexedarray wrap cloned\nindices become None"]
    G --> H["setattr dst name wrapped"]
    B -- "Struct" --> I["Recurse into nested struct fields"]
    B -- "other" --> J["setattr dst name value"]
    subgraph "set_indexedarray_value setter"
        K{"value is None?"}
        K -- Yes --> L["null indexedarray_t via var_type.__ctype__()"]
        K -- No --> M["validate dtype then set __ctype__()"]
    end
Loading

Reviews (4): Last reviewed commit: "Add support for indexed arrays in struct..." | Re-trigger Greptile

Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
warp/tests/test_indexedarray.py (1)

92-158: Add a regression test for nullable struct fields + .to(device)

The linked objective includes assigning None to indexedarray struct fields and moving structs across devices. Adding one focused test for s.iarr = None followed by s.to(device) would lock this behavior and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/tests/test_indexedarray.py` around lines 92 - 158, Add a regression test
that assigns None to an IndexedArrayStruct field and then calls .to(device) to
ensure nullable struct fields survive device moves: create an instance of
IndexedArrayStruct, set s.iarr = None, call s.to(device) (or
struct_arr.to(device) if testing arrays), and assert the field remains None (or
numpy_dtype/ numpy_value reflect null) and no exceptions are raised; reference
IndexedArrayStruct, s.iarr, and the .to(device) call in the new test (e.g.,
test_indexedarray_nullable_field_to_device).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 80: Update the changelog entry for the fix "Fix `@wp.struct` packing to
support `wp.indexedarray` fields (assignment, device transfer, and NumPy
structured values)" to include the issue reference that this PR closes (GH-1327)
by appending the issue link reference format (e.g.
"([GH-1327](https://github.com/<org>/<repo>/issues/1327))") to that line so it
complies with the changelog guideline; ensure the exact entry text remains
unchanged except for adding the issue reference.

In `@warp/_src/codegen.py`:
- Around line 333-335: StructInstance.to() currently calls value.to(device) for
fields detected by matches_array_class(var.type, indexedarray) without checking
for None; change that branch to guard nullable indexedarray fields by checking
if value is not None before calling value.to(device) and otherwise set the
destination attribute to None (i.e., replace the unconditional setattr(dst,
name, value.to(device)) with a conditional that calls value.to(device) only when
value is not None and sets dst.name to None when it is).

In `@warp/_src/types.py`:
- Around line 2207-2218: The numpy_value method currently forwards
self.data.numpy_value(), but when self.data is None the fallback ends up
creating a 1-D array which loses the original ndim; update numpy_value so that
if self.data is None you construct a placeholder numpy/ctype array with ndim
equal to len(self.shape) (or the stored ndim if available) and the same shape
(use self.shape to derive dimensions) before calling its numpy_value(), so
serialized tuples preserve the original ndim for wp.indexedarray fields; keep
the existing indices handling and return signature (self.data.numpy_value(),
indices, list(self.shape)).

In `@warp/tests/test_indexedarray.py`:
- Line 104: After each wp.launch call in the test functions (e.g., the
wp.launch(kernel_indexedarray_in_struct, dim=iarr.size, inputs=[s,
expected_arr], device=device) invocation), add an explicit
wp.synchronize_device(device) immediately after the launch so kernel-side
assertions (wp.expect_eq) are flushed and failures surface deterministically;
apply the same change for the other wp.launch calls referenced (the launches at
the other test locations around lines 122 and 138) to ensure all launches are
followed by wp.synchronize_device(device).

---

Nitpick comments:
In `@warp/tests/test_indexedarray.py`:
- Around line 92-158: Add a regression test that assigns None to an
IndexedArrayStruct field and then calls .to(device) to ensure nullable struct
fields survive device moves: create an instance of IndexedArrayStruct, set
s.iarr = None, call s.to(device) (or struct_arr.to(device) if testing arrays),
and assert the field remains None (or numpy_dtype/ numpy_value reflect null) and
no exceptions are raised; reference IndexedArrayStruct, s.iarr, and the
.to(device) call in the new test (e.g.,
test_indexedarray_nullable_field_to_device).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b9dd14c8-7838-40c4-8118-767015ef7d55

📥 Commits

Reviewing files that changed from the base of the PR and between 63e1840 and 06e268d.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py

Comment on lines +333 to +335
elif matches_array_class(var.type, indexedarray):
# indexedarray_t
setattr(dst, name, value.to(device))
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 | 🔴 Critical

Guard nullable indexedarray fields before device transfer

StructInstance.to() currently calls value.to(device) unconditionally for indexedarray fields. If the field is set to None (which your setter supports), this raises at runtime.

💡 Proposed fix
-            if matches_array_class(var.type, array):
-                # array_t
-                setattr(dst, name, value.to(device))
-            elif matches_array_class(var.type, indexedarray):
-                # indexedarray_t
-                setattr(dst, name, value.to(device))
+            if matches_array_class(var.type, array) or matches_array_class(var.type, indexedarray):
+                # array_t / indexedarray_t
+                setattr(dst, name, None if value is None else value.to(device))
🧰 Tools
🪛 Ruff (0.15.7)

[error] 333-333: matches_array_class may be undefined, or defined from star imports

(F405)


[error] 333-333: indexedarray may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 333 - 335, StructInstance.to() currently
calls value.to(device) for fields detected by matches_array_class(var.type,
indexedarray) without checking for None; change that branch to guard nullable
indexedarray fields by checking if value is not None before calling
value.to(device) and otherwise set the destination attribute to None (i.e.,
replace the unconditional setattr(dst, name, value.to(device)) with a
conditional that calls value.to(device) only when value is not None and sets
dst.name to None when it is).

Comment on lines +2207 to +2218
def numpy_value(self):
# pointers are represented as unsigned 64-bit integers
indices = []
for i in range(ARRAY_MAX_DIMS):
v = self.indices[i]
# v may be a ctypes.c_void_p instance
if isinstance(v, ctypes.c_void_p):
indices.append(0 if v.value is None else int(v.value))
else:
indices.append(0 if v is None else int(v))

return (self.data.numpy_value(), indices, list(self.shape))
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

Preserve ndim when serializing None indexedarrays.

numpy_value() forwards self.data.numpy_value(), but the data is None branch still seeds self.data via array().__ctype__(), so every null indexedarray is serialized with data.ndim == 1. That breaks wp.indexedarray(..., ndim>1) struct fields when they are set to None and marshalled through the new structured NumPy path.

💡 Proposed fix
 class indexedarray_t(ctypes.Structure):
     def __init__(self, data, indices, shape):
         if data is None:
-            self.data = array().__ctype__()
+            ndim = len(shape)
+            self.data = array_t(data=0, grad=0, ndim=ndim, shape=(0,) * ndim, strides=(0,) * ndim)
             for i in range(ARRAY_MAX_DIMS):
                 self.indices[i] = ctypes.c_void_p(None)
                 self.shape[i] = 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/types.py` around lines 2207 - 2218, The numpy_value method
currently forwards self.data.numpy_value(), but when self.data is None the
fallback ends up creating a 1-D array which loses the original ndim; update
numpy_value so that if self.data is None you construct a placeholder numpy/ctype
array with ndim equal to len(self.shape) (or the stored ndim if available) and
the same shape (use self.shape to derive dimensions) before calling its
numpy_value(), so serialized tuples preserve the original ndim for
wp.indexedarray fields; keep the existing indices handling and return signature
(self.data.numpy_value(), indices, list(self.shape)).

@shi-eric shi-eric requested a review from nvlukasz March 30, 2026 15:21
Copy link
Copy Markdown
Contributor

@nvlukasz nvlukasz left a comment

Choose a reason for hiding this comment

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

Thanks, this looks pretty good. Just a few minor comments worth addressing.

setattr(dst, name, value.to(device))
elif matches_array_class(var.type, indexedarray):
# indexedarray_t
setattr(dst, name, value.to(device))
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.

Note that indexedarray.to() will create a regular array if it needs to be copied to a different device.

We could consider doing a full clone of data + indices to create an indexedarray, but I think that's beyond the scope of this PR. So in the meantime, we could raise an exception if the device does not match.

Copy link
Copy Markdown
Author

@FabienPean-Virtonomy FabienPean-Virtonomy Mar 31, 2026

Choose a reason for hiding this comment

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

Ah indeed, and the test only test same device copy. I find the shrinking to an array a good approach in general, I would prefer to leave it up to the user to consider where they store the backing array if they care about reuse or optimization.

An easy way around would be to use the passthrough property of indexedarray_t when indices=None as it would behave like an array_t if I read right.
So it means modifying either at codegen point above, or adding method to on python side indexedarray, or modifying noncontiguous base to method. Would any of the three be ok with you, are there issues I did not foresee?

Copy link
Copy Markdown
Author

@FabienPean-Virtonomy FabienPean-Virtonomy Mar 31, 2026

Choose a reason for hiding this comment

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

The different possibilities:

Option 1: at codegen, coerce to indexedarray only in struct packing

setattr(dst, name, value.to(device) if value.device is device else wp.indexedarray(value.to(device))

Option 2: in indexedarray, always return an indexedarray and discards return array path, which means a pointer check for index access compared to array path

    def to(self, device):
        device = warp.get_device(device)
        if self.device == device:
            return self

        dense = warp.clone(self, device=device)
        return indexedarray(data=dense, indices=None)

Option 3: in noncontiguousbase, not applicable due to other child class indexedfabricarray

# types.py L4437
return type(self)(warp.clone(self, device=device))

Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
warp/_src/codegen.py (1)

333-344: ⚠️ Potential issue | 🔴 Critical

Guard nullable indexedarray fields before dereferencing value.device.

Line 338 assumes every field holds a concrete object, but Lines 442-453 explicitly allow None. s.iarr = None; s.to(device) will currently fail with AttributeError instead of preserving the nullable field.

💡 Suggested fix
             elif matches_array_class(var.type, indexedarray):
                 # indexedarray_t
+                if value is None:
+                    setattr(dst, name, None)
+                    continue
                 # only support same-device no-op transfers for now
                 # cross-device transfers `iarr.to` returns an array_t
                 target_device = warp.get_device(device)
                 if value.device != target_device:
                     raise RuntimeError(

Also applies to: 442-453

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 333 - 344, The nullable indexedarray field
case must be guarded before dereferencing value.device: in the block that
handles matches_array_class(var.type, indexedarray) check if value is None (or
uses a safe attribute access) and if so set dst's field to None (preserving the
nullable) instead of accessing value.device; otherwise proceed to get
target_device, compare devices and call value.to(target_device) and setattr(dst,
name, ...) as before. Ensure the same null-check pattern is applied to the
duplicate block around lines 442-453 that also handles indexedarray fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@warp/_src/codegen.py`:
- Around line 333-344: The nullable indexedarray field case must be guarded
before dereferencing value.device: in the block that handles
matches_array_class(var.type, indexedarray) check if value is None (or uses a
safe attribute access) and if so set dst's field to None (preserving the
nullable) instead of accessing value.device; otherwise proceed to get
target_device, compare devices and call value.to(target_device) and setattr(dst,
name, ...) as before. Ensure the same null-check pattern is applied to the
duplicate block around lines 442-453 that also handles indexedarray fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 57fc8d8b-a7b8-4d73-97cb-38576bf9bf32

📥 Commits

Reviewing files that changed from the base of the PR and between 06e268d and f26c05b.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from f26c05b to 5cf0b56 Compare April 1, 2026 09:18
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
warp/_src/types.py (1)

2206-2218: ⚠️ Potential issue | 🟠 Major

Preserve ndim when serializing null indexed arrays.

numpy_value() still forwards self.data.numpy_value(), but the null-data path in indexedarray_t.__init__() seeds self.data with array().__ctype__(), which hard-codes ndim == 1. A wp.indexedarray(..., ndim>1) struct field set to None will therefore round-trip through the new structured NumPy path with the wrong dimensionality.

💡 Proposed fix
 class indexedarray_t(ctypes.Structure):
     def __init__(self, data, indices, shape):
         if data is None:
-            self.data = array().__ctype__()
+            ndim = len(shape)
+            self.data = array_t(data=0, grad=0, ndim=ndim, shape=(0,) * ndim, strides=(0,) * ndim)
             for i in range(ARRAY_MAX_DIMS):
                 self.indices[i] = ctypes.c_void_p(None)
                 self.shape[i] = 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/types.py` around lines 2206 - 2218, The numpy_value() serializer
for indexedarray_t should preserve the original ndim when self.data was seeded
with a default placeholder (array().__ctype__()) in indexedarray_t.__init__;
currently it forwards self.data.numpy_value() which hard-codes ndim==1 for
null-data cases. Update numpy_value() to detect the placeholder/null-data case
(the default array ctype created in indexedarray_t.__init__) and synthesize a
numpy_value for an empty array that uses self.ndim (or otherwise set the correct
ndim) instead of blindly using self.data.numpy_value(); keep the rest of the
returned tuple (indices, shape) intact so a None-valued indexedarray(...,
ndim>1) round-trips with the correct dimensionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@warp/_src/types.py`:
- Around line 2206-2218: The numpy_value() serializer for indexedarray_t should
preserve the original ndim when self.data was seeded with a default placeholder
(array().__ctype__()) in indexedarray_t.__init__; currently it forwards
self.data.numpy_value() which hard-codes ndim==1 for null-data cases. Update
numpy_value() to detect the placeholder/null-data case (the default array ctype
created in indexedarray_t.__init__) and synthesize a numpy_value for an empty
array that uses self.ndim (or otherwise set the correct ndim) instead of blindly
using self.data.numpy_value(); keep the rest of the returned tuple (indices,
shape) intact so a None-valued indexedarray(..., ndim>1) round-trips with the
correct dimensionality.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 227bd925-f201-4056-943e-f9a3667833a1

📥 Commits

Reviewing files that changed from the base of the PR and between f26c05b and 5cf0b56.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • warp/native/array.h
👮 Files not reviewed due to content moderation or server errors (2)
  • warp/_src/codegen.py
  • warp/tests/test_indexedarray.py

Signed-off-by: Fabien Péan <pean@virtonomy.io>
@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from 5cf0b56 to 02d0e90 Compare April 1, 2026 10:10
Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
warp/_src/codegen.py (1)

320-324: ⚠️ Potential issue | 🔴 Critical

Handle None before moving indexedarray fields.

Line 323 still calls value.to(device) unconditionally. The new constructor/setter path allows indexedarray fields to be None, so StructInstance.to() now raises for a valid struct state.

🐛 Proposed fix
             elif matches_array_class(var.type, indexedarray):
                 # indexedarray_t
-                # `.to` returns an array if on different device, force to identity indexedarray
-                cloned = value.to(device)
-                setattr(dst, name, cloned if isinstance(cloned, indexedarray) else indexedarray(cloned))
+                if value is None:
+                    setattr(dst, name, None)
+                else:
+                    # `.to()` returns an array if on different device, force to identity indexedarray
+                    cloned = value.to(device)
+                    setattr(dst, name, cloned if isinstance(cloned, indexedarray) else indexedarray(cloned))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 320 - 324, The code calls value.to(device)
unconditionally for fields matched by matches_array_class(var.type,
indexedarray), but fields may be None; update the block in codegen.py (the
branch handling indexedarray) to first check if value is None and if so set
setattr(dst, name, None) and skip the .to() call, otherwise perform cloned =
value.to(device) and set setattr(dst, name, cloned if isinstance(cloned,
indexedarray) else indexedarray(cloned)); this prevents StructInstance.to() from
raising when indexedarray fields are None.
🧹 Nitpick comments (1)
warp/tests/test_indexedarray.py (1)

154-160: Exercise the structured NumPy round-trip, not just the wrapper shape.

This still passes if numpy_dtype() returns incompatible formats/offsets or if numpy_value() serializes the wrong payload. Materializing a structured NumPy array from both values will catch the interop breakage this test is meant to cover.

🧪 Proposed enhancement
     # Just ensure these are functional for structs embedding indexedarray_t
     dtype = IndexedArrayStruct.numpy_dtype()
     value = s.numpy_value()
+    structured = np.array([value], dtype=np.dtype(dtype))
 
     test.assertIsInstance(dtype, dict)
     test.assertEqual(dtype["names"], ["iarr"])
     test.assertEqual(len(value), 1)
+    test.assertEqual(structured.dtype.names, ("iarr",))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/tests/test_indexedarray.py` around lines 154 - 160, The test currently
only checks dtype names and wrapper shape; instead materialize a structured
NumPy array using the dtype returned by IndexedArrayStruct.numpy_dtype() and the
value returned by s.numpy_value() to verify they interoperate correctly: call
numpy.array(value, dtype=dtype) (or create a zero-length structured array and
set the field) and then assert the resulting array has the expected shape and
contents (e.g., len == 1 and the field "iarr" yields the same data as
s.numpy_value()); update assertions to validate that the constructed structured
array round-trips rather than only checking dtype["names"] and len(value).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/codegen.py`:
- Around line 422-433: set_indexedarray_value is missing the grad-buffer
keepalive that set_array_value uses; mirror the same logic from set_array_value:
after writing the ctype payload (the setattr(inst._ctype, field, ...)) store or
clear the strong reference to the source grad buffer in the instance's
grad-keepalive storage (the same attribute set_array_value uses, e.g.
inst._grad_keepalive[field] = value._grad_keepalive or similar) when value is
not None, and remove/clear that entry when value is None, then leave the
cls.__setattr__(inst, field, value) call as-is.

---

Duplicate comments:
In `@warp/_src/codegen.py`:
- Around line 320-324: The code calls value.to(device) unconditionally for
fields matched by matches_array_class(var.type, indexedarray), but fields may be
None; update the block in codegen.py (the branch handling indexedarray) to first
check if value is None and if so set setattr(dst, name, None) and skip the .to()
call, otherwise perform cloned = value.to(device) and set setattr(dst, name,
cloned if isinstance(cloned, indexedarray) else indexedarray(cloned)); this
prevents StructInstance.to() from raising when indexedarray fields are None.

---

Nitpick comments:
In `@warp/tests/test_indexedarray.py`:
- Around line 154-160: The test currently only checks dtype names and wrapper
shape; instead materialize a structured NumPy array using the dtype returned by
IndexedArrayStruct.numpy_dtype() and the value returned by s.numpy_value() to
verify they interoperate correctly: call numpy.array(value, dtype=dtype) (or
create a zero-length structured array and set the field) and then assert the
resulting array has the expected shape and contents (e.g., len == 1 and the
field "iarr" yields the same data as s.numpy_value()); update assertions to
validate that the constructed structured array round-trips rather than only
checking dtype["names"] and len(value).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 2cbe8274-d019-4e67-9fe0-e61a048ab4d4

📥 Commits

Reviewing files that changed from the base of the PR and between 5cf0b56 and 02d0e90.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • warp/native/array.h
  • warp/_src/types.py

Comment on lines +422 to +433
def set_indexedarray_value(inst, value):
if value is None:
# create indexedarray with null pointers
setattr(inst._ctype, field, var_type.__ctype__())
else:
assert isinstance(value, indexedarray)
assert types_equal(value.dtype, var_type.dtype), (
f"assign to struct member variable {field} failed, expected type {type_repr(var_type.dtype)}, got type {type_repr(value.dtype)}"
)
setattr(inst._ctype, field, value.__ctype__())

cls.__setattr__(inst, field, value)
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

Mirror the array-field grad keepalive here too.

set_array_value() already keeps a strong reference to the grad buffer after writing the ctype payload, but set_indexedarray_value() does not. That means an indexedarray struct field can lose its grad-buffer keepalive even though the stored indexedarray_t still carries the pointer.

🧰 Tools
🪛 Ruff (0.15.7)

[error] 427-427: indexedarray may be undefined, or defined from star imports

(F405)


[error] 428-428: types_equal may be undefined, or defined from star imports

(F405)


[error] 429-429: type_repr may be undefined, or defined from star imports

(F405)


[error] 429-429: type_repr may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 422 - 433, set_indexedarray_value is
missing the grad-buffer keepalive that set_array_value uses; mirror the same
logic from set_array_value: after writing the ctype payload (the
setattr(inst._ctype, field, ...)) store or clear the strong reference to the
source grad buffer in the instance's grad-keepalive storage (the same attribute
set_array_value uses, e.g. inst._grad_keepalive[field] = value._grad_keepalive
or similar) when value is not None, and remove/clear that entry when value is
None, then leave the cls.__setattr__(inst, field, value) call as-is.

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.

Support indexedarray fields in struct

3 participants