Skip to content

Add support for Disjoint Normal Form (DNF) types #8725

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions Zend/Optimizer/dfa_pass.c
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,32 @@ static bool safe_instanceof(zend_class_entry *ce1, zend_class_entry *ce2) {
return instanceof_function(ce1, ce2);
}

static inline bool can_elide_list_type(
const zend_script *script, const zend_op_array *op_array,
const zend_ssa_var_info *use_info, zend_type type)
{
zend_type *single_type;
/* For intersection: result==false is failure, default is success.
* For union: result==true is success, default is failure. */
bool is_intersection = ZEND_TYPE_IS_INTERSECTION(type);
ZEND_TYPE_FOREACH(type, single_type) {
if (ZEND_TYPE_HAS_LIST(*single_type)) {
ZEND_ASSERT(!is_intersection);
return can_elide_list_type(script, op_array, use_info, *single_type);
}
if (ZEND_TYPE_HAS_NAME(*single_type)) {
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
zend_string_release(lcname);
bool result = ce && safe_instanceof(use_info->ce, ce);
if (result == !is_intersection) {
return result;
}
}
} ZEND_TYPE_FOREACH_END();
return is_intersection;
}

static inline bool can_elide_return_type_check(
const zend_script *script, zend_op_array *op_array, zend_ssa *ssa, zend_ssa_op *ssa_op) {
zend_arg_info *arg_info = &op_array->arg_info[-1];
Expand All @@ -286,22 +312,7 @@ static inline bool can_elide_return_type_check(
}

if (disallowed_types == MAY_BE_OBJECT && use_info->ce && ZEND_TYPE_IS_COMPLEX(arg_info->type)) {
zend_type *single_type;
/* For intersection: result==false is failure, default is success.
* For union: result==true is success, default is failure. */
bool is_intersection = ZEND_TYPE_IS_INTERSECTION(arg_info->type);
ZEND_TYPE_FOREACH(arg_info->type, single_type) {
if (ZEND_TYPE_HAS_NAME(*single_type)) {
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
zend_string_release(lcname);
bool result = ce && safe_instanceof(use_info->ce, ce);
if (result == !is_intersection) {
return result;
}
}
} ZEND_TYPE_FOREACH_END();
return is_intersection;
return can_elide_list_type(script, op_array, use_info, arg_info->type);
}

return false;
Expand Down
63 changes: 63 additions & 0 deletions Zend/tests/type_declarations/dnf_types/dnf_2_intersection.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
Union of two intersection type
--FILE--
<?php

interface W {}
interface X {}
interface Y {}
interface Z {}

class A implements X, Y {}
class B implements W, Z {}
class C {}

function foo1((X&Y)|(W&Z) $v): (X&Y)|(W&Z) {
return $v;
}
function foo2((W&Z)|(X&Y) $v): (W&Z)|(X&Y) {
return $v;
}

function bar1(): (X&Y)|(W&Z) {
return new C();
}
function bar2(): (W&Z)|(X&Y) {
return new C();
}

$a = new A();
$b = new B();

$o = foo1($a);
var_dump($o);
$o = foo2($a);
var_dump($o);
$o = foo1($b);
var_dump($o);
$o = foo2($b);
var_dump($o);

try {
bar1();
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
bar2();
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}

?>
--EXPECTF--
object(A)#%d (0) {
}
object(A)#%d (0) {
}
object(B)#%d (0) {
}
object(B)#%d (0) {
}
bar1(): Return value must be of type (X&Y)|(W&Z), C returned
bar2(): Return value must be of type (W&Z)|(X&Y), C returned
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
--TEST--
Union of null and intersection type
--FILE--
<?php

interface X {}
interface Y {}

class A implements X, Y {}
class C {}

class Test {
public (X&Y)|null $prop1;
public null|(X&Y) $prop2;

public function foo1((X&Y)|null $v): (X&Y)|null {
var_dump($v);
return $v;
}
public function foo2(null|(X&Y) $v): null|(X&Y) {
var_dump($v);
return $v;
}
}

$test = new Test();
$a = new A();
$n = null;

$test->foo1($a);
$test->foo2($a);
$test->foo1($n);
$test->foo2($n);
$test->prop1 = $a;
$test->prop1 = $n;
$test->prop2 = $a;
$test->prop2 = $n;

$c = new C();
try {
$test->foo1($c);
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->foo2($c);
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->prop1 = $c;
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->prop2 = $c;
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}

