diff --git a/numpower.c b/numpower.c index f2e7507..f4cd122 100644 --- a/numpower.c +++ b/numpower.c @@ -297,6 +297,108 @@ PHP_METHOD(NDArray, gpu) { #endif } +ZEND_BEGIN_ARG_INFO(arginfo_setdtype, 0) + ZEND_ARG_INFO(0, dtype) +ZEND_END_ARG_INFO(); +/** + * @brief Sets the data type of this NDArray in place. + * + * ``` + * setDataType(string $dtype): void + * ``` + * + * Re-casts the array to the requested dtype, releasing the previous + * storage (unless still referenced by a live slice). This method mutates + * `$this` and returns nothing, consistent with the other in-place method + * `fill()`. The device (CPU or GPU) is preserved. If the target dtype + * equals the current dtype, the call is a true no-op with no allocation. + * + * Supported dtypes: float4, float8, float16, float32, float64, float128, + * int8, uint8, int16, uint16, int32, uint32, int64, uint64. + * + * @param dtype Target dtype alias. + * + * @throws \Error If the dtype is unknown. + */ +PHP_METHOD(NDArray, setDataType) { + char *dtype; + size_t dtypeLen = 0; + zval *obj_zval = getThis(); + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(dtype, dtypeLen) + ZEND_PARSE_PARAMETERS_END(); + + NDArray *ndarray = NDArrayFactory_restoreFromZval(obj_zval); + if (ndarray == NULL) { + return; + } + + const char *canonical = type_canonicalize(dtype); + if (canonical == NULL) { + zend_throw_error(NULL, + "Invalid data type '%s'. Supported: float4, float8, float16, " + "float32, float64, float128, int8, uint8, int16, uint16, " + "int32, uint32, int64, uint64", dtype); + return; + } + + /* Fast path: already this dtype — true no-op, no allocation, no swap. */ + if (is_type(NDArray_TYPE(ndarray), canonical)) { + return; + } + + /* NDArray_AsType always returns a fresh, caller-owned, unbuffered + NDArray; the source `ndarray` is left intact. We then swap the new + array into the same buffer slot (preserving the object's uuid) and + release the previous occupant. NDArray_FREE is refcount-aware: if a + live slice still references the old array, its data survives until + the last view is freed. */ + NDArray *casted = NDArray_AsType(ndarray, canonical); + if (casted == NULL) { + return; /* Original NDArray remains untouched. */ + } + + int uuid = NDArray_UUID(ndarray); + NDArray *prev = buffer_replace(uuid, casted); + if (prev) { + NDArray_FREE(prev); + } +} + +ZEND_BEGIN_ARG_INFO(arginfo_datatype, 0) +ZEND_END_ARG_INFO(); +/** + * @brief Returns the data type of this NDArray. + * + * ``` + * dataType(): string + * ``` + * + * One of the supported dtypes: float4, float8, float16, float32, + * float64, float128, int8, uint8, int16, uint16, int32, uint32, + * int64, uint64. + * + * Pair with `setDataType(string $dtype): void`. + */ +PHP_METHOD(NDArray, dataType) { + zval *obj_zval = getThis(); + + ZEND_PARSE_PARAMETERS_START(0, 0) + ZEND_PARSE_PARAMETERS_END(); + + NDArray *ndarray = NDArrayFactory_restoreFromZval(obj_zval); + if (ndarray == NULL) { + return; + } + + /* NDArray_TYPE() points at a canonical static string from src/types.h + (e.g. "float32"); RETVAL_STRINGL copies the bytes into a fresh + PHP string zval so the descriptor's storage is not affected. */ + const char *type = NDArray_TYPE(ndarray); + RETVAL_STRINGL(type, (size_t)strlen(type)); +} + /** * @brief Fills the NDArray with a specified value. * @@ -7422,6 +7524,8 @@ static const zend_function_entry class_NDArray_methods[] = { ZEND_ME(NDArray, gpu, arginfo_gpu, ZEND_ACC_PUBLIC) ZEND_ME(NDArray, cpu, arginfo_cpu, ZEND_ACC_PUBLIC) ZEND_ME(NDArray, isGPU, arginfo_is_gpu, ZEND_ACC_PUBLIC) + ZEND_ME(NDArray, setDataType, arginfo_setdtype, ZEND_ACC_PUBLIC) + ZEND_ME(NDArray, dataType, arginfo_datatype, ZEND_ACC_PUBLIC) ZEND_ME(NDArray, size, arginfo_size, ZEND_ACC_PUBLIC) ZEND_ME(NDArray, count, arginfo_count, ZEND_ACC_PUBLIC) diff --git a/tests/types/029-setdatatype.phpt b/tests/types/029-setdatatype.phpt new file mode 100644 index 0000000..c0d5a55 --- /dev/null +++ b/tests/types/029-setdatatype.phpt @@ -0,0 +1,125 @@ +--TEST-- +NDArray::setDataType() mutates dtype in place; dataType() returns current type +--FILE-- + int32: values truncate, elements become ints; + in-place: same PHP object identity, alias observes the change */ +$a = new NDArray([1.5, 2.5, 3.7], 'float32'); +$aliasA = $a; /* alias must share the object */ +$idA = spl_object_id($a); +$pre = $a->dataType(); +$ret = $a->setDataType('int32'); +$php = $a->toArray(); +$post = $a->dataType(); +$aliasOK = (spl_object_id($aliasA) === $idA) + && ($aliasA->dataType() === 'int32') + && ($aliasA->toArray() === [1, 2, 3]) + && (array_map('gettype', $aliasA->toArray()) === ['integer','integer','integer']); +$ok = ($pre === 'float32') && $post === 'int32' + && (spl_object_id($a) === $idA) /* same PHP object after setDataType */ + && ($ret === null) && $php === [1, 2, 3] + && array_map('gettype', $php) === ['integer', 'integer', 'integer'] + && !$a->isGPU() + && $aliasOK; +echo "float32->int32: pre=$pre post=$post ret=", ($ret === null ? 'null' : 'non-null'), + " values=", json_encode($php), + " ok=", ($ok ? 'OK' : 'BAD'), "\n"; + +/* 2. int32 -> float64: elements become PHP floats (values preserved) */ +$b = new NDArray([1, 2, 3], 'int32'); +$before = $b->dataType(); +$b->setDataType('float64'); +$after = $b->dataType(); +$php = $b->toArray(); +$ok = $before === 'int32' && $after === 'float64' + && array_map('gettype', $php) === ['double', 'double', 'double'] + && $php === [1.0, 2.0, 3.0]; +echo "int32->float64: pre=$before post=$after", + " values=", json_encode($php), + " types=", json_encode(array_map('gettype', $php)), + " ok=", ($ok ? 'OK' : 'BAD'), "\n"; + +/* 3. unknown dtype throws; dataType() and values unchanged */ +$c = new NDArray([1.0, 2.0], 'float64'); +$idC = spl_object_id($c); +$beforeType = $c->dataType(); +$beforeVals = $c->toArray(); +$threw = false; $tclass = ''; +try { $c->setDataType('badtype'); } +catch (Throwable $t) { $threw = true; $tclass = get_class($t); } +$ok = $threw + && (spl_object_id($c) === $idC) /* object NOT replaced on throw */ + && $c->dataType() === $beforeType + && $c->toArray() === $beforeVals + && array_map('gettype', $c->toArray()) === ['double', 'double']; +echo "badtype: threw=", $tclass, " pre=$beforeType post=", $c->dataType(), + " vals=", json_encode($c->toArray()), + " ok=", ($ok ? 'OK' : 'BAD'), "\n"; + +/* 4. same-dtype cast is a true no-op: dtype and values unchanged */ +$d = new NDArray([4, 5, 6], 'int32'); +$idD = spl_object_id($d); +$d->setDataType('int32'); +$ok = (spl_object_id($d) === $idD) /* no-op must not replace object */ + && $d->dataType() === 'int32' + && $d->toArray() === [4, 5, 6] + && array_map('gettype', $d->toArray()) === ['integer','integer','integer'] + && !$d->isGPU(); +echo "no-op: type=", $d->dataType(), " vals=", json_encode($d->toArray()), + " ok=", ($ok ? 'OK' : 'BAD'), "\n"; + +/* 5. 0-D scalar (shape []) stays an NDArray; __toString is "7\n" */ +$e = new NDArray(7.0, 'float64'); +$pre = $e->dataType(); +$e->setDataType('int32'); +$post = $e->dataType(); +$ok = ($e instanceof NDArray) && $e->shape() === [] + && ($pre === 'float64') && ($post === 'int32') + && trim((string)$e) === '7'; +echo "0-d: shape=", json_encode($e->shape()), " pre=$pre post=$post", + " str=", json_encode((string)$e), " ok=", ($ok ? 'OK' : 'BAD'), "\n"; + +/* 6. device preserved: a CPU array stays CPU after setDataType */ +$f = new NDArray([1.0, 2.0, 3.0], 'float64'); +$f->setDataType('float32'); +$ok = ($f instanceof NDArray) && !$f->isGPU() + && $f->toArray() === [1.0, 2.0, 3.0] + && $f->dataType() === 'float32'; +echo "cpu: isGPU=", ($f->isGPU() ? 1 : 0), " type=", $f->dataType(), + " vals=", json_encode($f->toArray()), " ok=", ($ok ? 'OK' : 'BAD'), "\n"; + +/* 7. wrong argument counts reject */ +try { + (new NDArray([1, 2, 3], 'float32'))->setDataType(); + echo "no-arg setDataType: NO-THROW\n"; +} catch (Throwable $t) { + echo "no-arg setDataType threw: ", get_class($t), "\n"; +} +try { + (new NDArray([1, 2, 3], 'float32'))->dataType(123); + echo "dataType(123): NO-THROW\n"; +} catch (Throwable $t) { + echo "dataType(123) threw: ", get_class($t), "\n"; +} +?> +--EXPECT-- +float32->int32: pre=float32 post=int32 ret=null values=[1,2,3] ok=OK +int32->float64: pre=int32 post=float64 values=[1,2,3] types=["double","double","double"] ok=OK +badtype: threw=Error pre=float64 post=float64 vals=[1,2] ok=OK +no-op: type=int32 vals=[4,5,6] ok=OK +0-d: shape=[] pre=float64 post=int32 str="7\n" ok=OK +cpu: isGPU=0 type=float32 vals=[1,2,3] ok=OK +no-arg setDataType threw: ArgumentCountError +dataType(123) threw: ArgumentCountError diff --git a/tests/types/030-setdatatype-gpu.phpt b/tests/types/030-setdatatype-gpu.phpt new file mode 100644 index 0000000..200193e --- /dev/null +++ b/tests/types/030-setdatatype-gpu.phpt @@ -0,0 +1,50 @@ +--TEST-- +NDArray::setDataType() on GPU array stays on GPU and converts values in place +--SKIPIF-- +gpu(); } catch (Error $e) { die('skip ' . $e->getMessage()); } ?> +--FILE-- +cpu() is used only to read values back — it returns a new array and + leaves $g on the GPU, so the isGPU() assertion is still meaningful. */ + +$cases = [ + ['float32', 'int32', [1.5, 2.5, 3.5], [1, 2, 3]], + ['int32', 'float64', [1, 2, 3], [1, 2, 3]], + ['float64', 'float32', [1.5, 2.5, 0.5], [1.5, 2.5, 0.5]], + ['int32', 'int16', [1, 2, 3], [1, 2, 3]], +]; + +foreach ($cases as [$src, $dst, $vals, $expect]) { + $g = (new NDArray($vals, $src))->gpu(); + $pre = $g->dataType(); + $g->setDataType($dst); + $post = $g->dataType(); + $on_gpu = $g->isGPU(); /* still after setDataType() */ + $back = $g->cpu()->toArray(); /* cpu() returns a new array; $g stays GPU */ + $ok = ($g instanceof NDArray) && $pre === $src && $post === $dst && $on_gpu && $back === $expect; + echo "$src->$dst: pre=$pre post=$post", + " isGPU=", ($g->isGPU() ? 1 : 0), + " vals=", json_encode($back), + " ok=", ($ok ? 'OK' : 'BAD'), "\n"; +} + +/* unknown dtype on GPU throws too, leaving the array intact on GPU */ +$g2 = (new NDArray([1.0, 2.0], 'float32'))->gpu(); +$pre2 = $g2->dataType(); +try { + $g2->setDataType('nope'); + echo "badtype: NO-THROW\n"; +} catch (Throwable $t) { + echo "badtype: pre=$pre2 post=", $g2->dataType(), + " isGPU=", ($g2->isGPU() ? 1 : 0), + " vals=", json_encode($g2->cpu()->toArray()), "\n"; +} +?> +--EXPECT-- +float32->int32: pre=float32 post=int32 isGPU=1 vals=[1,2,3] ok=OK +int32->float64: pre=int32 post=float64 isGPU=1 vals=[1,2,3] ok=OK +float64->float32: pre=float64 post=float32 isGPU=1 vals=[1.5,2.5,0.5] ok=OK +int32->int16: pre=int32 post=int16 isGPU=1 vals=[1,2,3] ok=OK +badtype: pre=float32 post=float32 isGPU=1 vals=[1,2]