fix: 修复生成器返回类型 #31

Merged
韩天峰 merged 3 commits from compiler-keep-generator-retype-covariance into master 1 month ago
  1. 14
      phpunit/code/inheritance_error_generator_return_widened.php
  2. 5
      phpunit/src/InheritanceErrorTest.php
  3. 14
      src/Entity/FunctionDef.php
  4. 18
      src/Generator/FiberGenerator.php
  5. 25
      src/Translator.php
  6. 57
      tests/compiler/generator/generator-foreach-yield.phpt
  7. 53
      tests/compiler/generator/generator-return-type-generator.phpt
  8. 96
      tests/compiler/generator/interface-return-type-variants.phpt
  9. 38
      tests/compiler/generator/interface-return-type.phpt

@ -0,0 +1,14 @@
<?php
interface GeneratorReturnContract
{
public function values(): \Generator;
}
class GeneratorReturnImplementation implements GeneratorReturnContract
{
public function values(): iterable
{
yield 1;
}
}

@ -73,6 +73,11 @@ class InheritanceErrorTest extends TestCase
$this->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');

@ -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;

@ -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

@ -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']],

@ -0,0 +1,57 @@
--TEST--
generator re-yielding array elements via foreach with \Generator return type
--FILE--
<?php
function main()
{
$g = test([1, 2, 3]);
var_dump($g);
foreach ($g as $value)
{
var_dump($value);
}
}
function test(array $array): \Generator
{
foreach ($array as $value)
{
yield $value;
}
}
// main();
?>
--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)

@ -0,0 +1,53 @@
--TEST--
generator return type accepts \Generator for methods, nullable and union variants
--FILE--
<?php
class Box
{
public function gen(array $array): \Generator
{
foreach ($array as $value) {
yield $value * 2;
}
}
}
function nullableGen(array $array): ?\Generator
{
foreach ($array as $value) {
yield $value;
}
}
function unionGen(array $array): \Generator|\Iterator
{
foreach ($array as $value) {
yield $value;
}
}
function main()
{
$b = new Box();
foreach ($b->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)

@ -0,0 +1,96 @@
--TEST--
generator methods implementing interfaces with iterable, nullable and union return types
--FILE--
<?php
interface GenInterface
{
public function gen(array $array): \Generator;
}
interface IterableInterface
{
public function it(array $array): iterable;
public function narrowed(array $array): iterable;
}
interface NullableInterface
{
public function nullable(array $array): ?\Generator;
}
interface UnionInterface
{
public function union(array $array): \Generator|\Iterator;
}
class Box implements GenInterface, IterableInterface, NullableInterface, UnionInterface
{
public function gen(array $array): \Generator
{
foreach ($array as $value) {
yield $value * 2;
}
}
public function it(array $array): iterable
{
foreach ($array as $value) {
yield $value;
}
}
public function narrowed(array $array): \Generator
{
foreach ($array as $value) {
yield $value;
}
}
public function nullable(array $array): ?\Generator
{
foreach ($array as $value) {
yield $value;
}
}
public function union(array $array): \Generator|\Iterator
{
foreach ($array as $value) {
yield $value;
}
}
}
function main()
{
$box = new Box();
foreach ($box->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)

@ -0,0 +1,38 @@
--TEST--
generator method implementing an interface that declares \Generator return type
--FILE--
<?php
interface T
{
public function test(array $array): \Generator;
}
class TestClass implements T
{
public function test(array $array): \Generator
{
foreach ($array as $value) {
yield $value;
}
}
}
function main()
{
$test = new TestClass;
$g = $test->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)
Loading…
Cancel
Save