From c2b74e1d2af328f26ac71a2aac0df8ae6742215f Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 4 Jun 2026 19:43:49 +0000 Subject: [PATCH] Fix enum case fetch when constants table stores backing scalars (#5832). Materialize enum cases on class const fetch via enumCases metadata so E::A === 1 stays false when legacy scalar storage drops canonical names; add compile-time isEnumCase fallback when php-cfg flags are missing. Co-authored-by: Cursor --- lib/Compiler.php | 21 +++++++- lib/VM.php | 20 +------ lib/VM/ClassConstExpr.php | 9 +--- lib/VM/EnumCaseSupport.php | 38 +++++++++++++ lib/VM/EnumSupport.php | 20 +++++++ ...aintainer_enum_compare_regression_5798.php | 23 ++++++++ ...EnumCompareBackingScalarRegressionTest.php | 53 +++++++++++++++++++ 7 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 test/repro/maintainer_enum_compare_regression_5798.php create mode 100644 test/unit/VmEnumCompareBackingScalarRegressionTest.php diff --git a/lib/Compiler.php b/lib/Compiler.php index fadb275d233..d847959bb05 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -3099,7 +3099,7 @@ protected function compileClassConstDeclaration(Op\Terminal\Const_ $child, Block $constOp->classConstVisibilityFlags = property_exists($child, 'flags') ? (int) $child->flags : CfgFunc::FLAG_PUBLIC; - if (property_exists($child, 'isEnumCase') && $child->isEnumCase) { + if ($this->cfgTerminalConstIsEnumCase($child)) { $constOp->isEnumCaseDeclare = true; if (null !== $this->compilingClassLc) { $constName = $this->staticNameFromOperand($child->name); @@ -3134,6 +3134,25 @@ protected function compileClassConstDeclaration(Op\Terminal\Const_ $child, Block } } + /** + * Distinguish enum `case` from user `const` when php-cfg isEnumCase is missing (#5832). + */ + private function cfgTerminalConstIsEnumCase(Op\Terminal\Const_ $child): bool + { + if (property_exists($child, 'isEnumCase') && $child->isEnumCase) { + return true; + } + if (null === $this->compilingClassLc + || !array_key_exists($this->compilingClassLc, $this->compileTimeEnumBackedTypes)) { + return false; + } + if (property_exists($child, 'declaredType') && null !== $child->declaredType) { + return false; + } + + return 0 === (property_exists($child, 'flags') ? (int) $child->flags : 0); + } + /** * Compile-time enum case singleton for folds (default args, class const inits; #5514). */ diff --git a/lib/VM.php b/lib/VM.php index 0315131a172..1d7121a93f1 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -7831,25 +7831,7 @@ private function copyClassConstOrStaticPropertyByName( if ($classEntry->isEnum && null !== $classEntry->backedType) { VM\EnumSupport::ensureBackedEnumValuesUnique($classEntry); } - if ($classEntry->isEnum && isset($classEntry->enumCaseCanonicalNames[$memberLc])) { - $canonical = $classEntry->enumCaseCanonicalNames[$memberLc]; - $stored = $classEntry->constants[$memberLc]->resolveIndirect(); - $backing = new Variable(Variable::TYPE_NULL); - $backing->null(); - if (null !== $classEntry->backedType) { - if (Variable::TYPE_OBJECT === $stored->type && EnumCaseSupport::isEnumCase($stored->toObject())) { - $caseValue = $stored->toObject()->enumCaseValue; - if (null !== $caseValue) { - $backing->copyFrom($caseValue); - } - } elseif (Variable::TYPE_ENUM_CASE === $stored->type) { - $backing->copyFrom($stored->toEnumCase()->backingValue); - } else { - $backing->copyFrom($classEntry->constants[$memberLc]); - } - } - $dest->enumCase(new EnumCaseEntry($classEntry, $canonical, $backing)); - + if (EnumCaseSupport::tryMaterializeEnumCaseConstantFetch($classEntry, $memberLc, $dest)) { return true; } $dest->copyFrom($classEntry->constants[$memberLc]); diff --git a/lib/VM/ClassConstExpr.php b/lib/VM/ClassConstExpr.php index e67b41c08e9..01ed022a6cd 100644 --- a/lib/VM/ClassConstExpr.php +++ b/lib/VM/ClassConstExpr.php @@ -165,14 +165,7 @@ private static function executeClassConstFetch( if (!isset($classEntry->constants[$constName])) { throw new \LogicException("Undefined class constant {$className}::{$constName}"); } - if ($classEntry->isEnum && isset($classEntry->enumCaseCanonicalNames[$constName])) { - $canonical = $classEntry->enumCaseCanonicalNames[$constName]; - $backing = new Variable(); - $backing->copyFrom($classEntry->constants[$constName]); - $frame->scope[$op->arg1]->enumCase( - new EnumCaseEntry($classEntry, $canonical, $backing) - ); - + if (EnumCaseSupport::tryMaterializeEnumCaseConstantFetch($classEntry, $constName, $frame->scope[$op->arg1])) { return; } $frame->scope[$op->arg1]->copyFrom($classEntry->constants[$constName]); diff --git a/lib/VM/EnumCaseSupport.php b/lib/VM/EnumCaseSupport.php index 73dd2966295..7431d6ebe61 100644 --- a/lib/VM/EnumCaseSupport.php +++ b/lib/VM/EnumCaseSupport.php @@ -16,6 +16,44 @@ public static function isEnumCase(ObjectEntry $object): bool return $object->isEnumCase; } + /** + * Class const fetch on enum cases — upgrade legacy backing scalars to TYPE_ENUM_CASE (#5832, #5798). + */ + public static function tryMaterializeEnumCaseConstantFetch( + ClassEntry $enum, + string $memberLc, + Variable $dest + ): bool { + if (!isset($enum->constants[$memberLc])) { + return false; + } + $canonical = EnumSupport::enumCaseNameForConstantMember($enum, $memberLc); + if (null === $canonical) { + return false; + } + if (null !== $enum->backedType) { + EnumSupport::ensureBackedEnumValuesUnique($enum); + } + $stored = $enum->constants[$memberLc]->resolveIndirect(); + $backing = new Variable(Variable::TYPE_NULL); + $backing->null(); + if (null !== $enum->backedType) { + if (Variable::TYPE_OBJECT === $stored->type && self::isEnumCase($stored->toObject())) { + $caseValue = $stored->toObject()->enumCaseValue; + if (null !== $caseValue) { + $backing->copyFrom($caseValue); + } + } elseif (Variable::TYPE_ENUM_CASE === $stored->type) { + $backing->copyFrom($stored->toEnumCase()->backingValue); + } else { + $backing->copyFrom($enum->constants[$memberLc]); + } + } + $dest->enumCase(new EnumCaseEntry($enum, $canonical, $backing)); + + return true; + } + public static function createCase(ClassEntry $enum, string $caseName, Variable $backedValue): Variable { if (!$enum->isEnum) { diff --git a/lib/VM/EnumSupport.php b/lib/VM/EnumSupport.php index a0b3d23100b..4929ba8fa1a 100644 --- a/lib/VM/EnumSupport.php +++ b/lib/VM/EnumSupport.php @@ -85,4 +85,24 @@ public static function ensureBackedEnumValuesUnique(ClassEntry $entry): void } $entry->backedEnumTableBuilt = true; } + + /** + * Enum `case` member name for a constants-table key, or null for user `const` (#5832, #5054). + */ + public static function enumCaseNameForConstantMember(ClassEntry $enum, string $memberLc): ?string + { + if (!$enum->isEnum) { + return null; + } + if (isset($enum->enumCaseCanonicalNames[$memberLc])) { + return $enum->enumCaseCanonicalNames[$memberLc]; + } + foreach ($enum->enumCases as $case) { + if (strtolower($case['name']) === $memberLc) { + return $case['name']; + } + } + + return null; + } } diff --git a/test/repro/maintainer_enum_compare_regression_5798.php b/test/repro/maintainer_enum_compare_regression_5798.php new file mode 100644 index 00000000000..5fb5d2a4258 --- /dev/null +++ b/test/repro/maintainer_enum_compare_regression_5798.php @@ -0,0 +1,23 @@ +run($runtime->parseAndCompile(<<<'PHP' +vmContext->classes['e']; + $scalar = new VM\Variable(VM\Variable::TYPE_INTEGER); + $scalar->int(1); + $enum->constants['a'] = $scalar; + unset($enum->enumCaseCanonicalNames['a']); + + ob_start(); + $runtime->run($runtime->parseAndCompile(<<<'PHP' +assertSame( + "array (\n 0 => false,\n 1 => false,\n 2 => false,\n 3 => false,\n)", + $output + ); + } + + public function testUserEnumConstStillComparesAsScalar(): void + { + ob_start(); + $runtime = new Runtime(); + $runtime->run($runtime->parseAndCompile(<<<'PHP' +assertSame("array (\n 0 => true,\n 1 => true,\n)", $output); + } +}