fix(reflection): correct _IS_BOOL/_IS_NUMBER for PHP 8.1+ and add named cast-type enums - #155
Merged
Merged
Conversation
…ed cast-type enums PHP 8.1 inserted IS_NEVER = 17 into the zval type table, shifting _IS_BOOL to 18 and _IS_NUMBER to 19, but ReflectionValue still declared the pre-8.1 values. The drift was invisible and worse than a miss: the engine passes 18 for every boolean cast, which the stale table resolves to _IS_NUMBER, so cast handlers dispatching on the documented constants misroute (bool) into their numeric branch. The generated ground truth in include/8.4/*/constants.php has carried the correct values all along — ReflectionValue was simply not covered by EngineConstantsTest, so the drift went unnoticed. It is covered now, for both the IS_* and _IS_* prefixes. Because namesake integer constants can drift silently between minors, the hooks also gain a named, ground-truth-guarded API: - CastType (backed enum): Long, Double, String, Array, Object, Bool, Number — with CastObjectHook::getCastTypeEnum() - PropertyPurpose (backed enum): Debug, ArrayCast, Serialize, VarExport, Json, GetObjectVars — with GetPropertiesForHook::getPurposeEnum() CastType case values are asserted against the generated constants in EngineConstantsTest; PropertyPurpose mirrors zend_prop_purpose, which the generator manifest does not export yet. The cast-handler test now dispatches on the enum and asserts that (bool) yields true: the engine only accepts IS_TRUE/IS_FALSE as a boolean cast result, so a handler misrouted into the numeric branch produces long(1), which the engine reports as false — the assertion fails on the stale ids and passes on the corrected ones. Fixes #152 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
lisachenko
marked this pull request as ready for review
August 7, 2026 22:02
lisachenko
pushed a commit
to lisachenko/native-php-matrix
that referenced
this pull request
Aug 7, 2026
…-through z-engine 8.4.1 (PRs lisachenko/z-engine#155 and #156) ships the API the local workarounds stood in for: CastType/PropertyPurpose enums exposed via getCastTypeEnum()/getPurposeEnum(), guarded upstream against the generated engine ground truth, and a CastObjectHook whose fall-through behaves exactly like an uninstalled handler. Drop the local ENGINE_IS_BOOL/ENGINE_IS_NUMBER/PROP_PURPOSE_* constants — values that can silently drift between PHP minors — and dispatch on the named cases instead. The numeric-cast fallback is gone entirely: __cast now defers to proceed()/getResult(), and the failed cast propagates to the engine caller, which emits its own "could not be converted to int/float" warning and substitutes 1 — the previously undeliverable default diagnostic is restored, and the fallback test asserts it. Raises the z-engine floor to ~8.4.1 accordingly, in lockstep with the PHP 8.4 pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
lisachenko
pushed a commit
to lisachenko/native-php-matrix
that referenced
this pull request
Aug 7, 2026
…-through z-engine 8.4.1 (PRs lisachenko/z-engine#155 and #156) ships the API the local workarounds stood in for: CastType/PropertyPurpose enums exposed via getCastTypeEnum()/getPurposeEnum(), guarded upstream against the generated engine ground truth, and a CastObjectHook whose fall-through behaves exactly like an uninstalled handler. Drop the local ENGINE_IS_BOOL/ENGINE_IS_NUMBER/PROP_PURPOSE_* constants — values that can silently drift between PHP minors — and dispatch on the named cases instead. The numeric-cast fallback is gone entirely: __cast now defers to proceed()/getResult(), and the failed cast propagates to the engine caller, which emits its own "could not be converted to int/float" warning and substitutes 1 — the previously undeliverable default diagnostic is restored, and the fallback test asserts it. Raises the z-engine floor to ~8.4.1 accordingly, in lockstep with the PHP 8.4 pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
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.
Fixes #152.
The bug
PHP 8.1 inserted
IS_NEVER = 17into the zval type table (Zend/zend_types.h), shifting_IS_BOOLto 18 and_IS_NUMBERto 19 — butReflectionValuestill declared the pre-8.1 values. The drift was worse than a miss: the engine passes 18 for every boolean cast, which the stale table resolves to_IS_NUMBER, so a cast handler dispatching on the documented constants misroutes(bool)into its numeric branch silently.The generated ground truth in
include/8.4/*/constants.phphas carried the correct values all along —ReflectionValuesimply wasn't covered byEngineConstantsTest. It is now, for both theIS_*and_IS_*prefixes, so this class of drift fails CI in the future.The API addition
Because namesake integer constants can drift silently between minors, the hooks gain a named, ground-truth-guarded API (as discussed in #152):
CastType(int-backed enum):Long,Double,String,Array,Object,Bool,Number— surfaced viaCastObjectHook::getCastTypeEnum(): ?CastType. Case values are asserted against the generated constants inEngineConstantsTest.PropertyPurpose(int-backed enum):Debug,ArrayCast,Serialize,VarExport,Json,GetObjectVars— surfaced viaGetPropertiesForHook::getPurposeEnum(): ?PropertyPurpose.zend_prop_purposeis not exported by the generator manifest yet, so this one carries no generated guard — addingZEND_PROP_PURPOSE_*totools/generator/symbols.php(with a header regeneration) would close that gap as a follow-up.Both
getCastType(): int/getPurpose(): intremain untouched for BC.The regression test
testInstallCastObjectHandlerpreviously asserted(bool)→falseand passed by accident: the bool cast (18) fell into the_IS_NUMBERbranch returninglong(1), and the engine reports any non-IS_TRUEretval of a boolean cast asfalse. The handler now dispatches on the enum and asserts(bool)→true, which only theBoolbranch can produce — the assertion fails on the stale ids and passes on the corrected ones.Verified: full suite green on PHP 8.4.19 NTS (411 tests), PHPStan level max clean, PER-CS2.0 clean.
Note for merge-up:
master(8.5) declares the same stale values, so the cascade applies cleanly; the 8.0 branch is untouched and correct (IS_NEVERdid not exist there).Downstream context: found implementing casts in lisachenko/native-php-matrix#17, which carries a local-constant workaround that can be dropped once this ships in a tagged release.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
Generated by Claude Code