Skip to content

[Type] ndarray typing 3: Add cook_dtype calls at C++ boundaries#413

Draft
hughperkins wants to merge 6 commits intohp/typing-t4-2-ndarray-subscriptfrom
hp/typing-t4-3-cook-dtype
Draft

[Type] ndarray typing 3: Add cook_dtype calls at C++ boundaries#413
hughperkins wants to merge 6 commits intohp/typing-t4-2-ndarray-subscriptfrom
hp/typing-t4-3-cook-dtype

Conversation

@hughperkins
Copy link
Collaborator

…refactor)

Add cook_dtype() calls at all points where dtype values are passed to C++ code. Make PyQuadrants.default_fp/ip/up into properties that always store DataTypeCxx. Rename shadowed dtype var in create_field_member. All changes are behavioral no-ops with current code, preparing for a future refactor of primitive dtypes into Python classes.

Issue: #

Brief Summary

copilot:summary

Walkthrough

copilot:walkthrough

@hughperkins hughperkins force-pushed the hp/typing-t4-3-cook-dtype branch from 89f34d8 to 72a9636 Compare March 12, 2026 03:49
@hughperkins hughperkins force-pushed the hp/typing-t4-2-ndarray-subscript branch from 219619c to 5762be5 Compare March 12, 2026 03:49
@hughperkins
Copy link
Collaborator Author

Opus 4.6 review:

PR Review: hp/typing-t4-3-cook-dtype

Branch: hp/typing-t4-3-cook-dtype (parent: hp/typing-t4-2-ndarray-subscript)

Summary

This PR adds cook_dtype() calls at C++ boundaries (expr, impl, matrix, sparse_matrix, sparse_solver) and refactors default_fp/default_ip/default_up in PyQuadrants into properties with backing _default_* attributes. It is a preparatory refactor for a future PR that will convert dtypes to Python classes; with current DataTypeCxx instances, cook_dtype() is effectively a no-op (identity for DataTypeCxx subclasses).

Issues Found

1. Bug fix (positive finding): create_field_member variable shadowing

The rename from dtype to checkbit_dtype in the adjoint checkbit block fixes a real bug. In the original code:

if prog.config().debug:
    dtype = u8
    if prog.config().arch == _qd_core.vulkan:
        dtype = i32
    x_grad_checkbit.ptr = _qd_core.expr_field(..., cook_dtype(dtype))
# ...
x_dual.ptr = _qd_core.expr_field(x_dual.ptr, dtype)  # BUG: dtype was overwritten to u8/i32!

The inner block overwrote the dtype parameter, so x_dual was incorrectly created with u8 or i32 instead of the field’s actual dtype when prog.config().debug was True. The rename correctly isolates the checkbit dtype.

2. Minor: Redundant cook_dtype in make_constant_expr

In make_constant_expr, when dtype is not None, dtype is already cooked at the top. For float/int branches, constant_dtype is then cooked again. Since cook_dtype is idempotent for DataTypeCxx, this is harmless but redundant. Consider removing the second cook_dtype(constant_dtype) for the dtype is not None path, or leave as-is for consistency and future-proofing.

Correctness

  • No-op behavior: cook_dtype() returns dtype unchanged when type(dtype) is a subclass of _qd_core.DataTypeCxx. Current primitives (f32, i32, u1, etc.) are such instances, so behavior is unchanged.
  • Property refactor: The default_fp/default_ip/default_up properties correctly preserve read/write behavior. The setters apply cook_dtype(value), so future Python dtype classes can be normalized when assigned.
  • set_default_ip: Uses self.default_ip = ip and self.default_up = u32 if ip == i32 else u64, which works with current DataTypeCxx values. A future Python dtype class would need == to compare correctly with i32/i64 for this logic to remain correct.

Edge Cases

  • cook_dtype(None): Would raise ValueError. Call sites guard with if dtype is not None where appropriate (e.g. make_constant_expr).
  • qd.default_fp = float: cook_dtype(float) returns impl.get_runtime().default_fp, i.e. the current value. Assigning float is effectively a no-op and may be surprising; acceptable for a preparatory refactor.
  • SharedArray / expr_init_shared_array: element_type is now cooked before passing to C++. Correct and consistent with other boundaries.

Style

  • Consistent use of cook_dtype() at C++ boundaries.
  • Clear variable naming (checkbit_dtype, dtype_cxx in sparse modules).
  • Imports and formatting are consistent with the rest of the codebase.

Suggestions for Improvement

  1. Document the bug fix: Add a brief comment near the checkbit_dtype rename noting that it fixes the previous shadowing of dtype, which caused x_dual to get the wrong dtype in debug mode.
  2. Optional: Add a test that exercises create_field_member with debug=True and needs_dual=True to lock in the fix and guard against regressions.
  3. Future PR: When introducing Python dtype classes, ensure set_default_ip’s ip == i32 comparison works for both DataTypeCxx and the new Python dtype wrappers (e.g. via cook_dtype(ip) == cook_dtype(i32) or an explicit equality method).

Verdict

The PR is correct and behaves as a no-op for current dtypes. It also fixes a real bug in create_field_member when debug mode is enabled. Tests pass. Ready to merge with optional minor suggestions above.

@hughperkins hughperkins force-pushed the hp/typing-t4-3-cook-dtype branch from 72a9636 to 9b5e1a8 Compare March 12, 2026 04:33
@hughperkins hughperkins force-pushed the hp/typing-t4-2-ndarray-subscript branch from 5762be5 to a402c99 Compare March 12, 2026 04:33
@hughperkins hughperkins force-pushed the hp/typing-t4-3-cook-dtype branch from 9b5e1a8 to 1adb863 Compare March 12, 2026 04:34
…refactor)

Add cook_dtype() calls at all points where dtype values are passed to C++ code.
Make PyQuadrants.default_fp/ip/up into properties that always store DataTypeCxx.
Rename shadowed dtype var in create_field_member. All changes are behavioral
no-ops with current code, preparing for a future refactor of primitive dtypes
into Python classes.
@hughperkins hughperkins force-pushed the hp/typing-t4-3-cook-dtype branch from 1adb863 to 71dcd62 Compare March 12, 2026 04:37
The previous code overwrote the outer `dtype` parameter in the debug
checkbit block, causing x_dual to be created with the wrong dtype.
Verify that forward-mode AD produces correct results when debug=True,
guarding against the previous bug where the checkbit block's local
dtype variable shadowed the outer dtype parameter.
The regression test covers this; no need for a code comment.
When dtype is provided it is already cooked at the top of the function,
so the per-branch cook_dtype(constant_dtype) was a no-op. Now only the
fallback default_fp/default_ip paths are cooked.
@hughperkins hughperkins marked this pull request as ready for review March 12, 2026 04:55
@hughperkins hughperkins marked this pull request as draft March 12, 2026 05:00
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.

1 participant