Skip to content

Commit f905590

Browse files
Girgiassgolemon
andauthored
Add support for Disjoint Normal Form (DNF) types (#8725)
RFC: https://wiki.php.net/rfc/dnf_types This allows to combine union and intersection types together in the following form (A&B)|(X&Y)|T but not of the form (X|A)&(Y|B) or (X|A)&(Y|B)|T. * Improve union type parsing Co-authored-by: Sara Golemon <[email protected]>
1 parent d3c8652 commit f905590

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1339
-111
lines changed

Zend/Optimizer/dfa_pass.c

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,32 @@ static bool safe_instanceof(zend_class_entry *ce1, zend_class_entry *ce2) {
265265
return instanceof_function(ce1, ce2);
266266
}
267267

268+
static inline bool can_elide_list_type(
269+
const zend_script *script, const zend_op_array *op_array,
270+
const zend_ssa_var_info *use_info, zend_type type)
271+
{
272+
zend_type *single_type;
273+
/* For intersection: result==false is failure, default is success.
274+
* For union: result==true is success, default is failure. */
275+
bool is_intersection = ZEND_TYPE_IS_INTERSECTION(type);
276+
ZEND_TYPE_FOREACH(type, single_type) {
277+
if (ZEND_TYPE_HAS_LIST(*single_type)) {
278+
ZEND_ASSERT(!is_intersection);
279+
return can_elide_list_type(script, op_array, use_info, *single_type);
280+
}
281+
if (ZEND_TYPE_HAS_NAME(*single_type)) {
282+
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
283+
zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
284+
zend_string_release(lcname);
285+
bool result = ce && safe_instanceof(use_info->ce, ce);
286+
if (result == !is_intersection) {
287+
return result;
288+
}
289+
}
290+
} ZEND_TYPE_FOREACH_END();
291+
return is_intersection;
292+
}
293+
268294
static inline bool can_elide_return_type_check(
269295
const zend_script *script, zend_op_array *op_array, zend_ssa *ssa, zend_ssa_op *ssa_op) {
270296
zend_arg_info *arg_info = &op_array->arg_info[-1];
@@ -286,22 +312,7 @@ static inline bool can_elide_return_type_check(
286312
}
287313

288314
if (disallowed_types == MAY_BE_OBJECT && use_info->ce && ZEND_TYPE_IS_COMPLEX(arg_info->type)) {
289-
zend_type *single_type;
290-
/* For intersection: result==false is failure, default is success.
291-
* For union: result==true is success, default is failure. */
292-
bool is_intersection = ZEND_TYPE_IS_INTERSECTION(arg_info->type);
293-
ZEND_TYPE_FOREACH(arg_info->type, single_type) {
294-
if (ZEND_TYPE_HAS_NAME(*single_type)) {
295-
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
296-
zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
297-
zend_string_release(lcname);
298-
bool result = ce && safe_instanceof(use_info->ce, ce);
299-
if (result == !is_intersection) {
300-
return result;
301-
}
302-
}
303-
} ZEND_TYPE_FOREACH_END();
304-
return is_intersection;
315+
return can_elide_list_type(script, op_array, use_info, arg_info->type);
305316
}
306317

307318
return false;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
Union of two intersection type
3+
--FILE--
4+
<?php
5+
6+
interface W {}
7+
interface X {}
8+
interface Y {}
9+
interface Z {}
10+
11+
class A implements X, Y {}
12+
class B implements W, Z {}
13+
class C {}
14+
15+
function foo1((X&Y)|(W&Z) $v): (X&Y)|(W&Z) {
16+
return $v;
17+
}
18+
function foo2((W&Z)|(X&Y) $v): (W&Z)|(X&Y) {
19+
return $v;
20+
}
21+
22+
function bar1(): (X&Y)|(W&Z) {
23+
return new C();
24+
}
25+
function bar2(): (W&Z)|(X&Y) {
26+
return new C();
27+
}
28+
29+
$a = new A();
30+
$b = new B();
31+
32+
$o = foo1($a);
33+
var_dump($o);
34+
$o = foo2($a);
35+
var_dump($o);
36+
$o = foo1($b);
37+
var_dump($o);
38+
$o = foo2($b);
39+
var_dump($o);
40+
41+
try {
42+
bar1();
43+
} catch (\TypeError $e) {
44+
echo $e->getMessage(), \PHP_EOL;
45+
}
46+
try {
47+
bar2();
48+
} catch (\TypeError $e) {
49+
echo $e->getMessage(), \PHP_EOL;
50+
}
51+
52+
?>
53+
--EXPECTF--
54+
object(A)#%d (0) {
55+
}
56+
object(A)#%d (0) {
57+
}
58+
object(B)#%d (0) {
59+
}
60+
object(B)#%d (0) {
61+
}
62+
bar1(): Return value must be of type (X&Y)|(W&Z), C returned
63+
bar2(): Return value must be of type (W&Z)|(X&Y), C returned
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
--TEST--
2+
Union of null and intersection type
3+
--FILE--
4+
<?php
5+
6+
interface X {}
7+
interface Y {}
8+
9+
class A implements X, Y {}
10+
class C {}
11+
12+
class Test {
13+
public (X&Y)|null $prop1;
14+
public null|(X&Y) $prop2;
15+
16+
public function foo1((X&Y)|null $v): (X&Y)|null {
17+
var_dump($v);
18+
return $v;
19+
}
20+
public function foo2(null|(X&Y) $v): null|(X&Y) {
21+
var_dump($v);
22+
return $v;
23+
}
24+
}
25+
26+
$test = new Test();
27+
$a = new A();
28+
$n = null;
29+
30+
$test->foo1($a);
31+
$test->foo2($a);
32+
$test->foo1($n);
33+
$test->foo2($n);
34+
$test->prop1 = $a;
35+
$test->prop1 = $n;
36+
$test->prop2 = $a;
37+
$test->prop2 = $n;
38+
39+
$c = new C();
40+
try {
41+
$test->foo1($c);
42+
} catch (\TypeError $e) {
43+
echo $e->getMessage(), \PHP_EOL;
44+
}
45+
try {
46+
$test->foo2($c);
47+
} catch (\TypeError $e) {
48+
echo $e->getMessage(), \PHP_EOL;
49+
}
50+
try {
51+
$test->prop1 = $c;
52+
} catch (\TypeError $e) {
53+
echo $e->getMessage(), \PHP_EOL;
54+
}
55+
try {
56+
$test->prop2 = $c;
57+
} catch (\TypeError $e) {
58+
echo $e->getMessage(), \PHP_EOL;
59+
}
60+
61+
?>
62+
===DONE===
63+
--EXPECTF--
64+
object(A)#2 (0) {
65+
}
66+
object(A)#2 (0) {
67+
}
68+
NULL
69+
NULL
70+
Test::foo1(): Argument #1 ($v) must be of type (X&Y)|null, C given, called in %s on line %d
71+
Test::foo2(): Argument #1 ($v) must be of type (X&Y)|null, C given, called in %s on line %d
72+
Cannot assign C to property Test::$prop1 of type (X&Y)|null
73+
Cannot assign C to property Test::$prop2 of type (X&Y)|null
74+
===DONE===
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
--TEST--
2+
Union of a simple and intersection type
3+
--FILE--
4+
<?php
5+
6+
interface X {}
7+
interface Y {}
8+
9+
class A implements X, Y {}
10+
class B {}
11+
class C {}
12+
13+
class Test {
14+
public (X&Y)|int $prop1;
15+
public int|(X&Y) $prop2;
16+
public (X&Y)|B $prop3;
17+
public B|(X&Y) $prop4;
18+
19+
public function foo1((X&Y)|int $v): (X&Y)|int {
20+
var_dump($v);
21+
return $v;
22+
}
23+
public function foo2(int|(X&Y) $v): int|(X&Y) {
24+
var_dump($v);
25+
return $v;
26+
}
27+
public function bar1(B|(X&Y) $v): B|(X&Y) {
28+
var_dump($v);
29+
return $v;
30+
}
31+
public function bar2((X&Y)|B $v): (X&Y)|B {
32+
var_dump($v);
33+
return $v;
34+
}
35+
}
36+
37+
$test = new Test();
38+
$a = new A();
39+
$b = new B();
40+
$i = 10;
41+
42+
$test->foo1($a);
43+
$test->foo2($a);
44+
$test->foo1($i);
45+
$test->foo2($i);
46+
$test->prop1 = $a;
47+
$test->prop1 = $i;
48+
$test->prop2 = $a;
49+
$test->prop2 = $i;
50+
51+
$test->bar1($a);
52+
$test->bar2($a);
53+
$test->bar1($b);
54+
$test->bar2($b);
55+
$test->prop3 = $a;
56+
$test->prop4 = $b;
57+
$test->prop3 = $a;
58+
$test->prop4 = $b;
59+
60+
$c = new C();
61+
try {
62+
$test->foo1($c);
63+
} catch (\TypeError $e) {
64+
echo $e->getMessage(), \PHP_EOL;
65+
}
66+
try {
67+
$test->foo2($c);
68+
} catch (\TypeError $e) {
69+
echo $e->getMessage(), \PHP_EOL;
70+
}
71+
try {
72+
$test->bar1($c);
73+
} catch (\TypeError $e) {
74+
echo $e->getMessage(), \PHP_EOL;
75+
}
76+
try {
77+
$test->bar2($c);
78+
} catch (\TypeError $e) {
79+
echo $e->getMessage(), \PHP_EOL;
80+
}
81+
try {
82+
$test->prop1 = $c;
83+
} catch (\TypeError $e) {
84+
echo $e->getMessage(), \PHP_EOL;
85+
}
86+
try {
87+
$test->prop2 = $c;
88+
} catch (\TypeError $e) {
89+
echo $e->getMessage(), \PHP_EOL;
90+
}
91+
try {
92+
$test->prop3 = $c;
93+
} catch (\TypeError $e) {
94+
echo $e->getMessage(), \PHP_EOL;
95+
}
96+
try {
97+
$test->prop4 = $c;
98+
} catch (\TypeError $e) {
99+
echo $e->getMessage(), \PHP_EOL;
100+
}
101+
102+
?>
103+
===DONE===
104+
--EXPECTF--
105+
object(A)#2 (0) {
106+
}
107+
object(A)#2 (0) {
108+
}
109+
int(10)
110+
int(10)
111+
object(A)#2 (0) {
112+
}
113+
object(A)#2 (0) {
114+
}
115+
object(B)#3 (0) {
116+
}
117+
object(B)#3 (0) {
118+
}
119+
Test::foo1(): Argument #1 ($v) must be of type (X&Y)|int, C given, called in %s on line %d
120+
Test::foo2(): Argument #1 ($v) must be of type (X&Y)|int, C given, called in %s on line %d
121+
Test::bar1(): Argument #1 ($v) must be of type B|(X&Y), C given, called in %s on line %d
122+
Test::bar2(): Argument #1 ($v) must be of type (X&Y)|B, C given, called in %s on line %d
123+
Cannot assign C to property Test::$prop1 of type (X&Y)|int
124+
Cannot assign C to property Test::$prop2 of type (X&Y)|int
125+
Cannot assign C to property Test::$prop3 of type (X&Y)|B
126+
Cannot assign C to property Test::$prop4 of type B|(X&Y)
127+
===DONE===
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
Duplicate class alias type
3+
--FILE--
4+
<?php
5+
6+
interface X {}
7+
8+
use A as B;
9+
function foo(): (X&A)|(X&B) {}
10+
11+
?>
12+
--EXPECTF--
13+
Fatal error: Type X&A is redundant with type X&A in %s on line %d
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Duplicate class alias type at runtime
3+
--FILE--
4+
<?php
5+
6+
class A {}
7+
interface X {}
8+
9+
class_alias('A', 'B');
10+
function foo(): (X&A)|(X&B) {}
11+
12+
?>
13+
===DONE===
14+
--EXPECT--
15+
===DONE===
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Intersection with child class
3+
--FILE--
4+
<?php
5+
6+
interface X {}
7+
class A {}
8+
class B extends A {}
9+
10+
function test(): (A&X)|(B&X) {}
11+
12+
?>
13+
===DONE===
14+
--EXPECT--
15+
===DONE===
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
A less restrictive type constrain is part of the DNF type 001
3+
--FILE--
4+
<?php
5+
6+
interface A {}
7+
interface B {}
8+
9+
function test(): (A&B)|A {}
10+
11+
?>
12+
===DONE===
13+
--EXPECTF--
14+
Fatal error: Type A&B is redundant as it is more restrictive than type A in %s on line %d
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
A less restrictive type constrain is part of the DNF type 002
3+
--FILE--
4+
<?php
5+
6+
interface A {}
7+
interface B {}
8+
9+
function test(): A|(A&B) {}
10+
11+
?>
12+
===DONE===
13+
--EXPECTF--
14+
Fatal error: Type A&B is redundant as it is more restrictive than type A in %s on line %d

0 commit comments

Comments
 (0)