Skip to content
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

Iterable pseudo-type #1941

Merged
merged 13 commits into from
Jul 4, 2016
2 changes: 1 addition & 1 deletion Zend/tests/return_types/generators002.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ function test1() : StdClass {
}

--EXPECTF--
Fatal error: Generators may only declare a return type of Generator, Iterator or Traversable, StdClass is not permitted in %s on line %d
Fatal error: Generators may only declare a return type of Generator, Iterator, Traversable, or iterable, StdClass is not permitted in %s on line %d
48 changes: 48 additions & 0 deletions Zend/tests/type_declarations/iterable_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
iterable type#001
--FILE--
<?php

function test(iterable $iterable) {
var_dump($iterable);
}

function gen() {
yield 1;
yield 2;
yield 3;
};

test([1, 2, 3]);
test(gen());
test(new ArrayIterator([1, 2, 3]));

try {
test(1);
} catch (Throwable $e) {
echo $e->getMessage();
}

--EXPECTF--
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
object(Generator)#1 (0) {
}
object(ArrayIterator)#1 (1) {
["storage":"ArrayIterator":private]=>
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
}
Argument 1 passed to test() must be iterable, integer given, called in %s on line %d
21 changes: 21 additions & 0 deletions Zend/tests/type_declarations/iterable_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
iterable type#002 - Default values
--FILE--
<?php

function foo(iterable $iterable = null) {
// Null should be allowed as a default value
}

function bar(iterable $iterable = []) {
// Array should be allowed as a default value
}

function baz(iterable $iterable = 1) {
// No other values should be allowed as defaults
}

?>
--EXPECTF--

Fatal error: Default value for parameters with iterable type can only be an array or NULL in %s on line %d
32 changes: 32 additions & 0 deletions Zend/tests/type_declarations/iterable_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
iterable type#003 - Return types
--FILE--
<?php

function foo(): iterable {
return [];
}
function bar(): iterable {
return (function () { yield; })();
}

function baz(): iterable {
return 1;
}

var_dump(foo());
var_dump(bar());

try {
baz();
} catch (Throwable $e) {
echo $e->getMessage();
}

?>
--EXPECT--
array(0) {
}
object(Generator)#2 (0) {
}
Return value of baz() must be iterable, integer returned
25 changes: 25 additions & 0 deletions Zend/tests/type_declarations/iterable_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
iterable type#004 - Parameter covariance
--FILE--
<?php

class Foo {
function testArray(array $array) {}

function testTraversable(Traversable $traversable) {}

function testScalar(int $int) {}
}

class Bar extends Foo {
function testArray(iterable $iterable) {}

function testTraversable(iterable $iterable) {}

function testScalar(iterable $iterable) {}
}

?>
--EXPECTF--

Warning: Declaration of Bar::testScalar(iterable $iterable) should be compatible with Foo::testScalar(int $int) in %s on line %d
33 changes: 33 additions & 0 deletions Zend/tests/type_declarations/iterable_005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
iterable type#005 - Return type covariance
--FILE--
<?php

class Test {
function method(): iterable {
return [];
}
}

class TestArray extends Test {
function method(): array {
return [];
}
}

class TestTraversable extends Test {
function method(): Traversable {
return new ArrayIterator([]);
}
}

class TestScalar extends Test {
function method(): int {
return 1;
}
}

?>
--EXPECTF--

Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): iterable in %s on line %d
16 changes: 16 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "zend_modules.h"
#include "zend_extensions.h"
#include "zend_constants.h"
#include "zend_interfaces.h"
#include "zend_exceptions.h"
#include "zend_closures.h"
#include "zend_inheritance.h"
Expand Down Expand Up @@ -182,6 +183,8 @@ ZEND_API char *zend_get_type_by_const(int type) /* {{{ */
return "null";
case IS_CALLABLE:
return "callable";
case IS_ITERABLE:
return "iterable";
case IS_ARRAY:
return "array";
case IS_VOID:
Expand Down Expand Up @@ -4201,6 +4204,19 @@ ZEND_API const char *zend_get_object_type(const zend_class_entry *ce) /* {{{ */
}
/* }}} */

ZEND_API zend_bool zend_is_iterable(zval *iterable) /* {{{ */
{
switch (Z_TYPE_P(iterable)) {
case IS_ARRAY:
return 1;
case IS_OBJECT:
return instanceof_function(Z_OBJCE_P(iterable), zend_ce_traversable);
default:
return 0;
}
}
/* }}} */

