Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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).
*/
Expand Down
20 changes: 1 addition & 19 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
9 changes: 1 addition & 8 deletions lib/VM/ClassConstExpr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
38 changes: 38 additions & 0 deletions lib/VM/EnumCaseSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions lib/VM/EnumSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
23 changes: 23 additions & 0 deletions test/repro/maintainer_enum_compare_regression_5798.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* Maintainer repro for #5798 / regression #5832 — enum case must not === / == its backing scalar.
*
* Zend: E::A === 1 and E::A == 1 are both false.
*/
enum E: int
{
case A = 1;
}

echo 'int-ident:', var_export(E::A === 1, true), "\n";
echo 'int-equal:', var_export(E::A == 1, true), "\n";
echo 'int-ident-rev:', var_export(1 === E::A, true), "\n";
echo 'int-equal-rev:', var_export(1 == E::A, true), "\n";

enum S: string
{
case B = 'x';
}

echo 'str-ident:', var_export(S::B === 'x', true), "\n";
echo 'str-equal:', var_export(S::B == 'x', true), "\n";
53 changes: 53 additions & 0 deletions test/unit/VmEnumCompareBackingScalarRegressionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPUnit\Framework\TestCase;

/**
* Enum case === / == with backing scalar must be false even when constants table stores scalars (#5832, #5798).
*/
final class VmEnumCompareBackingScalarRegressionTest extends TestCase
{
public function testLegacyScalarEnumConstantFetchRejectsBackingScalarCompare(): void
{
$runtime = new Runtime();
$runtime->run($runtime->parseAndCompile(<<<'PHP'
<?php
enum E: int { case A = 1; }
PHP, 'enum_decl.php'));
$enum = $runtime->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'
<?php
var_export([E::A === 1, E::A == 1, 1 === E::A, 1 == E::A]);
PHP, 'enum_compare.php'));
$output = ob_get_clean();

$this->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'
<?php
enum E: int { case A = 1; public const FOO = 2; }
var_export([E::FOO === 2, E::FOO == 2]);
PHP, 'enum_user_const.php'));
$output = ob_get_clean();

$this->assertSame("array (\n 0 => true,\n 1 => true,\n)", $output);
}
}