diff --git a/phpunit/code/const_override_dnf_covariant.php b/phpunit/code/const_override_dnf_covariant.php new file mode 100644 index 00000000..7571a703 --- /dev/null +++ b/phpunit/code/const_override_dnf_covariant.php @@ -0,0 +1,28 @@ +compile('const_override_covariant.php'); } + public function testNarrowingDnfDeclaredTypeCompiles(): void + { + $this->compile('const_override_dnf_covariant.php'); + } + public function testWideningDeclaredTypeIsRejected(): void { $this->exec( diff --git a/phpunit/src/InterfaceConstantTest.php b/phpunit/src/InterfaceConstantTest.php index c96a17be..c5368532 100644 --- a/phpunit/src/InterfaceConstantTest.php +++ b/phpunit/src/InterfaceConstantTest.php @@ -71,4 +71,20 @@ class InterfaceConstantTest extends BaseTest 'enum_interface_const_final.php', ); } + + public function testInterfaceInheritanceCycleFailsWithoutRecursingForever(): void + { + $this->exec( + 'Interface inheritance cycle detected', + 'interface_const_inheritance_cycle.php', + ); + } + + public function testTraitConstantMustSatisfyImplementedInterface(): void + { + $this->exec( + 'Declaration of `TraitConstantImplementation::VALUE` must be compatible with `TraitConstantContract::VALUE`', + 'interface_const_trait_mismatch.php', + ); + } } diff --git a/src/Translator.php b/src/Translator.php index cf443aaf..6990efb5 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -91,6 +91,9 @@ class Translator extends Preprocessor * @var array> */ private array $effectiveConstantTables = []; + + /** @var array Class-like constants tables being constructed. */ + private array $effectiveConstantTableVisiting = []; protected array $globalHeaders = [ 'cstring', 'phpx.h', @@ -5794,42 +5797,52 @@ CODE; * * @return array */ - private function getEffectiveConstantTable(ClassDef|InterfaceDef $def): array + private function getEffectiveConstantTable(ClassDef|InterfaceDef $def, NodeAbstract $errorNode): array { $ownName = $def->getNamespacedName(false); $key = strtolower($ownName); if (isset($this->effectiveConstantTables[$key])) { return $this->effectiveConstantTables[$key]; } - $table = []; - if ($def instanceof ClassDef) { - if ($def->extends !== '' && !$def->inheritedFromInternalClass && $this->hasClass($def->extends)) { - foreach ($this->getEffectiveConstantTable($this->getClass($def->extends)) as $name => $entry) { - // Private constants are not inherited. - if (!($entry['const']->flags & Modifiers::PRIVATE)) { - $table[$name] = $entry; + if (isset($this->effectiveConstantTableVisiting[$key])) { + $kind = $def instanceof InterfaceDef ? 'Interface' : 'Class'; + $this->fatalError($errorNode, "{$kind} inheritance cycle detected at `{$ownName}`"); + } + + $this->effectiveConstantTableVisiting[$key] = true; + try { + $table = []; + if ($def instanceof ClassDef) { + if ($def->extends !== '' && !$def->inheritedFromInternalClass && $this->hasClass($def->extends)) { + foreach ($this->getEffectiveConstantTable($this->getClass($def->extends), $errorNode) as $name => $entry) { + // Private constants are not inherited. + if (!($entry['const']->flags & Modifiers::PRIVATE)) { + $table[$name] = $entry; + } } } + foreach ($def->constants as $name => $const) { + $table[$name] = ['const' => $const, 'origin' => $ownName]; + } + $parents = $def->implements; + } else { + foreach ($def->constants as $name => $const) { + $table[$name] = ['const' => $const, 'origin' => $ownName]; + } + $parents = $def->extendsList ?: ($def->extends ? [$def->extends] : []); } - foreach ($def->constants as $name => $const) { - $table[$name] = ['const' => $const, 'origin' => $ownName]; - } - $parents = $def->implements; - } else { - foreach ($def->constants as $name => $const) { - $table[$name] = ['const' => $const, 'origin' => $ownName]; - } - $parents = $def->extendsList ?: ($def->extends ? [$def->extends] : []); - } - foreach ($parents as $interfaceName) { - if (!$this->hasInterface($interfaceName)) { - continue; - } - foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName)) as $name => $entry) { - $table[$name] ??= $entry; + foreach ($parents as $interfaceName) { + if (!$this->hasInterface($interfaceName)) { + continue; + } + foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName), $errorNode) as $name => $entry) { + $table[$name] ??= $entry; + } } + return $this->effectiveConstantTables[$key] = $table; + } finally { + unset($this->effectiveConstantTableVisiting[$key]); } - return $this->effectiveConstantTables[$key] = $table; } /** @@ -5854,7 +5867,7 @@ CODE; // parent chain's effective table, then the class's own declarations. $table = []; if ($classDef->extends !== '' && !$classDef->inheritedFromInternalClass && $this->hasClass($classDef->extends)) { - foreach ($this->getEffectiveConstantTable($this->getClass($classDef->extends)) as $name => $entry) { + foreach ($this->getEffectiveConstantTable($this->getClass($classDef->extends), $classStmt) as $name => $entry) { if (!($entry['const']->flags & Modifiers::PRIVATE)) { $table[$name] = $entry; } @@ -5881,7 +5894,7 @@ CODE; if (!$this->hasInterface($interfaceName)) { continue; } - foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName)) as $name => $entry) { + foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName), $classStmt) as $name => $entry) { if (!isset($table[$name])) { $table[$name] = $entry; continue; @@ -5920,7 +5933,7 @@ CODE; if (!$this->hasInterface($parentName)) { continue; } - foreach ($this->getEffectiveConstantTable($this->getInterface($parentName)) as $constName => $entry) { + foreach ($this->getEffectiveConstantTable($this->getInterface($parentName), $interfaceStmt) as $constName => $entry) { if (!isset($table[$constName])) { $table[$constName] = $entry; continue; diff --git a/src/gen_stub.php b/src/gen_stub.php index 25533823..1aa274a3 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2728,9 +2728,17 @@ class EvaluatedValue $result = $evaluator->evaluateDirectly($expr); + // The declared type is useful when an UNKNOWN placeholder must be + // emitted through its @cvalue macro. For a concrete null expression, + // however, the zval must be initialized as null even when the declared + // type is nullable (for example, `const ?int VALUE = null`). + $valueType = $result === null && !$isUnknownConstValue + ? SimpleType::null() + : ($constType ?? SimpleType::fromValue($result)); + return 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), + $valueType, $cConstName === null ? $expr : new Expr\ConstFetch(new Node\Name($cConstName)), $visitor->visitedConstants, $isUnknownConstValue @@ -2907,7 +2915,7 @@ abstract class VariableLike $typeCode = ""; if ($this->type) { if ($this->type->isDnf()) { - assert($this instanceof PropertyInfo); + assert($this instanceof PropertyInfo || $this instanceof ConstInfo); return $this->type->getDnfTypeExpression($this->getDnfTypeFactorySymbol(), '0'); } $arginfoType = $this->type->toArginfoType(); @@ -3069,6 +3077,22 @@ class ConstInfo extends VariableLike return "constant"; } + public function getDnfTypeFactorySymbol(): string + { + assert($this->name instanceof ClassConstName); + return 'constant_' . implode('_', $this->name->class->getParts()) + . '_' . $this->name->getDeclarationName() . '_dnf'; + } + + public function getDnfTypeFactoryCode(): string + { + if (!$this->type?->isDnf()) { + return ''; + } + assert($this->name instanceof ClassConstName); + return $this->type->getDnfTypeDeclarations($this->getDnfTypeFactorySymbol()); + } + protected function getFieldSynopsisDefaultLinkend(): string { $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); @@ -4055,6 +4079,15 @@ class ClassInfo { return $code; } + public function getDnfConstantTypeFactoryCode(): string + { + $code = ''; + foreach ($this->constInfos as $constInfo) { + $code .= $constInfo->getDnfTypeFactoryCode(); + } + return $code; + } + /** @param array $allConstInfos */ public function getRegistration(array $allConstInfos): string { @@ -5854,6 +5887,7 @@ function generateArgInfoCode( . " * Stub hash: $stubHash */\n"; foreach ($fileInfo->classInfos as $classInfo) { + $code .= $classInfo->getDnfConstantTypeFactoryCode(); $code .= $classInfo->getDnfPropertyTypeFactoryCode(); } if (!str_ends_with($code, "*/\n")) { diff --git a/tests/compiler/class/typed-class-constant-covariance.phpt b/tests/compiler/class/typed-class-constant-covariance.phpt new file mode 100644 index 00000000..8fa53a0d --- /dev/null +++ b/tests/compiler/class/typed-class-constant-covariance.phpt @@ -0,0 +1,51 @@ +--TEST-- +Typed class and interface constants support covariant overrides at runtime +--FILE-- + +--EXPECT-- +int(1) +NULL +int(2) +int(6) +int(4) +int(5) +int(3)