Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,25 @@ var_dump($a != $c); // bool(true)

Equality is element-wise and strict. Ordering operators (`<`, `>`) are deliberately not meaningful for matrices — the `compare` hook reports "unordered" rather than inventing a total order.

### Casting

Casts are dispatched by the engine too — the `cast_object` and `get_properties_for` handlers:

```php
$m = new Matrix([[1, 2], [3, 4]]);

var_dump((array) $m); // [[1, 2], [3, 4]] — the rows, not the object's internals

echo (string) $m; // [1, 2]
// [3, 4]

var_dump((bool) $m); // bool(true) — a valid matrix is never empty by construction
```

Numeric casts keep the engine's default behaviour (`(int) $m` gives `1`), and everything that is
not a cast — `var_dump()`, `serialize()`, `var_export()`, `json_encode()`, `get_object_vars()` —
still sees the object exactly as before.

### Beyond operators

The class is a normal PHP object too:
Expand All @@ -154,7 +173,6 @@ Tracked as [GitHub issues](https://github.com/lisachenko/native-php-matrix/issue
- **Array-style row access** — `$matrix[0]` and `$matrix[0][1]` via the `read_dimension` handler
- **`count($matrix)`** — row count through `Countable`, installed at the engine level
- **`foreach` iteration** — row-by-row traversal via the `get_iterator` handler
- **Scalar casts** — `(string)` and `(float)` behaviour through `cast_object`
- **Friendly `var_dump()`** — a `get_debug_info` handler that prints the matrix instead of its internals
- **FFI BLAS backend** — hand multiplication off to a real BLAS library for performance, keeping the pure-PHP path as the fallback

Expand Down
2 changes: 2 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
colors="true"
bootstrap="./vendor/autoload.php"
failOnWarning="true"
failOnSkipped="true"
failOnIncomplete="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerDeprecations="true">
Expand Down
115 changes: 114 additions & 1 deletion src/Matrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
namespace Lisachenko\NativePhpMatrix;

use function array_column;
use function array_filter;

use const ARRAY_FILTER_USE_KEY;

use function array_is_list;
use function array_keys;
use function count;
use function get_mangled_object_vars;
use function implode;

use InvalidArgumentException;

Expand All @@ -28,13 +34,20 @@
use LogicException;

use function sprintf;
use function str_starts_with;

use ZEngine\ClassExtension\Hook\CastObjectHook;
use ZEngine\ClassExtension\Hook\CastType;
use ZEngine\ClassExtension\Hook\CompareValuesHook;
use ZEngine\ClassExtension\Hook\DoOperationHook;
use ZEngine\ClassExtension\Hook\GetPropertiesForHook;
use ZEngine\ClassExtension\Hook\PropertyPurpose;
use ZEngine\ClassExtension\ObjectCastInterface;
use ZEngine\ClassExtension\ObjectCompareValuesInterface;
use ZEngine\ClassExtension\ObjectCreateInterface;
use ZEngine\ClassExtension\ObjectCreateTrait;
use ZEngine\ClassExtension\ObjectDoOperationInterface;
use ZEngine\ClassExtension\ObjectGetPropertiesForInterface;
use ZEngine\System\OpCode;

/**
Expand All @@ -46,7 +59,12 @@
*
* @template-covariant T of int|float
*/
final class Matrix implements ObjectCreateInterface, ObjectDoOperationInterface, ObjectCompareValuesInterface
final class Matrix implements
ObjectCastInterface,
ObjectCompareValuesInterface,
ObjectCreateInterface,
ObjectDoOperationInterface,
ObjectGetPropertiesForInterface
{
use ObjectCreateTrait;

Expand Down Expand Up @@ -385,6 +403,101 @@ public static function __compare(CompareValuesHook $hook): int
return -2;
}

/**
* Performs casting of this object to another type, requested by the engine
*
* Unlike the operation and comparison hooks this handler never throws: it runs inside an FFI
* callback, and PHP escalates any exception crossing that boundary into an engine-level
* fatal error. Cast types that are not implemented here defer to the default engine behaviour
* via {@see CastObjectHook::proceed()} — on the z-engine dev lines this package tracks that
* fall-through behaves exactly like an uninstalled handler, so failed numeric casts propagate
* to the engine caller, which emits its own warning and substitutes the value 1.
*
* @param CastObjectHook $hook Instance of current hook
*
* @return mixed Casted value
*/
public static function __cast(CastObjectHook $hook): mixed
{
$object = $hook->getObject();

if ($object instanceof self) {
switch ($hook->getCastTypeEnum()) {
case CastType::Array:
// PHP 8.4 routes `(array)` casts through the get_properties_for handler (see
// __getFields), the branch stays for engine paths that pass IS_ARRAY directly
return $object->toArray();
case CastType::String:
return $object->toString();
case CastType::Bool:
// A valid matrix holds at least one cell by construction, so it is never "empty"
return true;
default:
break;
}
}

$hook->proceed();

return $hook->getResult();
}

/**
* Returns an array representation of this object for the purpose requested by the engine
*
* PHP 8.4 does not route `(array)` casts through the cast_object handler: they arrive here,
* at the get_properties_for handler, with the ARRAY_CAST purpose. Every other purpose
* (debugging, serialization, var_export, JSON encoding, get_object_vars) keeps the default
* property table, reproduced with get_mangled_object_vars() because the raw hashtable
* returned by the original engine handler cannot cross the hook boundary as a PHP array.
* This handler runs in non-throwing engine contexts and therefore never throws.
*
* One deliberate deviation: get_object_vars() is scope-sensitive by default (a closure bound
* to Matrix would see the private properties), but the calling scope is not recoverable from
* inside this FFI callback, so every caller receives the public view — an empty array.
*
* @param GetPropertiesForHook $hook Instance of current hook
*
* @return array<array-key, mixed> Key-value pairs for the requested purpose
*/
public static function __getFields(GetPropertiesForHook $hook): array
{
$object = $hook->getObject();
$purpose = $hook->getPurposeEnum();

if ($object instanceof self && $purpose === PropertyPurpose::ArrayCast) {
return $object->toArray();
}

if ($purpose === PropertyPurpose::GetObjectVars) {
// The engine hands the returned table to get_object_vars() callers without applying
// any visibility filtering once a custom handler is installed, so only the publicly
// visible entries may be exposed here: every property of Matrix is private, exactly
// like the default handlers would show to an outside caller. Class-scoped callers
// lose their privileged view — see the deviation note in the method docblock
return array_filter(
get_mangled_object_vars($object),
static fn(int|string $key): bool => !is_string($key) || !str_starts_with($key, "\0"),
ARRAY_FILTER_USE_KEY,
);
}

return get_mangled_object_vars($object);
}

/**
* Returns a human-readable representation of this matrix, one row per line
*/
private function toString(): string
{
$rows = [];
foreach ($this->matrix as $row) {
$rows[] = '[' . implode(', ', $row) . ']';
}

return implode("\n", $rows);
}

/**
* Casts an operand received from the engine into a native scalar value, if possible
*
Expand Down
38 changes: 38 additions & 0 deletions tests/Functional/testCanCastMatrixToArray.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
Matrix can be cast to array with "(array)" operator
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

$matrix = new Matrix([[1, 2, 3], [4, 5, 6]]);
var_dump((array) $matrix);
?>
--EXPECT--
array(2) {
[0]=>
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
[1]=>
array(3) {
[0]=>
int(4)
[1]=>
int(5)
[2]=>
int(6)
}
}
23 changes: 23 additions & 0 deletions tests/Functional/testCanCastMatrixToBool.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Matrix can be cast to bool with "(bool)" operator and is always truthy
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

$matrix = new Matrix([[0]]);
var_dump((bool) $matrix);
if ($matrix) {
echo "truthy\n";
}
?>
--EXPECT--
bool(true)
truthy
24 changes: 24 additions & 0 deletions tests/Functional/testCanCastMatrixToNumberUsingEngineFallback.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Numeric casts of a Matrix fall back to the default engine behaviour (warning and substitute value)
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

$matrix = new Matrix([[1, 2], [3, 4]]);
var_dump((int) $matrix);
var_dump((float) $matrix);
?>
--EXPECTREGEX--
Warning: Object of class Lisachenko\\NativePhpMatrix\\Matrix could not be converted to int in .+ on line \d+
int\(1\)

Warning: Object of class Lisachenko\\NativePhpMatrix\\Matrix could not be converted to float in .+ on line \d+
float\(1\)
20 changes: 20 additions & 0 deletions tests/Functional/testCanCastMatrixToString.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Matrix can be cast to string with "(string)" operator
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

$matrix = new Matrix([[1, 2, 3], [4, 5.5, 6]]);
echo (string) $matrix, "\n";
?>
--EXPECT--
[1, 2, 3]
[4, 5.5, 6]
26 changes: 26 additions & 0 deletions tests/Functional/testKeepsDefaultDebugOutput.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Debugging a Matrix keeps the default engine property table with visibility markers
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

$matrix = new Matrix([[1, 2]]);
ob_start();
var_dump($matrix);
$output = ob_get_clean();
var_dump(str_contains($output, '["matrix":"Lisachenko\NativePhpMatrix\Matrix":private]'));
var_dump(str_contains($output, '["rows":"Lisachenko\NativePhpMatrix\Matrix":private]'));
var_dump(str_contains($output, '["columns":"Lisachenko\NativePhpMatrix\Matrix":private]'));
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
get_object_vars() on a Matrix returns the public view even for class-scoped callers (known deviation)
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

// Default engine behaviour would grant a Matrix-scoped closure the private properties, but the
// calling scope is not recoverable inside the get_properties_for FFI callback, so the handler
// deliberately serves every caller the public (empty) view — see Matrix::__getFields()
$matrix = new Matrix([[1, 2]]);
$scoped = Closure::bind(static fn (Matrix $m): array => get_object_vars($m), null, Matrix::class);
var_dump($scoped($matrix));
?>
--EXPECT--
array(0) {
}
20 changes: 20 additions & 0 deletions tests/Functional/testKeepsPropertyVisibilityForGetObjectVars.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
get_object_vars() on a Matrix keeps default property visibility for outside callers
--INI--
ffi.enable=1
opcache.jit=off
error_reporting=E_ALL & ~E_DEPRECATED
--FILE--
<?php
declare(strict_types=1);

use Lisachenko\NativePhpMatrix\Matrix;

include __DIR__ . '/../../vendor/autoload.php';

$matrix = new Matrix([[1, 2]]);
var_dump(get_object_vars($matrix));
?>
--EXPECT--
array(0) {
}