?>
===DONE===
--EXPECTF--
object(A)#2 (0) {
}
object(A)#2 (0) {
}
NULL
NULL
Test::foo1(): Argument #1 ($v) must be of type (X&Y)|null, C given, called in %s on line %d
Test::foo2(): Argument #1 ($v) must be of type (X&Y)|null, C given, called in %s on line %d
Cannot assign C to property Test::$prop1 of type (X&Y)|null
Cannot assign C to property Test::$prop2 of type (X&Y)|null
===DONE===
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
--TEST--
Union of a simple and intersection type
--FILE--
<?php

interface X {}
interface Y {}

class A implements X, Y {}
class B {}
class C {}

class Test {
public (X&Y)|int $prop1;
public int|(X&Y) $prop2;
public (X&Y)|B $prop3;
public B|(X&Y) $prop4;

public function foo1((X&Y)|int $v): (X&Y)|int {
var_dump($v);
return $v;
}
public function foo2(int|(X&Y) $v): int|(X&Y) {
var_dump($v);
return $v;
}
public function bar1(B|(X&Y) $v): B|(X&Y) {
var_dump($v);
return $v;
}
public function bar2((X&Y)|B $v): (X&Y)|B {
var_dump($v);
return $v;
}
}

$test = new Test();
$a = new A();
$b = new B();
$i = 10;

$test->foo1($a);
$test->foo2($a);
$test->foo1($i);
$test->foo2($i);
$test->prop1 = $a;
$test->prop1 = $i;
$test->prop2 = $a;
$test->prop2 = $i;

$test->bar1($a);
$test->bar2($a);
$test->bar1($b);
$test->bar2($b);
$test->prop3 = $a;
$test->prop4 = $b;
$test->prop3 = $a;
$test->prop4 = $b;

$c = new C();
try {
$test->foo1($c);
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->foo2($c);
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->bar1($c);
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->bar2($c);
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->prop1 = $c;
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->prop2 = $c;
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->prop3 = $c;
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}
try {
$test->prop4 = $c;
} catch (\TypeError $e) {
echo $e->getMessage(), \PHP_EOL;
}

?>
===DONE===
--EXPECTF--
object(A)#2 (0) {
}
object(A)#2 (0) {
}
int(10)
int(10)
object(A)#2 (0) {
}
object(A)#2 (0) {
}
object(B)#3 (0) {
}
object(B)#3 (0) {
}
Test::foo1(): Argument #1 ($v) must be of type (X&Y)|int, C given, called in %s on line %d
Test::foo2(): Argument #1 ($v) must be of type (X&Y)|int, C given, called in %s on line %d
Test::bar1(): Argument #1 ($v) must be of type B|(X&Y), C given, called in %s on line %d
Test::bar2(): Argument #1 ($v) must be of type (X&Y)|B, C given, called in %s on line %d
Cannot assign C to property Test::$prop1 of type (X&Y)|int
Cannot assign C to property Test::$prop2 of type (X&Y)|int
Cannot assign C to property Test::$prop3 of type (X&Y)|B
Cannot assign C to property Test::$prop4 of type B|(X&Y)
===DONE===
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Duplicate class alias type
--FILE--
<?php

interface X {}

use A as B;
function foo(): (X&A)|(X&B) {}

?>
--EXPECTF--
Fatal error: Type X&A is redundant with type X&A in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Duplicate class alias type at runtime
--FILE--
<?php

class A {}
interface X {}

class_alias('A', 'B');
function foo(): (X&A)|(X&B) {}

?>
===DONE===
--EXPECT--
===DONE===
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Intersection with child class
--FILE--
<?php

interface X {}
class A {}
class B extends A {}

function test(): (A&X)|(B&X) {}

?>
===DONE===
--EXPECT--
===DONE===
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
A less restrictive type constrain is part of the DNF type 001
--FILE--
<?php

interface A {}
interface B {}

function test(): (A&B)|A {}

?>
===DONE===
--EXPECTF--
Fatal error: Type A&B is redundant as it is more restrictive than type A in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
A less restrictive type constrain is part of the DNF type 002
--FILE--
<?php

interface A {}
interface B {}

function test(): A|(A&B) {}

?>
===DONE===
--EXPECTF--
Fatal error: Type A&B is redundant as it is more restrictive than type A in %s on line %d
Loading