Skip to content
Open
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
24 changes: 24 additions & 0 deletions phpunit/code/enum-case-class-constant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
enum CodegenEnum: int
{
case B = 4;
case A = 1 + 1;
}

enum CodegenTyped
{
case A;
}

class CodegenHolder
{
public const CB = CodegenEnum::B;
public const PICKED = true ? CodegenEnum::A : CodegenEnum::B;
public const CodegenTyped CASE_VALUE = CodegenTyped::A;
public const MODE = RoundingMode::HalfEven;
}

function main(): void
{
var_dump(CodegenHolder::CB === CodegenEnum::B);
}
61 changes: 61 additions & 0 deletions phpunit/src/EnumCaseClassConstantTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

use TypePhp\CompilerTest;

/**
* A class constant valued by an enum case must register a persistent
* IS_CONSTANT_AST (`Enum::Case`) instead of a folded scalar: the engine then
* separates the constants table per request, evaluates the fetch there, and
* cleans it up — preserving case identity for static access, constant(), and
* reflection, safely under concurrent ZTS requests.
*/
final class EnumCaseClassConstantTest extends \BaseTest
{
private string $arginfo;

protected function setUp(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/enum-case-class-constant.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$compiler->convertFile($source);
$this->arginfo = file_get_contents(
TYPEPHP_ROOT_PATH . '/' . 'build/include/' . basename($compiler->getArgInfoHeaderFile($source))
);
}

public function testDirectCaseRegistersConstantAst(): void
{
self::assertStringContainsString('const_CB_value_fetch_ast->kind = ZEND_AST_CLASS_CONST;', $this->arginfo);
self::assertStringContainsString('zend_string_init_interned("CodegenEnum", sizeof("CodegenEnum") - 1, 1)', $this->arginfo);
self::assertStringNotContainsString('ZVAL_LONG(&const_CB_value', $this->arginfo);
}

public function testConstantExpressionFoldsToCaseIdentity(): void
{
// true ? A : B folds to the A case identity, not to a scalar.
self::assertMatchesRegularExpression(
'/const_PICKED_value_case_name = zend_string_init_interned\("A"/',
$this->arginfo,
);
}

public function testTypedConstantKeepsDeclaredTypeAndAstValue(): void
{
self::assertStringContainsString('const_CASE_VALUE_value_fetch_ast->kind = ZEND_AST_CLASS_CONST;', $this->arginfo);
self::assertStringContainsString('zend_declare_typed_class_constant(class_entry, const_CASE_VALUE_name', $this->arginfo);
}

public function testInternalEnumCaseRegistersConstantAst(): void
{
self::assertStringContainsString('zend_string_init_interned("RoundingMode", sizeof("RoundingMode") - 1, 1)', $this->arginfo);
}

public function testExpressionValuedBackedCaseRegistersComputedValue(): void
{
self::assertStringContainsString('ZVAL_LONG(&enum_case_A_value, 2);', $this->arginfo);
}
}
25 changes: 25 additions & 0 deletions src/Entity/EnumCaseRef.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/

namespace TypePhp\Entity;

