[Type] ndarray typing 3: Add cook_dtype calls at C++ boundaries#413
[Type] ndarray typing 3: Add cook_dtype calls at C++ boundaries#413hughperkins wants to merge 30 commits intomainfrom
Conversation
…annotations Support `from __future__ import annotations` by resolving stringified type annotations at signature inspection time. Catch NameError from invalid string annotations and raise a clear QuadrantsSyntaxError.
89f34d8 to
72a9636
Compare
219619c to
5762be5
Compare
|
Opus 4.6 review: PR Review: hp/typing-t4-3-cook-dtypeBranch: SummaryThis PR adds Issues Found1. Bug fix (positive finding):
|
The register decorator's inspect.signature() call was missing eval_str=True, inconsistent with all other call sites.
Wrap all inspect.signature(eval_str=True) calls with try/except that catches both NameError and AttributeError, re-raising as QuadrantsSyntaxError for clear user-facing errors. Previously only check_parameter_annotations had error handling and it only caught NameError.
72a9636 to
9b5e1a8
Compare
5762be5 to
a402c99
Compare
9b5e1a8 to
1adb863
Compare
Centralize the inspect.signature(eval_str=True) + error handling pattern into a shared get_func_signature() helper in exception.py, replacing 5 inline try/except blocks across _func_base.py, _kernel_impl_dataclass.py, and _perf_dispatch.py.
Verify that kernel parameter annotations are correctly resolved when the module uses PEP 563 stringified annotations.
NdarrayType.__class_getitem__ crashed when called with a single arg (e.g. NdarrayType[dtype]) because it tried to unpack a non-tuple. Wrap single args in a tuple before passing to __init__.
…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.
Test that NDArray[dtype] (without ndim) works and produces an NdarrayType with the correct dtype and ndim=None.
1adb863 to
71dcd62
Compare
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.
Pylint flagged C0415 (import-outside-toplevel) for the local inspect import in exception.py and the local get_func_signature import in _kernel_impl_dataclass.py. Hoist both to module top-level imports. Made-with: Cursor
Made-with: Cursor # Conflicts: # python/quadrants/lang/_perf_dispatch.py
…ataclass Made-with: Cursor
Fixes pylint C0415 (import-outside-toplevel) on `inspect` in `exception.py` and `get_func_signature` in `_kernel_impl_dataclass.py`. Made-with: Cursor
The Vulkan/Metal backends on macOS lack f64 support and crash when running this test, which uses qd.f64 fields. Add require=qd.extension.data64 to skip on backends without double-precision support. Made-with: Cursor
…type Made-with: Cursor # Conflicts: # python/quadrants/lang/_perf_dispatch.py
- Move get_func_signature out of exception.py into a new quadrants.lang._signature module so the helper lives somewhere whose name actually describes its purpose. - Add explicit type annotations (Callable -> inspect.Signature); the new module is not blanket-`# type: ignore`d so the signature is type-checked. - Generalize the error message from "Invalid type annotation of Taichi kernel" to "Invalid type annotation in `<func.__qualname__>`" since the helper is also used by @perf_dispatch and non-kernel FuncBase paths. - Update import sites in _func_base, _kernel_impl_dataclass, and _perf_dispatch to pull from the new module.
inspect.signature(func, eval_str=True) can raise SyntaxError for syntactically invalid string annotations (e.g. `a: "NDArray["`) and TypeError for annotations that cannot be evaluated as types. These were previously leaking as raw Python tracebacks instead of being wrapped as QuadrantsSyntaxError, contradicting the helper's docstring.
inspect.signature itself raises TypeError for non-introspectable objects (some C built-ins, etc.), so wrapping that as "invalid type annotation" would be misleading. Narrow the except tuple back to (NameError, AttributeError, SyntaxError) and document why TypeError is excluded.
…bscript # Conflicts: # python/quadrants/lang/exception.py
…ook-dtype # Conflicts: # python/quadrants/lang/exception.py
The previous version used qd.f64 and was gated on qd.extension.data64, so it was skipped on backends without f64 support (Vulkan/Metal on macOS) -- meaning the regression wasn't exercised everywhere. Rewrite with f32 (universally supported) and pick values whose dual is a non-integer exactly representable in f32 (x=1.25, dual=2x=2.5). Under the original bug the dual field was created as u8 (or i32 on Vulkan), which would truncate 2.5 to 2 and fail the assertion -- so the test still catches the regression on every backend.
Run the regression test on both f32 and f64, using test_utils.skip_if_f64_unsupported to skip f64 only on backends that don't reliably support it (Metal, Vulkan). f32 still runs everywhere so the bug is exercised on every backend; f64 adds extra coverage where supported.
…type Made-with: Cursor # Conflicts: # tests/python/test_ndarray_typing.py
The AMD GPU runner has pytest-timeout installed, which registers the same hooks (pytest_timeout_cancel_timer, pytest_timeout_set_timer) as our custom pytest_hardtle plugin, causing a ValueError at startup. Made-with: Cursor
|
@claude review |
There was a problem hiding this comment.
LGTM — mechanical cook_dtype refactor with a real shadowing bug fix and regression test.
Extended reasoning...
Overview
This PR pushes cook_dtype() calls to the Python/C++ boundary in expr.py, impl.py, matrix.py, sparse_matrix.py, and sparse_solver.py. It also converts PyQuadrants.default_fp/ip/up from raw attributes into property getter/setter pairs that store pre-cooked DataTypeCxx values, fixes a shadowing bug in create_field_member (renaming the inner checkbit dtype so it no longer clobbers the outer dtype parameter), and adds a regression test for the bug. The run_tests.py change disables pytest-timeout to avoid a conflict with pytest_hardtle, matching recent commit 2a885b1 on main.
Security risks
None. This is internal type-handling machinery; no auth, crypto, input validation, or data exposure surface.
Level of scrutiny
Low-to-moderate. The changes are mechanical and individually small, and cook_dtype() is a no-op (identity) for current DataTypeCxx instances, so behavior is preserved. The property conversion is a subtle public-API change (code introspecting vars() or __dict__ would be affected), but typical runtime.default_fp reads and qd.init(default_fp=qd.f64) writes continue to work unchanged.
Other factors
The create_field_member bug fix is a real improvement — the prior code silently overwrote the caller-visible dtype parameter in debug mode, causing x_dual to be created with the checkbit dtype (u8 or i32) instead of the field's actual dtype. The new parametrized test (test_dual_field_dtype_preserved_in_debug_mode) exercises this with x=1.25 -> dual=2.5 so a truncation regression would be immediately caught. A prior reviewer-posted Opus 4.6 review already reached the same ready-to-merge conclusion, and I independently agree.
…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
Summary
Pushes
cook_dtype()(the conversion from Python-side dtype objects to the C++DataTypeCxxrepresentation) closer to its consumers, so that:default_fp,default_ip,default_up) onPyQuadrantsare stored already-cooked. Direct attribute access is preserved via@propertygetter/setter pairs that auto-cook on assignment, so user code likeqd.init(default_fp=qd.f64)keeps working.expr_alloca_shared_array, constant-expression makers, etc.) receive cooked dtypes at the call site rather than relyingon the C++ binding to coerce — making the Python/C++ boundary explicit and catching un-cooked types earlier.
create_field_memberwhere the local checkbit dtype was being assigned to the function parameterdtype, mutating thecaller-visible variable. Renamed to
checkbit_dtypeand added a debug-mode regression test.This PR is stacked on top of
hp/typing-t4-2-ndarray-subscript(which itself stackshp/typing-t4-1-eval-str); both are merged in.Changes
cook_dtypeboundary workpython/quadrants/lang/impl.py—PyQuadrants.default_fp/default_ip/default_upconverted from raw attributes into properties with auto-cooking setters; backingfields (
_default_fpetc.) are pre-cooked in__init__.expr_init_shared_arraynow cookselement_typebefore passing to the AST builder.python/quadrants/lang/expr.py—make_constant_exprcooksdtypeonce up-front and cooks thedefault_fp/default_ip/u1fallbacks. Adds an inline commentexplaining the strategy (cook normalised input once, then cook only the runtime defaults in each branch).
python/quadrants/lang/matrix.py—make_matrixempty-vector path now usescook_dtype(primitive_types.i32)instead of the raw Python type.python/quadrants/linalg/sparse_matrix.py,sparse_solver.py—SparseMatrix,SparseMatrixBuilder, andSparseSolverconstructors cookdtypeonce and passthe cooked value to the C++ factories.
Bug fix
python/quadrants/lang/impl.py— Increate_field_member's debug-mode branch, the local variable holding the checkbit dtype was nameddtype, shadowing the functionparameter. Renamed to
checkbit_dtype. Added documentation comment explaining the rename.Tests
tests/python/test_ad_basics_fwd.py—test_dual_field_dtype_preserved_in_debug_mode: regression test for the checkbit shadowing bug, gated ondata64since it usesqd.f64.Merged-in work
hp/typing-t4-1-eval-str(get_func_signaturehelper,eval_str=Truesupport for stringified annotations) andhp/typing-t4-2-ndarray-subscript(
NDArray[i32]single-arg subscript fix) come along via merge.Good points
dtypeshadowing increate_field_memberwas silently mutating the wrong variable — caller-visible only in subtle debug-mode AD scenarios, which isexactly why it had gone unnoticed. Now caught by an explicit regression test.
cook_dtypecalls being scattered across deep call paths makes it hard to tell where Python-side dtype objects can vs. can'tappear. Cooking at the C++ boundary means downstream code (and types — see the
DataTypeCxxannotations on the new properties) can assume cooked types from then on.default_fp/default_ip/default_upto properties preserves all existing reads and assignments(
get_runtime().default_fp,qd.init(default_fp=qd.f64), etc.) — the cooking happens transparently.cook_dtypecalls. A follow-up commit (215544f15) removes redundant cooks inmake_constant_exprnow that the up-front normalisation handlesthem.
Bad / weak points (worth reviewer attention)
default_fp/ip/upare documented attributes. Code that doesruntime.default_fp = some_already_cooked_valueisfine (cooking is idempotent), but anything that introspects via
vars(runtime)orruntime.__dict__["default_fp"]will now get nothing / find_default_fpinstead. Risk islow but not zero.
DataTypeCxx, notAny. The new properties annotate the return type asDataTypeCxxbut the setter acceptsAny. That's correct for thecook-on-write pattern, but the asymmetric annotation may confuse type checkers downstream.
receiving raw Python dtypes. This PR doesn't claim to be exhaustive, but reviewers should weigh whether to expand or leave a TODO.
Test gating.
[fixed]test_dual_field_dtype_preserved_in_debug_moderequiresqd.extension.data64(added after a CI failure on backends without f64 support — see commitdcc12164f). This means the regression won't be exercised on f32-only backends; if the bug ever recurs in a different shape there, the test won't catch it.mainshows the union of t4-1 + t4-2 + t4-3 changes (~150 lines). Reviewers should look at this branch after t4-2 lands, or usegit diff hp/typing-t4-2-ndarray-subscript...HEADto see only the t4-3-specific delta.copilot:summary
Walkthrough
copilot:walkthrough