/*
* Local variables:
* tab-width: 4
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ ZEND_API zend_string *zend_resolve_method_name(zend_class_entry *ce, zend_functi

ZEND_API const char *zend_get_object_type(const zend_class_entry *ce);

ZEND_API zend_bool zend_is_iterable(zval *iterable);

#define add_method(arg, key, method) add_assoc_function((arg), (key), (method))

ZEND_API ZEND_FUNCTION(display_disabled_function);
Expand Down
28 changes: 20 additions & 8 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ static const struct reserved_class_name reserved_class_names[] = {
{ZEND_STRL("string")},
{ZEND_STRL("true")},
{ZEND_STRL("void")},
{ZEND_STRL("iterable")},
{NULL, 0}
};

Expand Down Expand Up @@ -204,6 +205,7 @@ static const builtin_type_info builtin_types[] = {
{ZEND_STRL("string"), IS_STRING},
{ZEND_STRL("bool"), _IS_BOOL},
{ZEND_STRL("void"), IS_VOID},
{ZEND_STRL("iterable"), IS_ITERABLE},
{NULL, 0, IS_UNDEF}
};

Expand Down Expand Up @@ -1251,17 +1253,20 @@ static void zend_mark_function_as_generator() /* {{{ */
}

if (CG(active_op_array)->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
const char *msg = "Generators may only declare a return type of Generator, Iterator or Traversable, %s is not permitted";
zend_arg_info return_info = CG(active_op_array)->arg_info[-1];

if (!return_info.class_name) {
zend_error_noreturn(E_COMPILE_ERROR, msg, zend_get_type_by_const(return_info.type_hint));
}
if (return_info.type_hint != IS_ITERABLE) {
const char *msg = "Generators may only declare a return type of Generator, Iterator, Traversable, or iterable, %s is not permitted";

if (!return_info.class_name) {
zend_error_noreturn(E_COMPILE_ERROR, msg, zend_get_type_by_const(return_info.type_hint));
}

if (!zend_string_equals_literal_ci(return_info.class_name, "Traversable")
&& !zend_string_equals_literal_ci(return_info.class_name, "Iterator")
&& !zend_string_equals_literal_ci(return_info.class_name, "Generator")) {
zend_error_noreturn(E_COMPILE_ERROR, msg, ZSTR_VAL(return_info.class_name));
if (!zend_string_equals_literal_ci(return_info.class_name, "Traversable")
&& !zend_string_equals_literal_ci(return_info.class_name, "Iterator")
&& !zend_string_equals_literal_ci(return_info.class_name, "Generator")) {
zend_error_noreturn(E_COMPILE_ERROR, msg, ZSTR_VAL(return_info.class_name));
}
}
}

Expand Down Expand Up @@ -5073,6 +5078,13 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast) /* {{{ */
"with a float type can only be float, integer, or NULL");
}
break;

case IS_ITERABLE:
if (Z_TYPE(default_node.u.constant) != IS_ARRAY) {
zend_error_noreturn(E_COMPILE_ERROR, "Default value for parameters "
"with iterable type can only be an array or NULL");
}
break;