/**
* Compile-time identity of an enum case flowing through constant-expression
* evaluation. Enum case objects have request lifetime, so a constant whose
* value is a case cannot be folded to its backing scalar (identity would be
* lost) nor embedded in persistent class metadata as an object; carriers of
* this value register an IS_CONSTANT_AST the engine evaluates per request.
*/
final class EnumCaseRef
{
public function __construct(
public readonly string $enumClass,
public readonly string $caseName,
) {
}
}
37 changes: 36 additions & 1 deletion src/Parser/PropertyAccessTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,7 @@ protected function parsePropertyFetch(Expr\PropertyFetch $expr): string
return $this->getNativeObjectMemberReceiver($objectName)
. $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef);
}
$objectVar = $objectName;
$objectVar = $this->parenthesizeOpenOperand($objectName);
if ($this->usesTraitPropertyScope($objectVar)) {
$getProperty = 'typephp_read_property_scoped('
. $objectVar . ', ' . $id . ', php::FakeScopeGuard::current(), ' . $this->escapeAttrMode($update) . ')';
Expand Down Expand Up @@ -1155,4 +1155,39 @@ private function emitNativeInstancePropertyTypedFetch(
return $result;
}


/**
* A folded constant value can be a full C++ expression (e.g. the ternary
* of `const VALUE = cond ? E::A : E::B;`). Appending `.attr(...)` to it
* unparenthesized would bind the member access to the last operand only,
* so any operand with top-level operators is wrapped first. Simple
* identifiers and closed call chains stay untouched.
*/
private function parenthesizeOpenOperand(string $code): string
{
$depth = 0;
$inString = false;
$length = strlen($code);
for ($i = 0; $i < $length; $i++) {
$char = $code[$i];
if ($inString) {
if ($char === '\\') {
$i++;
} elseif ($char === '"') {
$inString = false;
}
continue;
}
if ($char === '"') {
$inString = true;
} elseif ($char === '(' || $char === '{' || $char === '[') {
$depth++;
} elseif ($char === ')' || $char === '}' || $char === ']') {
$depth--;
} elseif ($depth === 0 && ($char === ' ' || $char === '?')) {
return '(' . $code . ')';
}
}
return $code;
}
}
11 changes: 10 additions & 1 deletion src/Preprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,16 @@ protected function prepareClass(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum
break;
case 'Stmt_EnumCase':
$caseName = $this->parseIdentifier($v->name);
$this->classDef->enumCases[$caseName] = $v->expr?->value;
// Only literal backing values are recorded here; an
// expression-valued case (`case A = 1 + 1;`) cannot be
// evaluated while declarations are still being collected,
// and no compile-time consumer needs the scalar: case
// identity flows as EnumCaseRef and gen_stub evaluates
// the registration value from the AST itself.
$this->classDef->enumCases[$caseName] =
$v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_
? $v->expr->value
: null;
break;
case 'Stmt_ClassMethod':
$this->prepareClassMethod($v, $class);
Expand Down
43 changes: 39 additions & 4 deletions src/Resolver/ClassConstantValueTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PhpParser\Node;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ConstantDef;
use TypePhp\Entity\EnumCaseRef;

trait ClassConstantValueTrait
{
Expand Down Expand Up @@ -48,7 +49,12 @@ public function getClassConstValue(NodeAbstract $expr, string $_class, string $n
if ($this->isInternalClass($class)) {
$constName = $class . '::' . $name;
if (defined($constName)) {
return constant($constName);
$value = constant($constName);
// Internal enum cases (and internal constants holding one)
// must keep their identity through constant evaluation.
return $value instanceof \UnitEnum
? new EnumCaseRef(get_class($value), $value->name)
: $value;
}
}
[$inheritedFound, $inherited] = $this->resolveInheritedClassConst($class, $name);
Expand All @@ -58,8 +64,10 @@ public function getClassConstValue(NodeAbstract $expr, string $_class, string $n
if ($this->hasClass($class)) {
$classDef = $this->getClass($class);
if ($classDef->enum && array_key_exists($name, $classDef->enumCases)) {
$caseValue = $classDef->enumCases[$name];
return $caseValue ?? $name;
// The case IDENTITY is the constant's value; folding to the
// backing scalar (or the case name) would make
// `K::CONST === E::Case` false through every dynamic path.
return new EnumCaseRef($classDef->getNamespacedName(false), $name);
}
}
$this->fatalError($expr, "Class constant `{$class}::{$name}` not found");
Expand Down Expand Up @@ -89,7 +97,10 @@ protected function resolveInheritedClassConst(string $class, string $name): arra
} elseif (Reflection::isInternalClass($current)) {
$constName = $current . '::' . $name;
if (defined($constName)) {
return [true, constant($constName)];
$value = constant($constName);
return [true, $value instanceof \UnitEnum
? new EnumCaseRef(get_class($value), $value->name)
: $value];
}
break;
} else {
Expand Down Expand Up @@ -144,6 +155,30 @@ protected function evaluateClassConstValue(?NodeAbstract $origin, ConstantDef $c
return $evaluator->evaluateDirectly($valueExpr);
}

/**
* The pre-AST representation of an enum case for consumers that cannot
* register an IS_CONSTANT_AST (property and parameter defaults, attribute
* arguments): internal enums degrade to the host case object, compiled
* enums to the literal backing value or the case name — exactly the
* values those paths consumed before case identity existed.
*/
public function enumCaseLegacyValue(\TypePhp\Entity\EnumCaseRef $ref): mixed
{
if ($this->isInternalClass($ref->enumClass)) {
$constName = $ref->enumClass . '::' . $ref->caseName;
if (defined($constName)) {
return constant($constName);
}
}
if ($this->hasClass($ref->enumClass)) {
$classDef = $this->getClass($ref->enumClass);
if (array_key_exists($ref->caseName, $classDef->enumCases)) {
return $classDef->enumCases[$ref->caseName] ?? $ref->caseName;
}
}
return $ref->caseName;
}

public function getConstValue(string $name): mixed
{
if ($this->isInternalConstant($name)) {
Expand Down
70 changes: 67 additions & 3 deletions src/gen_stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -2569,6 +2569,10 @@ class EvaluatedValue
public SimpleType $type;
public Expr $expr;
public bool $isUnknownConstValue;
/** Case identity when the expression evaluates to an enum case; only
* class-constant registration may use it (persistent AST) — every other
* consumer sees the legacy scalar/object in $value. */
public ?\TypePhp\Entity\EnumCaseRef $enumCaseRef = null;
/** @var ConstInfo[] */
public array $originatingConsts;

Expand Down Expand Up @@ -2728,13 +2732,25 @@ static function (Expr $expr) use (

$result = $evaluator->evaluateDirectly($expr);

return new EvaluatedValue(
$enumCaseRef = null;
if ($result instanceof \TypePhp\Entity\EnumCaseRef) {
// Property/parameter defaults and attribute arguments must keep
// consuming the legacy value (persistent tables reject refcounted
// zvals, and those paths have their own runtime restore
// machinery); only class-constant registration uses the identity.
$enumCaseRef = $result;
$result = getTranslator()->enumCaseLegacyValue($result);
}

$evaluated = new EvaluatedValue(
$result, // note: we are generally not interested in the actual value of $result, unless it's a bare value, without constants
$constType ?? SimpleType::fromValue($result),
$cConstName === null ? $expr : new Expr\ConstFetch(new Node\Name($cConstName)),
$visitor->visitedConstants,
$isUnknownConstValue
);
$evaluated->enumCaseRef = $enumCaseRef;
return $evaluated;
}

public static function null(): EvaluatedValue
Expand All @@ -2755,8 +2771,11 @@ private function __construct($value, SimpleType $type, Expr $expr, array $origin
$this->isUnknownConstValue = $isUnknownConstValue;
}

public function initializeZval(string $zvalName, bool $alreadyExists = false, string $forStringDef = '', string $varName = ''): string
public function initializeZval(string $zvalName, bool $alreadyExists = false, string $forStringDef = '', string $varName = '', bool $allowConstantAst = false): string
{
if ($this->enumCaseRef !== null && $allowConstantAst) {
return $this->initializeEnumCaseZval($zvalName, $alreadyExists);
}
$cExpr = $this->getCExpr();

$code = '';
Expand Down Expand Up @@ -2803,6 +2822,51 @@ public function initializeZval(string $zvalName, bool $alreadyExists = false, st
return $code;
}

/**
* Initialize the zval as a persistent IS_CONSTANT_AST holding
* `EnumClass::CaseName`. Enum case objects have request lifetime and can
* never sit in the persistent class-entry tables, so the engine's own
* mechanism for internal enums is reused: declaring an AST constant makes
* Zend separate the class constants table into request-local mutable
* storage, evaluate the fetch there on first access, and clean it up at
* request shutdown. This keeps case identity intact for static access,
* constant(), and reflection, and is safe under concurrent ZTS requests.
*/
private function initializeEnumCaseZval(string $zvalName, bool $alreadyExists): string
{
$case = $this->enumCaseRef;
$enumCName = '"' . getTranslator()->escapeString(ltrim($case->enumClass, '\\')) . '"';
$caseCName = '"' . getTranslator()->escapeString($case->caseName) . '"';
$id = preg_replace('/[^A-Za-z0-9_]/', '_', $zvalName);

$code = $alreadyExists ? '' : "\tzval $zvalName;\n";
$code .= "\t{\n";
$code .= "\t\tzend_string *{$id}_enum_name = zend_string_init_interned($enumCName, sizeof($enumCName) - 1, 1);\n";
$code .= "\t\tzend_string *{$id}_case_name = zend_string_init_interned($caseCName, sizeof($caseCName) - 1, 1);\n";
$code .= "\t\tzend_ast_zval *{$id}_class_ast = (zend_ast_zval *) pemalloc(sizeof(zend_ast_zval), 1);\n";
$code .= "\t\t{$id}_class_ast->kind = ZEND_AST_ZVAL;\n";
$code .= "\t\t{$id}_class_ast->attr = ZEND_NAME_FQ;\n";
$code .= "\t\tZVAL_INTERNED_STR(&{$id}_class_ast->val, {$id}_enum_name);\n";
$code .= "\t\tZ_LINENO({$id}_class_ast->val) = 0;\n";
$code .= "\t\tzend_ast_zval *{$id}_const_ast = (zend_ast_zval *) pemalloc(sizeof(zend_ast_zval), 1);\n";
$code .= "\t\t{$id}_const_ast->kind = ZEND_AST_ZVAL;\n";
$code .= "\t\t{$id}_const_ast->attr = 0;\n";
$code .= "\t\tZVAL_INTERNED_STR(&{$id}_const_ast->val, {$id}_case_name);\n";
$code .= "\t\tZ_LINENO({$id}_const_ast->val) = 0;\n";
$code .= "\t\tzend_ast_ref *{$id}_ast_ref = (zend_ast_ref *) pemalloc(sizeof(zend_ast_ref) + ZEND_MM_ALIGNED_SIZE(zend_ast_size(2)), 1);\n";
$code .= "\t\tGC_SET_REFCOUNT({$id}_ast_ref, 1);\n";
$code .= "\t\tGC_TYPE_INFO({$id}_ast_ref) = GC_CONSTANT_AST | ((GC_PERSISTENT | GC_IMMUTABLE) << GC_FLAGS_SHIFT);\n";
$code .= "\t\tzend_ast *{$id}_fetch_ast = GC_AST({$id}_ast_ref);\n";
$code .= "\t\t{$id}_fetch_ast->kind = ZEND_AST_CLASS_CONST;\n";
$code .= "\t\t{$id}_fetch_ast->attr = 0;\n";
$code .= "\t\t{$id}_fetch_ast->lineno = 0;\n";
$code .= "\t\t{$id}_fetch_ast->child[0] = (zend_ast *) {$id}_class_ast;\n";
$code .= "\t\t{$id}_fetch_ast->child[1] = (zend_ast *) {$id}_const_ast;\n";
$code .= "\t\tZVAL_AST(&$zvalName, {$id}_ast_ref);\n";
$code .= "\t}\n";
return $code;
}

public function getCExpr(): ?string
{
// $this->expr has all its PHP constants replaced by C constants
Expand Down Expand Up @@ -3224,7 +3288,7 @@ private function getClassConstDeclaration(EvaluatedValue $value): string
{
$constName = $this->name->getDeclarationName();

$zvalCode = $value->initializeZval("const_{$constName}_value");
$zvalCode = $value->initializeZval("const_{$constName}_value", allowConstantAst: true);

$code = "\n" . $zvalCode;

Expand Down
Loading
Loading