feat(compiler): add DNF type support for method signatures and variance

- Implement DNF method signatures with parameter contravariance and return covariance
- Add new test cases for DNF inheritance variance scenarios
- Extend stub generation to handle DNF union and intersection types
- Create DNF type declaration code generation in gen_stub.php
- Add support for DNF properties and dynamic property writes
- Implement closure support with DNF parameters and return types
- Add error handling for invalid DNF type operations
- Update inheritance checks to validate DNF parameter and return type compatibility
- Introduce test cases for DNF inheritance errors
- Refactor type comparison logic to support DNF clause subtyping relations
master
韩天峰 1 day ago
parent b2246cd456
commit 4c59d9a21b
  1. 15
      phpunit/code/inheritance_error_dnf_param_narrowed.php
  2. 18
      phpunit/code/inheritance_error_dnf_return_widened.php
  3. 10
      phpunit/src/InheritanceErrorTest.php
  4. 42
      src/Translator.php
  5. 315
      src/gen_stub.php
  6. 58
      tests/compiler/type_decl/dnf-inheritance-variance.phpt
  7. 44
      tests/compiler/type_decl/dnf-types.phpt

@ -0,0 +1,15 @@
<?php
interface DnfParamLeft {}
interface DnfParamRight {}
final class DnfParamFallback {}
interface DnfParamParent
{
public function consume(DnfParamLeft|DnfParamFallback $value): void;
}
final class DnfParamChild implements DnfParamParent
{
public function consume((DnfParamLeft&DnfParamRight)|DnfParamFallback $value): void {}
}

@ -0,0 +1,18 @@
<?php
interface DnfReturnLeft {}
interface DnfReturnRight {}
final class DnfReturnFallback {}
interface DnfReturnParent
{
public function create(): (DnfReturnLeft&DnfReturnRight)|DnfReturnFallback;
}
final class DnfReturnChild implements DnfReturnParent
{
public function create(): DnfReturnLeft|DnfReturnFallback
{
return new DnfReturnFallback();
}
}

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

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

@ -539,6 +539,8 @@ class StubType {
/** @var SimpleType[] */
public /* readonly */ array $types;
public /* readonly */ bool $isIntersection;
/** @var ?array<int, array<int, SimpleType>> 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<string, ConstInfo> $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(

@ -0,0 +1,58 @@
--TEST--
DNF method signatures support parameter contravariance and return covariance
--FILE--
<?php
interface DnfVarianceLeft {}
interface DnfVarianceRight {}
final class DnfVarianceBoth implements DnfVarianceLeft, DnfVarianceRight {}
final class DnfVarianceFallback {}
final class DnfVarianceOnlyLeft implements DnfVarianceLeft {}
interface DnfVarianceContract
{
public function adapt(
(DnfVarianceLeft&DnfVarianceRight)|DnfVarianceFallback $value,
): DnfVarianceLeft|DnfVarianceFallback;
}
final class DnfVarianceImplementation implements DnfVarianceContract
{
// A DNF parameter may be widened in an implementation.
public function adapt(
DnfVarianceLeft|DnfVarianceFallback $value,
): (DnfVarianceLeft&DnfVarianceRight)|DnfVarianceFallback {
// A union return may be narrowed to a DNF type.
if ($value instanceof DnfVarianceFallback) {
return $value;
}
return new DnfVarianceBoth();
}
}
function invoke_dnf_variance(
DnfVarianceContract $implementation,
(DnfVarianceLeft&DnfVarianceRight)|DnfVarianceFallback $value,
): DnfVarianceLeft|DnfVarianceFallback {
return $implementation->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)

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

Loading…
Cancel
Save