default:
if (!ZEND_SAME_FAKE_TYPE(arg_info->type_hint, Z_TYPE(default_node.u.constant))) {
Expand Down
23 changes: 23 additions & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,11 @@ static int zend_verify_internal_arg_type(zend_function *zf, uint32_t arg_num, zv
zend_verify_arg_error(zf, arg_num, "be callable", "", zend_zval_type_name(arg), "");
return 0;
}
} else if (cur_arg_info->type_hint == IS_ITERABLE) {
if (!zend_is_iterable(arg)) {
zend_verify_arg_error(zf, arg_num, "be iterable", "", zend_zval_type_name(arg), "");
return 0;
}
} else if (cur_arg_info->type_hint == _IS_BOOL &&
EXPECTED(Z_TYPE_P(arg) == IS_FALSE || Z_TYPE_P(arg) == IS_TRUE)) {
/* pass */
Expand Down Expand Up @@ -849,6 +854,11 @@ static zend_always_inline int zend_verify_arg_type(zend_function *zf, uint32_t a
zend_verify_arg_error(zf, arg_num, "be callable", "", zend_zval_type_name(arg), "");
return 0;
}
} else if (cur_arg_info->type_hint == IS_ITERABLE) {
if (!zend_is_iterable(arg)) {
zend_verify_arg_error(zf, arg_num, "be iterable", "", zend_zval_type_name(arg), "");
return 0;
}
} else if (cur_arg_info->type_hint == _IS_BOOL &&
EXPECTED(Z_TYPE_P(arg) == IS_FALSE || Z_TYPE_P(arg) == IS_TRUE)) {
/* pass */
Expand Down Expand Up @@ -893,6 +903,8 @@ static zend_always_inline int zend_verify_missing_arg_type(zend_function *zf, ui
zend_verify_arg_error(zf, arg_num, need_msg, ZSTR_VAL(ce->name), "none", "");
} else if (cur_arg_info->type_hint == IS_CALLABLE) {
zend_verify_arg_error(zf, arg_num, "be callable", "", "none", "");
} else if (cur_arg_info->type_hint == IS_ITERABLE) {
zend_verify_arg_error(zf, arg_num, "be iterable", "", "none", "");
} else {
zend_verify_arg_error(zf, arg_num, "be of the type ", zend_get_type_by_const(cur_arg_info->type_hint), "none", "");
}
Expand Down Expand Up @@ -998,6 +1010,11 @@ static int zend_verify_internal_return_type(zend_function *zf, zval *ret)
zend_verify_internal_return_error(zf, "be callable", "", zend_zval_type_name(ret), "");
return 0;
}
} else if (ret_info->type_hint == IS_ITERABLE) {
if (!zend_is_iterable(ret) && (Z_TYPE_P(ret) != IS_NULL || !ret_info->allow_null)) {
zend_verify_internal_return_error(zf, "be iterable", "", zend_zval_type_name(ret), "");
return 0;
}
} else if (ret_info->type_hint == _IS_BOOL &&
EXPECTED(Z_TYPE_P(ret) == IS_FALSE || Z_TYPE_P(ret) == IS_TRUE)) {
/* pass */
Expand Down Expand Up @@ -1060,6 +1077,10 @@ static zend_always_inline void zend_verify_return_type(zend_function *zf, zval *
if (!zend_is_callable(ret, IS_CALLABLE_CHECK_SILENT, NULL)) {
zend_verify_return_error(zf, "be callable", "", zend_zval_type_name(ret), "");
}
} else if (ret_info->type_hint == IS_ITERABLE) {
if (!zend_is_iterable(ret)) {
zend_verify_return_error(zf, "be iterable", "", zend_zval_type_name(ret), "");
}
} else if (ret_info->type_hint == _IS_BOOL &&
EXPECTED(Z_TYPE_P(ret) == IS_FALSE || Z_TYPE_P(ret) == IS_TRUE)) {
/* pass */
Expand Down Expand Up @@ -1101,6 +1122,8 @@ static ZEND_COLD int zend_verify_missing_return_type(zend_function *zf, void **c
return 0;
} else if (ret_info->type_hint == IS_CALLABLE) {
zend_verify_return_error(zf, "be callable", "", "none", "");
} else if (ret_info->type_hint == IS_ITERABLE) {
zend_verify_return_error(zf, "be iterable", "", "none", "");
} else {
zend_verify_return_error(zf, "be of the type ", zend_get_type_by_const(ret_info->type_hint), "none", "");
}
Expand Down
30 changes: 26 additions & 4 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,20 @@ char *zend_visibility_string(uint32_t fn_flags) /* {{{ */
}
/* }}} */

static zend_bool zend_iterable_type_check(zend_arg_info *arg_info) /* {{{ */
{
if (arg_info->type_hint == IS_ITERABLE || arg_info->type_hint == IS_ARRAY) {
return 1;
}

if (arg_info->class_name && zend_string_equals_literal_ci(arg_info->class_name, "Traversable")) {
return 1;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need these special compatibility rules? why not just check for IS_ITERABLE?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows a degree of co/contravariance in extending/implementing classes. Parameter types of array or Traversable can be broadened to iterable or return types declaring iterable can be restricted to either array or Traversable.


return 0;
}
/* }}} */

static int zend_do_perform_type_hint_check(const zend_function *fe, zend_arg_info *fe_arg_info, const zend_function *proto, zend_arg_info *proto_arg_info) /* {{{ */
{
if (ZEND_LOG_XOR(fe_arg_info->class_name, proto_arg_info->class_name)) {
Expand Down Expand Up @@ -314,8 +328,12 @@ static zend_bool zend_do_perform_implementation_check(const zend_function *fe, c
} else {
proto_arg_info = &proto->common.arg_info[proto->common.num_args];
}

if (!zend_do_perform_type_hint_check(fe, fe_arg_info, proto, proto_arg_info)) {

if (fe_arg_info->type_hint == IS_ITERABLE) {
if (!zend_iterable_type_check(proto_arg_info)) {
return 0;
}
} else if (!zend_do_perform_type_hint_check(fe, fe_arg_info, proto, proto_arg_info)) {
return 0;
}

Expand All @@ -338,8 +356,12 @@ static zend_bool zend_do_perform_implementation_check(const zend_function *fe, c
if (!(fe->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE)) {
return 0;
}

if (!zend_do_perform_type_hint_check(fe, fe->common.arg_info - 1, proto, proto->common.arg_info - 1)) {

if (proto->common.arg_info[-1].type_hint == IS_ITERABLE) {
if (!zend_iterable_type_check(fe->common.arg_info - 1)) {
return 0;
}
} else if (!zend_do_perform_type_hint_check(fe, fe->common.arg_info - 1, proto, proto->common.arg_info - 1)) {
return 0;
}

Expand Down