Repository navigation
merge: cascade 8.5 into master (PHP 8.6 class-type fix) - #289
Conversation
…lias and accept method donors redefine() now takes `\Closure|FunctionLikeInterface $newBody` and an optional `?string $preserveAs`: - a user method of another class (the donor) can be adopted as the new body in place, exactly like a closure body; the entry keeps its identity (name, scope, prototype, visibility, static/final flags) while the compiled body, signature, attributes and doc comment follow the donor; - with `preserveAs` the previous body is published before the swap as a private method of the declaring class (ZEND_ACC_CHANGED when an ancestor declares a private method of that name, shared into subclasses that were linked before) or as a second global function, so it stays callable and can be swapped back in with a later redefine(). ReflectionClass::addMethod() accepts a method donor as well and publishes an override of an inherited method with the engine's inheritance rules (prototype kept, no final override, no visibility narrowing, same static-ness, parent-private redeclaration marked CHANGED); the class's magic slots and the method tables of subclasses linked before follow the override. FunctionBodySwap allocates the fresh run-time cache of a swapped or published entry from request memory WITHOUT ZEND_ACC_HEAP_RT_CACHE and releases it on the next swap of the entry: a heap-flagged cache shared by a parent and a subclass method table was efree()d once per table at a destructor-path shutdown (USE_ZEND_ALLOC=0), i.e. for every subclass linked after a swap. New RedefinitionException (named factories) and SharedMemoryException::sharedEntryHasLinkedSubclasses(); docs/hot-swap.md documents method donors, preserveAs, the addMethod() override and the run-time cache rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…odySwap header The class header still described the fresh run-time cache as heap-flagged and freed by the engine; since the previous commit it is request memory without ZEND_ACC_HEAP_RT_CACHE that z-engine releases itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…onor signature check PHP 8.4 reflection reports the self and parent keywords of a type verbatim, PHP 8.5 resolves them to the class names: comparing the stringified types made a method donor spelling the target class fail the signature check on 8.4 while it passed on 8.5. The signature now renders each side with the keywords resolved against its own scope (the declaring class, or the bound scope of a closure donor), which is what the types accept at run time on both versions; static stays as is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…lias and accept method donors redefine() now takes `\Closure|FunctionLikeInterface $newBody` and an optional `?string $preserveAs`: - a user method of another class (the donor) can be adopted as the new body in place, exactly like a closure body; the entry keeps its identity (name, scope, prototype, visibility, static/final flags) while the compiled body, signature, attributes and doc comment follow the donor; - with `preserveAs` the previous body is published before the swap as a private method of the declaring class (ZEND_ACC_CHANGED when an ancestor declares a private method of that name, shared into subclasses that were linked before) or as a second global function, so it stays callable and can be swapped back in with a later redefine(). ReflectionClass::addMethod() accepts a method donor as well and publishes an override of an inherited method with the engine's inheritance rules (prototype kept, no final override, no visibility narrowing, same static-ness, parent-private redeclaration marked CHANGED); the class's magic slots and the method tables of subclasses linked before follow the override. FunctionBodySwap allocates the fresh run-time cache of a swapped or published entry from request memory WITHOUT ZEND_ACC_HEAP_RT_CACHE and releases it on the next swap of the entry: a heap-flagged cache shared by a parent and a subclass method table was efree()d once per table at a destructor-path shutdown (USE_ZEND_ALLOC=0), i.e. for every subclass linked after a swap. The donor signature check renders `self` and `parent` resolved against each side's own scope (declaring class, or the bound scope of a closure donor): PHP 8.4 reflection reports the keywords verbatim, so a method donor spelling the target class is compared by what its types accept. PHP 8.4 specifics of this port: the statics-ownership bookkeeping of this branch stays in place next to the run-time cache records - a closure donor owns its statics defaults table, so the swap duplicates it (duplicateStatics: true), while a method donor's table lives as long as the body the entry now holds a reference on (duplicateStatics: false, the same rule as for published overrides); PendingBodySwap and releaseSwappedInBody() carry both records. New RedefinitionException (named factories) and SharedMemoryException::sharedEntryHasLinkedSubclasses(); docs/hot-swap.md documents method donors, preserveAs, the addMethod() override and the run-time cache rule. (cherry picked from commits 5471958, 67e183b and 91a170c of feature/redefine-preserve-alias) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
PHPStan 2.3 folds more constants through loops and casts: the nested iteration count of the get-iterator scenario and the property snapshot of the resolution-handlers test read as constant on the first iteration, which made their guards look always-true and the loops look endless, and an ArrayObject built without a type reads as holding nothing. - the scenario compares the nested iteration count with the item count of the iterated instance instead of a literal; - the resolution-handlers test asserts the snapshot with assertSame() instead of a guard ending the loop with fail(); - the error-callback test declares the element type of its log. No behaviour of the tests changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2 (cherry picked from commit 8fc1949)
PHPStan 2.3 folds more constants through loops and casts: the nested iteration count of the get-iterator scenario and the property snapshot of the resolution-handlers test read as constant on the first iteration, which made their guards look always-true and the loops look endless, and an ArrayObject built without a type reads as holding nothing. - the scenario compares the nested iteration count with the item count of the iterated instance instead of a literal; - the resolution-handlers test asserts the snapshot with assertSame() instead of a guard ending the loop with fail(); - the error-callback test declares the element type of its log. No behaviour of the tests changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
PHPStan 2.3 folds more constants through loops and casts: the nested iteration count of the get-iterator scenario and the property snapshot of the resolution-handlers test read as constant on the first iteration, which made their guards look always-true and the loops look endless, and an ArrayObject built without a type reads as holding nothing. - the scenario compares the nested iteration count with the item count of the iterated instance instead of a literal; - the resolution-handlers test asserts the snapshot with assertSame() instead of a guard ending the loop with fail(); - the error-callback test declares the element type of its log. No behaviour of the tests changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2 (cherry picked from commit 4c0316e)
…ading it Completes the previous commit for PHPStan 2.3: ArrayObject::offsetGet() may return null, so the entry is asserted before its keys are read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…ase what the swap owns An in-place body swap and the publications built on it (the preserveAs alias, the addMethod() override) copied the entry structure where the engine also copies it, and freed or shared what those copies still used: - the run-time cache minted for an entry was freed by its next swap while a fake closure made over the entry (Closure::fromCallable, a first-class callable, ReflectionMethod::getClosure) or the engine clone a subclass implementing an interface holds still read it (use-after-free). The release now finds those copies first: a live frame on the block keeps it, a fake closure gets a private heap copy the engine frees with it, a clone gets a fresh minted cache (ObjectStore::instancesOf walks EG(objects_store) without letting its own handles alias the slots it reads); - on 8.4 an alias or override adopting a body whose statics defaults were minted by a swap, and a method donor carrying such a table or a closure's own table (a method published from a closure), shared it and had it freed from under them; the adopter now duplicates it like a closure donor's, the container release returns the duplicate, and the previous record is read before the new swap overwrites it, so the eager release of a minted table matches again instead of leaking one table per swap; - the materialized live statics of a preserved body were orphaned: the alias now takes them over (the preserved body keeps its state), a swap destroys the previous body's table unless a fake closure or a clone borrowed it, and removeMethods() releases the live statics, the minted duplicate and the minted cache of an entry it unpublishes from the only table holding it; - the alias reached only subclasses sharing the entry pointer: every subclass linked before receives it now (a subclass declaring the name itself is marked ZEND_ACC_CHANGED), the shared-memory guard refuses any linked subclass, and an override carries ZEND_ACC_CHANGED whenever the inherited method does, as the engine propagates it; - engine clones of an inherited entry (subclass implementing an interface that declares the method) follow a redefinition in place and are replaced by an override like pointer-sharing buckets. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…ase what the swap owns An in-place body swap and the publications built on it (the preserveAs alias, the addMethod() override) copied the entry structure where the engine also copies it, and freed or shared what those copies still used: - the run-time cache minted for an entry was freed by its next swap while a fake closure made over the entry (Closure::fromCallable, a first-class callable, ReflectionMethod::getClosure) or the engine clone a subclass implementing an interface holds still read it (use-after-free). The release now finds those copies first: a live frame on the block keeps it, a fake closure gets a private heap copy the engine frees with it, a clone gets a fresh minted cache (ObjectStore::instancesOf walks EG(objects_store) without letting its own handles alias the slots it reads); - the materialized live statics of a preserved body were orphaned: the alias now takes them over (the preserved body keeps its state), a swap destroys the previous body's table unless a fake closure or a clone borrowed it, and removeMethods() releases the live statics and the minted cache of an entry it unpublishes from the only table holding it; - the alias reached only subclasses sharing the entry pointer: every subclass linked before receives it now (a subclass declaring the name itself is marked ZEND_ACC_CHANGED), the shared-memory guard refuses any linked subclass, and an override carries ZEND_ACC_CHANGED whenever the inherited method does, as the engine propagates it; - engine clones of an inherited entry (subclass implementing an interface that declares the method) follow a redefinition in place and are replaced by an override like pointer-sharing buckets. Port of the 8.4 fix (ed9548b on feature/redefine-preserve-alias-8.4) without its 8.4-only statics-ownership parts: on 8.5 no donor kind owns the defaults table, so nothing is minted or duplicated for it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…at preserveAs keeps Fake closures and interface clones of a swapped entry, the live statics a preserved alias takes over, the subclasses an alias reaches, the CHANGED propagation of overrides, the per-adopter statics duplicates on 8.4 and the resources removeMethods() releases - as implemented by ed9548b. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…s, clones and live frames Fake closures of every kind made over a redefined method or function keep executing the previous body on a private heap copy of the block the next swap retires, an interface clone of a subclass linked after a swap follows the next swap with a minted cache of its own, a fake closure redefining its own method mid-flight keeps the block it runs on, a hundred swaps next to a live fake closure stay memory-flat, and a fake closure over a preserved alias survives a redefinition of the alias. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…s, clones and live frames Fake closures of every kind made over a redefined method or function keep executing the previous body on a private heap copy of the block the next swap retires, an interface clone of a subclass linked after a swap follows the next swap with a minted cache of its own, a fake closure redefining its own method mid-flight keeps the block it runs on, a hundred swaps next to a live fake closure stay memory-flat, and a fake closure over a preserved alias survives a redefinition of the alias. The mid-flight scenario keeps its state in static properties of the test class: on PHP 8.5 a closure donor's bound variables stay with the closure object, so the entry adopting the body would read them unbound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
Same shape as on the 8.5 branch, where a closure donor's bound variables stay with the closure object and the swapped-in body would read them unbound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…nts on 8.4 Every adopter of a body carrying a minted defaults table - the preserving alias, an addMethod() override, an entry taking it as a method donor - mints a duplicate of its own and keeps it when the source entry's next swap releases the source's, the alias continues on the live statics it took over while a never-called alias starts from its duplicate of the defaults, and consecutive closure-donor swaps of a function and of a method stay memory-flat (the previous duplicate used to leak once per swap). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…lass An alias reaches the subclasses linked before that override the redefined method (the preserved body runs on their instances through parent::), a subclass declaring a private method of the alias name keeps it and gets ZEND_ACC_CHANGED so the base scope still resolves the base alias, the shared-memory guard refuses both shapes when the base is immutable, and subclasses linked after inherit the alias the engine's way - with one body share per publishing table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…lass An alias reaches the subclasses linked before that override the redefined method (the preserved body runs on their instances through parent::), a subclass declaring a private method of the alias name keeps it and gets ZEND_ACC_CHANGED so the base scope still resolves the base alias, the shared-memory guard refuses both shapes when the base is immutable, and subclasses linked after inherit the alias the engine's way - with one body share per publishing table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…nd removeMethods() releases The alias of a method or function continues on the very live statics table the preserved body had materialized while the new body starts afresh, removing the alias destroys that table and releases the minted run-time cache the bucket destructor cannot reach, publish/call/restore/remove cycles stay within the container baseline, a table a fake closure borrows survives both a swap and the removal, and an alias still published by a subclass table keeps its statics until the last table drops it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…nd removeMethods() releases The alias of a method or function continues on the very live statics table the preserved body had materialized while the new body starts afresh, removing the alias destroys that table and releases the minted run-time cache the bucket destructor cannot reach, publish/call/restore/remove cycles stay within the container baseline, a table a fake closure borrows survives both a swap and the removal, and an alias still published by a subclass table keeps its statics until the last table drops it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…y flags A method entry that adopted a body from an opcache-cached donor carries ZEND_ACC_IMMUTABLE as a body-level flag. An interface-induced engine clone copied from such an entry, or a subclass method redeclaring an alias, then looked immutable although its class lives in request memory, so redefine() skipped the clone (the subclass kept the previous body) and markRedeclaration() refused a perfectly writable class. Decide by the holder class: a clone inside a mutable class follows the swap, a redeclaring mutable subclass receives ZEND_ACC_CHANGED, and only a shared-memory class is refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…verrides An override installed over an inherited method must keep the engine's inheritance semantics: it carries ZEND_ACC_CHANGED when the inherited method was CHANGED or private (so an ancestor's private method still wins from the ancestor's own scope), takes the inherited prototype otherwise, and a fresh method gets neither. Covers: a three-level chain whose leaf overrides a CHANGED method and a subclass linked afterwards, an early-linked subclass tree, a private ancestor method shadowed by an override, and a plain override that must not acquire the flag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…d entry A subclass that implements an interface declaring an inherited method holds an engine clone of the entry rather than the entry pointer. The clone must follow redefine() in place, carry a preserveAs alias, be reachable by an addMethod() override through the clone-holding descendants, keep its own run-time cache after the swap, and be refused only when its class lives in shared memory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
On PHP 8.4 a method installed from a closure keeps its live statics slot pointing at its defaults table. Taking that method as a donor must give the entry its own duplicate, so the two methods count independently, the donor can be removed under a live entry, and a plain method donor still shares. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…y flags A method entry that adopted a body from an opcache-cached donor carries ZEND_ACC_IMMUTABLE as a body-level flag. An interface-induced engine clone copied from such an entry, or a subclass method redeclaring an alias, then looked immutable although its class lives in request memory, so redefine() skipped the clone (the subclass kept the previous body) and markRedeclaration() refused a perfectly writable class. Decide by the holder class: a clone inside a mutable class follows the swap, a redeclaring mutable subclass receives ZEND_ACC_CHANGED, and only a shared-memory class is refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…verrides An override installed over an inherited method must keep the engine's inheritance semantics: it carries ZEND_ACC_CHANGED when the inherited method was CHANGED or private (so an ancestor's private method still wins from the ancestor's own scope), takes the inherited prototype otherwise, and a fresh method gets neither. Covers: a three-level chain whose leaf overrides a CHANGED method and a subclass linked afterwards, an early-linked subclass tree, a private ancestor method shadowed by an override, and a plain override that must not acquire the flag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…d entry A subclass that implements an interface declaring an inherited method holds an engine clone of the entry rather than the entry pointer. The clone must follow redefine() in place, carry a preserveAs alias, be reachable by an addMethod() override through the clone-holding descendants, keep its own run-time cache after the swap, and be refused only when its class lives in shared memory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…arios of the fixes The matrix script runs under both allocators and both opcache modes and shuts the process down through the destructor path, which is where the fixed defects used to crash. Five scenarios are added: fake closures over a redefined method surviving its next redefinition, an interface clone linked between two redefinitions following the second, a closure donor with statics preserved under an alias and swapped back before the alias is removed, an addMethod() override under a redeclared ancestor-private method carrying ZEND_ACC_CHANGED, and an overriding subclass linked before a preserving redefine (refused on a shared-memory entry, served through parent:: otherwise). Every scenario also pins the engine state it relies on by pointer identity and flag bits, and the hot loop drives the new entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
… question A body swap used to walk the whole class table for every question it asked about its entry (its published shares, its clones, the shares of each clone, and again at each commit for the cache release and the statics checks) and the object store once per check, building a reflection wrapper per visited class: about 190 ms per redefine() in a process holding 1,800 classes, with the swap tests pacing their plateau loops around it. EntryPublications takes one survey per redefinition - the buckets named like the entry, read through an integer view of the class table (HashTable::pointerValueAddresses(), the technique ObjectStore already uses) with one method lookup per class and no wrapper until a class actually publishes the name - and answers every question of the entry swap, the clone swaps and their commits from memory. A commit gathers the live closures once and takes every decision that reads them before anything is destroyed. The survey keeps addresses only, so the object-store walk does not pay for its own bookkeeping. MethodPublisher reaches the linked subclasses through ReflectionClass::getLinkedSubclasses(), the same walk with a parent-chain check instead of a wrapper per class. Each clone is listed once, with the class the engine separated it for (found by ancestry: table order lies after delayed early binding), so a redefinition swaps a clone shared into a grand-subclass once instead of once per table publishing it. Same 1,800-class process: a redefine() takes 7 ms with 1,500 subclasses sharing the entry and 2-3 ms among unrelated classes; the default suite runs in 24 s instead of 67 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
… question A body swap used to walk the whole class table for every question it asked about its entry (its published shares, its clones, the shares of each clone, and again at each commit for the cache release and the statics checks) and the object store once per check, building a reflection wrapper per visited class: about 190 ms per redefine() in a process holding 1,800 classes, with the swap tests pacing their plateau loops around it. EntryPublications takes one survey per redefinition - the buckets named like the entry, read through an integer view of the class table (HashTable::pointerValueAddresses(), the technique ObjectStore already uses) with one method lookup per class and no wrapper until a class actually publishes the name - and answers every question of the entry swap, the clone swaps and their commits from memory. A commit gathers the live closures once and takes every decision that reads them before anything is destroyed. The survey keeps addresses only, so the object-store walk does not pay for its own bookkeeping. MethodPublisher reaches the linked subclasses through ReflectionClass::getLinkedSubclasses(), the same walk with a parent-chain check instead of a wrapper per class. Each clone is listed once, with the class the engine separated it for (found by ancestry: table order lies after delayed early binding), so a redefinition swaps a clone shared into a grand-subclass once instead of once per table publishing it. Same 1,800-class process: a redefine() takes 5 ms with 1,500 subclasses sharing the entry and 2 ms among unrelated classes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…arios of the fixes The matrix script runs under both allocators and both opcache modes and shuts the process down through the destructor path, which is where the fixed defects used to crash. Five scenarios are added: fake closures over a redefined method surviving its next redefinition, an interface clone linked between two redefinitions following the second, a closure donor with statics preserved under an alias and swapped back before the alias is removed, an addMethod() override under a redeclared ancestor-private method carrying ZEND_ACC_CHANGED, and an overriding subclass linked before a preserving redefine (refused on a shared-memory entry, served through parent:: otherwise). Every scenario also pins the engine state it relies on by pointer identity and flag bits, and the hot loop drives the new entries. On PHP 8.5 the statics scenario pins the refcount-guarded defaults table the alias and the restored body share with the preserved body, instead of the duplicates PHP 8.4 mints. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
feat(reflection): preserve the previous body of redefine() under an alias and accept method donors
test: keep the test suite clean under PHPStan 2.3
merge: cascade 8.4 into 8.5
…as-8.4 feat(reflection): preserve the previous body of redefine() under an alias and accept method donors (PHP 8.4)
Resolves the conflicts of the automated cascade (#287) in favour of the 8.5 implementation: #282 is the 8.5 resolution of the feature #284 brought to 8.4 (redefine() with preserveAs, method donors, addMethod() overrides, the review fixes and the one-survey-per-swap performance work), so FunctionBodySwap, PendingBodySwap, FunctionLikeTrait, MethodPublisher, ClassDelta, the hot-swap docs and the alias matrix keep their 8.5 shape. The statics-defaults duplicates PHP 8.4 mints (and the tests pinning them, RedefineMintedStaticsTest and RedefineClosureBackedDonorTest with their stubs) do not apply to PHP 8.5, where the defaults table is guarded by the body refcount, and are not carried over. The resulting tree is identical to 8.5; the commit records the cascade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
merge: cascade 8.4 into 8.5 (conflicts resolved)
Carries the redefine() feature (#282, #284, #288: preserveAs aliases, method donors, addMethod() overrides, the review fixes and the one-survey-per-swap performance work) into the PHP 8.6 line. The merge itself is clean; the one 8.6 adjustment is in ReflectionClass: since PHP 8.6 ce->type is a uint8-typed enum (zend_class_type) rather than a char, so findUserMethodAt() and descendsFromAddress() compare the value directly, as isUserDefined() on this branch already does, instead of through ord(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
…he live closures A swap walks the live closure objects to find the copies of its entry (fake closures share the entry's run-time cache and borrow its live statics). The walk built a user-function view over every closure and asked it whether it was user-defined - but a closure over an INTERNAL function (a first-class callable of strlen(), Closure::fromCallable() over an internal function, the closure of an internal method) keeps its name in the internal_function member, and building the view threw "Attempt to read undefined field 'function_name'" before the question could be asked. Every redefine() that released a minted cache, took over live statics or retired an alias failed while such a closure was alive; PHP 8.6 keeps such closures in its callable caches, so there it failed always. The function type is now read off the union before any view is made, and internal closures are skipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2 (cherry picked from commit c0b3406)
|
CI: the The test and PHPStan failures of the first head were this PR's and are fixed: the uint8 Generated by Claude Code |
|
Status on the header-drift checks after 0b8ba1f:
All PHP 8.6 code jobs on this PR are green. Generated by Claude Code |
master's own tree no longer passes `composer phpstan` with the PHPStan 2.3 the unlocked dev dependencies resolve to today: five findings in three test files. The 8.5 branch already carries the fixes (3843b85, b807faa) and they reach master with the pending cascade (#289); this ports the same three-file change so this PR's CI is green now. It no-ops once the cascade merges. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2
What this changes
The resolved form of the automated cascade #285 (
8.5→master). The merge itself is clean;mastertargets PHP 8.6, which surfaced two defects in the merged code:ce->typeis a uint8 enum on 8.6, not a char. The two class-table walks added by the swap performance work (ReflectionClass::findUserMethodAt()anddescendsFromAddress(), from feat(reflection): preserve the previous body of redefine() under an alias and accept method donors #282/merge: cascade 8.4 into 8.5 (conflicts resolved) #288) read it throughord(), which is aTypeErroron 8.6 - everyredefine()died in the survey (42 errors, 9 failures, two PHPStan findings on merge: cascade 8.5 into master #285). The merge commit compares the value directly, asisUserDefined()onmasteralready does;8.4and8.5keepord(), so the difference lives only in the 8.6 line.FunctionBodySwap::liveClosureFunctions()built a user-function view over every live closure before checking its kind, and a closure over an internal function has no userfunction_nameto read. PHP 8.6 keeps such closures in its callable caches, so there it failed always; it reproduces on 8.4 and 8.5 with astrlen(...)kept alive, so the fix landed on8.4as fix(reflection): skip closures over internal functions when walking the live closures #290 (now cascaded through8.5) and is carried here as the same commit.Everything else in #285 comes over unchanged: the
redefine()feature withpreserveAsaliases, method donors andaddMethod()overrides, the review fixes, the one-survey-per-swap performance work and their tests.Header drift, resolved: the generated-headers jobs were red on #285 because the committed 8.6 artifacts predated the engine's
vm_stack_page_cache*executor-globals fields, and the linux job could not regenerate them (docker-library'sphp:8.6-rc-cliis a ZTS build). #292 moved linux generation to native setup-php and refreshed the linux and darwin artifacts onmaster; this branch hasmastermerged in (f80cb87), so every header-drift leg is green here.Merge with a merge commit (not squash or rebase), so
8.5becomes an ancestor ofmasterand the cascade flow in.github/branch-flow.jsonstays intact. #285 can then be closed as superseded.Environment it was verified on
php -v): CI, PHP 8.6.0-dev via setup-php on every leg (locally PHP 8.5.11 for PHPStan and coding standards)--enable-debug)? the debug-build CI legs are greenChecklist
master= newest supported minor,8.4= PHP 8.4, …) — fixes cascade upward, never downward (this is the cascade step itself; the uint8 comparison is 8.6-only, the closure fix is fix(reflection): skip closures over internal functions when walking the live closures #290 on8.4)composer testpasses on the matching PHP minor — every CI test leg is green on the current headcomposer phpstan(level max) andcomposer cs:checkare greenlayout_structsintools/generator/symbols.php(nothing new)tools/generator/symbols.phpchanged: regenerated withcomposer gen-headersand committed the result — nothing underinclude/,stubs/or.phpstorm.meta.phpwas hand-edited (the refreshed artifacts came from the generate workflows via ci: generate the linux engine definitions natively instead of through docker #292)🤖 Generated with Claude Code
https://claude.ai/code/session_01VLQcsvgdQUzwBFsoKUAcG2