diff --git a/phpunit/code/enum-case-constant-expressions.php b/phpunit/code/enum-case-constant-expressions.php new file mode 100644 index 00000000..e5830cf0 --- /dev/null +++ b/phpunit/code/enum-case-constant-expressions.php @@ -0,0 +1,32 @@ +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 +{ +} diff --git a/phpunit/src/EnumCaseConstantExpressionTest.php b/phpunit/src/EnumCaseConstantExpressionTest.php new file mode 100644 index 00000000..3806f207 --- /dev/null +++ b/phpunit/src/EnumCaseConstantExpressionTest.php @@ -0,0 +1,151 @@ +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, <<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); + } +} diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index f62a4dde..415420f8 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -56,6 +56,13 @@ class ClassDef extends ClassLikeDef * @var array */ public array $enumCases = []; + + /** + * Backing-value ASTs waiting for declaration-expression finalization. + * These expressions are never emitted as runtime calculations. + * @var array + */ + public array $enumCaseExpressions = []; /** * Abstract method name (lowercase) => flags * @var array diff --git a/src/Entity/ClassLikeDef.php b/src/Entity/ClassLikeDef.php index 32c36de3..ee28b174 100644 --- a/src/Entity/ClassLikeDef.php +++ b/src/Entity/ClassLikeDef.php @@ -13,6 +13,8 @@ class ClassLikeDef public string $name; public string $namespace; public string $extends = ''; + /** Source file owning this declaration, used by compile-time expressions. */ + public string $sourceFile = ''; public function __construct(string $name, string $namespace = '') { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index dd84c317..64d357a3 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -604,6 +604,10 @@ class Preprocessor extends CompilerBase } continue; } + if ($statement instanceof Node\Stmt\EnumCase) { + $this->finalizePreparedEnumCase($statement); + continue; + } if (!$statement instanceof Node\Stmt\ClassMethod) { 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 { $this->resetClass(); @@ -1355,6 +1415,7 @@ class Preprocessor extends CompilerBase } $this->classDef = new ClassDef($this->class, $flags, $this->namespace); + $this->classDef->sourceFile = $this->file; $this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class); $this->classDef->exported = !$this->hasNoExportAttribute($class); 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->value : null; + if ($v->expr !== null) { + $this->classDef->enumCaseExpressions[$caseName] = $v->expr; + } break; case 'Stmt_ClassMethod': $this->prepareClassMethod($v, $class); @@ -2638,6 +2702,7 @@ class Preprocessor extends CompilerBase $name = $this->parseIdentifier($v->name); $this->interface = $name; $this->interfaceDef = new InterfaceDef($name, $this->namespace); + $this->interfaceDef->sourceFile = $this->file; $interfaceName = $this->interfaceDef->getNamespacedName(false); $interfaceNameLower = strtolower($interfaceName); diff --git a/src/Resolver/ClassConstantValueTrait.php b/src/Resolver/ClassConstantValueTrait.php index 649f7a5b..366cdade 100644 --- a/src/Resolver/ClassConstantValueTrait.php +++ b/src/Resolver/ClassConstantValueTrait.php @@ -11,11 +11,23 @@ namespace TypePhp\Resolver; use PhpParser\ConstExprEvaluator; use PhpParser\Node; use PhpParser\NodeAbstract; +use TypePhp\Entity\ClassDef; +use TypePhp\Entity\ClassLikeDef; use TypePhp\Entity\ConstantDef; use TypePhp\Entity\EnumCaseRef; +use TypePhp\Exception\TestError; trait ClassConstantValueTrait { + /** @var array Class names are case-insensitive; case names are not. */ + private array $enumCaseBackingEvaluationsInProgress = []; + + /** @var array */ + private array $globalConstantEvaluationsInProgress = []; + + /** @var array Class names are case-insensitive; constant names are not. */ + private array $classConstantEvaluationsInProgress = []; + public function getDefinedConstants(): array { return $this->internalConstants; @@ -179,6 +191,401 @@ trait ClassConstantValueTrait 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 { if ($this->isInternalConstant($name)) { diff --git a/src/gen_stub.php b/src/gen_stub.php index fd977b65..651d82ac 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -3910,10 +3910,12 @@ class PropertyInfo extends VariableLike } class EnumCaseInfo { + private /* readonly */ string $enumClass; private /* readonly */ string $name; 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->value = $value; } @@ -3924,7 +3926,18 @@ class EnumCaseInfo { if ($this->value === null) { $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n"; } 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"; $code = "\n" . $value->initializeZval($zvalName); @@ -5141,7 +5154,7 @@ class FileInfo { ); } else if ($classStmt instanceof Stmt\EnumCase) { $enumCaseInfos[] = new EnumCaseInfo( - $classStmt->name->toString(), $classStmt->expr); + $className->toString(), $classStmt->name->toString(), $classStmt->expr); } else if ($classStmt instanceof Stmt\TraitUse) { continue; } else { diff --git a/tests/compiler/enum/backed-enum-case-constant-expressions.phpt b/tests/compiler/enum/backed-enum-case-constant-expressions.phpt new file mode 100644 index 00000000..fb47642b --- /dev/null +++ b/tests/compiler/enum/backed-enum-case-constant-expressions.phpt @@ -0,0 +1,50 @@ +--TEST-- +Backed enum case constant expressions are folded before code generation +--FILE-- +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"