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
25 changes: 16 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ on:
permissions:
contents: read

# This library targets PHP 8.4 only - it rides z-engine's engine bindings.
env:
PHP_MINOR: '8.4'

# z-engine tracks one PHP minor per release line; the "8.4.x-dev || 8.5.x-dev"
# constraint lets Composer resolve the line matching each runner's PHP, so the
# suite runs on PHP 8.4 and 8.5 in parallel.
jobs:
tests:
name: Tests
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- uses: actions/checkout@v7

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: ${{ matrix.php }}
extensions: ffi
ini-values: ffi.enable=1, zend.assertions=1, opcache.jit=off
coverage: none
Expand All @@ -35,13 +38,17 @@ jobs:
run: composer test

static-analysis:
name: PHPStan (level max)
name: PHPStan (level max, PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: ${{ matrix.php }}
extensions: ffi
ini-values: ffi.enable=1, zend.assertions=1, opcache.jit=off
coverage: none
Expand All @@ -55,7 +62,7 @@ jobs:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: '8.4'
extensions: ffi
ini-values: ffi.enable=1, zend.assertions=1, opcache.jit=off
coverage: none
Expand Down
50 changes: 27 additions & 23 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@ to install `create_object`, `do_operation` and `compare` handlers directly onto
`Matrix` class entry through PHP FFI. The matrix arithmetic underneath is ordinary,
readable PHP; the only unusual part is how the engine is convinced to call it.

## The one rule that is non-negotiable: the PHP version is pinned

`composer.json` requires **`php: ~8.4.0`** — an exact minor, not a floor. z-engine
reads engine structures (`zend_class_entry`, `zval`, `zend_object_handlers`) by byte
offset, and those offsets change on every PHP minor release. Running against the
wrong minor does not throw a nice exception; it reads and writes the wrong memory.

`ZEngine\Core::init()` (called from `bootstrap.php`) enforces the match and aborts
with a clear message. **Never "fix" an initialization failure by loosening the PHP
constraint, skipping `Core::init()`, or defeating the guard.** If the environment's
PHP does not satisfy `~8.4.0`, the environment is wrong — say so and stop.

The same applies in reverse: z-engine's own FFI definitions live on per-minor
branches, so this package must consume the z-engine line built for PHP 8.4.
## The one rule that is non-negotiable: PHP minor and z-engine line move together

`composer.json` requires **`php: ^8.4`**, and PHP 8.4 and 8.5 are supported **in
parallel** — each minor riding its own z-engine line. z-engine reads engine
structures (`zend_class_entry`, `zval`, `zend_object_handlers`) by byte offset, and
those offsets change on every PHP minor release. Running against the wrong minor
does not throw a nice exception; it reads and writes the wrong memory.

That is why z-engine is required as **`8.4.x-dev || 8.5.x-dev`**: Composer resolves
the line matching the running PHP (the `8.4` branch on PHP 8.4, `master` — aliased
`8.5.x-dev` — on PHP 8.5). `ZEngine\Core::init()` (called from `bootstrap.php`)
enforces the exact match and aborts with a clear message. **Never "fix" an
initialization failure by loosening the constraints past the minors z-engine has
definitions for, skipping `Core::init()`, or defeating the guard.** If the
environment's PHP does not satisfy `^8.4` (8.4 or 8.5), the environment is wrong —
say so and stop.

## Running tests

Expand Down Expand Up @@ -139,7 +141,7 @@ tests/Functional/*.phpt the functional suite, one behaviour per file
phpunit.xml.dist PHPUnit 12 config (suite points at tests/, suffix .phpt)
phpstan.dist.neon static analysis config, level max
.php-cs-fixer.dist.php coding standards config (PER-CS2.0)
.github/workflows/ci.yml jobs: tests, static-analysis, coding-standards — PHP 8.4
.github/workflows/ci.yml jobs: tests, static-analysis, coding-standards — PHP 8.4 and 8.5
```

`src/Matrix.php` is the whole library. There is no framework here to hide behind: a
Expand All @@ -152,7 +154,7 @@ at once.
are **static**. They do throw — `InvalidArgumentException` for a dimension
mismatch, `LogicException` for an operand combination the class does not
implement — but be precise about what that means for a caller: these hooks run
inside an **FFI callback**, and PHP 8.4 does not let an exception cross that
inside an **FFI callback**, and PHP does not let an exception cross that
boundary. The engine reports the exception and then aborts with
`Fatal error: Throwing from FFI callbacks is not allowed`. The script dies with
exit code 255; a userland `try`/`catch` around `$a + $b` **will not catch it**.
Expand All @@ -177,7 +179,7 @@ at once.
feat(matrix): support element-wise exponentiation by scalar
fix(bootstrap): install create_object handler before do_operation
test(tests): cover division by zero
ci: run the suite on PHP 8.4 with ffi.enable=1
ci: run the suite on PHP 8.4 and 8.5 with ffi.enable=1
docs: rewrite the README in the z-engine style
```

Expand All @@ -188,11 +190,13 @@ proposing a change rather than hand-formatting.

## Dependency policy

- `lisachenko/z-engine` is required as **`dev-master`**. There is no stable tag with
PHP 8.4 support yet, so the root `composer.json` also carries
- `lisachenko/z-engine` is required as **`8.4.x-dev || 8.5.x-dev`** — one dev line
per supported PHP minor, resolved by Composer to match the running PHP. Those are
development branches, so the root `composer.json` also carries
`"minimum-stability": "dev"` with `"prefer-stable": true` — Composer resolves
development stability only at the root level, so consumers need the same pair.
- PHP stays pinned at `~8.4.0`, in lockstep with the z-engine line this package
tracks. Bumping one without the other is never correct.
- When z-engine ships a stable PHP 8.4 release, both the constraint and the
root stability flags should be tightened in a single change.
- PHP stays at `^8.4`, in lockstep with the set of z-engine lines this package
tracks: a new PHP minor is added here only together with the z-engine line built
for it, and never one without the other.
- When z-engine ships stable releases for the supported minors, the constraint and
the root stability flags should be tightened in a single change.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

[![CI](https://img.shields.io/github/actions/workflow/status/lisachenko/native-php-matrix/ci.yml?branch=master&label=CI)](https://github.com/lisachenko/native-php-matrix/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/lisachenko/native-php-matrix.svg)](https://github.com/lisachenko/native-php-matrix/releases/latest)
[![PHP Version](https://img.shields.io/badge/php-8.4-8892BF.svg)](https://php.net/)
[![PHP Version](https://img.shields.io/badge/php-8.4%20%7C%208.5-8892BF.svg)](https://php.net/)
[![License](https://img.shields.io/packagist/l/lisachenko/native-php-matrix.svg)](https://packagist.org/packages/lisachenko/native-php-matrix)
[![PHPStan](https://img.shields.io/badge/PHPStan-level%20max-brightgreen.svg)](https://phpstan.org/)

Expand Down Expand Up @@ -39,7 +39,7 @@ The trick is [lisachenko/z-engine](https://github.com/lisachenko/z-engine), whic

Every operation returns a **new** `Matrix`; the class is `final` and its state is `readonly`, so nothing is ever mutated in place. The constructor validates that you passed a rectangular list of rows holding only `int`/`float` cells and raises a catchable `InvalidArgumentException` when it does not.

Operator-level failures are a different story, and it is worth being blunt about it: a dimension mismatch (`InvalidArgumentException`) or an unimplemented operand combination such as `$a - 2` (`LogicException`) is raised *inside an FFI callback*, and PHP 8.4 does not allow an exception to cross that boundary. The engine prints the exception and then halts with `Fatal error: Throwing from FFI callbacks is not allowed`. You cannot `try`/`catch` it — check your dimensions before you multiply.
Operator-level failures are a different story, and it is worth being blunt about it: a dimension mismatch (`InvalidArgumentException`) or an unimplemented operand combination such as `$a - 2` (`LogicException`) is raised *inside an FFI callback*, and PHP does not allow an exception to cross that boundary. The engine prints the exception and then halts with `Fatal error: Throwing from FFI callbacks is not allowed`. You cannot `try`/`catch` it — check your dimensions before you multiply.

Generics are honest about arithmetic: `Matrix<int>` divided by `2` is a `Matrix<int|float>`, because PHP's `/` widens. Nothing pretends to preserve `T` where the maths does not.

Expand All @@ -55,18 +55,18 @@ The maths is plain PHP. The magic is only in getting the engine to call it.

## Requirements

- **PHP `~8.4.0`** — an exact minor, not a floor. Z-Engine reads engine structures by byte offset and those offsets change on every PHP minor release; `Core::init()` refuses to boot on a mismatch rather than corrupting memory.
- **PHP `^8.4`** — 8.4 and 8.5 are supported in parallel. Z-Engine reads engine structures by byte offset and those offsets change on every PHP minor release, so each minor rides its own Z-Engine line; `Core::init()` refuses to boot on a mismatch rather than corrupting memory.
- **`ext-ffi` enabled**, with `ffi.enable=1` for CLI usage.
- **x64, non-thread-safe (NTS)** build — the same platform limitations as [Z-Engine](https://github.com/lisachenko/z-engine#requirements--support-matrix).
- The **matching Z-Engine minor branch**. This package tracks the PHP 8.4 line; mixing a Z-Engine built for another minor is not a configuration choice, it is undefined behaviour.
- The **matching Z-Engine minor branch**. Composer resolves it for you from the `8.4.x-dev || 8.5.x-dev` constraint; mixing a Z-Engine built for another minor is not a configuration choice, it is undefined behaviour.

## Installation

```bash
composer require lisachenko/native-php-matrix:dev-master
```

Z-Engine ships stable PHP 8.4 releases (`8.4.0` and up), and this package requires it as `~8.4.0` (z-engine minors track PHP minors and are not interchangeable) — no dev dependency there anymore. Until the next native-php-matrix release is tagged, the package itself is still consumed from `dev-master`; Composer only resolves development stability at the **root** level, so your `composer.json` needs:
This package requires Z-Engine as `8.4.x-dev || 8.5.x-dev` — z-engine minors track PHP minors and are not interchangeable, so Composer resolves the line matching your PHP automatically (the `8.4` branch on PHP 8.4, `master` on PHP 8.5). Those are development branches, and the package itself is consumed from `dev-master`; Composer only resolves development stability at the **root** level, so your `composer.json` needs:

```json
{
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
}
],
"require": {
"php": "~8.4.0",
"php": "^8.4",
"ext-ffi": "*",
"lisachenko/z-engine": "~8.4.0"
"lisachenko/z-engine": "8.4.x-dev || 8.5.x-dev"
},
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75",
"phpstan/phpstan": "^2.1",
Expand Down