Skip to content

Commit 7f1c3bf

Browse files
authored
Adds support for DNF types in internal functions and properties (#11969)
Note that this does not add support for items generated by gen_stubs, only for items registered dynamically via the Zend API. Closes GH-10120
1 parent 4ff93f7 commit 7f1c3bf

File tree

8 files changed

+205
-24
lines changed

8 files changed

+205
-24
lines changed

Zend/tests/type_declarations/typed_properties_095.phpt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ object(_ZendTestClass)#1 (3) {
7575
uninitialized(Traversable&Countable)
7676
["readonlyProp"]=>
7777
uninitialized(int)
78+
["dnfProperty"]=>
79+
uninitialized(Iterator|(Traversable&Countable))
7880
}
7981
int(123)
8082
Cannot assign string to property _ZendTestClass::$intProp of type int
@@ -91,6 +93,8 @@ object(Test)#4 (3) {
9193
uninitialized(Traversable&Countable)
9294
["readonlyProp"]=>
9395
uninitialized(int)
96+
["dnfProperty"]=>
97+
uninitialized(Iterator|(Traversable&Countable))
9498
}
9599
int(123)
96100
Cannot assign string to property _ZendTestClass::$staticIntProp of type int

Zend/zend_API.c

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2756,6 +2756,28 @@ ZEND_API void zend_add_magic_method(zend_class_entry *ce, zend_function *fptr, z
27562756
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arg_info_toString, 0, 0, IS_STRING, 0)
27572757
ZEND_END_ARG_INFO()
27582758

2759+
static zend_always_inline void zend_normalize_internal_type(zend_type *type) {
2760+
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*type));
2761+
zend_type *current;
2762+
ZEND_TYPE_FOREACH(*type, current) {
2763+
if (ZEND_TYPE_HAS_NAME(*current)) {
2764+
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*current));
2765+
zend_alloc_ce_cache(name);
2766+
ZEND_TYPE_SET_PTR(*current, name);
2767+
} else if (ZEND_TYPE_HAS_LIST(*current)) {
2768+
zend_type *inner;
2769+
ZEND_TYPE_FOREACH(*current, inner) {
2770+
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*inner) && !ZEND_TYPE_HAS_LIST(*inner));
2771+
if (ZEND_TYPE_HAS_NAME(*inner)) {
2772+
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*inner));
2773+
zend_alloc_ce_cache(name);
2774+
ZEND_TYPE_SET_PTR(*inner, name);
2775+
}
2776+
} ZEND_TYPE_FOREACH_END();
2777+
}
2778+
} ZEND_TYPE_FOREACH_END();
2779+
}
2780+
27592781
/* registers all functions in *library_functions in the function hash */
27602782
ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend_function_entry *functions, HashTable *function_table, int type) /* {{{ */
27612783
{
@@ -2934,10 +2956,12 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
29342956
memcpy(new_arg_info, arg_info, sizeof(zend_internal_arg_info) * num_args);
29352957
reg_function->arg_info = new_arg_info + 1;
29362958
for (i = 0; i < num_args; i++) {
2937-
if (ZEND_TYPE_IS_COMPLEX(new_arg_info[i].type)) {
2938-
ZEND_ASSERT(ZEND_TYPE_HAS_NAME(new_arg_info[i].type)
2939-
&& "Should be stored as simple name");
2959+
if (ZEND_TYPE_HAS_LITERAL_NAME(new_arg_info[i].type)) {
2960+
// gen_stubs.php does not support codegen for DNF types in arg infos.
2961+
// As a temporary workaround, we split the type name on `|` characters,
2962+
// converting it to an union type if necessary.
29402963
const char *class_name = ZEND_TYPE_LITERAL_NAME(new_arg_info[i].type);
2964+
new_arg_info[i].type.type_mask &= ~_ZEND_TYPE_LITERAL_NAME_BIT;
29412965

29422966
size_t num_types = 1;
29432967
const char *p = class_name;
@@ -2948,8 +2972,10 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
29482972

29492973
if (num_types == 1) {
29502974
/* Simple class type */
2951-
ZEND_TYPE_SET_PTR(new_arg_info[i].type,
2952-
zend_string_init_interned(class_name, strlen(class_name), 1));
2975+
zend_string *str = zend_string_init_interned(class_name, strlen(class_name), 1);
2976+
zend_alloc_ce_cache(str);
2977+
ZEND_TYPE_SET_PTR(new_arg_info[i].type, str);
2978+
new_arg_info[i].type.type_mask |= _ZEND_TYPE_NAME_BIT;
29532979
} else {
29542980
/* Union type */
29552981
zend_type_list *list = malloc(ZEND_TYPE_LIST_SIZE(num_types));
@@ -2961,8 +2987,8 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
29612987
uint32_t j = 0;
29622988
while (true) {
29632989
const char *end = strchr(start, '|');
2964-
zend_string *str = zend_string_init_interned(
2965-
start, end ? end - start : strlen(start), 1);
2990+
zend_string *str = zend_string_init_interned(start, end ? end - start : strlen(start), 1);
2991+
zend_alloc_ce_cache(str);
29662992
list->types[j] = (zend_type) ZEND_TYPE_INIT_CLASS(str, 0, 0);
29672993
if (!end) {
29682994
break;
@@ -2977,10 +3003,14 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
29773003
zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable,"
29783004
" regenerate the argument info via the php-src gen_stub build script");
29793005
*/
2980-
zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
2981-
(new_arg_info[i].type.type_mask|MAY_BE_ARRAY));
3006+
zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_MASK(
3007+
ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
3008+
(new_arg_info[i].type.type_mask | MAY_BE_ARRAY)
3009+
);
29823010
new_arg_info[i].type = legacy_iterable;
29833011
}
3012+
3013+
zend_normalize_internal_type(&new_arg_info[i].type);
29843014
}
29853015
}
29863016

@@ -4367,16 +4397,7 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
43674397
property_info->type = type;
43684398

43694399
if (is_persistent_class(ce)) {
4370-
zend_type *single_type;
4371-
ZEND_TYPE_FOREACH(property_info->type, single_type) {
4372-
// TODO Add support and test cases when gen_stub support added
4373-
ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*single_type));
4374-
if (ZEND_TYPE_HAS_NAME(*single_type)) {
4375-
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*single_type));
4376-
ZEND_TYPE_SET_PTR(*single_type, name);
4377-
zend_alloc_ce_cache(name);
4378-
}
4379-
} ZEND_TYPE_FOREACH_END();
4400+
zend_normalize_internal_type(&property_info->type);
43804401
}
43814402

