summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/include/numpy/experimental_dtype_api.h306
-rw-r--r--numpy/core/include/numpy/ndarraytypes.h30
-rw-r--r--numpy/core/setup.py2
-rw-r--r--numpy/core/src/multiarray/array_method.c91
-rw-r--r--numpy/core/src/multiarray/array_method.h5
-rw-r--r--numpy/core/src/multiarray/descriptor.c27
-rw-r--r--numpy/core/src/multiarray/dtypemeta.c2
-rw-r--r--numpy/core/src/multiarray/dtypemeta.h44
-rw-r--r--numpy/core/src/multiarray/experimental_public_dtype_api.c363
-rw-r--r--numpy/core/src/multiarray/experimental_public_dtype_api.h18
-rw-r--r--numpy/core/src/multiarray/multiarraymodule.c5
11 files changed, 817 insertions, 76 deletions
diff --git a/numpy/core/include/numpy/experimental_dtype_api.h b/numpy/core/include/numpy/experimental_dtype_api.h
new file mode 100644
index 000000000..22854a725
--- /dev/null
+++ b/numpy/core/include/numpy/experimental_dtype_api.h
@@ -0,0 +1,306 @@
+/*
+ * This header exports the new experimental DType API as proposed in
+ * NEPs 41 to 43. For background, please check these NEPs. Otherwise,
+ * this header also serves as documentation for the time being.
+ *
+ * Please do not hesitate to contact @seberg with questions. This is
+ * developed together with https://github.com/seberg/experimental_user_dtypes
+ * and those interested in experimenting are encouraged to contribute there.
+ *
+ * To use the functions defined in the header, call::
+ *
+ * if (import_experimental_dtype_api(version) < 0) {
+ * return NULL;
+ * }
+ *
+ * in your module init. (A version mismatch will be reported, just update
+ * to the correct one, this will alert you of possible changes.)
+ *
+ * The two main symbols exported are:
+ *
+ * - PyUFunc_AddLoopFromSpec (Register a new loop for a ufunc)
+ * - PyArrayInitDTypeMeta_FromSpec (Create a new DType)
+ *
+ * Please check the in-line documentation for details and do not hesitate to
+ * ask for help.
+ *
+ * WARNING
+ * =======
+ *
+ * By using this header, you understand that this is a fully experimental
+ * exposure. Details are expected to change, and some options may have no
+ * effect. (Please contact @seberg if you have questions!)
+ * If the exposure stops working, please file a bug report with NumPy.
+ * Further, a DType created using this API/header should still be expected
+ * to be incompatible with some functionality inside and outside of NumPy.
+ * In this case crashes must be expected. Please report any such problems
+ * so that they can be fixed before final exposure.
+ * Furthermore, expect missing checks for programming errors which the final
+ * API is expected to have.
+ *
+ * Symbols with a leading underscore are likely to not be included in the
+ * first public version, if these are central to your use-case, please let
+ * us know, so that we can reconsider.
+ *
+ * "Array-like" consumer API not yet under considerations
+ * ======================================================
+ *
+ * The new DType API is designed in a way to make it potentially useful for
+ * alternative "array-like" implementations. This will require careful
+ * exposure of details and functions and is not part of this experimental API.
+ */
+
+#ifndef NUMPY_CORE_INCLUDE_NUMPY_EXPERIMENTAL_DTYPE_API_H_
+#define NUMPY_CORE_INCLUDE_NUMPY_EXPERIMENTAL_DTYPE_API_H_
+
+#include <Python.h>
+#include "ndarraytypes.h"
+
+
+/*
+ * Just a hack so I don't forget importing as much myself, I spend way too
+ * much time noticing it the first time around :).
+ */
+static void
+__not_imported(void)
+{
+ printf("*****\nCritical error, dtype API not imported\n*****\n");
+}
+static void *__uninitialized_table[] = {
+ &__not_imported, &__not_imported, &__not_imported, &__not_imported};
+
+
+static void **__experimental_dtype_api_table = __uninitialized_table;
+
+/*
+ * ******************************************************
+ * ArrayMethod API (Casting and UFuncs)
+ * ******************************************************
+ */
+/*
+ * NOTE: Expected changes:
+ * * invert logic of floating point error flag
+ * * probably split runtime and general flags into two
+ * * should possibly not use an enum for typdef for more stable ABI?
+ */
+typedef enum {
+ /* Flag for whether the GIL is required */
+ NPY_METH_REQUIRES_PYAPI = 1 << 1,
+ /*
+ * Some functions cannot set floating point error flags, this flag
+ * gives us the option (not requirement) to skip floating point error
+ * setup/check. No function should set error flags and ignore them
+ * since it would interfere with chaining operations (e.g. casting).
+ */
+ NPY_METH_NO_FLOATINGPOINT_ERRORS = 1 << 2,
+ /* Whether the method supports unaligned access (not runtime) */
+ NPY_METH_SUPPORTS_UNALIGNED = 1 << 3,
+
+ /* All flags which can change at runtime */
+ NPY_METH_RUNTIME_FLAGS = (
+ NPY_METH_REQUIRES_PYAPI |
+ NPY_METH_NO_FLOATINGPOINT_ERRORS),
+} NPY_ARRAYMETHOD_FLAGS;
+
+
+/*
+ * The main object for creating a new ArrayMethod. We use the typical `slots`
+ * mechanism used by the Python limited API (see below for the slot defs).
+ */
+typedef struct {
+ const char *name;
+ int nin, nout;
+ NPY_CASTING casting;
+ NPY_ARRAYMETHOD_FLAGS flags;
+ PyObject **dtypes; /* array of DType class objects */
+ PyType_Slot *slots;
+} PyArrayMethod_Spec;
+
+
+typedef PyObject *_ufunc_addloop_fromspec_func(
+ PyObject *ufunc, PyArrayMethod_Spec *spec);
+/*
+ * The main ufunc registration function. This adds a new implementation/loop
+ * to a ufunc. It replaces `PyUFunc_RegisterLoopForType`.
+ */
+#define PyUFunc_AddLoopFromSpec \
+ (*(_ufunc_addloop_fromspec_func *)(__experimental_dtype_api_table[0]))
+
+
+/*
+ * In addition to the normal casting levels, NPY_CAST_IS_VIEW indicates
+ * that no cast operation is necessary at all (although a copy usually will be)
+ *
+ * NOTE: The most likely modification here is to add an additional
+ * `view_offset` output to resolve_descriptors. If set, it would
+ * indicate both that it is a view and what offset to use. This means that
+ * e.g. `arr.imag` could be implemented by an ArrayMethod.
+ */
+#define NPY_CAST_IS_VIEW _NPY_CAST_IS_VIEW
+
+/*
+ * The resolve descriptors function, must be able to handle NULL values for
+ * all output (but not input) `given_descrs` and fill `loop_descrs`.
+ * Return -1 on error or 0 if the operation is not possible without an error
+ * set. (This may still be in flux.)
+ * Otherwise must return the "casting safety", for normal functions, this is
+ * almost always "safe" (or even "equivalent"?).
+ *
+ * `resolve_descriptors` is optional if all output DTypes are non-parametric.
+ */
+#define NPY_METH_resolve_descriptors 1
+typedef NPY_CASTING (resolve_descriptors_function)(
+ /* "method" is currently opaque (necessary e.g. to wrap Python) */
+ PyObject *method,
+ /* DTypes the method was created for */
+ PyObject **dtypes,
+ /* Input descriptors (instances). Outputs may be NULL. */
+ PyArray_Descr **given_descrs,
+ /* Exact loop descriptors to use, must not hold references on error */
+ PyArray_Descr **loop_descrs);
+
+/* NOT public yet: Signature needs adapting as external API. */
+#define _NPY_METH_get_loop 2
+
+/*
+ * Current public API to define fast inner-loops. You must provide a
+ * strided loop. If this is a cast between two "versions" of the same dtype
+ * you must also provide an unaligned strided loop.
+ * Other loops are useful to optimize the very common contiguous case.
+ *
+ * NOTE: As of now, NumPy will NOT use unaligned loops in ufuncs!
+ */
+#define NPY_METH_strided_loop 3
+#define NPY_METH_contiguous_loop 4
+#define NPY_METH_unaligned_strided_loop 5
+#define NPY_METH_unaligned_contiguous_loop 6
+
+
+typedef struct {
+ PyObject *caller; /* E.g. the original ufunc, may be NULL */
+ PyObject *method; /* The method "self". Currently an opaque object */
+
+ /* Operand descriptors, filled in by resolve_descriptors */
+ PyArray_Descr **descriptors;
+ /* Structure may grow (this is harmless for DType authors) */
+} PyArrayMethod_Context;
+
+typedef int (PyArrayMethod_StridedLoop)(PyArrayMethod_Context *context,
+ char *const *data, const npy_intp *dimensions, const npy_intp *strides,
+ NpyAuxData *transferdata);
+
+
+
+/*
+ * ****************************
+ * DTYPE API
+ * ****************************
+ */
+
+#define NPY_DT_ABSTRACT 1 << 1
+#define NPY_DT_PARAMETRIC 1 << 2
+
+#define NPY_DT_discover_descr_from_pyobject 1
+#define _NPY_DT_is_known_scalar_type 2
+#define NPY_DT_default_descr 3
+#define NPY_DT_common_dtype 4
+#define NPY_DT_common_instance 5
+#define NPY_DT_setitem 6
+#define NPY_DT_getitem 7
+
+
+// TODO: These slots probably still need some thought, and/or a way to "grow"?
+typedef struct{
+ PyTypeObject *typeobj; /* type of python scalar or NULL */
+ int flags; /* flags, including parametric and abstract */
+ /* NULL terminated cast definitions. Use NULL for the newly created DType */
+ PyArrayMethod_Spec **casts;
+ PyType_Slot *slots;
+ /* Baseclass or NULL (will always subclass `np.dtype`) */
+ PyTypeObject *baseclass;
+} PyArrayDTypeMeta_Spec;
+
+
+/*
+ * DTypeMeta struct, the content may be made fully opaque (except the size).
+ * We may also move everything into a single `void *dt_slots`.
+ */
+typedef struct {
+ PyHeapTypeObject super;
+ PyArray_Descr *singleton;
+ int type_num;
+ PyTypeObject *scalar_type;
+ npy_uint64 flags;
+ void *dt_slots;
+ void *reserved[3];
+} PyArray_DTypeMeta;
+
+
+#define PyArrayDTypeMeta_Type \
+ (&(PyTypeObject *)__experimental_dtype_api_table[1])
+
+typedef int __dtypemeta_fromspec(
+ PyArray_DTypeMeta *DType, PyArrayDTypeMeta_Spec *dtype_spec);
+/*
+ * Finalize creation of a DTypeMeta. You must ensure that the DTypeMeta is
+ * a proper subclass. The DTypeMeta object has additional fields compared to
+ * a normal PyTypeObject!
+ * The only (easy) creation of a new DType is to create a static Type which
+ * inherits `PyArray_DescrType`, sets its type to `PyArrayDTypeMeta_Type` and
+ * uses `PyArray_DTypeMeta` defined above as the C-structure.
+ */
+#define PyArrayInitDTypeMeta_FromSpec \
+ ((__dtypemeta_fromspec *)(__experimental_dtype_api_table[2]))
+
+
+
+/*
+ * ********************************
+ * Initialization
+ * ********************************
+ *
+ * Import the experimental API, the version must match the one defined in
+ * the header to ensure changes are taken into account. NumPy will further
+ * runtime-check this.
+ * You must call this function to use the symbols defined in this file.
+ */
+#define __EXPERIMENTAL_DTYPE_VERSION 1
+
+static int
+import_experimental_dtype_api(int version)
+{
+ if (version != __EXPERIMENTAL_DTYPE_VERSION) {
+ PyErr_Format(PyExc_RuntimeError,
+ "DType API version %d did not match header version %d. Please "
+ "update the import statement and check for API changes.",
+ version, __EXPERIMENTAL_DTYPE_VERSION);
+ return -1;
+ }
+ if (__experimental_dtype_api_table != __uninitialized_table) {
+ /* already imported. */
+ return 0;
+ }
+
+ PyObject *multiarray = PyImport_ImportModule("numpy.core._multiarray_umath");
+ if (multiarray == NULL) {
+ return -1;
+ }
+
+ PyObject *api = PyObject_CallMethod(multiarray,
+ "_get_experimental_dtype_api", "i", version);
+ Py_DECREF(multiarray);
+ if (api == NULL) {
+ return -1;
+ }
+ __experimental_dtype_api_table = PyCapsule_GetPointer(api,
+ "experimental_dtype_api_table");
+ Py_DECREF(api);
+
+ if (__experimental_dtype_api_table == NULL) {
+ __experimental_dtype_api_table = __uninitialized_table;
+ return -1;
+ }
+ return 0;
+}
+
+#endif /* NUMPY_CORE_INCLUDE_NUMPY_EXPERIMENTAL_DTYPE_API_H_ */
diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h
index 740223882..8d810fa64 100644
--- a/numpy/core/include/numpy/ndarraytypes.h
+++ b/numpy/core/include/numpy/ndarraytypes.h
@@ -1858,32 +1858,14 @@ typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, size_t size,
*/
#if defined(NPY_INTERNAL_BUILD) && NPY_INTERNAL_BUILD
/*
- * The Structures defined in this block are considered private API and
- * may change without warning!
+ * The Structures defined in this block are currently considered
+ * private API and may change without warning!
+ * Part of this (at least the size) is exepcted to be public API without
+ * further modifications.
*/
/* TODO: Make this definition public in the API, as soon as its settled */
NPY_NO_EXPORT extern PyTypeObject PyArrayDTypeMeta_Type;
- typedef struct PyArray_DTypeMeta_tag PyArray_DTypeMeta;
-
- typedef PyArray_Descr *(discover_descr_from_pyobject_function)(
- PyArray_DTypeMeta *cls, PyObject *obj);
-
- /*
- * Before making this public, we should decide whether it should pass
- * the type, or allow looking at the object. A possible use-case:
- * `np.array(np.array([0]), dtype=np.ndarray)`
- * Could consider arrays that are not `dtype=ndarray` "scalars".
- */
- typedef int (is_known_scalar_type_function)(
- PyArray_DTypeMeta *cls, PyTypeObject *obj);
-
- typedef PyArray_Descr *(default_descr_function)(PyArray_DTypeMeta *cls);
- typedef PyArray_DTypeMeta *(common_dtype_function)(
- PyArray_DTypeMeta *dtype1, PyArray_DTypeMeta *dtyep2);
- typedef PyArray_Descr *(common_instance_function)(
- PyArray_Descr *dtype1, PyArray_Descr *dtyep2);
-
/*
* While NumPy DTypes would not need to be heap types the plan is to
* make DTypes available in Python at which point they will be heap types.
@@ -1894,7 +1876,7 @@ typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, size_t size,
* it is a fairly complex construct which may be better to allow
* refactoring of.
*/
- struct PyArray_DTypeMeta_tag {
+ typedef struct {
PyHeapTypeObject super;
/*
@@ -1922,7 +1904,7 @@ typedef void (PyDataMem_EventHookFunc)(void *inp, void *outp, size_t size,
*/
void *dt_slots;
void *reserved[3];
- };
+ } PyArray_DTypeMeta;
#endif /* NPY_INTERNAL_BUILD */
diff --git a/numpy/core/setup.py b/numpy/core/setup.py
index bde81bf2f..f2524ae13 100644
--- a/numpy/core/setup.py
+++ b/numpy/core/setup.py
@@ -810,6 +810,7 @@ def configuration(parent_package='',top_path=None):
join('src', 'multiarray', 'dragon4.h'),
join('src', 'multiarray', 'einsum_debug.h'),
join('src', 'multiarray', 'einsum_sumprod.h'),
+ join('src', 'multiarray', 'experimental_public_dtype_api.h'),
join('src', 'multiarray', 'getset.h'),
join('src', 'multiarray', 'hashdescr.h'),
join('src', 'multiarray', 'iterators.h'),
@@ -877,6 +878,7 @@ def configuration(parent_package='',top_path=None):
join('src', 'multiarray', 'dtype_transfer.c'),
join('src', 'multiarray', 'einsum.c.src'),
join('src', 'multiarray', 'einsum_sumprod.c.src'),
+ join('src', 'multiarray', 'experimental_public_dtype_api.c'),
join('src', 'multiarray', 'flagsobject.c'),
join('src', 'multiarray', 'getset.c'),
join('src', 'multiarray', 'hashdescr.c'),
diff --git a/numpy/core/src/multiarray/array_method.c b/numpy/core/src/multiarray/array_method.c
index c4db73c3b..406b0c6ff 100644
--- a/numpy/core/src/multiarray/array_method.c
+++ b/numpy/core/src/multiarray/array_method.c
@@ -58,16 +58,10 @@ default_resolve_descriptors(
{
int nin = method->nin;
int nout = method->nout;
- int all_defined = 1;
for (int i = 0; i < nin + nout; i++) {
PyArray_DTypeMeta *dtype = dtypes[i];
- if (dtype == NULL) {
- output_descrs[i] = NULL;
- all_defined = 0;
- continue;
- }
- if (NPY_DTYPE(input_descrs[i]) == dtype) {
+ if (input_descrs[i] != NULL) {
output_descrs[i] = ensure_dtype_nbo(input_descrs[i]);
}
else {
@@ -77,41 +71,11 @@ default_resolve_descriptors(
goto fail;
}
}
- if (all_defined) {
- return method->casting;
- }
-
- if (NPY_UNLIKELY(nin == 0 || dtypes[0] == NULL)) {
- /* Registration should reject this, so this would be indicates a bug */
- PyErr_SetString(PyExc_RuntimeError,
- "Invalid use of default resolver without inputs or with "
- "input or output DType incorrectly missing.");
- goto fail;
- }
- /* We find the common dtype of all inputs, and use it for the unknowns */
- PyArray_DTypeMeta *common_dtype = dtypes[0];
- assert(common_dtype != NULL);
- for (int i = 1; i < nin; i++) {
- Py_SETREF(common_dtype, PyArray_CommonDType(common_dtype, dtypes[i]));
- if (common_dtype == NULL) {
- goto fail;
- }
- }
- for (int i = nin; i < nin + nout; i++) {
- if (output_descrs[i] != NULL) {
- continue;
- }
- if (NPY_DTYPE(input_descrs[i]) == common_dtype) {
- output_descrs[i] = ensure_dtype_nbo(input_descrs[i]);
- }
- else {
- output_descrs[i] = NPY_DT_CALL_default_descr(common_dtype);
- }
- if (NPY_UNLIKELY(output_descrs[i] == NULL)) {
- goto fail;
- }
- }
-
+ /*
+ * If we relax the requirement for specifying all `dtypes` (e.g. allow
+ * abstract ones or unspecified outputs). We can use the common-dtype
+ * operation to provide a default here.
+ */
return method->casting;
fail:
@@ -219,9 +183,18 @@ validate_spec(PyArrayMethod_Spec *spec)
}
for (int i = 0; i < nargs; i++) {
- if (spec->dtypes[i] == NULL && i < spec->nin) {
+ /*
+ * Note that we could allow for output dtypes to not be specified
+ * (the array-method would have to make sure to support this).
+ * We could even allow for some dtypes to be abstract.
+ * For now, assume that this is better handled in a promotion step.
+ * One problem with providing all DTypes is the definite need to
+ * hold references. We probably, eventually, have to implement
+ * traversal and trust the GC to deal with it.
+ */
+ if (spec->dtypes[i] == NULL) {
PyErr_Format(PyExc_TypeError,
- "ArrayMethod must have well defined input DTypes. "
+ "ArrayMethod must provide all input and output DTypes. "
"(method: %s)", spec->name);
return -1;
}
@@ -231,10 +204,10 @@ validate_spec(PyArrayMethod_Spec *spec)
"(method: %s)", spec->dtypes[i], spec->name);
return -1;
}
- if (NPY_DT_is_abstract(spec->dtypes[i]) && i < spec->nin) {
+ if (NPY_DT_is_abstract(spec->dtypes[i])) {
PyErr_Format(PyExc_TypeError,
- "abstract DType %S are currently not allowed for inputs."
- "(method: %s defined at %s)", spec->dtypes[i], spec->name);
+ "abstract DType %S are currently not supported."
+ "(method: %s)", spec->dtypes[i], spec->name);
return -1;
}
}
@@ -323,7 +296,7 @@ fill_arraymethod_from_slots(
PyErr_Format(PyExc_TypeError,
"Must specify output DTypes or use custom "
"`resolve_descriptors` when there are no inputs. "
- "(method: %s defined at %s)", spec->name);
+ "(method: %s)", spec->name);
return -1;
}
}
@@ -370,6 +343,26 @@ fill_arraymethod_from_slots(
}
+/*
+ * Public version of `PyArrayMethod_FromSpec_int` (see below).
+ *
+ * TODO: Error paths will probably need to be improved before a release into
+ * the non-experimental public API.
+ */
+NPY_NO_EXPORT PyObject *
+PyArrayMethod_FromSpec(PyArrayMethod_Spec *spec)
+{
+ for (int i = 0; i < spec->nin + spec->nout; i++) {
+ if (!PyObject_TypeCheck(spec->dtypes[i], &PyArrayDTypeMeta_Type)) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "ArrayMethod spec contained a non DType.");
+ return NULL;
+ }
+ }
+ return (PyObject *)PyArrayMethod_FromSpec_int(spec, 0);
+}
+
+
/**
* Create a new ArrayMethod (internal version).
*
@@ -683,7 +676,7 @@ boundarraymethod__simple_strided_call(
"All arrays must have the same length.");
return NULL;
}
- if (i >= nout) {
+ if (i >= nin) {
if (PyArray_FailUnlessWriteable(
arrays[i], "_simple_strided_call() output") < 0) {
return NULL;
diff --git a/numpy/core/src/multiarray/array_method.h b/numpy/core/src/multiarray/array_method.h
index 3017abf25..b29c7c077 100644
--- a/numpy/core/src/multiarray/array_method.h
+++ b/numpy/core/src/multiarray/array_method.h
@@ -170,6 +170,11 @@ PyArrayMethod_GetMaskedStridedLoop(
NPY_ARRAYMETHOD_FLAGS *flags);
+
+NPY_NO_EXPORT PyObject *
+PyArrayMethod_FromSpec(PyArrayMethod_Spec *spec);
+
+
/*
* TODO: This function is the internal version, and its error paths may
* need better tests when a public version is exposed.
diff --git a/numpy/core/src/multiarray/descriptor.c b/numpy/core/src/multiarray/descriptor.c
index 082876aa2..6a09f92ac 100644
--- a/numpy/core/src/multiarray/descriptor.c
+++ b/numpy/core/src/multiarray/descriptor.c
@@ -2304,6 +2304,33 @@ arraydescr_new(PyTypeObject *subtype,
PyObject *args, PyObject *kwds)
{
if (subtype != &PyArrayDescr_Type) {
+ if (Py_TYPE(subtype) == &PyArrayDTypeMeta_Type &&
+ !(PyType_GetFlags(Py_TYPE(subtype)) & Py_TPFLAGS_HEAPTYPE) &&
+ (NPY_DT_SLOTS((PyArray_DTypeMeta *)subtype)) != NULL) {
+ /*
+ * Appears to be a properly initialized user DType. Allocate
+ * it and initialize the main part as best we can.
+ * TODO: This should probably be a user function, and enforce
+ * things like the `elsize` being correctly set.
+ * TODO: This is EXPERIMENTAL API!
+ */
+ PyArray_DTypeMeta *DType = (PyArray_DTypeMeta *)subtype;
+ PyArray_Descr *descr = (PyArray_Descr *)subtype->tp_alloc(subtype, 0);
+ if (descr == 0) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+ PyObject_Init((PyObject *)descr, subtype);
+ descr->f = &NPY_DT_SLOTS(DType)->f;
+ Py_XINCREF(DType->scalar_type);
+ descr->typeobj = DType->scalar_type;
+ descr->type_num = DType->type_num;
+ descr->flags = NPY_USE_GETITEM|NPY_USE_SETITEM;
+ descr->byteorder = '|'; /* If DType uses it, let it override */
+ descr->elsize = -1; /* Initialize to invalid value */
+ descr->hash = -1;
+ return (PyObject *)descr;
+ }
/* The DTypeMeta class should prevent this from happening. */
PyErr_Format(PyExc_SystemError,
"'%S' must not inherit np.dtype.__new__().", subtype);
diff --git a/numpy/core/src/multiarray/dtypemeta.c b/numpy/core/src/multiarray/dtypemeta.c
index cbde91b76..cd489d5e7 100644
--- a/numpy/core/src/multiarray/dtypemeta.c
+++ b/numpy/core/src/multiarray/dtypemeta.c
@@ -290,7 +290,7 @@ void_common_instance(PyArray_Descr *descr1, PyArray_Descr *descr2)
return descr1;
}
-static int
+NPY_NO_EXPORT int
python_builtins_are_known_scalar_types(
PyArray_DTypeMeta *NPY_UNUSED(cls), PyTypeObject *pytype)
{
diff --git a/numpy/core/src/multiarray/dtypemeta.h b/numpy/core/src/multiarray/dtypemeta.h
index fb772c07d..05e9e2394 100644
--- a/numpy/core/src/multiarray/dtypemeta.h
+++ b/numpy/core/src/multiarray/dtypemeta.h
@@ -8,6 +8,35 @@
#define NPY_DT_PARAMETRIC 1 << 2
+typedef PyArray_Descr *(discover_descr_from_pyobject_function)(
+ PyArray_DTypeMeta *cls, PyObject *obj);
+
+/*
+ * Before making this public, we should decide whether it should pass
+ * the type, or allow looking at the object. A possible use-case:
+ * `np.array(np.array([0]), dtype=np.ndarray)`
+ * Could consider arrays that are not `dtype=ndarray` "scalars".
+ */
+typedef int (is_known_scalar_type_function)(
+ PyArray_DTypeMeta *cls, PyTypeObject *obj);
+
+typedef PyArray_Descr *(default_descr_function)(PyArray_DTypeMeta *cls);
+typedef PyArray_DTypeMeta *(common_dtype_function)(
+ PyArray_DTypeMeta *dtype1, PyArray_DTypeMeta *dtype2);
+typedef PyArray_Descr *(common_instance_function)(
+ PyArray_Descr *dtype1, PyArray_Descr *dtype2);
+
+/*
+ * TODO: These two functions are currently only used for experimental DType
+ * API support. Their relation should be "reversed": NumPy should
+ * always use them internally.
+ * There are open points about "casting safety" though, e.g. setting
+ * elements is currently always unsafe.
+ */
+typedef int(setitemfunction)(PyArray_Descr *, PyObject *, char *);
+typedef PyObject *(getitemfunction)(PyArray_Descr *, char *);
+
+
typedef struct {
/* DType methods, these could be moved into its own struct */
discover_descr_from_pyobject_function *discover_descr_from_pyobject;
@@ -16,6 +45,12 @@ typedef struct {
common_dtype_function *common_dtype;
common_instance_function *common_instance;
/*
+ * Currently only used for experimental user DTypes.
+ * Typing as `void *` until NumPy itself uses these (directly).
+ */
+ setitemfunction *setitem;
+ getitemfunction *getitem;
+ /*
* The casting implementation (ArrayMethod) to convert between two
* instances of this DType, stored explicitly for fast access:
*/
@@ -58,7 +93,10 @@ typedef struct {
NPY_DT_SLOTS(dtype)->default_descr(dtype)
#define NPY_DT_CALL_common_dtype(dtype, other) \
NPY_DT_SLOTS(dtype)->common_dtype(dtype, other)
-
+#define NPY_DT_CALL_getitem(descr, data_ptr) \
+ NPY_DT_SLOTS(NPY_DTYPE(descr))->getitem(descr, data_ptr)
+#define NPY_DT_CALL_setitem(descr, value, data_ptr) \
+ NPY_DT_SLOTS(NPY_DTYPE(descr))->setitem(descr, value, data_ptr)
/*
* This function will hopefully be phased out or replaced, but was convenient
@@ -78,6 +116,10 @@ PyArray_DTypeFromTypeNum(int typenum)
NPY_NO_EXPORT int
+python_builtins_are_known_scalar_types(
+ PyArray_DTypeMeta *cls, PyTypeObject *pytype);
+
+NPY_NO_EXPORT int
dtypemeta_wrap_legacy_descriptor(PyArray_Descr *dtypem);
#endif /* NUMPY_CORE_SRC_MULTIARRAY_DTYPEMETA_H_ */
diff --git a/numpy/core/src/multiarray/experimental_public_dtype_api.c b/numpy/core/src/multiarray/experimental_public_dtype_api.c
new file mode 100644
index 000000000..1e8abe9d6
--- /dev/null
+++ b/numpy/core/src/multiarray/experimental_public_dtype_api.c
@@ -0,0 +1,363 @@
+#include <Python.h>
+
+#define NPY_NO_DEPRECATED_API NPY_API_VERSION
+#define _UMATHMODULE
+#define _MULTIARRAYMODULE
+#include <numpy/npy_common.h>
+#include "numpy/arrayobject.h"
+#include "numpy/ufuncobject.h"
+#include "common.h"
+
+#include "experimental_public_dtype_api.h"
+#include "array_method.h"
+#include "dtypemeta.h"
+#include "array_coercion.h"
+#include "convert_datatype.h"
+
+
+#define EXPERIMENTAL_DTYPE_API_VERSION 1
+
+
+typedef struct{
+ PyTypeObject *typeobj; /* type of python scalar or NULL */
+ int flags; /* flags, including parametric and abstract */
+ /* NULL terminated cast definitions. Use NULL for the newly created DType */
+ PyArrayMethod_Spec **casts;
+ PyType_Slot *slots;
+} PyArrayDTypeMeta_Spec;
+
+
+
+static PyArray_DTypeMeta *
+dtype_does_not_promote(
+ PyArray_DTypeMeta *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(other))
+{
+ /* `other` is guaranteed not to be `self`, so we don't have to do much... */
+ Py_INCREF(Py_NotImplemented);
+ return (PyArray_DTypeMeta *)Py_NotImplemented;
+}
+
+
+static PyArray_Descr *
+discover_as_default(PyArray_DTypeMeta *cls, PyObject *NPY_UNUSED(obj))
+{
+ return NPY_DT_CALL_default_descr(cls);
+}
+
+
+static PyArray_Descr *
+use_new_as_default(PyArray_DTypeMeta *self)
+{
+ PyObject *res = PyObject_CallObject((PyObject *)self, NULL);
+ if (res == NULL) {
+ return NULL;
+ }
+ /*
+ * Lets not trust that the DType is implemented correctly
+ * TODO: Should probably do an exact type-check (at least unless this is
+ * an abstract DType).
+ */
+ if (!PyArray_DescrCheck(res)) {
+ PyErr_Format(PyExc_RuntimeError,
+ "Instantiating %S did not return a dtype instance, this is "
+ "invalid (especially without a custom `default_descr()`).",
+ self);
+ Py_DECREF(res);
+ return NULL;
+ }
+ PyArray_Descr *descr = (PyArray_Descr *)res;
+ /*
+ * Should probably do some more sanity checks here on the descriptor
+ * to ensure the user is not being naughty. But in the end, we have
+ * only limited control anyway.
+ */
+ return descr;
+}
+
+
+static int
+legacy_setitem_using_DType(PyObject *obj, void *data, void *arr)
+{
+ if (arr == NULL) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Using legacy SETITEM with NULL array object is only "
+ "supported for basic NumPy DTypes.");
+ return -1;
+ }
+ setitemfunction *setitem;
+ setitem = NPY_DT_SLOTS(NPY_DTYPE(PyArray_DESCR(arr)))->setitem;
+ return setitem(PyArray_DESCR(arr), obj, data);
+}
+
+
+static PyObject *
+legacy_getitem_using_DType(void *data, void *arr)
+{
+ if (arr == NULL) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Using legacy SETITEM with NULL array object is only "
+ "supported for basic NumPy DTypes.");
+ return NULL;
+ }
+ getitemfunction *getitem;
+ getitem = NPY_DT_SLOTS(NPY_DTYPE(PyArray_DESCR(arr)))->getitem;
+ return getitem(PyArray_DESCR(arr), data);
+}
+
+
+/*
+ * The descr->f structure used user-DTypes. Some functions may be filled
+ * from the user in the future and more could get defaults for compatibility.
+ */
+PyArray_ArrFuncs default_funcs = {
+ .setitem = &legacy_setitem_using_DType,
+ .getitem = &legacy_getitem_using_DType
+};
+
+
+/* other slots are in order, so keep only last around: */
+#define NUM_DTYPE_SLOTS 7
+
+
+int
+PyArrayInitDTypeMeta_FromSpec(
+ PyArray_DTypeMeta *DType, PyArrayDTypeMeta_Spec *spec)
+{
+ if (!PyObject_TypeCheck(DType, &PyArrayDTypeMeta_Type)) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Passed in DType must be a valid (initialized) DTypeMeta "
+ "instance!");
+ return -1;
+ }
+
+ if (spec->typeobj == NULL || !PyType_Check(spec->typeobj)) {
+ PyErr_SetString(PyExc_TypeError,
+ "Not giving a type object is currently not supported, but "
+ "is expected to be supported eventually. This would mean "
+ "that e.g. indexing a NumPy array will return a 0-D array "
+ "and not a scalar.");
+ return -1;
+ }
+
+ if (DType->dt_slots != NULL) {
+ PyErr_Format(PyExc_RuntimeError,
+ "DType %R appears already registered?", DType);
+ return -1;
+ }
+
+ /* Check and handle flags: */
+ if (spec->flags & ~(NPY_DT_PARAMETRIC|NPY_DT_ABSTRACT)) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "invalid DType flags specified, only parametric and abstract "
+ "are valid flags for user DTypes.");
+ return -1;
+ }
+
+ DType->flags = spec->flags;
+ DType->dt_slots = PyMem_Calloc(1, sizeof(NPY_DType_Slots));
+ if (DType->dt_slots == NULL) {
+ return -1;
+ }
+
+ /* Set default values (where applicable) */
+ NPY_DT_SLOTS(DType)->discover_descr_from_pyobject = &discover_as_default;
+ NPY_DT_SLOTS(DType)->is_known_scalar_type = (
+ &python_builtins_are_known_scalar_types);
+ NPY_DT_SLOTS(DType)->default_descr = use_new_as_default;
+ NPY_DT_SLOTS(DType)->common_dtype = dtype_does_not_promote;
+ /* May need a default for non-parametric? */
+ NPY_DT_SLOTS(DType)->common_instance = NULL;
+ NPY_DT_SLOTS(DType)->setitem = NULL;
+ NPY_DT_SLOTS(DType)->getitem = NULL;
+
+ PyType_Slot *spec_slot = spec->slots;
+ while (1) {
+ int slot = spec_slot->slot;
+ void *pfunc = spec_slot->pfunc;
+ spec_slot++;
+ if (slot == 0) {
+ break;
+ }
+ if (slot > NUM_DTYPE_SLOTS || slot < 0) {
+ PyErr_Format(PyExc_RuntimeError,
+ "Invalid slot with value %d passed in.", slot);
+ return -1;
+ }
+ /*
+ * It is up to the user to get this right, and slots are sorted
+ * exactly like they are stored right now:
+ */
+ void **current = (void **)(&(
+ NPY_DT_SLOTS(DType)->discover_descr_from_pyobject));
+ current += slot - 1;
+ *current = pfunc;
+ }
+ if (NPY_DT_SLOTS(DType)->setitem == NULL
+ || NPY_DT_SLOTS(DType)->getitem == NULL) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "A DType must provide a getitem/setitem (there may be an "
+ "exception here in the future if no scalar type is provided)");
+ return -1;
+ }
+
+ /*
+ * Now that the spec is read we can check that all required functions were
+ * defined by the user.
+ */
+ if (spec->flags & NPY_DT_PARAMETRIC) {
+ if (NPY_DT_SLOTS(DType)->common_instance == NULL ||
+ NPY_DT_SLOTS(DType)->discover_descr_from_pyobject
+ == &discover_as_default) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Parametric DType must define a common-instance and "
+ "descriptor discovery function!");
+ return -1;
+ }
+ }
+ NPY_DT_SLOTS(DType)->f = default_funcs;
+ /* invalid type num. Ideally, we get away with it! */
+ DType->type_num = -1;
+
+ /*
+ * Handle the scalar type mapping.
+ */
+ Py_INCREF(spec->typeobj);
+ DType->scalar_type = spec->typeobj;
+ if (PyType_GetFlags(spec->typeobj) & Py_TPFLAGS_HEAPTYPE) {
+ if (PyObject_SetAttrString((PyObject *)DType->scalar_type,
+ "__associated_array_dtype__", (PyObject *)DType) < 0) {
+ Py_DECREF(DType);
+ return -1;
+ }
+ }
+ if (_PyArray_MapPyTypeToDType(DType, DType->scalar_type, 0) < 0) {
+ Py_DECREF(DType);
+ return -1;
+ }
+
+ /* Ensure cast dict is defined (not sure we have to do it here) */
+ NPY_DT_SLOTS(DType)->castingimpls = PyDict_New();
+ if (NPY_DT_SLOTS(DType)->castingimpls == NULL) {
+ return -1;
+ }
+ /*
+ * And now, register all the casts that are currently defined!
+ */
+ PyArrayMethod_Spec **next_meth_spec = spec->casts;
+ while (1) {
+ PyArrayMethod_Spec *meth_spec = *next_meth_spec;
+ next_meth_spec++;
+ if (meth_spec == NULL) {
+ break;
+ }
+ /*
+ * The user doesn't know the name of DType yet, so we have to fill it
+ * in for them!
+ */
+ for (int i=0; i < meth_spec->nin + meth_spec->nout; i++) {
+ if (meth_spec->dtypes[i] == NULL) {
+ meth_spec->dtypes[i] = DType;
+ }
+ }
+ /* Register the cast! */
+ int res = PyArray_AddCastingImplementation_FromSpec(meth_spec, 0);
+
+ /* Also clean up again, so nobody can get bad ideas... */
+ for (int i=0; i < meth_spec->nin + meth_spec->nout; i++) {
+ if (meth_spec->dtypes[i] == DType) {
+ meth_spec->dtypes[i] = NULL;
+ }
+ }
+
+ if (res < 0) {
+ return -1;
+ }
+ }
+
+ if (NPY_DT_SLOTS(DType)->within_dtype_castingimpl == NULL) {
+ /*
+ * We expect this for now. We should have a default for DType that
+ * only supports simple copy (and possibly byte-order assuming that
+ * they swap the full itemsize).
+ */
+ PyErr_SetString(PyExc_RuntimeError,
+ "DType must provide a function to cast (or just copy) between "
+ "its own instances!");
+ return -1;
+ }
+
+ /* And finally, we have to register all the casts! */
+ return 0;
+}
+
+
+/* Function is defined in umath/dispatching.c (same/one compilation unit) */
+NPY_NO_EXPORT int
+PyUFunc_AddLoop(PyUFuncObject *ufunc, PyObject *info, int ignore_duplicate);
+
+static int
+PyUFunc_AddLoopFromSpec(PyObject *ufunc, PyArrayMethod_Spec *spec)
+{
+ if (!PyObject_TypeCheck(ufunc, &PyUFunc_Type)) {
+ PyErr_SetString(PyExc_TypeError,
+ "ufunc object passed is not a ufunc!");
+ return -1;
+ }
+ PyBoundArrayMethodObject *bmeth =
+ (PyBoundArrayMethodObject *)PyArrayMethod_FromSpec(spec);
+ if (bmeth == NULL) {
+ return -1;
+ }
+ int nargs = bmeth->method->nin + bmeth->method->nout;
+ PyObject *dtypes = PyArray_TupleFromItems(
+ nargs, (PyObject **)bmeth->dtypes, 1);
+ if (dtypes == NULL) {
+ return -1;
+ }
+ PyObject *info = PyTuple_Pack(2, dtypes, bmeth->method);
+ Py_DECREF(bmeth);
+ Py_DECREF(dtypes);
+ if (info == NULL) {
+ return -1;
+ }
+ return PyUFunc_AddLoop((PyUFuncObject *)ufunc, info, 0);
+}
+
+
+NPY_NO_EXPORT PyObject *
+_get_experimental_dtype_api(PyObject *NPY_UNUSED(mod), PyObject *arg)
+{
+ static void *experimental_api_table[] = {
+ &PyUFunc_AddLoopFromSpec,
+ &PyArrayDTypeMeta_Type,
+ &PyArrayInitDTypeMeta_FromSpec,
+ NULL,
+ };
+
+ char *env = getenv("NUMPY_EXPERIMENTAL_DTYPE_API");
+ if (env == NULL || strcmp(env, "1") != 0) {
+ PyErr_Format(PyExc_RuntimeError,
+ "The new DType API is currently in an exploratory phase and "
+ "should NOT be used for production code. "
+ "Expect modifications and crashes! "
+ "To experiment with the new API you must set "
+ "`NUMPY_EXPERIMENTAL_DTYPE_API=1` as an environment variable.");
+ return NULL;
+ }
+
+ long version = PyLong_AsLong(arg);
+ if (error_converting(version)) {
+ return NULL;
+ }
+ if (version != EXPERIMENTAL_DTYPE_API_VERSION) {
+ PyErr_Format(PyExc_RuntimeError,
+ "Experimental DType API version %d requested, but NumPy "
+ "is exporting version %d. Recompile your DType and/or upgrade "
+ "NumPy to match.",
+ version, EXPERIMENTAL_DTYPE_API_VERSION);
+ return NULL;
+ }
+
+ return PyCapsule_New(&experimental_api_table,
+ "experimental_dtype_api_table", NULL);
+}
diff --git a/numpy/core/src/multiarray/experimental_public_dtype_api.h b/numpy/core/src/multiarray/experimental_public_dtype_api.h
new file mode 100644
index 000000000..270cb82bf
--- /dev/null
+++ b/numpy/core/src/multiarray/experimental_public_dtype_api.h
@@ -0,0 +1,18 @@
+/*
+ * This file exports the experimental dtype API as exposed via the
+ * `numpy/core/include/numpy/experimental_dtype_api.h`
+ * header file.
+ *
+ * This file is a stub, all important definitions are in the code file.
+ *
+ * NOTE: This file is considered in-flux, exploratory and transitional.
+ */
+
+#ifndef NUMPY_CORE_SRC_MULTIARRAY_EXPERIMENTAL_PUBLIC_DTYPE_API_H_
+#define NUMPY_CORE_SRC_MULTIARRAY_EXPERIMENTAL_PUBLIC_DTYPE_API_H_
+
+NPY_NO_EXPORT PyObject *
+_get_experimental_dtype_api(PyObject *mod, PyObject *arg);
+
+
+#endif /* NUMPY_CORE_SRC_MULTIARRAY_EXPERIMENTAL_PUBLIC_DTYPE_API_H_ */
diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c
index 5ceed1678..d211f01bc 100644
--- a/numpy/core/src/multiarray/multiarraymodule.c
+++ b/numpy/core/src/multiarray/multiarraymodule.c
@@ -68,6 +68,7 @@ NPY_NO_EXPORT int NPY_NUMUSERTYPES = 0;
#include "typeinfo.h"
#include "get_attr_string.h"
+#include "experimental_public_dtype_api.h" /* _get_experimental_dtype_api */
/*
*****************************************************************************
@@ -4419,7 +4420,9 @@ static struct PyMethodDef array_module_methods[] = {
{"_discover_array_parameters", (PyCFunction)_discover_array_parameters,
METH_VARARGS | METH_KEYWORDS, NULL},
{"_get_castingimpl", (PyCFunction)_get_castingimpl,
- METH_VARARGS | METH_KEYWORDS, NULL},
+ METH_VARARGS | METH_KEYWORDS, NULL},
+ {"_get_experimental_dtype_api", (PyCFunction)_get_experimental_dtype_api,
+ METH_O, NULL},
/* from umath */
{"frompyfunc",
(PyCFunction) ufunc_frompyfunc,