Skip to content

adds support for DNF types in internal functions and properties #11969

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 14 commits into from
Aug 18, 2023
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
4 changes: 4 additions & 0 deletions Zend/tests/type_declarations/typed_properties_095.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ object(_ZendTestClass)#1 (3) {
uninitialized(Traversable&Countable)
["readonlyProp"]=>
uninitialized(int)
["dnfProperty"]=>
uninitialized(Iterator|(Traversable&Countable))
}
int(123)
Cannot assign string to property _ZendTestClass::$intProp of type int
Expand All @@ -91,6 +93,8 @@ object(Test)#4 (3) {
uninitialized(Traversable&Countable)
["readonlyProp"]=>
uninitialized(int)
["dnfProperty"]=>
uninitialized(Iterator|(Traversable&Countable))
}
int(123)
Cannot assign string to property _ZendTestClass::$staticIntProp of type int
Expand Down
59 changes: 40 additions & 19 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -2756,6 +2756,28 @@ ZEND_API void zend_add_magic_method(zend_class_entry *ce, zend_function *fptr, z
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arg_info_toString, 0, 0, IS_STRING, 0)
ZEND_END_ARG_INFO()

static zend_always_inline void zend_normalize_internal_type(zend_type *type) {
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*type));
zend_type *current;
ZEND_TYPE_FOREACH(*type, current) {
if (ZEND_TYPE_HAS_NAME(*current)) {
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*current));
zend_alloc_ce_cache(name);
ZEND_TYPE_SET_PTR(*current, name);
} else if (ZEND_TYPE_HAS_LIST(*current)) {
zend_type *inner;
ZEND_TYPE_FOREACH(*current, inner) {
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*inner) && !ZEND_TYPE_HAS_LIST(*inner));
if (ZEND_TYPE_HAS_NAME(*inner)) {
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*inner));
zend_alloc_ce_cache(name);
ZEND_TYPE_SET_PTR(*inner, name);
}
} ZEND_TYPE_FOREACH_END();
}
} ZEND_TYPE_FOREACH_END();
}

/* registers all functions in *library_functions in the function hash */
ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend_function_entry *functions, HashTable *function_table, int type) /* {{{ */
{
Expand Down Expand Up @@ -2934,10 +2956,12 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
memcpy(new_arg_info, arg_info, sizeof(zend_internal_arg_info) * num_args);
reg_function->arg_info = new_arg_info + 1;
for (i = 0; i < num_args; i++) {
if (ZEND_TYPE_IS_COMPLEX(new_arg_info[i].type)) {
ZEND_ASSERT(ZEND_TYPE_HAS_NAME(new_arg_info[i].type)
&& "Should be stored as simple name");
if (ZEND_TYPE_HAS_LITERAL_NAME(new_arg_info[i].type)) {
// gen_stubs.php does not support codegen for DNF types in arg infos.
// As a temporary workaround, we split the type name on `|` characters,
// converting it to an union type if necessary.
const char *class_name = ZEND_TYPE_LITERAL_NAME(new_arg_info[i].type);
new_arg_info[i].type.type_mask &= ~_ZEND_TYPE_LITERAL_NAME_BIT;

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

if (num_types == 1) {
/* Simple class type */
ZEND_TYPE_SET_PTR(new_arg_info[i].type,
zend_string_init_interned(class_name, strlen(class_name), 1));
zend_string *str = zend_string_init_interned(class_name, strlen(class_name), 1);
zend_alloc_ce_cache(str);
ZEND_TYPE_SET_PTR(new_arg_info[i].type, str);
new_arg_info[i].type.type_mask |= _ZEND_TYPE_NAME_BIT;
} else {
/* Union type */
zend_type_list *list = malloc(ZEND_TYPE_LIST_SIZE(num_types));
Expand All @@ -2961,8 +2987,8 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
uint32_t j = 0;
while (true) {
const char *end = strchr(start, '|');
zend_string *str = zend_string_init_interned(
start, end ? end - start : strlen(start), 1);
zend_string *str = zend_string_init_interned(start, end ? end - start : strlen(start), 1);
zend_alloc_ce_cache(str);
list->types[j] = (zend_type) ZEND_TYPE_INIT_CLASS(str, 0, 0);
if (!end) {
break;
Expand All @@ -2977,10 +3003,14 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
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));
zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_MASK(
ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
(new_arg_info[i].type.type_mask | MAY_BE_ARRAY)
);
new_arg_info[i].type = legacy_iterable;
}

zend_normalize_internal_type(&new_arg_info[i].type);
}
}

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

if (is_persistent_class(ce)) {
zend_type *single_type;
ZEND_TYPE_FOREACH(property_info->type, single_type) {
// TODO Add support and test cases when gen_stub support added
ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*single_type));
if (ZEND_TYPE_HAS_NAME(*single_type)) {
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*single_type));
ZEND_TYPE_SET_PTR(*single_type, name);
zend_alloc_ce_cache(name);
}
} ZEND_TYPE_FOREACH_END();
zend_normalize_internal_type(&property_info->type);
}

