fix(compiler): 保留生成器声明返回类型以满足接口协变检查

Cherry-pick of upstream 84ad3a5. The generator return type covariance
change is built on yurun's Translator.php, which diverged from
origin/master, so the files are materialized from the upstream commit
directly (they already include the cc25d71 FiberGenerator change).

- isReturnTypeOverrideCompatible now performs full covariance: every value
  the child can return must be acceptable under the parent's declared
  return type (nullable/union narrowing allowed).
- Add getReturnAcceptedTypes/isReturnTypeSubtype/isReturnTypeCoveredBy/
  isReturnTypeEntryCompatible helpers; generators use their source-level
  declared return type so interface/abstract covariance checks hold.
- FunctionDef gains declaredReturnType/declaredReturnClass/declaredReturnTypeCheck.

Regression tests: interface-return-type.phpt, interface-return-type-variants.phpt.
pull/31/head
Yurun 1 month ago
parent c0556882ab
commit cdf4e5f789
  1. 21
      src/Entity/FunctionDef.php
  2. 11
      src/Generator/FiberGenerator.php
  3. 199
      src/Translator.php
  4. 82
      tests/compiler/generator/interface-return-type-variants.phpt
  5. 38
      tests/compiler/generator/interface-return-type.phpt

@ -33,6 +33,14 @@ class FunctionDef
public string $attributeFactoryScope = '';
/** External library imported by the stub containing this function. */
public string $importLibrary = '';
/**
* True for a trait method whose body contains `parent::` calls. Such methods
* receive an implicit `zend_class_entry *trait_parent_ce` parameter (right after
* `this_`) so the `parent::` call can be bound to the class that composes the
* trait. Both the definition and the shared `func_decl.h` declaration must emit
* this parameter, otherwise the declaration/definition signatures disagree.
*/
public bool $traitParentCe = false;
public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false;
public bool $generator = false;
@ -74,6 +82,19 @@ 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 function __construct(string $name, string $returnType, string $namespace)
{
$this->name = $name;

@ -72,6 +72,17 @@ 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->generator = true;
$functionDef->returnType = Type::VAR;
$functionDef->returnClass = '';

@ -1602,6 +1602,12 @@ CODE;
$list = [];
if ($func->method) {
$list[] = Type::OBJECT . ' &this_';
// A trait method with `parent::` calls receives an implicit
// `trait_parent_ce` parameter right after `this_`. The definition
// adds it (see parseFunction); the declaration must match.
if ($func->traitParentCe) {
$list[] = 'zend_class_entry *trait_parent_ce';
}
}
$argInfoList = $func->argInfoList;
if ($argInfoList) {
@ -2514,6 +2520,8 @@ CODE;
case 'Stmt_Interface':
$this->validateInterfaceOverrideAttributes($v2);
break;
case 'Stmt_Nop':
break;
default:
abort($v2);
break;
@ -3464,6 +3472,9 @@ CODE;
$functionDeclCode .= Type::OBJECT . ' &this_';
if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) {
$functionDeclCode .= ', zend_class_entry *trait_parent_ce';
// Record the implicit parameter so the shared `func_decl.h`
// declaration (genFunctionDeclaration) emits the same signature.
$this->functionDef->traitParentCe = true;
}
if ($this->functionDef->params) {
$functionDeclCode .= ', ';
@ -3717,25 +3728,162 @@ CODE;
if ($childFuncDef->returnTypeUndeclared) {
return false;
}
if ($parentFuncDef->returnTypeCheck || $childFuncDef->returnTypeCheck) {
return $parentFuncDef->returnTypeStr === $childFuncDef->returnTypeStr;
}
// A parent that accepts everything (mixed/var) is compatible with any
// child return type.
if ($parentFuncDef->returnType === Type::VAR) {
return true;
}
if ($childFuncDef->returnType !== $parentFuncDef->returnType) {
$parentTypes = $this->getReturnAcceptedTypes($parentFuncDef);
$childTypes = $this->getReturnAcceptedTypes($childFuncDef);
// Return type covariance: every value the child can return must also be
// acceptable under the parent's declared return type. This allows a
// child to narrow a nullable/union return type (e.g. `?Base` -> `?Child`
// or `int|string` -> `int`) while still satisfying the parent contract.
return $this->isReturnTypeSubtype($childTypes, $parentTypes);
}
private function getReturnAcceptedTypes(FunctionDef $functionDef): array
{
if ($functionDef->generator) {
// The runtime return type of a generator is neutralized to VAR because
// it actually returns a `\FiberGenerator`. Use the source-level declared
// return type so interface/abstract covariance checks still work.
if (!empty($functionDef->declaredReturnTypeCheck)) {
return $functionDef->declaredReturnTypeCheck;
}
$type = $functionDef->declaredReturnType;
if ($type === Type::VAR) {
return [['kind' => 'isMixed']];
}
if ($type === Type::OBJECT) {
return $functionDef->declaredReturnClass
? [['kind' => 'instanceof', 'class' => $functionDef->declaredReturnClass]]
: [['kind' => 'isObject']];
}
return match ($type) {
Type::INT => [['kind' => 'isInt']],
Type::FLOAT => [['kind' => 'isFloat']],
Type::BOOL => [['kind' => 'isBool']],
Type::STR => [['kind' => 'isString']],
Type::ARRAY => [['kind' => 'isArray']],
Type::RESOURCE => [['kind' => 'isResource']],
default => [['kind' => 'isMixed']],
};
}
if (!empty($functionDef->returnTypeCheck)) {
return $functionDef->returnTypeCheck;
}
$type = $functionDef->returnType;
if ($type === Type::VAR) {
return [['kind' => 'isMixed']];
}
if ($type === Type::OBJECT) {
return $functionDef->returnClass
? [['kind' => 'instanceof', 'class' => $functionDef->returnClass]]
: [['kind' => 'isObject']];
}
return match ($type) {
Type::INT => [['kind' => 'isInt']],
Type::FLOAT => [['kind' => 'isFloat']],
Type::BOOL => [['kind' => 'isBool']],
Type::STR => [['kind' => 'isString']],
Type::ARRAY => [['kind' => 'isArray']],
Type::RESOURCE => [['kind' => 'isResource']],
default => [['kind' => 'isMixed']],
};
}
private function isReturnTypeSubtype(array $childTypes, array $parentTypes): bool
{
foreach ($childTypes as $childType) {
if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) {
return false;
}
if ($parentFuncDef->returnType !== Type::OBJECT) {
}
return true;
}
if ($childFuncDef->returnClass === $parentFuncDef->returnClass) {
private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool
{
$childKind = $childType['kind'] ?? null;
// Child is an intersection (A&B): it is a subtype only if every member
// is individually a subtype of the parent type.
if ($childKind === 'allOf') {
foreach ($childType['types'] as $member) {
if (!$this->isReturnTypeCoveredBy($member, $parentTypes)) {
return false;
}
}
return true;
}
if (!$childFuncDef->returnClass || !$parentFuncDef->returnClass) {
foreach ($parentTypes as $parentType) {
$parentKind = $parentType['kind'] ?? null;
// Parent is an intersection (A&B): the child must be a subtype of
// every member of the intersection.
if ($parentKind === 'allOf') {
$ok = true;
foreach ($parentType['types'] as $member) {
if (!$this->isReturnTypeCoveredBy($childType, [$member])) {
$ok = false;
break;
}
}
if ($ok) {
return true;
}
continue;
}
if ($this->isReturnTypeEntryCompatible($childKind, $childType, $parentKind, $parentType)) {
return true;
}
}
return false;
}
return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass);
private function isReturnTypeEntryCompatible(
?string $childKind,
array $childType,
?string $parentKind,
array $parentType
): bool {
if ($childKind === 'isNull') {
// A null value is only compatible with a nullable (isNull) parent.
return $parentKind === 'isNull';
}
if ($childKind === 'isObject') {
// Any object is compatible with a parent that accepts any object.
return $parentKind === 'isObject';
}
if ($childKind === 'isMixed') {
return $parentKind === 'isMixed';
}
if ($childKind === 'instanceof') {
if ($parentKind === 'isObject') {
return true;
}
if ($parentKind === 'instanceof') {
$childClass = $childType['class'] ?? '';
$parentClass = $parentType['class'] ?? '';
if ($childClass === '' || $parentClass === '' || $childClass === 'static' || $parentClass === 'static') {
return false;
}
if ($childClass === $parentClass) {
return true;
}
return $this->isInheritedFrom($childClass, $parentClass);
}
return false;
}
// Scalar kinds must match exactly.
return $childKind === $parentKind;
}
private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool
@ -4320,6 +4468,14 @@ CODE;
// function untouched.
$this->reresolveTraitLateBoundTypes($classDef, $methodDef);
// The wrapper is a method of the *composing* class, not a trait method, so
// it must not receive the implicit `trait_parent_ce` parameter (it computes
// the parent class entry itself when forwarding to the trait function). The
// cloned FunctionDef inherited `traitParentCe` from the trait's FunctionDef;
// clear it so the shared `func_decl.h` declaration matches the wrapper's
// own (2-parameter) definition.
$methodDef->functionDef->traitParentCe = false;
// Validate `parent::` calls emitted from this trait method against the
// parent of the class that is composing the trait. The trait itself has
// no parent at compile time, so this is the only place the parent class
@ -4390,28 +4546,11 @@ CODE;
private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void
{
$fn = $methodDef->functionDef;
$needsClone = false;
if ($fn->returnTypeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);
if ($resolved !== null && $resolved !== $fn->returnClass) {
$needsClone = true;
}
}
foreach ($fn->argInfoList as $arg) {
if ($arg->typeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $arg->typeKeyword);
if ($resolved !== null && ($resolved !== $arg->class || $resolved !== $arg->declaredClass)) {
$needsClone = true;
break;
}
}
}
if (!$needsClone) {
return;
}
// Always produce a distinct FunctionDef for the composing-class wrapper.
// The wrapper is a separate method (different name, and no implicit
// `trait_parent_ce` parameter) from the trait's own function, so it must
// not share the trait's FunctionDef object — mutating one (e.g. clearing
// `traitParentCe`) would otherwise leak into the trait's declaration.
$newFn = clone $fn;
if ($fn->returnTypeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);

@ -0,0 +1,82 @@
--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;
}
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 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->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(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