diff --git a/phpunit/code/inheritance_error_dnf_param_narrowed.php b/phpunit/code/inheritance_error_dnf_param_narrowed.php new file mode 100644 index 00000000..5c8df4b3 --- /dev/null +++ b/phpunit/code/inheritance_error_dnf_param_narrowed.php @@ -0,0 +1,15 @@ +exec('must be compatible', 'inheritance_error_return_intersection_missing.php'); } + public function testDnfReturnTypeCannotBeWidened(): void + { + $this->exec('must be compatible', 'inheritance_error_dnf_return_widened.php'); + } + public function testStaticReturnTypeCannotBeWidenedToSelf(): void { $this->exec('must be compatible', 'inheritance_error_return_static_widened.php'); @@ -88,6 +93,11 @@ class InheritanceErrorTest extends TestCase $this->exec('must be compatible', 'inheritance_error_param_covariant_class.php'); } + public function testDnfParameterTypeCannotBeNarrowed(): void + { + $this->exec('must be compatible', 'inheritance_error_dnf_param_narrowed.php'); + } + public function testUnionParameterCannotNarrowUntypedParent() { $this->exec('must be compatible', 'interface_param_union_narrows_untyped.php'); diff --git a/src/Translator.php b/src/Translator.php index d435588a..e8cc1f18 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -4461,43 +4461,11 @@ CODE; private function isAcceptedTypeCovered(array $parentType, array $childTypes): bool { - foreach ($childTypes as $childType) { - if ($this->isAcceptedTypeCompatible($parentType, $childType)) { - return true; - } - } - return false; - } - - private function isAcceptedTypeCompatible(array $parentType, array $childType): bool - { - $parentKind = $parentType['kind'] ?? null; - $childKind = $childType['kind'] ?? null; - - if ($parentKind === 'instanceof' && $childKind === 'isObject') { - return true; - } - if ($parentKind !== $childKind) { - return false; - } - - if ($parentKind === 'allOf') { - return $parentType == $childType; - } - - if ($parentKind !== 'instanceof') { - return true; - } - - $parentClass = $parentType['class'] ?? ''; - $childClass = $childType['class'] ?? ''; - if ($parentClass === $childClass) { - return true; - } - if ($parentClass === '' || $childClass === '') { - return false; - } - return $this->isInheritedFrom($parentClass, $childClass); + // Parameter contravariance requires every value accepted by the parent + // clause to also be accepted by at least one child clause. The same DNF + // clause-subtyping relation used for covariant returns applies here, + // with the parent clause as the narrower candidate. + return $this->isReturnTypeCoveredBy($parentType, $childTypes); } private function checkInterfaceImplementations(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void diff --git a/src/gen_stub.php b/src/gen_stub.php index 7e04e0e0..42d4b194 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -539,6 +539,8 @@ class StubType { /** @var SimpleType[] */ public /* readonly */ array $types; public /* readonly */ bool $isIntersection; + /** @var ?array> DNF union clauses, intersections contain multiple members. */ + public /* readonly */ ?array $dnfClauses; public static function fromNode(Node $node): StubType { if ($node instanceof Node\UnionType || $node instanceof Node\IntersectionType) { @@ -547,7 +549,23 @@ class StubType { foreach ($nestedTypeObjects as $typeObject) { array_push($types, ...$typeObject->types); } - return new StubType($types, ($node instanceof Node\IntersectionType)); + if ($node instanceof Node\UnionType) { + $dnfClauses = []; + $hasIntersection = false; + foreach ($node->types as $i => $memberNode) { + $memberType = $nestedTypeObjects[$i]; + if ($memberNode instanceof Node\IntersectionType) { + $dnfClauses[] = $memberType->types; + $hasIntersection = true; + } else { + foreach ($memberType->types as $simpleType) { + $dnfClauses[] = [$simpleType]; + } + } + } + return new StubType($types, false, $hasIntersection ? $dnfClauses : null); + } + return new StubType($types, true); } if ($node instanceof Node\NullableType) { @@ -614,9 +632,103 @@ class StubType { /** * @param SimpleType[] $types */ - private function __construct(array $types, bool $isIntersection) { + private function __construct(array $types, bool $isIntersection, ?array $dnfClauses = null) { $this->types = $types; $this->isIntersection = $isIntersection; + $this->dnfClauses = $dnfClauses; + } + + public function isDnf(): bool { + return $this->dnfClauses !== null; + } + + public function getDnfTypeDeclarations(string $symbol): string { + assert($this->dnfClauses !== null); + $code = "static zend_type create_{$symbol}(uint32_t extra_flags)\n{\n"; + $outerEntries = []; + foreach ($this->dnfClauses as $i => $clause) { + if (count($clause) === 1) { + $type = $clause[0]; + if (!$type->isBuiltin) { + $classVar = $symbol . '_class_' . $i; + $escapedName = $type->toEscapedName(); + $code .= "\tzend_string *{$classVar} = zend_string_init_interned(\"{$escapedName}\", " + . "sizeof(\"{$escapedName}\") - 1, true);\n"; + $code .= "\tzend_alloc_ce_cache({$classVar});\n"; + $outerEntries[] = '(zend_type) ZEND_TYPE_INIT_CLASS(' . $classVar . ', 0, 0)'; + } + continue; + } + + $intersection = $symbol . '_intersection_' . $i; + $code .= "\tzend_type_list *{$intersection} = (zend_type_list *) malloc(" + . 'ZEND_TYPE_LIST_SIZE(' . count($clause) . "));\n"; + $code .= "\t{$intersection}->num_types = " . count($clause) . ";\n"; + foreach ($clause as $j => $type) { + assert(!$type->isBuiltin); + $classVar = $symbol . '_class_' . $i . '_' . $j; + $escapedName = $type->toEscapedName(); + $code .= "\tzend_string *{$classVar} = zend_string_init_interned(\"{$escapedName}\", " + . "sizeof(\"{$escapedName}\") - 1, true);\n"; + $code .= "\tzend_alloc_ce_cache({$classVar});\n"; + $code .= "\t{$intersection}->types[{$j}] = (zend_type) ZEND_TYPE_INIT_CLASS(" + . "{$classVar}, 0, 0);\n"; + } + $outerEntries[] = '(zend_type) ZEND_TYPE_INIT_INTERSECTION(' . $intersection . ', 0)'; + } + + $code .= "\tzend_type_list *{$symbol} = (zend_type_list *) malloc(" + . 'ZEND_TYPE_LIST_SIZE(' . count($outerEntries) . "));\n"; + $code .= "\t{$symbol}->num_types = " . count($outerEntries) . ";\n"; + foreach ($outerEntries as $i => $entry) { + $code .= "\t{$symbol}->types[{$i}] = {$entry};\n"; + } + $masks = []; + foreach ($this->dnfClauses as $clause) { + if (count($clause) === 1 && $clause[0]->isBuiltin) { + $masks[] = $clause[0]->toTypeMask(); + } + } + $flags = $masks === [] ? 'extra_flags' : implode('|', $masks) . '|extra_flags'; + $code .= "\treturn (zend_type) ZEND_TYPE_INIT_UNION({$symbol}, {$flags});\n"; + $code .= "}\n"; + return $code; + } + + public function getDnfTypeExpression(string $symbol, string $extraFlags): string { + assert($this->dnfClauses !== null); + return 'create_' . $symbol . '(' . $extraFlags . ')'; + } + + public function getZendTypeExpression(string $extraFlags): string { + assert(!$this->isDnf()); + if (null !== $simpleType = $this->tryToSimpleType()) { + if ($simpleType->isBuiltin) { + return sprintf( + 'ZEND_TYPE_INIT_CODE(%s, %d, %s)', + $simpleType->toTypeCode(), + $this->isNullable() ? 1 : 0, + $extraFlags, + ); + } + return sprintf( + 'ZEND_TYPE_INIT_CLASS_CONST("%s", %d, %s)', + $simpleType->toEscapedName(), + $this->isNullable() ? 1 : 0, + $extraFlags, + ); + } + + $arginfoType = $this->toArginfoType(); + if ($arginfoType->hasClassType()) { + return sprintf( + 'ZEND_TYPE_INIT_CLASS_CONST_MASK("%s", %s|%s)', + $arginfoType->toClassTypeString(), + $arginfoType->toTypeMask(), + $extraFlags, + ); + } + return sprintf('ZEND_TYPE_INIT_MASK(%s|%s)', $arginfoType->toTypeMask(), $extraFlags); } public function isScalar(): bool { @@ -723,7 +835,10 @@ class StubType { return $a === $b; } - if (count($a->types) !== count($b->types)) { + if ($a->isIntersection !== $b->isIntersection + || ($a->dnfClauses === null) !== ($b->dnfClauses === null) + || count($a->types) !== count($b->types) + ) { return false; } @@ -733,6 +848,23 @@ class StubType { } } + if ($a->dnfClauses !== null) { + if (count($a->dnfClauses) !== count($b->dnfClauses)) { + return false; + } + foreach ($a->dnfClauses as $i => $clause) { + $otherClause = $b->dnfClauses[$i]; + if (count($clause) !== count($otherClause)) { + return false; + } + foreach ($clause as $j => $type) { + if (!$type->equals($otherClause[$j])) { + return false; + } + } + } + } + return true; } @@ -863,11 +995,28 @@ class ArgInfo { return $this->defaultValue; } - public function toZendInfo(): string { + public function toZendInfo(?string $dnfSymbol = null, bool $deferDnfInitialization = false): string { $argKind = $this->isVariadic ? "ARG_VARIADIC" : "ARG"; $argDefaultKind = $this->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : ""; $argType = $this->type; if ($argType !== null) { + if ($argType->isDnf() && $dnfSymbol !== null) { + $flags = sprintf( + '_ZEND_ARG_INFO_FLAGS(%s, %d, 0)', + $this->sendBy, + $this->isVariadic ? 1 : 0, + ); + $default = $this->isVariadic ? 'NULL' : $this->getDefaultValueAsArginfoString(); + $typeExpression = $deferDnfInitialization + ? 'ZEND_TYPE_INIT_NONE(' . $flags . ')' + : $argType->getDnfTypeExpression($dnfSymbol, $flags); + return sprintf( + "\t{ \"%s\", %s, %s },\n", + $this->name, + $typeExpression, + $default, + ); + } if (null !== $simpleArgType = $argType->tryToSimpleType()) { if ($simpleArgType->isBuiltin) { return sprintf( @@ -912,6 +1061,16 @@ class ArgInfo { $this->hasProperDefaultValue() ? ", " . $this->getDefaultValueAsArginfoString() : "" ); } + + public function getDnfInitialization(string $target, string $dnfSymbol): string { + assert($this->type?->isDnf()); + $flags = sprintf( + '_ZEND_ARG_INFO_FLAGS(%s, %d, 0)', + $this->sendBy, + $this->isVariadic ? 1 : 0, + ); + return "\t{$target}.type = " . $this->type->getDnfTypeExpression($dnfSymbol, $flags) . ";\n"; + } } interface VariableLikeName { @@ -1191,10 +1350,49 @@ class ReturnInfo { $this->refcount = $refcount; } - public function beginArgInfo(string $funcInfoName, int $minArgs): string { + public function beginArgInfo( + string $funcInfoName, + int $minArgs, + ?string $dnfSymbol = null, + bool $deferDnfInitialization = false, + ): string { + $effectiveType = $this->type ?? $this->phpDocType; + if ($deferDnfInitialization) { + $flags = sprintf( + '_ZEND_ARG_INFO_FLAGS(%d, 0, %d)', + $this->byRef ? 1 : 0, + $this->tentativeReturnType ? 1 : 0, + ); + if ($effectiveType?->isDnf()) { + assert($dnfSymbol !== null); + $typeExpression = 'ZEND_TYPE_INIT_NONE(' . $flags . ')'; + } elseif ($effectiveType !== null) { + $typeExpression = $effectiveType->getZendTypeExpression($flags); + } else { + $typeExpression = 'ZEND_TYPE_INIT_NONE(' . $flags . ')'; + } + return sprintf( + "static zend_internal_arg_info %s[] = {\n" + . "\t{ (const char*)(uintptr_t)(%d), %s, NULL },\n", + $funcInfoName, + $minArgs, + $typeExpression, + ); + } return $this->beginArgInfoCompatible($funcInfoName, $minArgs); } + public function getDnfInitialization(string $target, string $dnfSymbol): string { + $effectiveType = $this->type ?? $this->phpDocType; + assert($effectiveType?->isDnf()); + $flags = sprintf( + '_ZEND_ARG_INFO_FLAGS(%d, 0, %d)', + $this->byRef ? 1 : 0, + $this->tentativeReturnType ? 1 : 0, + ); + return "\t{$target}.type = " . $effectiveType->getDnfTypeExpression($dnfSymbol, $flags) . ";\n"; + } + /** * Assumes PHP 8.1 compatibility, if that is not the case the caller is * responsible for making the use of a tentative return type conditional @@ -1506,6 +1704,22 @@ class FuncInfo { return $this->name->getArgInfoName(); } + public function hasDnfArgInfo(): bool { + if ($this->return->getMethodSynopsisType()?->isDnf()) { + return true; + } + foreach ($this->args as $argInfo) { + if ($argInfo->type?->isDnf()) { + return true; + } + } + return false; + } + + public function getDnfArgInfoInitializerName(): string { + return 'init_' . $this->getArgInfoName() . '_dnf_types'; + } + public function getDeclarationKey(): string { $name = $this->alias ?? $this->name; @@ -2246,6 +2460,12 @@ OUPUT_EXAMPLE /** @param FuncInfo[] $generatedFuncInfos */ public function findEquivalent(array $generatedFuncInfos): ?FuncInfo { + // DNF arginfo owns runtime-allocated nested type lists and its matching + // initializer. Keep a distinct structure for every method instead of + // aliasing another method's arginfo without its initialization path. + if ($this->hasDnfArgInfo()) { + return null; + } foreach ($generatedFuncInfos as $generatedFuncInfo) { // TODO 从数组遍历元素,调用方法,在编译期无法获得元素的类型,因此判断作用域,必须为 public 方法 if ($generatedFuncInfo->equalsApartFromNameAndRefcount($this)) { @@ -2256,16 +2476,53 @@ OUPUT_EXAMPLE } public function toArgInfoCode(?int $minPHPCompatability): string { - $code = $this->return->beginArgInfo( - $this->getArgInfoName(), + $argInfoName = $this->getArgInfoName(); + $returnType = $this->return->getMethodSynopsisType(); + $returnDnfSymbol = null; + $argDnfSymbols = []; + $code = ''; + // Internal function tables are registered before MINIT, so global + // functions retain the flattened fallback used historically. Class + // methods can install their full DNF metadata immediately before the + // class entry is registered and Zend validates inheritance. + $deferDnfInitialization = $this->isMethod() && $this->hasDnfArgInfo(); + + if ($deferDnfInitialization && $returnType?->isDnf()) { + $returnDnfSymbol = $argInfoName . '_return_dnf'; + $code .= $returnType->getDnfTypeDeclarations($returnDnfSymbol); + } + foreach ($this->args as $i => $argInfo) { + if ($deferDnfInitialization && $argInfo->type?->isDnf()) { + $argDnfSymbols[$i] = $argInfoName . '_arg_' . $i . '_dnf'; + $code .= $argInfo->type->getDnfTypeDeclarations($argDnfSymbols[$i]); + } + } + + $code .= $this->return->beginArgInfo( + $argInfoName, $this->numRequiredArgs, + $returnDnfSymbol, + $deferDnfInitialization, ); - foreach ($this->args as $argInfo) { - $code .= $argInfo->toZendInfo(); + foreach ($this->args as $i => $argInfo) { + $code .= $argInfo->toZendInfo( + $argDnfSymbols[$i] ?? null, + $deferDnfInitialization, + ); } $code .= "ZEND_END_ARG_INFO()"; + if ($deferDnfInitialization) { + $code .= "\nstatic void " . $this->getDnfArgInfoInitializerName() . "()\n{\n"; + if ($returnDnfSymbol !== null) { + $code .= $this->return->getDnfInitialization("{$argInfoName}[0]", $returnDnfSymbol); + } + foreach ($argDnfSymbols as $i => $symbol) { + $code .= $this->args[$i]->getDnfInitialization("{$argInfoName}[" . ($i + 1) . "]", $symbol); + } + $code .= "}\n"; + } return $code . "\n"; } @@ -2578,6 +2835,10 @@ abstract class VariableLike $typeCode = ""; if ($this->type) { + if ($this->type->isDnf()) { + assert($this instanceof PropertyInfo); + return $this->type->getDnfTypeExpression($this->getDnfTypeFactorySymbol(), '0'); + } $arginfoType = $this->type->toArginfoType(); if ($arginfoType->hasClassType()) { if (count($arginfoType->classTypes) >= 2) { @@ -3242,6 +3503,20 @@ class PropertyInfo extends VariableLike return "property"; } + public function getDnfTypeFactorySymbol(): string + { + return 'property_' . implode('_', $this->name->class->getParts()) + . '_' . $this->name->getDeclarationName() . '_dnf'; + } + + public function getDnfTypeFactoryCode(): string + { + if (!$this->type?->isDnf()) { + return ''; + } + return $this->type->getDnfTypeDeclarations($this->getDnfTypeFactorySymbol()); + } + protected function getFieldSynopsisDefaultLinkend(): string { $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); @@ -3697,6 +3972,15 @@ class ClassInfo { $this->isUndocumentable = $isUndocumentable; } + public function getDnfPropertyTypeFactoryCode(): string + { + $code = ''; + foreach ($this->propertyInfos as $propertyInfo) { + $code .= $propertyInfo->getDnfTypeFactoryCode(); + } + return $code; + } + /** @param array $allConstInfos */ public function getRegistration(array $allConstInfos): string { @@ -3720,6 +4004,12 @@ class ClassInfo { $code .= "{\n"; + foreach ($this->funcInfos as $funcInfo) { + if ($funcInfo->hasDnfArgInfo()) { + $code .= "\t" . $funcInfo->getDnfArgInfoInitializerName() . "();\n"; + } + } + $flags = $this->getFlagsByPhpVersion(); $classMethods = ($this->funcInfos === []) ? 'NULL' : "class_{$escapedName}_methods"; @@ -5479,6 +5769,13 @@ function generateArgInfoCode( $code = "/* This is a generated file, edit the .stub.php file instead.\n" . " * Stub hash: $stubHash */\n"; + foreach ($fileInfo->classInfos as $classInfo) { + $code .= $classInfo->getDnfPropertyTypeFactoryCode(); + } + if (!str_ends_with($code, "*/\n")) { + $code .= "\n"; + } + $generatedFuncInfos = []; $argInfoCode = generateCodeWithConditions( diff --git a/tests/compiler/type_decl/dnf-inheritance-variance.phpt b/tests/compiler/type_decl/dnf-inheritance-variance.phpt new file mode 100644 index 00000000..e795dbdb --- /dev/null +++ b/tests/compiler/type_decl/dnf-inheritance-variance.phpt @@ -0,0 +1,58 @@ +--TEST-- +DNF method signatures support parameter contravariance and return covariance +--FILE-- +adapt($value); +} + +function main(): void +{ + $implementation = new DnfVarianceImplementation(); + + $both = invoke_dnf_variance($implementation, new DnfVarianceBoth()); + $fallback = invoke_dnf_variance($implementation, new DnfVarianceFallback()); + + var_dump($both instanceof DnfVarianceBoth); + var_dump($fallback instanceof DnfVarianceFallback); + + // Exercise the wider implementation parameter through the concrete type. + var_dump($implementation->adapt(new DnfVarianceOnlyLeft()) instanceof DnfVarianceBoth); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/tests/compiler/type_decl/dnf-types.phpt b/tests/compiler/type_decl/dnf-types.phpt index f73d7f4e..76ef7d49 100644 --- a/tests/compiler/type_decl/dnf-types.phpt +++ b/tests/compiler/type_decl/dnf-types.phpt @@ -71,6 +71,11 @@ final class DnfBox } } +function dnf_dynamic_property_write(mixed $box, mixed $value): void +{ + $box->value = $value; +} + function main(): void { $both = new DnfBoth(); @@ -84,6 +89,23 @@ function main(): void $box->value = $both; var_dump($box->value instanceof DnfBoth); + $dynamicBox = any(new DnfBox($fallback)); + dnf_dynamic_property_write($dynamicBox, $both); + var_dump($dynamicBox->value instanceof DnfBoth); + echo (new ReflectionProperty(DnfBox::class, 'value'))->getType(), "\n"; + + $closureIdentity = function ( + (DnfLeft&DnfRight)|DnfFallback $value, + ): (DnfLeft&DnfRight)|DnfFallback { + return $value; + }; + var_dump($closureIdentity($both) instanceof DnfBoth); + var_dump($closureIdentity($fallback) instanceof DnfFallback); + + $closureDynamicReturn = function (mixed $value): (DnfLeft&DnfRight)|DnfFallback { + return $value; + }; + $invalid = any(new DnfOnlyLeft()); try { dnf_label($invalid); @@ -100,6 +122,21 @@ function main(): void } catch (TypeError $error) { echo "property TypeError\n"; } + try { + dnf_dynamic_property_write($dynamicBox, $invalid); + } catch (TypeError $error) { + echo "dynamic property TypeError\n"; + } + try { + $closureIdentity($invalid); + } catch (TypeError $error) { + echo "closure parameter TypeError\n"; + } + try { + $closureDynamicReturn($invalid); + } catch (TypeError $error) { + echo "closure return TypeError\n"; + } } ?> --EXPECT-- @@ -108,6 +145,13 @@ string(8) "fallback" bool(true) bool(true) bool(true) +bool(true) +(DnfLeft&DnfRight)|DnfFallback +bool(true) +bool(true) parameter TypeError return TypeError property TypeError +dynamic property TypeError +closure parameter TypeError +closure return TypeError