zend_hash_update_ptr(&ce->properties_info, name, property_info);
Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -6402,7 +6402,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
/* Transform iterable into a type union alias */
if (type_code == IS_ITERABLE) {
/* Set iterable bit for BC compat during Reflection and string representation of type */
zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
(MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT));
return iterable;
}
Expand Down
15 changes: 12 additions & 3 deletions Zend/zend_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ typedef void (*copy_ctor_func_t)(zval *pElement);
* ZEND_TYPE_IS_ONLY_MASK() - checks if type-hint refer to standard type only
* ZEND_TYPE_IS_COMPLEX() - checks if type is a type_list, or contains a class either as a CE or as a name
* ZEND_TYPE_HAS_NAME() - checks if type-hint contains some class as zend_string *
* ZEND_TYPE_HAS_LITERAL_NAME() - checks if type-hint contains some class as const char *
* ZEND_TYPE_IS_INTERSECTION() - checks if the type_list represents an intersection type list
* ZEND_TYPE_IS_UNION() - checks if the type_list represents a union type list
*
Expand Down Expand Up @@ -145,8 +146,10 @@ typedef struct {
#define _ZEND_TYPE_MASK ((1u << 25) - 1)
/* Only one of these bits may be set. */
#define _ZEND_TYPE_NAME_BIT (1u << 24)
// Used to signify that type.ptr is not a `zend_string*` but a `const char*`,
#define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23)
#define _ZEND_TYPE_LIST_BIT (1u << 22)
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT)
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT)
/* For BC behaviour with iterable type */
#define _ZEND_TYPE_ITERABLE_BIT (1u << 21)
/* Whether the type list is arena allocated */
Expand All @@ -171,6 +174,9 @@ typedef struct {
#define ZEND_TYPE_HAS_NAME(t) \
((((t).type_mask) & _ZEND_TYPE_NAME_BIT) != 0)

#define ZEND_TYPE_HAS_LITERAL_NAME(t) \
((((t).type_mask) & _ZEND_TYPE_LITERAL_NAME_BIT) != 0)

#define ZEND_TYPE_HAS_LIST(t) \
((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0)

Expand Down Expand Up @@ -289,11 +295,14 @@ typedef struct {
#define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)

#define ZEND_TYPE_INIT_CLASS_MASK(class_name, type_mask) \
ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask))

#define ZEND_TYPE_INIT_CLASS_CONST(class_name, allow_null, extra_flags) \
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_LITERAL_NAME_BIT, allow_null, extra_flags)

#define ZEND_TYPE_INIT_CLASS_CONST_MASK(class_name, type_mask) \
ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask))
ZEND_TYPE_INIT_PTR_MASK(class_name, (_ZEND_TYPE_LITERAL_NAME_BIT | (type_mask)))

typedef union _zend_value {
zend_long lval; /* long value */
Expand Down
105 changes: 105 additions & 0 deletions ext/zend_test/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -884,11 +884,116 @@ static void le_throwing_resource_dtor(zend_resource *rsrc)
zend_throw_exception(NULL, "Throwing resource destructor called", 0);
}

static ZEND_METHOD(_ZendTestClass, takesUnionType)
{
zend_object *obj;
ZEND_PARSE_PARAMETERS_START(1, 1);
Z_PARAM_OBJ(obj)
ZEND_PARSE_PARAMETERS_END();
// we have to perform type-checking to avoid arginfo/zpp mismatch error
bool type_matches = (
instanceof_function(obj->ce, zend_standard_class_def)
||
instanceof_function(obj->ce, zend_ce_iterator)
);
if (!type_matches) {
zend_string *ty = zend_type_to_string(execute_data->func->internal_function.arg_info->type);
zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val);
zend_string_release(ty);
RETURN_THROWS();
}

RETURN_NULL();
}

// Returns a newly allocated DNF type `Iterator|(Traversable&Countable)`.
//
// We need to generate it "manually" because gen_stubs.php does not support codegen for DNF types ATM.
static zend_type create_test_dnf_type(void) {
zend_string *class_Iterator = zend_string_init_interned("Iterator", sizeof("Iterator") - 1, true);
zend_alloc_ce_cache(class_Iterator);
zend_string *class_Traversable = ZSTR_KNOWN(ZEND_STR_TRAVERSABLE);
zend_string *class_Countable = zend_string_init_interned("Countable", sizeof("Countable") - 1, true);
zend_alloc_ce_cache(class_Countable);
//
zend_type_list *intersection_list = malloc(ZEND_TYPE_LIST_SIZE(2));
intersection_list->num_types = 2;
intersection_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Traversable, 0, 0);
intersection_list->types[1] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Countable, 0, 0);
zend_type_list *union_list = malloc(ZEND_TYPE_LIST_SIZE(2));
union_list->num_types = 2;
union_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Iterator, 0, 0);
union_list->types[1] = (zend_type) ZEND_TYPE_INIT_INTERSECTION(intersection_list, 0);
return (zend_type) ZEND_TYPE_INIT_UNION(union_list, 0);
}

