feat: support PHP 8.4 and 8.5 in parallel from one codebase - #142
Closed
lisachenko wants to merge 21 commits into
Closed
feat: support PHP 8.4 and 8.5 in parallel from one codebase#142lisachenko wants to merge 21 commits into
lisachenko wants to merge 21 commits into
Conversation
PHP 8.5 constant expressions may embed a compiled static closure, which the compiler represents as a zend_ast_op_array node carrying a zend_op_array pointer instead of the child pointers the generic AST shape implies. The OpCache payload relocator has to dereference that node, so the type joins both the emitted-types list and the probed layout ground truth. The rest of the generator needed no change for 8.5: every sliceStructs() regex still matches (zend_closure, accel_time_t, zend_early_binding, zend_persistent_script, zend_file_cache_metainfo), the clang include list is unchanged, and FFI validates all 40 layouts against the C compiler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
Generated by tools/generator against PHP 8.5.9 NTS x64 (release) from the matching php-src tag, ZEND_MODULE_API_NO 20250925. Layout changes carried by these artifacts: zend_attribute gains validation_error (delayed attribute target validation), zend_object_handlers gains clone_obj_with (the `clone with` rework), zend_constant gains filename and attributes, and zend_executor_globals gains the fatal-error backtrace fields. The opcache structs (zend_persistent_script, zend_early_binding, zend_file_cache_metainfo) and zend_closure are byte-identical to 8.4. include/8.4 stays in the tree untouched: the 8.4 maintenance branch merges up into master, and removing it here would turn every merge-up into a modify/delete conflict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
ZEND_ACC_USE_GUARDS moved from 0x800 to 1 << 30 (0x40000000): 0x800 now means only ZEND_ACC_DEPRECATED, so the stale value would have silently mis-read every class with magic property accessors. OpCode gains DECLARE_ATTRIBUTED_CONST (210), which PHP 8.5 emits instead of DECLARE_CONST when a global const carries attributes. No opcode was renumbered. NodeKind is regenerated from the 8.5 enum: AST_OP_ARRAY, AST_CAST_VOID and AST_PIPE are new, AST_CLONE and AST_EXIT are gone (both constructs now compile to a ZEND_AST_CALL on the construct name), and 32 kinds shift as a result. Neither removed kind was referenced outside the table. LiveRange and ArgumentEntry were verified against Zend/zend_compile.h by hand - no engine export exists for those macros - and are unchanged in 8.5. OpCache: three file-cache serializer changes needed a matching port. Attributes carry the new validation_error string; constant-expression ASTs may hold a ZEND_AST_OP_ARRAY node (a compiled static closure) whose op_array pointer must be relocated and walked, or a ZEND_AST_CALLABLE_CONVERT node that only touches execution-only ZEND_MAP_PTR state; and an attributed global constant hangs its attribute table off a ZEND_OP_DATA operand as an IS_PTR literal, which the ordinary literal walk skips. Note that the operand is resolved as a literal index rather than through the engine's RT_CONSTANT() byte-offset math: opcache stores IS_CONST operands as indexes in the file cache and only converts them on load, and this port keeps every opline in its serialized form. The binary format itself is unchanged in 8.5 - same metainfo, same adler32 checksum, same system_id stamp, same bin path layout - so CacheMetaInfo, BinaryCacheFile and SystemId needed no change. The opcache fixture now compiles both new node shapes into the cached binary, so the relocator walks them on every round trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
master now builds against PHP 8.5; the 8.4 line lives on its own maintenance branch. Core's boot guard, the composer platform requirement, the CI matrix, both Dockerfiles and the PHPStan php version move together, since running z-engine against a PHP minor it was not generated for is memory corruption rather than a degraded mode. The generator keeps 8.4 in its default target list even though master targets 8.5: the 8.4 branch merges up into master, and dropping include/8.4 from the regenerated set would turn every merge-up into a modify/delete conflict. PHPStan: the single baseline entry that spelled out ZEND_ACC_USE_GUARDS' old numeric value is updated in place. A full baseline regeneration was rejected - it rewrites ~540 lines on the untouched 8.4 master too, so the churn is pre-existing drift and unrelated to this change. Verified that regenerating on 8.4 master and on this branch differ in exactly that one line, i.e. these edits introduce no new static-analysis errors. PHP 8.5 deprecates the report_memleaks directive; the test-suite calls that use it to suppress leak reports for immortal-by-design allocations are silenced, as it remains the only switch for that behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
master is now the PHP 8.5 line; 8.4 keeps its own maintenance branch and stays supported. The bug-report template asks for an 8.5 `php -v` line and spells out which branch each version lives on, so reports land against the right one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
… 8.5 The dual-target gen-headers run broke on the 8.4 image: zend_ast_op_array only exists since PHP 8.5, so gate the manifest entries on the running PHP version (symbols.php executes inside the target container). The debug image build required an opcache.so, but since PHP 8.5 opcache is linked statically - emit the zend_extension line only when the shared extension exists and rely on the existing extension_loaded sanity check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
Review feedback on #134: include/8.4 artifacts are maintained on the 8.4 branch and flow into master via the cascade merge-up, so master's gen-headers run (and the header-drift job) only needs the 8.5 target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
Review feedback on #134: master's generator only ever runs against PHP 8.5 now, so the manifest can list the type unconditionally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
The debug-build leak gate caught redefine-churn losing three blocks per cycle on PHP 8.5 (a zend_array, its arData and one key string - a whole static-variables table), leak-free on 8.4. PHP 8.5 changed who owns a closure's static variables. zend_create_closure_ex() used to duplicate the prototype defaults into the closure's OWN op_array.static_variables field; it now duplicates them into the ZEND_MAP_PTR slot only and leaves the struct field aliasing the prototype's table, whose single destroy is tied to the body refcount. The two releases that used to compensate went away with it: zend_closure_free_storage() no longer nulls static_variables, and destroy_op_array() no longer unconditionally frees the static_variables of a dying op_array's closure prototypes. The swap assumed the old model and minted the entry an independent duplicate, overwriting op_array.static_variables. On 8.5 that field is the only handle through which the entry's body reference could still reach the prototype's table, so once it was replaced nothing freed the original: the eval'd op_array had already returned early on the shared refcount, and the entry's own destroy_op_array went on to free the duplicate instead. No donor kind owns that table on 8.5, so the duplication is not merely unnecessary but actively wrong - the body refcount already guards it and the last holder frees it exactly once. Dropping it takes the whole minted-defaults bookkeeping with it (the registry, the rollback restore records and the duplicateStatics parameter, whose only `true` caller was redefine()). The live per-entry table is still unshared by dropping the ZEND_MAP_PTR slot, so statics keep materializing lazily from the defaults on first ZEND_BIND_STATIC. Verified against a locally built PHP 8.5.9 --enable-debug: all 25 leak scenarios report no leaks (redefine-churn was 300), and the internal group runs 138 tests with only 2 skips instead of 27, since the leak gates actually execute on a debug build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDZ2XsoVuB5uG4qXKL3ny
feat: move master to PHP 8.5
merge: cascade 8.4 into master
Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/13.2.6/ChangeLog-13.2.md) - [Commits](sebastianbergmann/phpunit@12.2.0...13.2.6) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 13.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
…hpunit-tw-12.2or-tw-13.0 chore(deps-dev): update phpunit/phpunit requirement from ^12.2 to ^12.2 || ^13.0
merge: cascade 8.4 into master
… PHP 8.5 testGetThis swaps the running frame's $this to a stdClass to demonstrate the capability, but left the foreign object in the frame's This slot. At frame cleanup the engine releases whatever sits there while the original object is still referenced elsewhere, and PHP 8.5's stricter allocator reports the resulting imbalance as 'zend_mm_heap corrupted' + SIGABRT (PHP 8.4 tolerated it silently). The fix restores the original $this before the frame unwinds: swapping is a real capability, not cleaning up after it in a *live* frame was the bug. Pre-existing on master; surfaced by the 8.4->master cascade running its suite on 8.5.9. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af
merge: cascade 8.4 into master (regenerated 8.5 headers + 8.5 heap-safety fix)
Engine definitions for both minors are bundled (include/8.4, include/8.5) and Core::init() already selects the set of the running interpreter; this widens the boot guard to [8.4, 8.6) and turns every hand-declared value that moved between minors into a PHP_VERSION_ID conditional: - Core::ZEND_ACC_USE_GUARDS moved from 0x800 to 0x40000000 in 8.5 - NodeKind AST_* kinds shifted when 8.5 inserted AST_OP_ARRAY and AST_CAST_VOID, added AST_PIPE and removed AST_CLONE/AST_EXIT; kinds absent on the running minor carry a negative sentinel and the name() reverse map only indexes real (non-negative, public) kinds - PayloadRelocator matches ZEND_AST_OP_ARRAY only on 8.5 (66 is AST_ZNODE on 8.4) and walks zend_attribute.validation_error and the DECLARE_ATTRIBUTED_CONST opline pair only where they exist - FunctionBodySwap restores the 8.4 statics-defaults duplication for closure donors (which own their table and destroy it on death there) while keeping the 8.5 rule of never duplicating (the table is guarded by the body refcount alone since zend_create_closure_ex changed); the ownership rules are documented on unshareStaticVariables() The 8.5-only file-cache shapes (attributed constants, const closures) use 8.5-only syntax, so they move out of the shared answer.php fixture into answer-php85.php with their own opcache-php85 test group, keeping the opcache group skip-free on every supported minor. Verified: full suite green on release 8.4.24 and 8.5.9, internal and opcache groups green on a debug 8.4 build, both header sets regenerate byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184AgTjKVX4S6qmvRsdM6dt
- composer.json widens php to ~8.4.0 || ~8.5.0 and gains the
test:opcache85 script for the version-gated file-cache shape tests
- generate.php defaults to BOTH minors, so a symbol change regenerates
every bundled definition set and header-drift checks the whole tree
- symbols.php restores the PHP_VERSION_ID gate around zend_ast_op_array
(the struct does not exist in 8.4 headers)
- CI runs tests, PHPStan and the debug internal/opcache groups on a
{8.4, 8.5} matrix; the opcache-php85 group is asserted skip-free on
8.5 only
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184AgTjKVX4S6qmvRsdM6dt
README support matrix, AGENTS.md version-matching and branch-model sections, CONTRIBUTING and the bug-report template now describe one line supporting both minors, with z-engine owning all header/version complexity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184AgTjKVX4S6qmvRsdM6dt
ConstantAttributesRule refuses to check attributed constants unless the RUNTIME is PHP 8.5, so the 8.4 matrix leg failed on answer-php85.php. The file is opcache fixture payload, not library code - excluded like answer.php. Also documents why phpVersion stays pinned to the newest supported minor instead of the [8.4, 8.5] range: the range folds the out-of-range boot guard into an always-false branch at level max. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184AgTjKVX4S6qmvRsdM6dt
Owner
Author
|
Please stop here, only relevant consumer library should support several versions in parallel, not the z-engine itself. Technically, master already supports parallel binaries, so not merge back here php8.5 changes |
Owner
Author
|
Understood — closing this, z-engine keeps its one-minor-per-branch model. The consumer-side goal is already achieved with no z-engine change needed: userland-php-generics#13 takes Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One z-engine install now runs on PHP 8.4 and 8.5 in parallel. The branch merges
master(which carried the 8.5 support) into the8.4line and unifies the two: both generated definition sets are bundled (include/8.4/,include/8.5/),Core::init()selects the set of the running interpreter, and every hand-declared value that moved between minors became aPHP_VERSION_IDconditional. z-engine owns all version-dependent header complexity — consumers (e.g.userland-php-generics) never see it.What changed
Version-dependent code unified (
src/)[80400, 80600); the refusal message now lists every supported minor.Core::ZEND_ACC_USE_GUARDSis0x800on 8.4 /0x40000000on 8.5, expressed as a conditional constant.NodeKind: 8.5 insertedAST_OP_ARRAY/AST_CAST_VOID(shifting later kinds), addedAST_PIPEand removedAST_CLONE/AST_EXIT. Every shifted kind is a conditional; kinds absent on the running minor get a negative sentinel, and thename()reverse map indexes only real kinds.PayloadRelocator: matchesZEND_AST_OP_ARRAYonly on 8.5 (kind 66 isAST_ZNODEon 8.4), and walkszend_attribute.validation_error+ theDECLARE_ATTRIBUTED_CONSTopline pair only where the engine has them.FunctionBodySwap/PendingBodySwap/ClassDelta: the 8.4 statics-defaults duplication for closure donors is restored and gated — on 8.4 a closure donor owns (and destroys) its defaults table so the entry must duplicate; since 8.5 the table is guarded by the body refcount alone and duplicating would leak. The ownership rules are documented onunshareStaticVariables().Tests
answer.phpfixture intoanswer-php85.phpwith a newPhp85SerializerShapesTestin groupopcache-php85, asserted skip-free viacomposer test:opcache85on 8.5. Theopcachegroup stays skip-free on every minor.Tooling / CI / docs
composer.json:php: ~8.4.0 || ~8.5.0.tools/generator/generate.phptargets both minors by default, socomposer gen-headersand theheader-driftjob cover the wholeinclude/tree;symbols.phpregains thePHP_VERSION_IDgate aroundzend_ast_op_array(absent from 8.4 headers).{8.4, 8.5}matrix.8.4andmasternow carry the same two-minor support;masterremains where the next minor lands first.Verification (all local, in containers matching CI)
--group internal(debug build, isolated)--group opcache --fail-on-skipped--group opcache-php85 --fail-on-skippedcomposer gen-headersdriftOne pre-existing issue surfaced while testing (not introduced here, reproduces on pristine
8.4): running the full default suite on a debug 8.4.24 build trips azend_hashconsistency assertion inClassDeltaTest::testRemovedOverrideRestoresInheritedMethod. CI never runs that combination (the default suite runs on release builds; debug jobs run only the internal/opcache groups, which are green) — left out of scope.Companion PR
lisachenko/userland-php-genericshas a matching PR that widens tophp: ^8.4andlisachenko/z-engine: 8.4.x-dev || 8.5.x-dev. Its 8.5 CI legs need this PR merged (and the Packagist mirror refreshed) before they can resolve.Generated by Claude Code