feat(enum): fold backed case constant expressions

master
韩天峰 2 days ago
parent 5f75c5b48c
commit b46f62da30
  1. 32
      phpunit/code/enum-case-constant-expressions.php
  2. 151
      phpunit/src/EnumCaseConstantExpressionTest.php
  3. 7
      src/Entity/ClassDef.php
  4. 2
      src/Entity/ClassLikeDef.php
  5. 65
      src/Preprocessor.php
  6. 407
      src/Resolver/ClassConstantValueTrait.php
  7. 19
      src/gen_stub.php
  8. 50
      tests/compiler/enum/backed-enum-case-constant-expressions.phpt

@ -0,0 +1,32 @@
<?php
namespace EnumExpressionFixture;
const TWO = 1 + 1;
const THREE = TWO + 1;
class Provider
{
public const BASE = THREE + 1;
}
enum Number: int
{
case Two = 1 + 1;
case Three = THREE;
case Four = Provider::BASE;
case Five = [5][0];
case Six = self::Two->value + 4;
case Seven = self::Eight->value - 1;
case Eight = 8;
}
enum Word: string
{
case Hello = 'hel' . 'lo';
case CaseName = Number::Two->name;
}
function main(): void
{
}

@ -0,0 +1,151 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
/**
* Backed enum case expressions are folded after declaration composition and
* before C++ generation. No source expression may survive into runtime code.
* @internal
* @coversNothing
*/
final class EnumCaseConstantExpressionTest extends PHPUnit\Framework\TestCase
{
public function testExpressionsAreFinalizedBeforeCodeGeneration(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$file = TYPEPHP_ROOT_PATH . '/phpunit/code/enum-case-constant-expressions.php';
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
$number = $compiler->getClassDef('EnumExpressionFixture\\Number');
self::assertNotNull($number);
self::assertNull($number->enumCases['Two']);
self::assertArrayHasKey('Two', $number->enumCaseExpressions);
$compiler->convertFile($file);
self::assertSame([
'Two' => 2,
'Three' => 3,
'Four' => 4,
'Five' => 5,
'Six' => 6,
'Seven' => 7,
'Eight' => 8,
], $number->enumCases);
self::assertSame([], $number->enumCaseExpressions);
$word = $compiler->getClassDef('EnumExpressionFixture\\Word');
self::assertNotNull($word);
self::assertSame([
'Hello' => 'hello',
'CaseName' => 'Two',
], $word->enumCases);
self::assertSame([], $word->enumCaseExpressions);
$header = file_get_contents($compiler->getArgInfoHeaderFile($file));
self::assertIsString($header);
self::assertStringContainsString('ZVAL_LONG(&enum_case_Six_value, 6);', $header);
self::assertStringContainsString('zend_string_init_interned("hello"', $header);
self::assertStringNotContainsString('php::getEnumCase', $header);
}
/**
* @dataProvider invalidExpressionProvider
*/
public function testInvalidExpressionFailsBeforeCodeGeneration(
string $expression,
string $expected,
string $declarations = '',
): void {
$root = sys_get_temp_dir() . '/typephp-enum-expression-' . bin2hex(random_bytes(8));
mkdir($root, 0777, true);
$file = $root . '/program.php';
file_put_contents($file, <<<PHP
<?php
{$declarations}
enum InvalidEnum: int
{
case A = {$expression};
}
function main(): void {}
PHP);
try {
global $translator;
$compiler = CompilerTest::create($root);
$translator = $compiler;
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
try {
$compiler->convertFile($file);
self::fail('Compilation unexpectedly succeeded');
} catch (TestError $error) {
self::assertStringContainsString($expected, $error->getMessage());
}
self::assertFileDoesNotExist($compiler->getCppFile($file));
} finally {
$this->removeTree($root);
}
}
public static function invalidExpressionProvider(): iterable
{
yield 'unknown runtime constant' => [
'RUNTIME_VALUE',
'backing value must be compile-time evaluable: Constant `RUNTIME_VALUE` is not known at compile time',
];
yield 'wrong scalar result type' => [
'1 / 2',
'backing value must be of type int, float given',
];
yield 'self-reference through enum value' => [
'self::A->value + 1',
'Cannot declare self-referencing constant `InvalidEnum::A`',
];
yield 'self-reference through enum name' => [
'self::A->name === "A" ? 1 : 2',
'Cannot declare self-referencing constant `InvalidEnum::A`',
];
yield 'class constant cycle' => [
'Cycle::A',
'Cannot declare self-referencing constant `Cycle::A`',
'class Cycle { public const A = self::B; public const B = self::A; }',
];
yield 'global constant cycle' => [
'FIRST',
'Cannot declare self-referencing constant `FIRST`',
'const FIRST = SECOND; const SECOND = FIRST;',
];
}
private function removeTree(string $root): void
{
if (!is_dir($root)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($iterator as $entry) {
if ($entry->isDir()) {
rmdir($entry->getPathname());
} else {
unlink($entry->getPathname());
}
}
rmdir($root);
}
}

@ -56,6 +56,13 @@ class ClassDef extends ClassLikeDef
* @var array<string, int|string|null> * @var array<string, int|string|null>
*/ */
public array $enumCases = []; public array $enumCases = [];
/**
* Backing-value ASTs waiting for declaration-expression finalization.
* These expressions are never emitted as runtime calculations.
* @var array<string, \PhpParser\Node\Expr>
*/
public array $enumCaseExpressions = [];
/** /**
* Abstract method name (lowercase) => flags * Abstract method name (lowercase) => flags
* @var array<string, int> * @var array<string, int>

@ -13,6 +13,8 @@ class ClassLikeDef
public string $name; public string $name;
public string $namespace; public string $namespace;
public string $extends = ''; public string $extends = '';
/** Source file owning this declaration, used by compile-time expressions. */
public string $sourceFile = '';
public function __construct(string $name, string $namespace = '') public function __construct(string $name, string $namespace = '')
{ {

@ -604,6 +604,10 @@ class Preprocessor extends CompilerBase
} }
continue; continue;
} }
if ($statement instanceof Node\Stmt\EnumCase) {
$this->finalizePreparedEnumCase($statement);
continue;
}
if (!$statement instanceof Node\Stmt\ClassMethod) { if (!$statement instanceof Node\Stmt\ClassMethod) {
continue; continue;
} }
@ -621,6 +625,62 @@ class Preprocessor extends CompilerBase
} }
} }
/**
* Resolve a backed enum case to its final scalar after every declaration
* and composed Trait member is known. The source expression is retained
* only until this point; no unresolved expression may reach codegen.
*/
private function finalizePreparedEnumCase(Node\Stmt\EnumCase $case): void
{
$name = $this->parseIdentifier($case->name);
$enumName = $this->classDef->getNamespacedName(false);
$backingType = $this->classDef->enumBackingType;
if ($backingType === null) {
if ($case->expr !== null) {
$this->fatalError(
$case,
"Case `{$name}` of non-backed enum `{$enumName}` must not have a value",
);
}
$this->classDef->enumCases[$name] = null;
unset($this->classDef->enumCaseExpressions[$name]);
return;
}
if ($case->expr === null) {
$this->fatalError(
$case,
"Case `{$name}` of backed enum `{$enumName}` must have a value",
);
}
// A recursive dependency may already have finalized this case while
// another case expression was being evaluated.
if (!isset($this->classDef->enumCaseExpressions[$name])) {
return;
}
$this->classDef->enumCases[$name] = $this->evaluatePreparedEnumCaseBackingValue(
$case,
$this->classDef,
$name,
);
unset($this->classDef->enumCaseExpressions[$name]);
}
/**
* Translator supplies the constant-expression evaluator. Keeping the hook
* here makes the prepare/compose/finalize phase boundary explicit.
*/
protected function evaluatePreparedEnumCaseBackingValue(
Node\Stmt\EnumCase $case,
ClassDef $classDef,
string $caseName,
): int|string {
throw new \LogicException('Enum case constant-expression evaluator is not available');
}
private function finalizeInterfaceDeclarationExpressions(Node\Stmt\Interface_ $interface): void private function finalizeInterfaceDeclarationExpressions(Node\Stmt\Interface_ $interface): void
{ {
$this->resetClass(); $this->resetClass();
@ -1355,6 +1415,7 @@ class Preprocessor extends CompilerBase
} }
$this->classDef = new ClassDef($this->class, $flags, $this->namespace); $this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->classDef->sourceFile = $this->file;
$this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class); $this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class);
$this->classDef->exported = !$this->hasNoExportAttribute($class); $this->classDef->exported = !$this->hasNoExportAttribute($class);
if ($this->classDef->nativeObject && $this->stubFile) { if ($this->classDef->nativeObject && $this->stubFile) {
@ -1485,6 +1546,9 @@ class Preprocessor extends CompilerBase
$v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_ $v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_
? $v->expr->value ? $v->expr->value
: null; : null;
if ($v->expr !== null) {
$this->classDef->enumCaseExpressions[$caseName] = $v->expr;
}
break; break;
case 'Stmt_ClassMethod': case 'Stmt_ClassMethod':
$this->prepareClassMethod($v, $class); $this->prepareClassMethod($v, $class);
@ -2638,6 +2702,7 @@ class Preprocessor extends CompilerBase
$name = $this->parseIdentifier($v->name); $name = $this->parseIdentifier($v->name);
$this->interface = $name; $this->interface = $name;
$this->interfaceDef = new InterfaceDef($name, $this->namespace); $this->interfaceDef = new InterfaceDef($name, $this->namespace);
$this->interfaceDef->sourceFile = $this->file;
$interfaceName = $this->interfaceDef->getNamespacedName(false); $interfaceName = $this->interfaceDef->getNamespacedName(false);
$interfaceNameLower = strtolower($interfaceName); $interfaceNameLower = strtolower($interfaceName);

@ -11,11 +11,23 @@ namespace TypePhp\Resolver;
use PhpParser\ConstExprEvaluator; use PhpParser\ConstExprEvaluator;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\ClassLikeDef;
use TypePhp\Entity\ConstantDef; use TypePhp\Entity\ConstantDef;
use TypePhp\Entity\EnumCaseRef; use TypePhp\Entity\EnumCaseRef;
use TypePhp\Exception\TestError;
trait ClassConstantValueTrait trait ClassConstantValueTrait
{ {
/** @var array<string, true> Class names are case-insensitive; case names are not. */
private array $enumCaseBackingEvaluationsInProgress = [];
/** @var array<string, true> */
private array $globalConstantEvaluationsInProgress = [];
/** @var array<string, true> Class names are case-insensitive; constant names are not. */
private array $classConstantEvaluationsInProgress = [];
public function getDefinedConstants(): array public function getDefinedConstants(): array
{ {
return $this->internalConstants; return $this->internalConstants;
@ -179,6 +191,401 @@ trait ClassConstantValueTrait
return $ref->caseName; return $ref->caseName;
} }
protected function evaluatePreparedEnumCaseBackingValue(
Node\Stmt\EnumCase $case,
ClassDef $classDef,
string $caseName,
): int|string {
return $this->evaluateAndStoreEnumCaseBackingValue($case, $classDef, $caseName);
}
/**
* Return the scalar produced during declaration-expression finalization.
* gen_stub.php consumes this value but never evaluates the source AST.
*/
public function getFinalizedEnumCaseBackingValue(string $enumClass, string $caseName): int|string
{
$classDef = $this->getClassDef(ltrim($enumClass, '\\'));
if ($classDef === null || !$classDef->enum || $classDef->enumBackingType === null) {
throw new \LogicException("Backed enum `{$enumClass}` is not declared");
}
if (isset($classDef->enumCaseExpressions[$caseName])) {
throw new \LogicException(
"Enum case `{$enumClass}::{$caseName}` reached code generation before constant-expression finalization",
);
}
$value = $classDef->enumCases[$caseName] ?? null;
if (!is_int($value) && !is_string($value)) {
throw new \LogicException("Enum case `{$enumClass}::{$caseName}` has no finalized backing value");
}
return $value;
}
private function evaluateAndStoreEnumCaseBackingValue(
NodeAbstract $origin,
ClassDef $classDef,
string $caseName,
): int|string {
$enumName = $classDef->getNamespacedName(false);
if (!isset($classDef->enumCaseExpressions[$caseName])) {
return $this->getFinalizedEnumCaseBackingValue($enumName, $caseName);
}
$key = strtolower($enumName) . '::' . $caseName;
if (isset($this->enumCaseBackingEvaluationsInProgress[$key])) {
$this->fatalError($origin, "Cannot declare self-referencing constant `{$enumName}::{$caseName}`");
}
$this->enumCaseBackingEvaluationsInProgress[$key] = true;
set_error_handler(static function (
int $severity,
string $message,
string $file,
int $line,
): never {
throw new \ErrorException($message, 0, $severity, $file, $line);
});
try {
$value = $this->evaluateCompileTimeExpression(
$classDef->enumCaseExpressions[$caseName],
$classDef,
);
} catch (TestError $error) {
throw $error;
} catch (\Throwable $error) {
$detail = $error->getMessage();
$suffix = $detail !== '' ? ": {$detail}" : '';
$this->fatalError(
$origin,
"Enum case `{$enumName}::{$caseName}` backing value must be compile-time evaluable{$suffix}",
);
} finally {
restore_error_handler();
unset($this->enumCaseBackingEvaluationsInProgress[$key]);
}
$expectedType = $classDef->enumBackingType;
$valid = $expectedType === 'int' ? is_int($value) : is_string($value);
if (!$valid) {
$actualType = get_debug_type($value);
$this->fatalError(
$origin,
"Enum case `{$enumName}::{$caseName}` backing value must be of type {$expectedType}, {$actualType} given",
);
}
$classDef->enumCases[$caseName] = $value;
unset($classDef->enumCaseExpressions[$caseName]);
return $value;
}
private function evaluateCompileTimeExpression(Node\Expr $expression, ClassLikeDef $scope): mixed
{
$evaluator = null;
$evaluator = new ConstExprEvaluator(function (Node\Expr $expr) use (&$evaluator, $scope): mixed {
if ($expr instanceof Node\Expr\Cast) {
$value = $evaluator->evaluateDirectly($expr->expr);
return match (true) {
$expr instanceof Node\Expr\Cast\Int_ => (int) $value,
$expr instanceof Node\Expr\Cast\Double => (float) $value,
$expr instanceof Node\Expr\Cast\Bool_ => (bool) $value,
$expr instanceof Node\Expr\Cast\String_ => (string) $value,
$expr instanceof Node\Expr\Cast\Array_ => (array) $value,
default => throw new \RuntimeException('Unsupported constant-expression cast'),
};
}
if ($expr instanceof Node\Expr\ConstFetch) {
return $this->evaluateCompileTimeConstantFetch($expr, $scope);
}
if ($expr instanceof Node\Expr\ClassConstFetch && $expr->class instanceof Node\Name) {
$class = $this->resolveCompileTimeClassName($expr->class, $scope);
$name = $expr->name instanceof Node\Identifier
? $expr->name->toString()
: $evaluator->evaluateDirectly($expr->name);
if (!is_string($name)) {
throw new \RuntimeException('A compile-time class constant name must evaluate to string');
}
if (strcasecmp($name, 'class') === 0) {
return ltrim($class, '\\');
}
$value = $this->evaluateCompileTimeClassConstantFetch($expr, $class, $name, $scope);
if ($value instanceof EnumCaseRef && $this->isEnumCaseBackingEvaluationInProgress($value)) {
$this->fatalError(
$expr,
"Cannot declare self-referencing constant `{$value->enumClass}::{$value->caseName}`",
);
}
return $value;
}
if ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\NullsafePropertyFetch) {
$object = $evaluator->evaluateDirectly($expr->var);
if ($object === null && $expr instanceof Node\Expr\NullsafePropertyFetch) {
return null;
}
if (!$object instanceof EnumCaseRef) {
throw new \RuntimeException('Compile-time property fetch is only supported on enum cases');
}
if ($this->isEnumCaseBackingEvaluationInProgress($object)) {
$this->fatalError(
$expr,
"Cannot declare self-referencing constant `{$object->enumClass}::{$object->caseName}`",
);
}
$property = $expr->name instanceof Node\Identifier
? $expr->name->toString()
: $evaluator->evaluateDirectly($expr->name);
if ($property === 'name') {
return $object->caseName;
}
if ($property === 'value') {
return $this->resolveCompileTimeEnumCaseBackingValue($expr, $object);
}
throw new \RuntimeException(
"Undefined enum case property `{$object->enumClass}::{$object->caseName}->{$property}`",
);
}
if ($expr instanceof Node\Scalar\MagicConst) {
return $this->evaluateCompileTimeMagicConstant($expr, $scope);
}
throw new \RuntimeException("Expression `{$expr->getType()}` is not compile-time evaluable");
});
return $evaluator->evaluateDirectly($expression);
}
private function evaluateCompileTimeClassConstantFetch(
NodeAbstract $origin,
string $class,
string $name,
ClassLikeDef $scope,
): mixed {
$class = ltrim($class, '\\');
if ($this->hasClass($class)) {
$current = $class;
$visited = [];
while ($current !== '' && !isset($visited[strtolower($current)])) {
$visited[strtolower($current)] = true;
$classDef = $this->getClass($current);
if ($classDef->enum && array_key_exists($name, $classDef->enumCases)) {
return new EnumCaseRef($classDef->getNamespacedName(false), $name);
}
if ($classDef->hasConstant($name)) {
return $this->evaluateCompileTimeClassConstant(
$origin,
$classDef,
$classDef->getConstant($name),
$name,
);
}
$current = ltrim($classDef->extends, '\\');
}
}
if ($this->hasInterface($class)) {
$constant = $this->findCompileTimeInterfaceConstant($class, $name);
if ($constant !== null) {
[$interface, $constantDef] = $constant;
return $this->evaluateCompileTimeClassConstant($origin, $interface, $constantDef, $name);
}
}
return $this->getClassConstValue(
$origin,
$class,
$name,
$scope->getNamespacedName(false),
);
}
private function evaluateCompileTimeClassConstant(
NodeAbstract $origin,
ClassLikeDef $scope,
ConstantDef $constant,
string $name,
): mixed {
if (!$constant->valueExpr instanceof Node\Expr) {
throw new \RuntimeException(
"Class constant `{$scope->getNamespacedName(false)}::{$name}` has no compile-time expression",
);
}
$key = strtolower($scope->getNamespacedName(false)) . '::' . $name;
if (isset($this->classConstantEvaluationsInProgress[$key])) {
$this->fatalError(
$origin,
"Cannot declare self-referencing constant `{$scope->getNamespacedName(false)}::{$name}`",
);
}
$this->classConstantEvaluationsInProgress[$key] = true;
try {
return $this->evaluateCompileTimeExpression($constant->valueExpr, $scope);
} finally {
unset($this->classConstantEvaluationsInProgress[$key]);
}
}
/** @return array{ClassLikeDef, ConstantDef}|null */
private function findCompileTimeInterfaceConstant(string $interface, string $name): ?array
{
$pending = [ltrim($interface, '\\')];
$visited = [];
while ($pending !== []) {
$current = array_pop($pending);
if (!is_string($current) || isset($visited[strtolower($current)])) {
continue;
}
$visited[strtolower($current)] = true;
if (!$this->hasInterface($current)) {
continue;
}
$interfaceDef = $this->getInterface($current);
if ($interfaceDef->hasConstant($name)) {
return [$interfaceDef, $interfaceDef->constants[$name]];
}
foreach ($interfaceDef->extendsList as $parent) {
$pending[] = ltrim($parent, '\\');
}
}
return null;
}
private function evaluateCompileTimeConstantFetch(Node\Expr\ConstFetch $expr, ClassLikeDef $scope): mixed
{
$name = ltrim($expr->name->toString(), '\\');
$candidates = [];
$resolved = $expr->name->getAttribute('resolvedName');
if ($resolved instanceof Node\Name) {
$candidates[] = ltrim($resolved->toString(), '\\');
}
$namespaced = $expr->name->getAttribute('namespacedName');
if ($namespaced instanceof Node\Name) {
$candidates[] = ltrim($namespaced->toString(), '\\');
}
if ($expr->name instanceof Node\Name\FullyQualified) {
$candidates[] = $name;
} elseif ($expr->name->isUnqualified()) {
if (isset($this->useConstants[$name])) {
$candidates[] = ltrim($this->useConstants[$name], '\\');
} elseif ($scope->namespace !== '') {
$candidates[] = $scope->namespace . '\\' . $name;
}
$candidates[] = $name;
} else {
$candidates[] = $scope->namespace !== '' ? $scope->namespace . '\\' . $name : $name;
}
foreach (array_unique($candidates) as $candidate) {
if ($this->hasConstant($candidate)) {
return $this->evaluateProjectConstant($expr, $candidate, $scope);
}
if ($this->isInternalScalarConstant($candidate)) {
return $this->internalConstants[$candidate];
}
if (defined($candidate)) {
$value = constant($candidate);
if (is_scalar($value) || $value === null) {
return $value;
}
}
}
throw new \RuntimeException("Constant `{$name}` is not known at compile time");
}
private function evaluateProjectConstant(
NodeAbstract $origin,
string $name,
ClassLikeDef $enumScope,
): mixed {
$key = $this->escapeConstVar($name);
$constant = $this->constants[$key] ?? null;
if ($constant === null || !$constant->valueExpr instanceof Node\Expr) {
throw new \RuntimeException("Constant `{$name}` has no compile-time expression");
}
if (isset($this->globalConstantEvaluationsInProgress[$key])) {
$this->fatalError($origin, "Cannot declare self-referencing constant `{$name}`");
}
$this->globalConstantEvaluationsInProgress[$key] = true;
try {
return $this->evaluateCompileTimeExpression($constant->valueExpr, $enumScope);
} finally {
unset($this->globalConstantEvaluationsInProgress[$key]);
}
}
private function resolveCompileTimeClassName(Node\Name $name, ClassLikeDef $scope): string
{
$raw = $name->toString();
if (strcasecmp($raw, 'self') === 0) {
return '\\' . $scope->getNamespacedName(false);
}
if (strcasecmp($raw, 'parent') === 0) {
if ($scope->extends === '') {
throw new \RuntimeException('Cannot use parent:: without a parent class');
}
return '\\' . ltrim($scope->extends, '\\');
}
if (strcasecmp($raw, 'static') === 0) {
throw new \RuntimeException('static:: is not compile-time evaluable');
}
$resolved = $name->getAttribute('resolvedName');
if ($resolved instanceof Node\Name) {
return '\\' . ltrim($resolved->toString(), '\\');
}
if ($name instanceof Node\Name\FullyQualified) {
return '\\' . $name->toString();
}
return '\\' . $this->getNamespacedClassName($raw, $scope->namespace);
}
private function resolveCompileTimeEnumCaseBackingValue(NodeAbstract $origin, EnumCaseRef $case): int|string
{
if ($this->isInternalClass($case->enumClass)) {
$value = constant(ltrim($case->enumClass, '\\') . '::' . $case->caseName);
if ($value instanceof \BackedEnum) {
return $value->value;
}
throw new \RuntimeException("Enum case `{$case->enumClass}::{$case->caseName}` has no backing value");
}
$classDef = $this->getClassDef(ltrim($case->enumClass, '\\'));
if ($classDef === null || !$classDef->enum || $classDef->enumBackingType === null) {
throw new \RuntimeException("Enum case `{$case->enumClass}::{$case->caseName}` has no backing value");
}
return $this->evaluateAndStoreEnumCaseBackingValue($origin, $classDef, $case->caseName);
}
private function isEnumCaseBackingEvaluationInProgress(EnumCaseRef $case): bool
{
return isset($this->enumCaseBackingEvaluationsInProgress[
strtolower(ltrim($case->enumClass, '\\')) . '::' . $case->caseName
]);
}
private function evaluateCompileTimeMagicConstant(Node\Scalar\MagicConst $expr, ClassLikeDef $scope): int|string
{
return match (true) {
$expr instanceof Node\Scalar\MagicConst\Line => $expr->getStartLine(),
$expr instanceof Node\Scalar\MagicConst\File => $scope->sourceFile,
$expr instanceof Node\Scalar\MagicConst\Dir => dirname($scope->sourceFile),
$expr instanceof Node\Scalar\MagicConst\Class_ => $scope->getNamespacedName(false),
$expr instanceof Node\Scalar\MagicConst\Namespace_ => $scope->namespace,
$expr instanceof Node\Scalar\MagicConst\Method,
$expr instanceof Node\Scalar\MagicConst\Function_,
$expr instanceof Node\Scalar\MagicConst\Trait_ => '',
default => throw new \RuntimeException("Magic constant `{$expr->getType()}` is not compile-time evaluable"),
};
}
public function getConstValue(string $name): mixed public function getConstValue(string $name): mixed
{ {
if ($this->isInternalConstant($name)) { if ($this->isInternalConstant($name)) {

@ -3910,10 +3910,12 @@ class PropertyInfo extends VariableLike
} }
class EnumCaseInfo { class EnumCaseInfo {
private /* readonly */ string $enumClass;
private /* readonly */ string $name; private /* readonly */ string $name;
private /* readonly */ ?Expr $value; private /* readonly */ ?Expr $value;
public function __construct(string $name, ?Expr $value) { public function __construct(string $enumClass, string $name, ?Expr $value) {
$this->enumClass = $enumClass;
$this->name = $name; $this->name = $name;
$this->value = $value; $this->value = $value;
} }
@ -3924,7 +3926,18 @@ class EnumCaseInfo {
if ($this->value === null) { if ($this->value === null) {
$code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n"; $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n";
} else { } else {
$value = EvaluatedValue::createFromExpression($this->value, null, null, $allConstInfos); // TypePHP finalizes every backed case expression after declaration
// and Trait composition, before code generation. Consume that
// scalar result here; never re-evaluate the source AST or defer it
// to request runtime.
$backingValue = getTranslator()->getFinalizedEnumCaseBackingValue(
$this->enumClass,
$this->name,
);
$expression = is_int($backingValue)
? new Node\Scalar\Int_($backingValue)
: new String_($backingValue);
$value = EvaluatedValue::createFromExpression($expression, null, null, $allConstInfos);
$zvalName = "enum_case_{$escapedName}_value"; $zvalName = "enum_case_{$escapedName}_value";
$code = "\n" . $value->initializeZval($zvalName); $code = "\n" . $value->initializeZval($zvalName);
@ -5141,7 +5154,7 @@ class FileInfo {
); );
} else if ($classStmt instanceof Stmt\EnumCase) { } else if ($classStmt instanceof Stmt\EnumCase) {
$enumCaseInfos[] = new EnumCaseInfo( $enumCaseInfos[] = new EnumCaseInfo(
$classStmt->name->toString(), $classStmt->expr); $className->toString(), $classStmt->name->toString(), $classStmt->expr);
} else if ($classStmt instanceof Stmt\TraitUse) { } else if ($classStmt instanceof Stmt\TraitUse) {
continue; continue;
} else { } else {

@ -0,0 +1,50 @@
--TEST--
Backed enum case constant expressions are folded before code generation
--FILE--
<?php
const TWO = 1 + 1;
const THREE = TWO + 1;
class Provider
{
public const BASE = THREE + 1;
}
enum Number: int
{
case Two = 1 + 1;
case Three = THREE;
case Four = Provider::BASE;
case Five = [5][0];
case Six = self::Two->value + 4;
case Seven = self::Eight->value - 1;
case Eight = 8;
}
enum Word: string
{
case Hello = 'hel' . 'lo';
case CaseName = Number::Two->name;
}
function main(): void
{
foreach (Number::cases() as $case) {
var_dump($case->value);
}
foreach (Word::cases() as $case) {
var_dump($case->value);
}
}
?>
--EXPECT--
int(2)
int(3)
int(4)
int(5)
int(6)
int(7)
int(8)
string(5) "hello"
string(3) "Two"
Loading…
Cancel
Save