Concisely describe the proposed feature
Quadrants cannot currently be type checked by a downstream project, for several independent reasons. This issue covers all of them; the annotation spellings below are the largest and the one that has to be fixed first, and a second set of blockers in kernel bodies, decorators and call sites is described further down.
Kernel parameter annotations that are built by calling a factory - qd.types.vector(3), qd.types.matrix(3, 3) - are not valid Python type expressions. A call is not part of the type-expression grammar, so it is rejected regardless of what the callee returns, and the rejection is not tool-specific: mypy 1.15 reports it as [valid-type], pyright 1.1.407 as reportInvalidTypeForm, ty 0.0.65 as invalid-type-form. Any downstream project that wants to run a type checker over code containing quadrants kernels therefore cannot, and this blocks Genesis-Embodied-AI/genesis-world#1271.
Quadrants already solves this for two of its three annotation families. Measuring on Genesis at 54b9ca2e5, ty check genesis reports 5376 diagnostics, of which 1716 are invalid-type-form across 54 files. Grouped by the exact annotation expression:
| annotation form |
sites |
type-expression-valid spelling available today |
qd.types.ndarray() |
813 |
yes - qd.types.NDArray |
qd.template() |
588 |
yes - qd.Template |
qd.types.ndarray(ndim=N) and other keyword variants |
53 |
yes - qd.types.NDArray[None, N] |
qd.types.vector(3), qd.types.vector(4) |
257 |
no |
So 1454 of the 1716 are already fixable downstream today, and the remaining 257 are blocked here.
The reason the first three work is that qd.types.ndarray and qd.template are aliases for classes (NdarrayType, Template), so the bare alias is a plain class reference - a valid type expression - and NdarrayType additionally defines __class_getitem__, so the subscripted form carries the parameters through. By contrast qd.types.vector and qd.types.matrix are factory functions returning VectorType / MatrixType instances; neither class is exported from quadrants.types nor subscriptable, so for those two there is no valid spelling at all.
Verified against quadrants 1.1.4 (commit 47e9dfb5) and ty 0.0.65 - the following file both runs correctly and reports All checks passed!:
import numpy as np
import quadrants as qd
qd.init(arch=qd.cpu)
@qd.kernel
def bare(x: qd.types.NDArray, s: qd.i32):
x[0] = s
@qd.kernel
def subscripted(x: qd.types.NDArray[qd.i32, 1], s: qd.i32):
x[0] = s
@qd.kernel
def templated(f: qd.Template, s: qd.i32):
f[0] = s
arr = np.zeros(4, dtype=np.int32)
bare(arr, 7)
subscripted(arr, 8)
templated(qd.field(qd.i32, shape=4), 9)
Rewriting any of those annotations into its call form (qd.types.ndarray(), qd.types.ndarray(ndim=1), qd.template()) leaves the runtime behaviour identical and produces error[invalid-type-form] Function calls are not allowed in parameter annotations. There is no equivalent rewrite for qd.types.vector(3).
Describe the solution you'd like (if any)
Export Vector and Matrix from quadrants.types as class aliases carrying __class_getitem__, mirroring exactly what NdarrayType already does, so that the dimensions and dtype travel through a subscript instead of a call:
qd.types.Vector[3] # equivalent to qd.types.vector(3)
qd.types.Vector[3, qd.f32] # equivalent to qd.types.vector(3, qd.f32)
qd.types.Matrix[3, 3, qd.f32] # equivalent to qd.types.matrix(3, 3, qd.f32)
Concretely: export the classes currently named VectorType and MatrixType under quadrants.types as Vector and Matrix, and give each a __class_getitem__ forwarding its arguments positionally to the constructor - the same few lines already present as NdarrayType.__class_getitem__ in python/quadrants/types/ndarray_type.py. Per the naming point below these should be renamed, not aliased, so the class name and the exported name coincide. The existing vector() and matrix() factories can keep working unchanged, which makes the subscript form purely additive on its own.
While settling on a spelling, the two families that already work are themselves inconsistent with each other, and that should be fixed rather than propagated to Vector / Matrix. Three separate things differ:
- Export depth.
Template is reachable at both qd.Template and qd.types.Template (they are the same object), whereas NDArray exists only under qd.types - qd.NDArray raises AttributeError. So the two type-expression-valid spellings a user has to write today are qd.Template and qd.types.NDArray, at different depths, for no reason a caller can infer. The top-level copies are also the minority case: vector, matrix, struct, NDArray and BufferViewType are already reachable only under qd.types, and the sole source of the leak is from quadrants.types.annotations import * at python/quadrants/__init__.py:52, whose __all__ is ["template", "sparse_matrix_builder", "Template"].
- A name that means two different things at the two depths.
qd.template is qd.types.template, but qd.ndarray is not qd.types.ndarray: at top level it is the allocation function (qd.ndarray(qd.i32, shape=4)), and only under qd.types is it the annotation class. Worse, that trap is invisible to the checker - reveal_type(qd.ndarray) is Unknown because the top-level re-export is not statically resolvable, so def k(x: qd.ndarray) passes ty check with All checks passed! and fails only at import time with QuadrantsSyntaxError: Invalid type annotation (argument 0) of Quadrants kernel: <function ndarray ...>. A user who has learned that qd.Template works at top level and reaches for qd.NDArray or qd.ndarray gets either an AttributeError or a spelling the type checker actively endorses and the runtime rejects.
- Class naming. Four styles coexist.
Template is plain CapWords. NdarrayType carries a Type suffix and a different casing of "ndarray" than the NDArray alias in front of it. BufferViewType is exported with its suffix intact, so the use site reads qd.types.BufferViewType. And sparse_matrix_builder is a lowercase class. The suffix is pure stutter under this namespace - qd.types.NdarrayType says "type" twice - so it should come off the classes themselves rather than being papered over by an alias, which also makes the class name and the exported name coincide. The same applies to the classes behind the request above (VectorType, MatrixType) and to StructType and CompoundType, so the new names should land suffix-free from the start rather than being renamed later.
The concrete request is therefore that qd.types become the single home for annotations, with exactly one statically-resolvable spelling per family and no Type suffix: qd.types.NDArray, qd.types.Vector, qd.types.Matrix, qd.types.Struct, qd.types.Template, qd.types.BufferView, qd.types.SparseMatrixBuilder. That means dropping the lowercase duplicates (qd.types.ndarray, qd.types.template) and deleting the from quadrants.types.annotations import * line so that nothing annotation-related remains at top level - which also dissolves the qd.ndarray collision structurally, since the top level would then hold only the allocation function and qd.types only the annotation. The from quadrants.types.primitive_types import * shortcut on the next line should stay as it is: qd.f32 and friends are dtypes rather than annotations, and are genuinely wanted at top level.
This is a breaking change and would need the MAJOR label, but the cost to downstream is close to zero if it ships alongside the Vector / Matrix addition, because every affected call site has to be rewritten anyway to become type-expression-valid - that is one migration instead of two. For the largest consumer it is free: Genesis reaches all of these exclusively through qd.types.* and qd.template(), and imports none of NdarrayType, VectorType, MatrixType, StructType, BufferViewType or CompoundType by name.
One dead end worth recording, so it is not mistaken for a solution: qd.math.vec3 looks like it already covers this case, and a checker does accept def k(v: qd.math.vec3) without complaint. That pass is vacuous. vec3 = vector(3, cfg().default_fp) is a module-level variable whose type a checker cannot infer (the cfg() chain is unresolvable), so it resolves to Unknown and the annotation is simply not checked rather than being valid - reveal_type(vec3) confirms Unknown. Pre-built instances hide the problem instead of fixing it; the spelling has to be a class reference or a subscript of one.
A second blocker: valid annotations are necessary but not sufficient
Fixing the annotations only clears a third of the picture, so it is worth scoping this issue as full typing compliance rather than just parameter declarations. Of the 5376 diagnostics on Genesis, 1716 are the annotations above and a further 1549 sit in the same 58 kernel-bearing files and come from kernel bodies, decorators and call sites. Not all of those are quadrants' responsibility - a large share is Genesis's own (164 from attributes initialised to None and dereferenced without narrowing, 251 from attributes never assigned in their class, and 88 from a Genesis-side name collision where genesis.utils.geom.qd_vec3(val) is star-imported into gs and then shadowed at gs.init() time by qd_vec3 = qd.types.vector(3, qd_float)). The following, though, are quadrants-side and account for 334 of them.
@qd.kernel(fastcache=False) matches no overload - 149 sites. The two @overload declarations for kernel in python/quadrants/lang/kernel_impl.py accept pure, graph and checkpoints, but the implementation below them also accepts fastcache, so the overloads have drifted from the implementation and every parameterised use of the decorator is an error.
qd.kernel erases the signature of the function it decorates, so no kernel call site is checked at all. Both overloads return Any / Callable[[Any], Any] rather than being generic in the decorated callable. qd.func already does this correctly one file over, with F = TypeVar("F", bound=Callable), which is the same kind of internal inconsistency as the naming one above - the fix for kernel is to mirror func.
qd.simt.block.SharedArray defines neither __getitem__ nor __setitem__ - 72 sites. Indexing a shared array (pivot_row[j]) or assigning through it is therefore an error wherever it is used.
There is no way to spell "field or tensor whose elements are T" - 113 sites. Genesis declares struct-element buffers (ContactFace, ContactNormal, MDVertex, EPAPolytopeFace, Witness, ContactHalfspace) and indexes them as gjk_state.contact_faces[i_b, i_n].normal1, but the only available annotation names the element type, which is not subscriptable. This is the same underlying gap as the vector/matrix request: the annotation describes the metadata object rather than the value the kernel body actually receives.
All three of the first points reproduce in this standalone file on quadrants 1.1.4 (commit 47e9dfb5) with ty 0.0.65:
from typing import reveal_type
import quadrants as qd
qd.init(arch=qd.cpu)
@qd.kernel
def plain(s: qd.i32) -> qd.i32:
return s + 1
@qd.kernel(fastcache=False)
def with_fastcache(s: qd.i32) -> qd.i32:
return s + 1
@qd.func
def helper(s: qd.i32) -> qd.i32:
return s + 1
reveal_type(plain) # Any <- signature erased
reveal_type(helper) # def helper(s: Unknown) -> Unknown
plain("this is not an i32", 1, 2, 3) # no diagnostic; the runtime does reject it
ty check reports exactly one error on it - error[no-matching-overload] No overload of function 'kernel' matches arguments on the fastcache line - while with_fastcache(4) runs fine, confirming the overloads and not the implementation are wrong. Swapping fastcache=False for graph=False removes the error, isolating it to that one missing parameter. The last line shows the consequence of the erased signature: four arguments of the wrong types to a one-argument kernel pass the checker silently, and only the runtime catches it.
The remaining 712 diagnostics in those files I have not attributed yet, and I have not checked whether they collapse to a similarly small number of root causes the way the annotations did.
Additional comments
Separately, the distributed package carries no py.typed marker and no .pyi stubs, so under PEP 561 a conforming checker is not supposed to use its inline types for a normal (non-editable) install at all. That is a different question from this one - whether quadrants wants to advertise types downstream - so I have not folded it in here, but happy to open it as its own issue if that is useful.
Concisely describe the proposed feature
Quadrants cannot currently be type checked by a downstream project, for several independent reasons. This issue covers all of them; the annotation spellings below are the largest and the one that has to be fixed first, and a second set of blockers in kernel bodies, decorators and call sites is described further down.
Kernel parameter annotations that are built by calling a factory -
qd.types.vector(3),qd.types.matrix(3, 3)- are not valid Python type expressions. A call is not part of the type-expression grammar, so it is rejected regardless of what the callee returns, and the rejection is not tool-specific: mypy 1.15 reports it as[valid-type], pyright 1.1.407 asreportInvalidTypeForm, ty 0.0.65 asinvalid-type-form. Any downstream project that wants to run a type checker over code containing quadrants kernels therefore cannot, and this blocks Genesis-Embodied-AI/genesis-world#1271.Quadrants already solves this for two of its three annotation families. Measuring on Genesis at
54b9ca2e5,ty check genesisreports 5376 diagnostics, of which 1716 areinvalid-type-formacross 54 files. Grouped by the exact annotation expression:qd.types.ndarray()qd.types.NDArrayqd.template()qd.Templateqd.types.ndarray(ndim=N)and other keyword variantsqd.types.NDArray[None, N]qd.types.vector(3),qd.types.vector(4)So 1454 of the 1716 are already fixable downstream today, and the remaining 257 are blocked here.
The reason the first three work is that
qd.types.ndarrayandqd.templateare aliases for classes (NdarrayType,Template), so the bare alias is a plain class reference - a valid type expression - andNdarrayTypeadditionally defines__class_getitem__, so the subscripted form carries the parameters through. By contrastqd.types.vectorandqd.types.matrixare factory functions returningVectorType/MatrixTypeinstances; neither class is exported fromquadrants.typesnor subscriptable, so for those two there is no valid spelling at all.Verified against quadrants 1.1.4 (commit
47e9dfb5) and ty 0.0.65 - the following file both runs correctly and reportsAll checks passed!:Rewriting any of those annotations into its call form (
qd.types.ndarray(),qd.types.ndarray(ndim=1),qd.template()) leaves the runtime behaviour identical and produceserror[invalid-type-form] Function calls are not allowed in parameter annotations. There is no equivalent rewrite forqd.types.vector(3).Describe the solution you'd like (if any)
Export
VectorandMatrixfromquadrants.typesas class aliases carrying__class_getitem__, mirroring exactly whatNdarrayTypealready does, so that the dimensions and dtype travel through a subscript instead of a call:Concretely: export the classes currently named
VectorTypeandMatrixTypeunderquadrants.typesasVectorandMatrix, and give each a__class_getitem__forwarding its arguments positionally to the constructor - the same few lines already present asNdarrayType.__class_getitem__inpython/quadrants/types/ndarray_type.py. Per the naming point below these should be renamed, not aliased, so the class name and the exported name coincide. The existingvector()andmatrix()factories can keep working unchanged, which makes the subscript form purely additive on its own.While settling on a spelling, the two families that already work are themselves inconsistent with each other, and that should be fixed rather than propagated to
Vector/Matrix. Three separate things differ:Templateis reachable at bothqd.Templateandqd.types.Template(they are the same object), whereasNDArrayexists only underqd.types-qd.NDArrayraisesAttributeError. So the two type-expression-valid spellings a user has to write today areqd.Templateandqd.types.NDArray, at different depths, for no reason a caller can infer. The top-level copies are also the minority case:vector,matrix,struct,NDArrayandBufferViewTypeare already reachable only underqd.types, and the sole source of the leak isfrom quadrants.types.annotations import *atpython/quadrants/__init__.py:52, whose__all__is["template", "sparse_matrix_builder", "Template"].qd.templateisqd.types.template, butqd.ndarrayis notqd.types.ndarray: at top level it is the allocation function (qd.ndarray(qd.i32, shape=4)), and only underqd.typesis it the annotation class. Worse, that trap is invisible to the checker -reveal_type(qd.ndarray)isUnknownbecause the top-level re-export is not statically resolvable, sodef k(x: qd.ndarray)passesty checkwithAll checks passed!and fails only at import time withQuadrantsSyntaxError: Invalid type annotation (argument 0) of Quadrants kernel: <function ndarray ...>. A user who has learned thatqd.Templateworks at top level and reaches forqd.NDArrayorqd.ndarraygets either anAttributeErroror a spelling the type checker actively endorses and the runtime rejects.Templateis plain CapWords.NdarrayTypecarries aTypesuffix and a different casing of "ndarray" than theNDArrayalias in front of it.BufferViewTypeis exported with its suffix intact, so the use site readsqd.types.BufferViewType. Andsparse_matrix_builderis a lowercase class. The suffix is pure stutter under this namespace -qd.types.NdarrayTypesays "type" twice - so it should come off the classes themselves rather than being papered over by an alias, which also makes the class name and the exported name coincide. The same applies to the classes behind the request above (VectorType,MatrixType) and toStructTypeandCompoundType, so the new names should land suffix-free from the start rather than being renamed later.The concrete request is therefore that
qd.typesbecome the single home for annotations, with exactly one statically-resolvable spelling per family and noTypesuffix:qd.types.NDArray,qd.types.Vector,qd.types.Matrix,qd.types.Struct,qd.types.Template,qd.types.BufferView,qd.types.SparseMatrixBuilder. That means dropping the lowercase duplicates (qd.types.ndarray,qd.types.template) and deleting thefrom quadrants.types.annotations import *line so that nothing annotation-related remains at top level - which also dissolves theqd.ndarraycollision structurally, since the top level would then hold only the allocation function andqd.typesonly the annotation. Thefrom quadrants.types.primitive_types import *shortcut on the next line should stay as it is:qd.f32and friends are dtypes rather than annotations, and are genuinely wanted at top level.This is a breaking change and would need the
MAJORlabel, but the cost to downstream is close to zero if it ships alongside theVector/Matrixaddition, because every affected call site has to be rewritten anyway to become type-expression-valid - that is one migration instead of two. For the largest consumer it is free: Genesis reaches all of these exclusively throughqd.types.*andqd.template(), and imports none ofNdarrayType,VectorType,MatrixType,StructType,BufferViewTypeorCompoundTypeby name.One dead end worth recording, so it is not mistaken for a solution:
qd.math.vec3looks like it already covers this case, and a checker does acceptdef k(v: qd.math.vec3)without complaint. That pass is vacuous.vec3 = vector(3, cfg().default_fp)is a module-level variable whose type a checker cannot infer (thecfg()chain is unresolvable), so it resolves toUnknownand the annotation is simply not checked rather than being valid -reveal_type(vec3)confirmsUnknown. Pre-built instances hide the problem instead of fixing it; the spelling has to be a class reference or a subscript of one.A second blocker: valid annotations are necessary but not sufficient
Fixing the annotations only clears a third of the picture, so it is worth scoping this issue as full typing compliance rather than just parameter declarations. Of the 5376 diagnostics on Genesis, 1716 are the annotations above and a further 1549 sit in the same 58 kernel-bearing files and come from kernel bodies, decorators and call sites. Not all of those are quadrants' responsibility - a large share is Genesis's own (164 from attributes initialised to
Noneand dereferenced without narrowing, 251 from attributes never assigned in their class, and 88 from a Genesis-side name collision wheregenesis.utils.geom.qd_vec3(val)is star-imported intogsand then shadowed atgs.init()time byqd_vec3 = qd.types.vector(3, qd_float)). The following, though, are quadrants-side and account for 334 of them.@qd.kernel(fastcache=False)matches no overload - 149 sites. The two@overloaddeclarations forkernelinpython/quadrants/lang/kernel_impl.pyacceptpure,graphandcheckpoints, but the implementation below them also acceptsfastcache, so the overloads have drifted from the implementation and every parameterised use of the decorator is an error.qd.kernelerases the signature of the function it decorates, so no kernel call site is checked at all. Both overloads returnAny/Callable[[Any], Any]rather than being generic in the decorated callable.qd.funcalready does this correctly one file over, withF = TypeVar("F", bound=Callable), which is the same kind of internal inconsistency as the naming one above - the fix forkernelis to mirrorfunc.qd.simt.block.SharedArraydefines neither__getitem__nor__setitem__- 72 sites. Indexing a shared array (pivot_row[j]) or assigning through it is therefore an error wherever it is used.There is no way to spell "field or tensor whose elements are
T" - 113 sites. Genesis declares struct-element buffers (ContactFace,ContactNormal,MDVertex,EPAPolytopeFace,Witness,ContactHalfspace) and indexes them asgjk_state.contact_faces[i_b, i_n].normal1, but the only available annotation names the element type, which is not subscriptable. This is the same underlying gap as the vector/matrix request: the annotation describes the metadata object rather than the value the kernel body actually receives.All three of the first points reproduce in this standalone file on quadrants 1.1.4 (commit
47e9dfb5) with ty 0.0.65:ty checkreports exactly one error on it -error[no-matching-overload] No overload of function 'kernel' matches argumentson thefastcacheline - whilewith_fastcache(4)runs fine, confirming the overloads and not the implementation are wrong. Swappingfastcache=Falseforgraph=Falseremoves the error, isolating it to that one missing parameter. The last line shows the consequence of the erased signature: four arguments of the wrong types to a one-argument kernel pass the checker silently, and only the runtime catches it.The remaining 712 diagnostics in those files I have not attributed yet, and I have not checked whether they collapse to a similarly small number of root causes the way the annotations did.
Additional comments
Separately, the distributed package carries no
py.typedmarker and no.pyistubs, so under PEP 561 a conforming checker is not supposed to use its inline types for a normal (non-editable) install at all. That is a different question from this one - whether quadrants wants to advertise types downstream - so I have not folded it in here, but happy to open it as its own issue if that is useful.