static void register_ZendTestClass_dnf_property(zend_class_entry *ce) {
zend_string *prop_name = zend_string_init_interned("dnfProperty", sizeof("dnfProperty") - 1, true);
zval default_value;
ZVAL_UNDEF(&default_value);
zend_type type = create_test_dnf_type();
zend_declare_typed_property(ce, prop_name, &default_value, ZEND_ACC_PUBLIC, NULL, type);
}

// arg_info for `zend_test_internal_dnf_arguments`
// The types are upgraded to DNF types in `register_dynamic_function_entries()`
static zend_internal_arg_info arginfo_zend_test_internal_dnf_arguments[] = {
// first entry is a zend_internal_function_info (see zend_compile.h): {argument_count, return_type, unused}
{(const char*)(uintptr_t)(1), {0}, NULL},
{"arg", {0}, NULL}
};

static ZEND_NAMED_FUNCTION(zend_test_internal_dnf_arguments)
{
zend_object *obj;
ZEND_PARSE_PARAMETERS_START(1, 1);
Z_PARAM_OBJ(obj)
ZEND_PARSE_PARAMETERS_END();
// we have to perform type-checking to avoid arginfo/zpp mismatch error
bool type_matches = (
instanceof_function(obj->ce, zend_ce_iterator)
|| (
instanceof_function(obj->ce, zend_ce_traversable)
&& instanceof_function(obj->ce, zend_ce_countable)
)
);
if (!type_matches) {
zend_string *ty = zend_type_to_string(arginfo_zend_test_internal_dnf_arguments[1].type);
zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val);
zend_string_release(ty);
RETURN_THROWS();
}

RETURN_OBJ_COPY(obj);
}

static const zend_function_entry dynamic_function_entries[] = {
{
.fname = "zend_test_internal_dnf_arguments",
.handler = zend_test_internal_dnf_arguments,
.arg_info = arginfo_zend_test_internal_dnf_arguments,
.num_args = 1,
.flags = 0,
},
ZEND_FE_END,
};

static void register_dynamic_function_entries(int module_type) {
// return-type is at index 0
arginfo_zend_test_internal_dnf_arguments[0].type = create_test_dnf_type();
arginfo_zend_test_internal_dnf_arguments[1].type = create_test_dnf_type();
//
zend_register_functions(NULL, dynamic_function_entries, NULL, module_type);
}

PHP_MINIT_FUNCTION(zend_test)
{
register_dynamic_function_entries(type);

zend_test_interface = register_class__ZendTestInterface();

zend_test_class = register_class__ZendTestClass(zend_test_interface);
register_ZendTestClass_dnf_property(zend_test_class);
zend_test_class->create_object = zend_test_class_new;
zend_test_class->get_static_method = zend_test_class_static_method_get;

Expand Down
2 changes: 2 additions & 0 deletions ext/zend_test/test.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public function returnsStatic(): static {}
public function returnsThrowable(): Throwable {}

static public function variadicTest(string|Iterator ...$elements) : static {}

public function takesUnionType(stdclass|Iterator $arg): void {}
}

class _ZendTestChildClass extends _ZendTestClass
Expand Down
8 changes: 7 additions & 1 deletion ext/zend_test/test_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions ext/zend_test/tests/internal_dnf_arguments.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
DNF types for internal functions
--EXTENSIONS--
zend_test
spl
reflection
--FILE--
<?php

$rf = new \ReflectionFunction('zend_test_internal_dnf_arguments');
var_dump((string)$rf->getReturnType());
$paramType = $rf->getParameters()[0]->getType();
var_dump((string)$paramType);

try {
zend_test_internal_dnf_arguments(new stdClass);
} catch (\Throwable $err) {
echo $err->getMessage(), "\n";
}

$obj = new \ArrayIterator([]);
$result = zend_test_internal_dnf_arguments($obj);
var_dump($result);

?>
--EXPECT--
string(32) "Iterator|(Traversable&Countable)"
string(32) "Iterator|(Traversable&Countable)"
zend_test_internal_dnf_arguments(): Argument #1 ($arg) must be of type Iterator|(Traversable&Countable), stdClass given
object(ArrayIterator)#5 (1) {
["storage":"ArrayIterator":private]=>
array(0) {
}
}