43824403
zend_hash_update_ptr(&ce->properties_info, name, property_info);

Zend/zend_compile.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6438,7 +6438,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
64386438
/* Transform iterable into a type union alias */
64396439
if (type_code == IS_ITERABLE) {
64406440
/* Set iterable bit for BC compat during Reflection and string representation of type */
6441-
zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
6441+
zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
64426442
(MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT));
64436443
return iterable;
64446444
}

Zend/zend_types.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ typedef void (*copy_ctor_func_t)(zval *pElement);
115115
* ZEND_TYPE_IS_ONLY_MASK() - checks if type-hint refer to standard type only
116116
* ZEND_TYPE_IS_COMPLEX() - checks if type is a type_list, or contains a class either as a CE or as a name
117117
* ZEND_TYPE_HAS_NAME() - checks if type-hint contains some class as zend_string *
118+
* ZEND_TYPE_HAS_LITERAL_NAME() - checks if type-hint contains some class as const char *
118119
* ZEND_TYPE_IS_INTERSECTION() - checks if the type_list represents an intersection type list
119120
* ZEND_TYPE_IS_UNION() - checks if the type_list represents a union type list
120121
*
@@ -145,8 +146,10 @@ typedef struct {
145146
#define _ZEND_TYPE_MASK ((1u << 25) - 1)
146147
/* Only one of these bits may be set. */
147148
#define _ZEND_TYPE_NAME_BIT (1u << 24)
149+
// Used to signify that type.ptr is not a `zend_string*` but a `const char*`,
150+
#define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23)
148151
#define _ZEND_TYPE_LIST_BIT (1u << 22)
149-
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT)
152+
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT)
150153
/* For BC behaviour with iterable type */
151154
#define _ZEND_TYPE_ITERABLE_BIT (1u << 21)
152155
/* Whether the type list is arena allocated */
@@ -171,6 +174,9 @@ typedef struct {
171174
#define ZEND_TYPE_HAS_NAME(t) \
172175
((((t).type_mask) & _ZEND_TYPE_NAME_BIT) != 0)
173176

177+
#define ZEND_TYPE_HAS_LITERAL_NAME(t) \
178+
((((t).type_mask) & _ZEND_TYPE_LITERAL_NAME_BIT) != 0)
179+
174180
#define ZEND_TYPE_HAS_LIST(t) \
175181
((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0)
176182

@@ -289,11 +295,14 @@ typedef struct {
289295
#define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \
290296
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
291297

298+
#define ZEND_TYPE_INIT_CLASS_MASK(class_name, type_mask) \
299+
ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask))
300+
292301
#define ZEND_TYPE_INIT_CLASS_CONST(class_name, allow_null, extra_flags) \
293-
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
302+
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_LITERAL_NAME_BIT, allow_null, extra_flags)
294303

