diff --git a/phpunit/code/inheritance_error_generator_return_widened.php b/phpunit/code/inheritance_error_generator_return_widened.php new file mode 100644 index 00000000..cccf6ae1 --- /dev/null +++ b/phpunit/code/inheritance_error_generator_return_widened.php @@ -0,0 +1,14 @@ +exec('must be compatible', 'inheritance_error_return_never_widened.php'); } + public function testGeneratorReturnTypeCannotBeWidenedToIterable(): void + { + $this->exec('must be compatible', 'inheritance_error_generator_return_widened.php'); + } + public function testIntersectionReturnTypeCanNarrowToIntersectionOrConcreteSubtype(): void { $this->assertCompiles('return_type_covariance_intersection.php'); diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 78e8c01b..028b69f9 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -74,6 +74,20 @@ class FunctionDef /** Original union/nullable return type AST node. */ public ?NodeAbstract $returnTypeNode = null; + /** + * Source-level return type declared on a generator method, preserved after + * `prepareGeneratorFunction()` neutralizes the runtime return type. A + * generator actually returns a `\FiberGenerator` (which implements + * `Iterator`), so the C++ return type and runtime type check are left + * neutral; this copy is only used by interface/abstract return-type + * covariance checks so a generator method can still satisfy a contract such + * as `: \Generator`. + */ + public ?string $declaredReturnType = null; + public string $declaredReturnClass = ''; + public ?array $declaredReturnTypeCheck = null; + public string $declaredReturnTypeStr = ''; + public function __construct(string $name, string $returnType, string $namespace) { $this->name = $name; diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index a67f5870..9aa44dbd 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -72,6 +72,18 @@ trait FiberGenerator if (!$this->generatorReturnTypeAcceptsFiber($v->returnType)) { $this->fatalError($v, 'Generator return type must accept \\FiberGenerator; use Iterator, Traversable, iterable, object, mixed, or omit the return type'); } + // Preserve the source-level declared return type before neutralizing the + // runtime return type. The override compatibility check still needs it so + // a generator method can satisfy an interface/abstract contract such as + // `: \Generator` (the runtime object is a `\FiberGenerator`, not a Zend + // `Generator`, so the C++ return type and runtime check stay neutral). + if ($v->returnType !== null) { + $declared = $this->buildTypeCheckFromNode($v->returnType); + $functionDef->declaredReturnTypeCheck = $declared['check'] ?: null; + } + $functionDef->declaredReturnType = $functionDef->returnType; + $functionDef->declaredReturnClass = $functionDef->returnClass; + $functionDef->declaredReturnTypeStr = $functionDef->returnTypeStr; $functionDef->generator = true; $functionDef->returnType = Type::VAR; $functionDef->returnClass = ''; @@ -112,7 +124,11 @@ trait FiberGenerator [, $class] = $this->resolveTypeDecl($type, self::DECL_TYPE_OF_RETURN); $class = strtolower(ltrim($class, '\\')); - return in_array($class, ['iterator', 'traversable', 'fibergenerator'], true); + // `\Generator` is the return type PHP programmers naturally write for a + // generator. TypePHP generators actually return a `\FiberGenerator`, so + // accepting the declared `Generator` type keeps PHP source compatible + // while the runtime object remains a `\FiberGenerator`. + return in_array($class, ['iterator', 'traversable', 'fibergenerator', 'generator'], true); } protected function parseYieldExpr(Yield_ $expr): string diff --git a/src/Translator.php b/src/Translator.php index 13ff107e..66568139 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3745,21 +3745,34 @@ CODE; private function getReturnAcceptedTypes(FunctionDef $functionDef, string $declaringClass): array { - if (!empty($functionDef->returnTypeCheck)) { + $returnTypeCheck = $functionDef->generator + ? $functionDef->declaredReturnTypeCheck + : $functionDef->returnTypeCheck; + $returnType = $functionDef->generator + ? $functionDef->declaredReturnType + : $functionDef->returnType; + $returnClass = $functionDef->generator + ? $functionDef->declaredReturnClass + : $functionDef->returnClass; + $returnTypeStr = $functionDef->generator + ? $functionDef->declaredReturnTypeStr + : $functionDef->returnTypeStr; + + if (!empty($returnTypeCheck)) { return array_map( fn (array $type): array => $this->normalizeReturnTypeEntry($type, $declaringClass), - $functionDef->returnTypeCheck, + $returnTypeCheck, ); } if ($functionDef->returnTypeKeyword === 'static') { return [['kind' => 'isStatic', 'class' => $declaringClass]]; } - if ($functionDef->returnType === Type::OBJECT && $functionDef->returnClass !== '') { - return [['kind' => 'instanceof', 'class' => $functionDef->returnClass]]; + if ($returnType === Type::OBJECT && $returnClass !== '') { + return [['kind' => 'instanceof', 'class' => $returnClass]]; } - $declaredType = strtolower($functionDef->returnTypeStr); + $declaredType = strtolower($returnTypeStr); return match ($declaredType) { 'mixed' => [['kind' => 'isMixed']], 'never' => [['kind' => 'isNever']], @@ -3770,7 +3783,7 @@ CODE; 'callable' => [['kind' => 'callable']], 'iterable' => [['kind' => 'iterable']], 'object' => [['kind' => 'isObject']], - default => match ($functionDef->returnType) { + default => match ($returnType) { Type::INT => [['kind' => 'isInt']], Type::FLOAT => [['kind' => 'isFloat']], Type::BOOL => [['kind' => 'isBool']], diff --git a/tests/compiler/generator/generator-foreach-yield.phpt b/tests/compiler/generator/generator-foreach-yield.phpt new file mode 100644 index 00000000..d2029fc9 --- /dev/null +++ b/tests/compiler/generator/generator-foreach-yield.phpt @@ -0,0 +1,57 @@ +--TEST-- +generator re-yielding array elements via foreach with \Generator return type +--FILE-- + +--EXPECTF-- +object(FiberGenerator)#%d (9) { + ["callback":"FiberGenerator":private]=> + object(Closure)#%d (2) { + ["function"]=> + string(19) "stdClass::{closure}" + ["this"]=> + object(stdClass)#%d (1) { + ["box"]=> + resource(%d) of type (php::box) + } + } + ["fiber":"FiberGenerator":private]=> + NULL + ["current":"FiberGenerator":private]=> + NULL + ["key":"FiberGenerator":private]=> + NULL + ["valid":"FiberGenerator":private]=> + bool(false) + ["state":"FiberGenerator":private]=> + int(0) + ["yield_count":"FiberGenerator":private]=> + int(0) + ["next_index":"FiberGenerator":private]=> + int(0) + ["return_value":"FiberGenerator":private]=> + NULL +} +int(1) +int(2) +int(3) diff --git a/tests/compiler/generator/generator-return-type-generator.phpt b/tests/compiler/generator/generator-return-type-generator.phpt new file mode 100644 index 00000000..4d5894cc --- /dev/null +++ b/tests/compiler/generator/generator-return-type-generator.phpt @@ -0,0 +1,53 @@ +--TEST-- +generator return type accepts \Generator for methods, nullable and union variants +--FILE-- +gen([1, 2, 3]) as $v) { + var_dump($v); + } + $g = nullableGen([4, 5]); + foreach ($g as $v) { + var_dump($v); + } + $u = unionGen([6, 7]); + foreach ($u as $v) { + var_dump($v); + } +} +?> +--EXPECT-- +int(2) +int(4) +int(6) +int(4) +int(5) +int(6) +int(7) diff --git a/tests/compiler/generator/interface-return-type-variants.phpt b/tests/compiler/generator/interface-return-type-variants.phpt new file mode 100644 index 00000000..d36b61e0 --- /dev/null +++ b/tests/compiler/generator/interface-return-type-variants.phpt @@ -0,0 +1,96 @@ +--TEST-- +generator methods implementing interfaces with iterable, nullable and union return types +--FILE-- +gen([1, 2, 3]) as $v) { + var_dump($v); + } + foreach ($box->it([4, 5]) as $v) { + var_dump($v); + } + foreach ($box->narrowed([10, 11]) as $v) { + var_dump($v); + } + foreach ($box->nullable([6, 7]) as $v) { + var_dump($v); + } + foreach ($box->union([8, 9]) as $v) { + var_dump($v); + } +} +?> +--EXPECT-- +int(2) +int(4) +int(6) +int(4) +int(5) +int(10) +int(11) +int(6) +int(7) +int(8) +int(9) diff --git a/tests/compiler/generator/interface-return-type.phpt b/tests/compiler/generator/interface-return-type.phpt new file mode 100644 index 00000000..82eb4ca4 --- /dev/null +++ b/tests/compiler/generator/interface-return-type.phpt @@ -0,0 +1,38 @@ +--TEST-- +generator method implementing an interface that declares \Generator return type +--FILE-- +test([1, 2, 3]); + // TypePHP generators return a \FiberGenerator which implements Iterator + // but is NOT the Zend \Generator class. + var_dump($g instanceof \Generator); + var_dump($g instanceof \Iterator); + foreach ($g as $value) { + var_dump($value); + } +} +?> +--EXPECT-- +bool(false) +bool(true) +int(1) +int(2) +int(3)