From c0556882ab9c56f78c750c772eaf3601b6651f1c Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 22 Jul 2026 16:43:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(generator):=20=E6=8E=A5=E5=8F=97=20\Gen?= =?UTF-8?q?erator=20=E4=BD=9C=E4=B8=BA=E5=90=88=E6=B3=95=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Generator/FiberGenerator.php | 6 +- .../generator/generator-foreach-yield.phpt | 57 +++++++++++++++++++ .../generator-return-type-generator.phpt | 53 +++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/generator/generator-foreach-yield.phpt create mode 100644 tests/compiler/generator/generator-return-type-generator.phpt diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index a67f5870..9e766fd2 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -112,7 +112,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/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) -- 2.34.1 From cdf4e5f789967f2c12f7ae042ee5c95c109cb646 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:43:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(compiler):=20=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=E5=A3=B0=E6=98=8E=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=BB=A5=E6=BB=A1=E8=B6=B3=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=8D=8F=E5=8F=98=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/Entity/FunctionDef.php | 21 ++ src/Generator/FiberGenerator.php | 11 + src/Translator.php | 203 +++++++++++++++--- .../interface-return-type-variants.phpt | 82 +++++++ .../generator/interface-return-type.phpt | 38 ++++ 5 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 tests/compiler/generator/interface-return-type-variants.phpt create mode 100644 tests/compiler/generator/interface-return-type.phpt diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 78e8c01b..8cf00783 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -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; diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index 9e766fd2..e547372b 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -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 = ''; diff --git a/src/Translator.php b/src/Translator.php index 14916aec..f1f3e55f 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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) { - return false; + + $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 ($parentFuncDef->returnType !== Type::OBJECT) { - return true; + + if (!empty($functionDef->returnTypeCheck)) { + return $functionDef->returnTypeCheck; } - if ($childFuncDef->returnClass === $parentFuncDef->returnClass) { + $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; + } + } + return true; + } + + 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; + } + + 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; } - return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass); + // 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); 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..c6ee5ef5 --- /dev/null +++ b/tests/compiler/generator/interface-return-type-variants.phpt @@ -0,0 +1,82 @@ +--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->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) 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) -- 2.34.1