295304
#define ZEND_TYPE_INIT_CLASS_CONST_MASK(class_name, type_mask) \
296-
ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask))
305+
ZEND_TYPE_INIT_PTR_MASK(class_name, (_ZEND_TYPE_LITERAL_NAME_BIT | (type_mask)))
297306

298307
typedef union _zend_value {
299308
zend_long lval; /* long value */

ext/zend_test/test.c

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,11 +884,116 @@ static void le_throwing_resource_dtor(zend_resource *rsrc)
884884
zend_throw_exception(NULL, "Throwing resource destructor called", 0);
885885
}
886886

887+
static ZEND_METHOD(_ZendTestClass, takesUnionType)
888+
{
889+
zend_object *obj;
890+
ZEND_PARSE_PARAMETERS_START(1, 1);
891+
Z_PARAM_OBJ(obj)
892+
ZEND_PARSE_PARAMETERS_END();
893+
// we have to perform type-checking to avoid arginfo/zpp mismatch error
894+
bool type_matches = (
895+
instanceof_function(obj->ce, zend_standard_class_def)
896+
||
897+
instanceof_function(obj->ce, zend_ce_iterator)
898+
);
899+
if (!type_matches) {
900+
zend_string *ty = zend_type_to_string(execute_data->func->internal_function.arg_info->type);
901+
zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val);
902+
zend_string_release(ty);
903+
RETURN_THROWS();
904+
}
905+
906+
RETURN_NULL();
907+
}
908+
909+
// Returns a newly allocated DNF type `Iterator|(Traversable&Countable)`.
910+
//
911+
// We need to generate it "manually" because gen_stubs.php does not support codegen for DNF types ATM.
912+
static zend_type create_test_dnf_type(void) {
913+
zend_string *class_Iterator = zend_string_init_interned("Iterator", sizeof("Iterator") - 1, true);
914+
zend_alloc_ce_cache(class_Iterator);
915+
zend_string *class_Traversable = ZSTR_KNOWN(ZEND_STR_TRAVERSABLE);
916+
zend_string *class_Countable = zend_string_init_interned("Countable", sizeof("Countable") - 1, true);
917+
zend_alloc_ce_cache(class_Countable);
918+
//
919+
zend_type_list *intersection_list = malloc(ZEND_TYPE_LIST_SIZE(2));
920+
intersection_list->num_types = 2;
921+
intersection_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Traversable, 0, 0);
922+
intersection_list->types[1] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Countable, 0, 0);
923+
zend_type_list *union_list = malloc(ZEND_TYPE_LIST_SIZE(2));
924+
union_list->num_types = 2;
925+
union_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Iterator, 0, 0);
926+
union_list->types[1] = (zend_type) ZEND_TYPE_INIT_INTERSECTION(intersection_list, 0);
927+
return (zend_type) ZEND_TYPE_INIT_UNION(union_list, 0);
928+
}
929+
930+
static void register_ZendTestClass_dnf_property(zend_class_entry *ce) {
931+
zend_string *prop_name = zend_string_init_interned("dnfProperty", sizeof("dnfProperty") - 1, true);
932+
zval default_value;
933+
ZVAL_UNDEF(&default_value);
934+
zend_type type = create_test_dnf_type();
935+
zend_declare_typed_property(ce, prop_name, &default_value, ZEND_ACC_PUBLIC, NULL, type);
936+
}
937+
938+
// arg_info for `zend_test_internal_dnf_arguments`
939+
// The types are upgraded to DNF types in `register_dynamic_function_entries()`
940+
static zend_internal_arg_info arginfo_zend_test_internal_dnf_arguments[] = {
941+
// first entry is a zend_internal_function_info (see zend_compile.h): {argument_count, return_type, unused}
942+
{(const char*)(uintptr_t)(1), {0}, NULL},
943+
{"arg", {0}, NULL}
944+
};
945+
946+
static ZEND_NAMED_FUNCTION(zend_test_internal_dnf_arguments)
947+
{
948+
zend_object *obj;
949+
ZEND_PARSE_PARAMETERS_START(1, 1);
950+
Z_PARAM_OBJ(obj)
951+
ZEND_PARSE_PARAMETERS_END();
952+
// we have to perform type-checking to avoid arginfo/zpp mismatch error
953+
bool type_matches = (
954+
instanceof_function(obj->ce, zend_ce_iterator)
955+
|| (
956+
instanceof_function(obj->ce, zend_ce_traversable)
957+
&& instanceof_function(obj->ce, zend_ce_countable)
958+
)
959+
);
960+
if (!type_matches) {
961+
zend_string *ty = zend_type_to_string(arginfo_zend_test_internal_dnf_arguments[1].type);
962+
zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val);
963+
zend_string_release(ty);
964+
RETURN_THROWS();
965+
}
966+
967+
RETURN_OBJ_COPY(obj);
968+
}
969+
970+
static const zend_function_entry dynamic_function_entries[] = {
971+
{
972+
.fname = "zend_test_internal_dnf_arguments",
973+
.handler = zend_test_internal_dnf_arguments,
974+
.arg_info = arginfo_zend_test_internal_dnf_arguments,
975+
.num_args = 1,
976+
.flags = 0,
977+
},
978+
ZEND_FE_END,
979+
};
980+
981+
static void register_dynamic_function_entries(int module_type) {
982+
// return-type is at index 0
983+
arginfo_zend_test_internal_dnf_arguments[0].type = create_test_dnf_type();
984+
arginfo_zend_test_internal_dnf_arguments[1].type = create_test_dnf_type();
985+
//
986+
zend_register_functions(NULL, dynamic_function_entries, NULL, module_type);
987+
}
988+
887989
PHP_MINIT_FUNCTION(zend_test)
888990
{
991+
register_dynamic_function_entries(type);
992+
889993
zend_test_interface = register_class__ZendTestInterface();
890994

891995
zend_test_class = register_class__ZendTestClass(zend_test_interface);
996+
register_ZendTestClass_dnf_property(zend_test_class);
892997
zend_test_class->create_object = zend_test_class_new;
893998
zend_test_class->get_static_method = zend_test_class_static_method_get;
894999

ext/zend_test/test.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public function returnsStatic(): static {}
5353
public function returnsThrowable(): Throwable {}
5454

5555
static public function variadicTest(string|Iterator ...$elements) : static {}
56+
57+
public function takesUnionType(stdclass|Iterator $arg): void {}
5658
}
5759

