Skip to content

Maintenance overhaul: PHP 8.4, z-engine dev-master, modern README, CI, PHPStan generics - #13

Merged
lisachenko merged 7 commits into
masterfrom
claude/native-matrix-maintenance-p0t7dm
Jul 31, 2026
Merged

Maintenance overhaul: PHP 8.4, z-engine dev-master, modern README, CI, PHPStan generics#13
lisachenko merged 7 commits into
masterfrom
claude/native-matrix-maintenance-p0t7dm

Conversation

@lisachenko

Copy link
Copy Markdown
Owner

Full maintenance pass bringing the library from its 2021 state (PHP 7.4/8.0, z-engine 0.9) to the modern z-engine stack.

Dependencies & runtime

  • php: ~8.4.0, ext-ffi: *, lisachenko/z-engine: dev-master (z-engine master has no branch alias or 8.4-compatible tag yet — tracked in chore: relax the z-engine dev-master pin once a stable 8.4 tag exists #12), root minimum-stability: dev + prefer-stable: true
  • Dev tooling aligned with z-engine: PHPUnit ^12.5, PHPStan ^2.1, php-cs-fixer ^3.75, with test / phpstan / cs:check / cs:fix composer scripts

Code (src/Matrix.php, bootstrap.php)

  • Matrix is now final with readonly properties and full constructor validation (empty, non-list, ragged, and non-int|float cells throw InvalidArgumentException) — previously new Matrix([]) crashed
  • Native int|float parameter types on scalar operations; numeric-string operands ($matrix * '2') still work via explicit coercion in the operator hook
  • PHPStan generics: @template-covariant T of int|float, internal state typed non-empty-list<non-empty-list<T>>; arithmetic results honestly widen to Matrix<int|float>. Level max, no baseline, zero errors
  • Fixed the inverted init guard in bootstrap.php: !class_exists(Core::class, false)!isset(Core::$executor) (the actual "already initialized" probe)
  • multiply() now transposes the multiplier once instead of calling array_column() inside the inner loop

Tests

CI/CD & tooling

  • New .github/workflows/ci.yml (replaces phpunit.yml): tests, static-analysis, coding-standards jobs on PHP 8.4 with FFI, checkout@v4 / setup-php@v2 / composer-install@v3, pull_request trigger and least-privilege permissions
  • dependabot.yml now also watches the github-actions ecosystem
  • New phpstan.dist.neon, .php-cs-fixer.dist.php (PER-CS2.0, z-engine ruleset), .editorconfig

Docs & Claude onboarding

  • README rewritten in z-engine marketing style: centered header, modern badge block (fixes the dead shields.io workflow badge), capability table now documenting the ** operator, accurate PHP 8.4/FFI requirements, dev-master install instructions, roadmap
  • New CLAUDE.md (modeled on z-engine's AGENTS.md) and .claude/agents/ definitions (test-runner, code-reviewer, phpt-author)
  • Docs now correctly state that operator-hook exceptions are not catchable on PHP 8.4 (throwing from FFI callbacks is a fatal error) — verified empirically

Roadmap issues registered

#6 (dimension access), #7 (count()), #8 (foreach iteration), #9 (casts), #10 (var_dump debug info), #11 (FFI BLAS backend), #12 (relax dev-master pin)

Verification

  • 12/12 .phpt tests pass on PHP 8.4.19 NTS against z-engine dev-master (0e544dc)
  • PHPStan level max: no errors; php-cs-fixer: clean
  • 24/24 behavioral sanity checks (+ - * / ** ==, scalar-left multiplication, numeric strings, all validation throws)

🤖 Generated with Claude Code

https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1


Generated by Claude Code

claude added 7 commits July 31, 2026 21:17
Z-Engine reads Zend Engine structures by byte offset, and those offsets
change on every PHP minor release, so the constraint is pinned to the exact
minor (~8.4.0) rather than a floor. There is no stable z-engine tag with PHP
8.4 support yet, so it is consumed from dev-master, which requires the root
package to carry minimum-stability "dev" with prefer-stable.

BREAKING CHANGE: PHP 7.4 and 8.0 are no longer supported; the package now
requires PHP ~8.4.0, ext-ffi and lisachenko/z-engine dev-master.

Also declares ext-ffi explicitly, adds the test/phpstan/cs:check/cs:fix
scripts and enables sort-packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
The class is now final with private readonly state, so a Matrix cannot be
mutated after construction and every operation returns a new instance.

The constructor validates its input up front instead of trusting the caller:
the outer array must be a non-empty list, every row must be a non-empty list
of the same length, and every cell must be an int or a float. Previously a
ragged or empty array produced a broken object that failed later, far from
the cause.

Scalar operands are now declared natively as int|float rather than being
checked with is_numeric() inside each method. Because the engine can hand a
numeric string to the operator hook, __doOperation() funnels operands through
a new asScalar() helper that coerces numeric strings, keeping "$matrix * '2'"
working under strict_types=1.

multiply() extracts the multiplier's columns once and reuses them across
every row of the left operand, instead of calling array_column() inside the
inner loop; the result is unchanged.

Adds PHPStan level-max generics (@template-covariant T of int|float). The
annotations stay honest about widening: division and exponentiation can turn
a Matrix<int> into a Matrix<int|float>, so operations return Matrix<int|float>
rather than pretending T is preserved.

bootstrap.php now probes the engine state with isset(Core::$executor) instead
of class_exists(Core::class, false). The old guard only asked whether the
class was loaded, which says nothing about whether another package had already
called Core::init(); $executor is a typed static assigned only by init(), so
an uninitialised property is the accurate "nobody has booted the engine" signal.

BREAKING CHANGE: Matrix is now final and cannot be extended, its constructor
rejects empty, ragged and non-numeric input with InvalidArgumentException, and
the scalar methods accept int|float instead of any numeric value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
…nit 12

testFailsOnSubtractingIncompatibleMatrices used the "+" operator, so it
asserted the addition failure path and the subtraction guard was never
actually exercised. It now uses "-".

Adds testFailsOnEmptyMatrix and testFailsOnRaggedMatrix to cover the new
constructor validation. These use try/catch because constructor validation is
ordinary userland code; the incompatible-dimension tests cannot, because an
exception thrown from an operator crosses an FFI callback boundary that PHP
8.4 refuses to let it cross, and surfaces as a fatal error instead.

Every .phpt now carries an explicit --INI-- section so the child processes
never depend on the parent's php.ini:

  - ffi.enable=1, which cannot be switched on at runtime
  - opcache.jit=off, because the JIT rewrites the executor internals the
    hooks patch
  - error_reporting=E_ALL & ~E_DEPRECATED, because z-engine dev-master still
    declares implicitly nullable parameters (ZEngine\Type\OpLine::__construct)
    which PHP 8.4 reports as a deprecation. PHPUnit's .phpt runner forces
    display_errors=1, so without it that dependency deprecation is prepended
    to the captured output of every test and each --EXPECT-- block fails on
    noise unrelated to this library. It can be dropped once z-engine declares
    those parameters ?Type.

phpunit.xml.dist moves to the PHPUnit 12.5 schema: the coverage/logging blocks
are replaced by <source>, and warnings, notices and deprecations are now
surfaced rather than silently swallowed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
PHPStan runs at level max over src and bootstrap.php with no baseline.
treatPhpDocTypesAsCertain is deliberately left false: the constructor's
runtime validation re-checks types the docblocks already promise, and turning
it on reports those defensive checks as alreadyNarrowedType errors even
though they are the only thing standing between a caller's untyped array and
a malformed Matrix.

The php-cs-fixer ruleset (PER-CS2.0 plus strict types, ordered imports and
minimal operator alignment) mirrors the one used by lisachenko/z-engine, so
both projects format identically.

.gitignore additionally excludes the PHPUnit and php-cs-fixer caches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
The old phpunit.yml built a matrix over PHP 7.4 and 8.0 with lowest/highest
dependency resolution, none of which can work now that the package is pinned
to PHP 8.4 and z-engine dev-master. It is replaced by ci.yml with three jobs:
tests, static-analysis and coding-standards.

All jobs run on PHP 8.4 with ffi.enable=1 and opcache.jit=off, the settings
the library needs to boot at all, and pin actions to current major versions.
Coverage is disabled because the suite is .phpt based and no coverage was
being published anyway.

Dependabot additionally tracks the github-actions ecosystem, so the workflow
action versions stay current alongside Composer dependencies.

BREAKING CHANGE: CI no longer tests PHP 7.4 or 8.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
Rewrites the README around what the library actually is: the first userland
PHP extension implementing real operator overloading, with no C and no PECL
build. Adds z-engine-style badges (the CI badge now points at ci.yml, not the
retired workflow), a requirements section that explains why the PHP version is
pinned rather than merely stating it, worked usage examples for every supported
operator, and a roadmap of the engine handlers not yet implemented.

Documents the failure semantics precisely: constructor validation raises a
catchable InvalidArgumentException, but a dimension mismatch or an unsupported
operand combination is raised inside an FFI callback, which PHP 8.4 turns into
a fatal error that userland cannot catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
CLAUDE.md records the contract a contributor or agent needs before touching
this repository: why the PHP version is pinned and must never be loosened to
silence an initialization failure, the three mandatory .phpt INI settings and
the reason for each, the hook contracts, and the rule that generics must stay
honest about arithmetic widening the cell type.

Adds three scoped agent definitions: test-runner (run and interpret the suite,
treating a segfault as an engine-level bug rather than a flaky test),
code-reviewer (read-only review against the hook, version-pin and generics
rules) and phpt-author (write .phpt tests to the house conventions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01514q9dSHg3UgK6PNdT6Tx1
@lisachenko
lisachenko marked this pull request as ready for review July 31, 2026 21:37
@lisachenko
lisachenko merged commit d6fa07f into master Jul 31, 2026
3 checks passed
@lisachenko
lisachenko deleted the claude/native-matrix-maintenance-p0t7dm branch July 31, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants