Skip to content

Commit

Permalink
Convert iterable into an internal alias for Traversable|array (php#7309)
Browse files Browse the repository at this point in the history
This does a compile time transformation of ``iterable`` into ``Traversable|array`` which simplifies some of the LSP variance handling.

The arginfo generation script from stubs is updated to produce a union type when it encounters the type ``iterable``
Extension functions which do not regenerate the arginfo, or write them manually are still supported by mimicking the compile time transformation while registering the function.

Type Reflection is preserved for single ``iterable`` (and ``?iterable``) to produce a ReflectionNamedType with name ``iterable``, however usage of ``iterable`` in union types will be converted to ``array|Traversable``
  • Loading branch information
Girgias authored Jun 7, 2022
1 parent 94183d3 commit b40ae80
Show file tree
Hide file tree
Showing 36 changed files with 341 additions and 198 deletions.
3 changes: 0 additions & 3 deletions Zend/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
Expand Up @@ -2114,9 +2114,6 @@ static uint32_t zend_convert_type_declaration_mask(uint32_t type_mask) {
if (type_mask & MAY_BE_CALLABLE) {
result_mask |= MAY_BE_STRING|MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
}
if (type_mask & MAY_BE_ITERABLE) {
result_mask |= MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
}
if (type_mask & MAY_BE_STATIC) {
result_mask |= MAY_BE_OBJECT;
}
Expand Down
7 changes: 7 additions & 0 deletions Zend/tests/return_types/generators001.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ function test6() : object|callable {
yield 6;
}

function test7() : iterable {
yield 7;
}

var_dump(
test1(),
test2(),
test3(),
test4(),
test5(),
test6(),
test7(),
);
?>
--EXPECTF--
Expand All @@ -48,3 +53,5 @@ object(Generator)#%d (%d) {
}
object(Generator)#%d (%d) {
}
object(Generator)#%d (%d) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ function foo(): iterable&Iterator {}

?>
--EXPECTF--
Fatal error: Type iterable cannot be part of an intersection type in %s on line %d
Fatal error: Type Traversable|array cannot be part of an intersection type in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ class Test2 extends Test {

?>
--EXPECTF--
Fatal error: Declaration of Test2::method(): X&Y must be compatible with Test::method(): iterable in %s on line %d
Fatal error: Declaration of Test2::method(): X&Y must be compatible with Test::method(): Traversable|array in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ object(ArrayIterator)#1 (1) {
int(3)
}
}
test(): Argument #1 ($iterable) must be of type iterable, int given, called in %s on line %d
test(): Argument #1 ($iterable) must be of type Traversable|array, int given, called in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ function baz(iterable $iterable = 1) {

?>
--EXPECTF--
Fatal error: Cannot use int as default value for parameter $iterable of type iterable in %s on line %d
Fatal error: Cannot use int as default value for parameter $iterable of type Traversable|array in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ array(0) {
}
object(Generator)#2 (0) {
}
baz(): Return value must be of type iterable, int returned
baz(): Return value must be of type Traversable|array, int returned
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ class Bar extends Foo {

?>
--EXPECTF--
Fatal error: Declaration of Bar::testScalar(iterable $iterable) must be compatible with Foo::testScalar(int $int) in %s on line %d
Fatal error: Declaration of Bar::testScalar(Traversable|array $iterable) must be compatible with Foo::testScalar(int $int) in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ class TestScalar extends Test {

?>
--EXPECTF--
Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): iterable in %s on line %d
Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): Traversable|array in %s on line %d
46 changes: 46 additions & 0 deletions Zend/tests/type_declarations/iterable/or_null.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
Test "or null"/"or be null" in type-checking errors for userland functions with iterable
--FILE--
<?php

// This should test every branch in zend_execute.c's `zend_verify_arg_type`, `zend_verify_return_type` and `zend_verify_missing_return_type` functions which produces an "or null"/"or be null" part in its error message

function iterableF(?iterable $param) {}
try {
iterableF(1);
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}

function returnIterable(): ?iterable {
return 1;
}

try {
returnIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}

function returnMissingIterable(): ?iterable {
}

try {
returnMissingIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
?>
--EXPECTF--
TypeError: iterableF(): Argument #1 ($param) must be of type Traversable|array|null, int given, called in %s on line %d and defined in %s:%d
Stack trace:
#0 %s(%d): iterableF(1)
#1 {main}
TypeError: returnIterable(): Return value must be of type Traversable|array|null, int returned in %s:%d
Stack trace:
#0 %s(%d): returnIterable()
#1 {main}
TypeError: returnMissingIterable(): Return value must be of type Traversable|array|null, none returned in %s:%d
Stack trace:
#0 %s(%d): returnMissingIterable()
#1 {main}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ function test(): iterable|Traversable {

?>
--EXPECTF--
Fatal error: Type Traversable|iterable contains both iterable and Traversable, which is redundant in %s on line %d
Fatal error: Duplicate type Traversable is redundant in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ function test(): iterable|Traversable|ArrayAccess {

?>
--EXPECTF--
Fatal error: Type Traversable|ArrayAccess|iterable contains both iterable and Traversable, which is redundant in %s on line %d
Fatal error: Duplicate type Traversable is redundant in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ function test(): iterable|array {

?>
--EXPECTF--
Fatal error: Type iterable|array contains both iterable and array, which is redundant in %s on line %d
Fatal error: Duplicate type array is redundant in %s on line %d
81 changes: 21 additions & 60 deletions Zend/tests/typehints/or_null.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ try {
echo $e, PHP_EOL;
}

function iterableF(?iterable $param) {}

try {
iterableF(1);
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}

function intF(?int $param) {}

try {
Expand Down Expand Up @@ -143,16 +135,6 @@ try {
echo $e, PHP_EOL;
}

function returnIterable(): ?iterable {
return 1;
}

try {
returnIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}

function returnInt(): ?int {
return new \StdClass;
}
Expand Down Expand Up @@ -199,15 +181,6 @@ try {
echo $e, PHP_EOL;
}

function returnMissingIterable(): ?iterable {
}

try {
returnMissingIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}

function returnMissingInt(): ?int {
}

Expand All @@ -221,97 +194,85 @@ try {
--EXPECTF--
TypeError: unloadedClass(): Argument #1 ($param) must be of type ?I\Dont\Exist, stdClass given, called in %s:%d
Stack trace:
#0 %s(8): unloadedClass(Object(stdClass))
#0 %s(%d): unloadedClass(Object(stdClass))
#1 {main}
TypeError: loadedClass(): Argument #1 ($param) must be of type ?RealClass, stdClass given, called in %s:%d
Stack trace:
#0 %s(20): loadedClass(Object(stdClass))
#0 %s(%d): loadedClass(Object(stdClass))
#1 {main}
TypeError: loadedInterface(): Argument #1 ($param) must be of type ?RealInterface, stdClass given, called in %s:%d
Stack trace:
#0 %s(26): loadedInterface(Object(stdClass))
#0 %s(%d): loadedInterface(Object(stdClass))
#1 {main}
TypeError: unloadedClass(): Argument #1 ($param) must be of type ?I\Dont\Exist, int given, called in %s:%d
Stack trace:
#0 %s(32): unloadedClass(1)
#0 %s(%d): unloadedClass(1)
#1 {main}
TypeError: loadedClass(): Argument #1 ($param) must be of type ?RealClass, int given, called in %s:%d
Stack trace:
#0 %s(38): loadedClass(1)
#0 %s(%d): loadedClass(1)
#1 {main}
TypeError: loadedInterface(): Argument #1 ($param) must be of type ?RealInterface, int given, called in %s:%d
Stack trace:
#0 %s(44): loadedInterface(1)
#0 %s(%d): loadedInterface(1)
#1 {main}
TypeError: callableF(): Argument #1 ($param) must be of type ?callable, int given, called in %s:%d
Stack trace:
#0 %s(52): callableF(1)
#1 {main}
TypeError: iterableF(): Argument #1 ($param) must be of type ?iterable, int given, called in %s:%d
Stack trace:
#0 %s(60): iterableF(1)
#0 %s(%d): callableF(1)
#1 {main}
TypeError: intF(): Argument #1 ($param) must be of type ?int, stdClass given, called in %s:%d
Stack trace:
#0 %s(68): intF(Object(stdClass))
#0 %s(%d): intF(Object(stdClass))
#1 {main}
TypeError: returnUnloadedClass(): Return value must be of type ?I\Dont\Exist, stdClass returned in %s:%d
Stack trace:
#0 %s(78): returnUnloadedClass()
#0 %s(%d): returnUnloadedClass()
#1 {main}
TypeError: returnLoadedClass(): Return value must be of type ?RealClass, stdClass returned in %s:%d
Stack trace:
#0 %s(88): returnLoadedClass()
#0 %s(%d): returnLoadedClass()
#1 {main}
TypeError: returnLoadedInterface(): Return value must be of type ?RealInterface, stdClass returned in %s:%d
Stack trace:
#0 %s(98): returnLoadedInterface()
#0 %s(%d): returnLoadedInterface()
#1 {main}
TypeError: returnUnloadedClassScalar(): Return value must be of type ?I\Dont\Exist, int returned in %s:%d
Stack trace:
#0 %s(108): returnUnloadedClassScalar()
#0 %s(%d): returnUnloadedClassScalar()
#1 {main}
TypeError: returnLoadedClassScalar(): Return value must be of type ?RealClass, int returned in %s:%d
Stack trace:
#0 %s(118): returnLoadedClassScalar()
#0 %s(%d): returnLoadedClassScalar()
#1 {main}
TypeError: returnLoadedInterfaceScalar(): Return value must be of type ?RealInterface, int returned in %s:%d
Stack trace:
#0 %s(128): returnLoadedInterfaceScalar()
#0 %s(%d): returnLoadedInterfaceScalar()
#1 {main}
TypeError: returnCallable(): Return value must be of type ?callable, int returned in %s:%d
Stack trace:
#0 %s(138): returnCallable()
#1 {main}
TypeError: returnIterable(): Return value must be of type ?iterable, int returned in %s:%d
Stack trace:
#0 %s(148): returnIterable()
#0 %s(%d): returnCallable()
#1 {main}
TypeError: returnInt(): Return value must be of type ?int, stdClass returned in %s:%d
Stack trace:
#0 %s(158): returnInt()
#0 %s(%d): returnInt()
#1 {main}
TypeError: returnMissingUnloadedClass(): Return value must be of type ?I\Dont\Exist, none returned in %s:%d
Stack trace:
#0 %s(167): returnMissingUnloadedClass()
#0 %s(%d): returnMissingUnloadedClass()
#1 {main}
TypeError: returnMissingLoadedClass(): Return value must be of type ?RealClass, none returned in %s:%d
Stack trace:
#0 %s(176): returnMissingLoadedClass()
#0 %s(%d): returnMissingLoadedClass()
#1 {main}
TypeError: returnMissingLoadedInterface(): Return value must be of type ?RealInterface, none returned in %s:%d
Stack trace:
#0 %s(185): returnMissingLoadedInterface()
#0 %s(%d): returnMissingLoadedInterface()
#1 {main}
TypeError: returnMissingCallable(): Return value must be of type ?callable, none returned in %s:%d
Stack trace:
#0 %s(194): returnMissingCallable()
#1 {main}
TypeError: returnMissingIterable(): Return value must be of type ?iterable, none returned in %s:%d
Stack trace:
#0 %s(203): returnMissingIterable()
#0 %s(%d): returnMissingCallable()
#1 {main}
TypeError: returnMissingInt(): Return value must be of type ?int, none returned in %s:%d
Stack trace:
#0 %s(212): returnMissingInt()
#0 %s(%d): returnMissingInt()
#1 {main}
10 changes: 10 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -2805,6 +2805,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
}
}

/* Rebuild arginfos if parameter/property types and/or a return type are used */
if (reg_function->common.arg_info &&
(reg_function->common.fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS))) {
/* convert "const char*" class type names into "zend_string*" */
Expand Down Expand Up @@ -2856,6 +2857,15 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
}
}
}
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(new_arg_info[i].type)) {
/* Warning generated an extension load warning which is emitted for every test
zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable,"
" regenerate the argument info via the php-src gen_stub build script");
*/
zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
(new_arg_info[i].type.type_mask|MAY_BE_ARRAY));
new_arg_info[i].type = legacy_iterable;
}
}
}

Expand Down
Loading

0 comments on commit b40ae80

Please sign in to comment.