5860
class _ZendTestChildClass extends _ZendTestClass

ext/zend_test/test_arginfo.h

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
DNF types for internal functions
3+
--EXTENSIONS--
4+
zend_test
5+
spl
6+
reflection
7+
--FILE--
8+
<?php
9+
10+
$rf = new \ReflectionFunction('zend_test_internal_dnf_arguments');
11+
var_dump((string)$rf->getReturnType());
12+
$paramType = $rf->getParameters()[0]->getType();
13+
var_dump((string)$paramType);
14+
15+
try {
16+
zend_test_internal_dnf_arguments(new stdClass);
17+
} catch (\Throwable $err) {
18+
echo $err->getMessage(), "\n";
19+
}
20+
21+
$obj = new \ArrayIterator([]);
22+
$result = zend_test_internal_dnf_arguments($obj);
23+
var_dump($result);
24+
25+
?>
26+
--EXPECT--
27+
string(32) "Iterator|(Traversable&Countable)"
28+
string(32) "Iterator|(Traversable&Countable)"
29+
zend_test_internal_dnf_arguments(): Argument #1 ($arg) must be of type Iterator|(Traversable&Countable), stdClass given
30+
object(ArrayIterator)#5 (1) {
31+
["storage":"ArrayIterator":private]=>
32+
array(0) {
33+
}
34+
}

0 commit comments

Comments
 (0)