diff --git a/Zend/tests/type_declarations/associated/associated_001.phpt b/Zend/tests/type_declarations/associated/associated_001.phpt new file mode 100644 index 0000000000000..f3b6d6cafaa87 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_001.phpt @@ -0,0 +1,32 @@ +--TEST-- +Associated types basic +--FILE-- +foo("Hello")); + +$ci = new CI(); +var_dump($ci->foo(5)); + +?> +--EXPECT-- +string(6) "Hello!" +int(47) diff --git a/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection1.phpt b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection1.phpt new file mode 100644 index 0000000000000..0ea969b68c499 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection1.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated type cannot be in intersection (simple intersection with class type) +--FILE-- + +--EXPECTF-- +Fatal error: Associated type cannot be part of an intersection type in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection2.phpt b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection2.phpt new file mode 100644 index 0000000000000..f44dab1a1e7f0 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection2.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated type cannot be in intersection (DNF type) +--FILE-- + +--EXPECTF-- +Fatal error: Associated type cannot be part of an intersection type in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union1.phpt b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union1.phpt new file mode 100644 index 0000000000000..7a4e1809a4d52 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union1.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated type cannot be in union (simple union with built-in type) +--FILE-- + +--EXPECTF-- +Fatal error: Associated type cannot be part of a union type in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union2.phpt b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union2.phpt new file mode 100644 index 0000000000000..904f428d09920 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union2.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated type cannot be in union (simple union with class type) +--FILE-- + +--EXPECTF-- +Fatal error: Associated type cannot be part of a union type in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union3.phpt b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union3.phpt new file mode 100644 index 0000000000000..a40109bb5ebe3 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union3.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated type cannot be in union (DNF type) +--FILE-- + +--EXPECTF-- +Fatal error: Associated type cannot be part of a union type in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_in_class.phpt b/Zend/tests/type_declarations/associated/associated_type_in_class.phpt new file mode 100644 index 0000000000000..d767621409d6c --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_in_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated types in class is invalid +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use associated types outside of interfaces, used in C in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_in_trait.phpt b/Zend/tests/type_declarations/associated/associated_type_in_trait.phpt new file mode 100644 index 0000000000000..6e944f47fcdc2 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_in_trait.phpt @@ -0,0 +1,13 @@ +--TEST-- +Associated types in trait is invalid +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use associated types outside of interfaces, used in C in %s on line %d diff --git a/Zend/tests/type_declarations/associated/associated_type_with_constraint.phpt b/Zend/tests/type_declarations/associated/associated_type_with_constraint.phpt new file mode 100644 index 0000000000000..9e855edd7edb2 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_with_constraint.phpt @@ -0,0 +1,32 @@ +--TEST-- +Associated type with a constraint +--FILE-- +foo("Hello")); + +$ci = new CI(); +var_dump($ci->foo(5)); + +?> +--EXPECT-- +string(6) "Hello!" +int(47) diff --git a/Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt b/Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt new file mode 100644 index 0000000000000..50f81abcbe830 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt @@ -0,0 +1,17 @@ +--TEST-- +Associated type with a constraint that is not satisfied +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of C::foo(float $param): float must be compatible with I::foo(T $param): T in %s on line %d diff --git a/Zend/tests/type_declarations/associated/extended_interface_associated_type_redeclared.phpt b/Zend/tests/type_declarations/associated/extended_interface_associated_type_redeclared.phpt new file mode 100644 index 0000000000000..7501eed0e9127 --- /dev/null +++ b/Zend/tests/type_declarations/associated/extended_interface_associated_type_redeclared.phpt @@ -0,0 +1,23 @@ +--TEST-- +Associated type behaviour in extended interface +--FILE-- + +--EXPECTF-- +Fatal error: Cannot redeclare associated type T in interface I2 inherited from interface I in %s on line %d diff --git a/Zend/tests/type_declarations/associated/extended_interface_associated_types.phpt b/Zend/tests/type_declarations/associated/extended_interface_associated_types.phpt new file mode 100644 index 0000000000000..107df2e23ed53 --- /dev/null +++ b/Zend/tests/type_declarations/associated/extended_interface_associated_types.phpt @@ -0,0 +1,22 @@ +--TEST-- +Associated type behaviour in extended interface +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of C::bar(int $o, float $param): float must be compatible with I2::bar(int $o, T $param): T in %s on line %d diff --git a/Zend/tests/type_declarations/associated/extended_interface_new_associated_types.phpt b/Zend/tests/type_declarations/associated/extended_interface_new_associated_types.phpt new file mode 100644 index 0000000000000..65a8f6f515767 --- /dev/null +++ b/Zend/tests/type_declarations/associated/extended_interface_new_associated_types.phpt @@ -0,0 +1,26 @@ +--TEST-- +Associated type behaviour in extended interface +--FILE-- + $o, T<(Traversable&Countable)|int|string> $param): T2 in %s on line %d +//Improve zend_append_type_hint()? +?> +--EXPECTF-- +Fatal error: Declaration of C::bar(float $o, string $param): float must be compatible with I2::bar(T2 $o, T $param): T2 in %s on line %d diff --git a/Zend/tests/type_declarations/associated/multiple_associated_type.phpt b/Zend/tests/type_declarations/associated/multiple_associated_type.phpt new file mode 100644 index 0000000000000..aae8ca4dda877 --- /dev/null +++ b/Zend/tests/type_declarations/associated/multiple_associated_type.phpt @@ -0,0 +1,74 @@ +--TEST-- +Multiple associated types +--FILE-- +a[$key] = $value . '!'; + } + public function get(int $key): string { + return $this->a[$key]; + } +} + +class C2 implements I { + public array $a = []; + public function set(string $key, object $value): void { + $this->a[$key] = $value; + } + public function get(string $key): object { + return $this->a[$key]; + } +} + +$c1 = new C1(); +$c1->set(5, "Hello"); +var_dump($c1->a); +var_dump($c1->get(5)); + +$c2 = new C2(); +$c2->set('C1', $c1); +var_dump($c2->a); +var_dump($c2->get('C1')); + +try { + $c1->set('blah', "Hello"); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + + +?> +--EXPECTF-- +array(1) { + [5]=> + string(6) "Hello!" +} +string(6) "Hello!" +array(1) { + ["C1"]=> + object(C1)#1 (1) { + ["a"]=> + array(1) { + [5]=> + string(6) "Hello!" + } + } +} +object(C1)#1 (1) { + ["a"]=> + array(1) { + [5]=> + string(6) "Hello!" + } +} +TypeError: C1::set(): Argument #1 ($key) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/type_declarations/associated/repeated_associated_type.phpt b/Zend/tests/type_declarations/associated/repeated_associated_type.phpt new file mode 100644 index 0000000000000..be65c60ef7767 --- /dev/null +++ b/Zend/tests/type_declarations/associated/repeated_associated_type.phpt @@ -0,0 +1,18 @@ +--TEST-- +Repeated associated type +--FILE-- + +--EXPECTF-- +Fatal error: Cannot have two associated types with the same name "T" in %s on line %d diff --git a/Zend/zend.c b/Zend/zend.c index 2d8a0f455f8b4..edec7ba675630 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -732,6 +732,7 @@ static void compiler_globals_ctor(zend_compiler_globals *compiler_globals) /* {{ compiler_globals->script_encoding_list = NULL; compiler_globals->current_linking_class = NULL; + compiler_globals->bound_associated_types = NULL; /* Map region is going to be created and resized at run-time. */ compiler_globals->map_ptr_real_base = NULL; diff --git a/Zend/zend.h b/Zend/zend.h index 0cf1faeb653fe..cff81284740c1 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -218,6 +218,9 @@ struct _zend_class_entry { zend_trait_precedence **trait_precedences; HashTable *attributes; + /* Only for interfaces */ + HashTable *associated_types; + uint32_t enum_backing_type; HashTable *backed_enum_table; diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 2551f876d4465..1eab1cbcc60a6 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2664,6 +2664,16 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appends(str, ": "); ast = ast->child[1]; goto tail_call; + case ZEND_AST_ASSOCIATED_TYPE: + smart_str_appends(str, "type "); + zend_ast_export_name(str, ast->child[0], 0, indent); + if (ast->child[1]) { + smart_str_appends(str, " : "); + smart_str_appends(str, " : "); + zend_ast_export_type(str, ast->child[1], indent); + } + smart_str_appendc(str, ';'); + break; /* 3 child nodes */ case ZEND_AST_METHOD_CALL: diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 9348c35f6cc07..81f86f0623f83 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -154,6 +154,7 @@ enum _zend_ast_kind { ZEND_AST_MATCH_ARM, ZEND_AST_NAMED_ARG, ZEND_AST_PARENT_PROPERTY_HOOK_CALL, + ZEND_AST_ASSOCIATED_TYPE, /* 3 child nodes */ ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3fa4c3959cb43..94dd993cc3f93 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -84,6 +84,8 @@ static inline uint32_t zend_alloc_cache_slot(void) { return zend_alloc_cache_slots(1); } +const zend_type zend_mixed_type = { NULL, MAY_BE_ANY }; + ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type); ZEND_API zend_op_array *(*zend_compile_string)(zend_string *source_string, const char *filename, zend_compile_position position); @@ -463,6 +465,7 @@ void init_compiler(void) /* {{{ */ CG(delayed_autoloads) = NULL; CG(unlinked_uses) = NULL; CG(current_linking_class) = NULL; + CG(bound_associated_types) = NULL; } /* }}} */ @@ -491,6 +494,12 @@ void shutdown_compiler(void) /* {{{ */ CG(unlinked_uses) = NULL; } CG(current_linking_class) = NULL; + /* This can happen during a fatal error */ + if (CG(bound_associated_types)) { + zend_hash_destroy(CG(bound_associated_types)); + FREE_HASHTABLE(CG(bound_associated_types)); + CG(bound_associated_types) = NULL; + } } /* }}} */ @@ -1432,6 +1441,26 @@ static zend_string *add_intersection_type(zend_string *str, return str; } +static zend_string *add_associated_type(zend_string *associated_type, zend_class_entry *scope) +{ + const zend_type *constraint = zend_hash_find_ptr(scope->associated_types, associated_type); + ZEND_ASSERT(constraint != NULL); + + zend_string *constraint_type_str = zend_type_to_string_resolved(*constraint, scope); + + size_t len = ZSTR_LEN(associated_type) + ZSTR_LEN(constraint_type_str) + strlen("<>"); + zend_string *result = zend_string_alloc(len, 0); + + memcpy(ZSTR_VAL(result), ZSTR_VAL(associated_type), ZSTR_LEN(associated_type)); + ZSTR_VAL(result)[ZSTR_LEN(associated_type)] = '<'; + memcpy(ZSTR_VAL(result) + ZSTR_LEN(associated_type) + 1, ZSTR_VAL(constraint_type_str), ZSTR_LEN(constraint_type_str)); + ZSTR_VAL(result)[len-1] = '>'; + ZSTR_VAL(result)[len] = '\0'; + + zend_string_release(constraint_type_str); + return result; +} + zend_string *zend_type_to_string_resolved(const zend_type type, zend_class_entry *scope) { zend_string *str = NULL; @@ -1455,6 +1484,8 @@ zend_string *zend_type_to_string_resolved(const zend_type type, zend_class_entry str = add_type_string(str, resolved, /* is_intersection */ false); zend_string_release(resolved); } ZEND_TYPE_LIST_FOREACH_END(); + } else if (ZEND_TYPE_IS_ASSOCIATED(type)) { + str = add_associated_type(ZEND_TYPE_NAME(type), scope); } else if (ZEND_TYPE_HAS_NAME(type)) { str = resolve_class_name(ZEND_TYPE_NAME(type), scope); } @@ -2072,6 +2103,7 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->default_static_members_count = 0; ce->properties_info_table = NULL; ce->attributes = NULL; + ce->associated_types = NULL; ce->enum_backing_type = IS_UNDEF; ce->backed_enum_table = NULL; @@ -6965,9 +6997,11 @@ ZEND_API void zend_set_function_arg_flags(zend_function *func) /* {{{ */ static zend_type zend_compile_single_typename(zend_ast *ast) { + zend_class_entry *ce = CG(active_class_entry); + ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE)); if (ast->kind == ZEND_AST_TYPE) { - if (ast->attr == IS_STATIC && !CG(active_class_entry) && zend_is_scope_known()) { + if (ast->attr == IS_STATIC && !ce && zend_is_scope_known()) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot use \"static\" when no class scope is active"); } @@ -6997,10 +7031,14 @@ static zend_type zend_compile_single_typename(zend_ast *ast) const char *correct_name; uint32_t fetch_type = zend_get_class_fetch_type_ast(ast); zend_string *class_name = type_name; + uint32_t flags = 0; if (fetch_type == ZEND_FETCH_CLASS_DEFAULT) { class_name = zend_resolve_class_name_ast(ast); zend_assert_valid_class_name(class_name, "a type name"); + if (ce && ce->associated_types && zend_hash_exists(ce->associated_types, class_name)) { + flags = _ZEND_TYPE_ASSOCIATED_BIT; + } } else { ZEND_ASSERT(fetch_type == ZEND_FETCH_CLASS_SELF || fetch_type == ZEND_FETCH_CLASS_PARENT); @@ -7008,14 +7046,14 @@ static zend_type zend_compile_single_typename(zend_ast *ast) if (fetch_type == ZEND_FETCH_CLASS_SELF) { /* Scope might be unknown for unbound closures and traits */ if (zend_is_scope_known()) { - class_name = CG(active_class_entry)->name; + class_name = ce->name; ZEND_ASSERT(class_name && "must know class name when resolving self type at compile time"); } } else { ZEND_ASSERT(fetch_type == ZEND_FETCH_CLASS_PARENT); /* Scope might be unknown for unbound closures and traits */ if (zend_is_scope_known()) { - class_name = CG(active_class_entry)->parent_name; + class_name = ce->parent_name; ZEND_ASSERT(class_name && "must know class name when resolving parent type at compile time"); } } @@ -7043,7 +7081,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast) class_name = zend_new_interned_string(class_name); zend_alloc_ce_cache(class_name); - return (zend_type) ZEND_TYPE_INIT_CLASS(class_name, /* allow null */ false, 0); + return (zend_type) ZEND_TYPE_INIT_CLASS(class_name, /* allow null */ false, flags); } } } @@ -7192,6 +7230,9 @@ static zend_type zend_compile_typename_ex( single_type = zend_compile_single_typename(type_ast); uint32_t single_type_mask = ZEND_TYPE_PURE_MASK(single_type); + if (ZEND_TYPE_IS_ASSOCIATED(single_type)) { + zend_error_noreturn(E_COMPILE_ERROR, "Associated type cannot be part of a union type"); + } if (single_type_mask == MAY_BE_ANY) { zend_error_noreturn(E_COMPILE_ERROR, "Type mixed can only be used as a standalone type"); } @@ -7274,6 +7315,9 @@ static zend_type zend_compile_typename_ex( zend_ast *type_ast = list->child[i]; zend_type single_type = zend_compile_single_typename(type_ast); + if (ZEND_TYPE_IS_ASSOCIATED(single_type)) { + zend_error_noreturn(E_COMPILE_ERROR, "Associated type cannot be part of an intersection type"); + } /* An intersection of union types cannot exist so invalidate it * Currently only can happen with iterable getting canonicalized to Traversable|array */ if (ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) { @@ -9020,6 +9064,48 @@ static void zend_compile_use_trait(zend_ast *ast) /* {{{ */ } /* }}} */ +static void zend_associated_table_ht_dtor(zval *val) { + /* NO OP as we only use it to be able to refer and save pointers to zend_types */ + // TODO do we actually want to store copies of types? + zend_type *associated_type = Z_PTR_P(val); + if (associated_type != &zend_mixed_type) { + zend_type_release(*associated_type, false); + efree(associated_type); + } +} + +static void zend_compile_associated_type(zend_ast *ast) { + zend_class_entry *ce = CG(active_class_entry); + HashTable *associated_types = ce->associated_types; + zend_ast *name_ast = ast->child[0]; + zend_ast *type_ast = ast->child[1]; + zend_string *name = zend_ast_get_str(name_ast); + + if ((ce->ce_flags & ZEND_ACC_INTERFACE) == 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot use associated types outside of interfaces, used in %s", ZSTR_VAL(ce->name)); + } + + ZEND_ASSERT(name != NULL); + bool persistent = ce->type == ZEND_INTERNAL_CLASS; + if (associated_types == NULL) { + ce->associated_types = pemalloc(sizeof(HashTable), persistent); + zend_hash_init(ce->associated_types, 8, NULL, zend_associated_table_ht_dtor, persistent); + associated_types = ce->associated_types; + } + if (zend_hash_exists(associated_types, name)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot have two associated types with the same name \"%s\"", ZSTR_VAL(name)); + } + + if (type_ast != NULL) { + zend_type type = zend_compile_typename(type_ast); + zend_hash_add_new_mem(associated_types, name, &type, sizeof(type)); + } else { + zend_hash_add_new_ptr(associated_types, name, (void*) &zend_mixed_type); + } +} + static void zend_compile_implements(zend_ast *ast) /* {{{ */ { zend_ast_list *list = zend_ast_get_list(ast); @@ -11586,6 +11672,9 @@ static void zend_compile_stmt(zend_ast *ast) /* {{{ */ case ZEND_AST_USE_TRAIT: zend_compile_use_trait(ast); break; + case ZEND_AST_ASSOCIATED_TYPE: + zend_compile_associated_type(ast); + break; case ZEND_AST_CLASS: zend_compile_class_decl(NULL, ast, 0); break; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index db087bdd60035..1397bec88bcc1 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -53,6 +53,7 @@ typedef struct _zend_op_array zend_op_array; typedef struct _zend_op zend_op; +extern const zend_type zend_mixed_type; /* On 64-bit systems less optimal, but more compact VM code leads to better * performance. So on 32-bit systems we use absolute addresses for jump diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 079bfb99caccf..dc1a06f52c539 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -151,6 +151,8 @@ struct _zend_compiler_globals { HashTable *delayed_autoloads; HashTable *unlinked_uses; zend_class_entry *current_linking_class; + /* Those are initialized and destroyed by zend_do_inheritance_ex() */ + HashTable *bound_associated_types; uint32_t rtd_key_counter; diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 090b1049418d2..6cee659eab9b2 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -428,16 +428,16 @@ static void track_class_dependency(zend_class_entry *ce, zend_string *class_name /* Check whether any type in the fe_type intersection type is a subtype of the proto class. */ static inheritance_status zend_is_intersection_subtype_of_class( - zend_class_entry *fe_scope, const zend_type fe_type, + zend_class_entry *fe_scope, const zend_type *fe_type_ptr, zend_class_entry *proto_scope, zend_string *proto_class_name, zend_class_entry *proto_ce) { - ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(fe_type)); + ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(*fe_type_ptr)); bool have_unresolved = false; const zend_type *single_type; /* Traverse the list of child types and check that at least one is * a subtype of the parent type being checked */ - ZEND_TYPE_FOREACH(fe_type, single_type) { + ZEND_TYPE_FOREACH(*fe_type_ptr, single_type) { zend_class_entry *fe_ce; zend_string *fe_class_name = NULL; if (ZEND_TYPE_HAS_NAME(*single_type)) { @@ -473,7 +473,9 @@ static inheritance_status zend_is_intersection_subtype_of_class( /* Check whether a single class proto type is a subtype of a potentially complex fe_type. */ static inheritance_status zend_is_class_subtype_of_type( zend_class_entry *fe_scope, zend_string *fe_class_name, - zend_class_entry *proto_scope, const zend_type proto_type) { + zend_class_entry *proto_scope, const zend_type *proto_type_ptr +) { + const zend_type proto_type = *proto_type_ptr; zend_class_entry *fe_ce = NULL; bool have_unresolved = 0; @@ -521,7 +523,7 @@ static inheritance_status zend_is_class_subtype_of_type( ZEND_TYPE_FOREACH(proto_type, single_type) { if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { inheritance_status subtype_status = zend_is_class_subtype_of_type( - fe_scope, fe_class_name, proto_scope, *single_type); + fe_scope, fe_class_name, proto_scope, single_type); switch (subtype_status) { case INHERITANCE_ERROR: @@ -606,9 +608,11 @@ static void register_unresolved_classes(zend_class_entry *scope, const zend_type } static inheritance_status zend_is_intersection_subtype_of_type( - zend_class_entry *fe_scope, const zend_type fe_type, - zend_class_entry *proto_scope, const zend_type proto_type) -{ + zend_class_entry *fe_scope, const zend_type *fe_type_ptr, + zend_class_entry *proto_scope, const zend_type *proto_type_ptr +) { + const zend_type fe_type = *fe_type_ptr; + const zend_type proto_type = *proto_type_ptr; bool have_unresolved = false; const zend_type *single_type; uint32_t proto_type_mask = ZEND_TYPE_PURE_MASK(proto_type); @@ -644,7 +648,7 @@ static inheritance_status zend_is_intersection_subtype_of_type( if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { status = zend_is_intersection_subtype_of_type( - fe_scope, fe_type, proto_scope, *single_type); + fe_scope, fe_type_ptr, proto_scope, single_type); } else { zend_string *proto_class_name = get_class_from_type(proto_scope, *single_type); if (!proto_class_name) { @@ -653,7 +657,7 @@ static inheritance_status zend_is_intersection_subtype_of_type( zend_class_entry *proto_ce = NULL; status = zend_is_intersection_subtype_of_class( - fe_scope, fe_type, proto_scope, proto_class_name, proto_ce); + fe_scope, fe_type_ptr, proto_scope, proto_class_name, proto_ce); } if (status == early_exit_status) { @@ -672,9 +676,57 @@ static inheritance_status zend_is_intersection_subtype_of_type( } ZEND_API inheritance_status zend_perform_covariant_type_check( - zend_class_entry *fe_scope, const zend_type fe_type, - zend_class_entry *proto_scope, const zend_type proto_type) + zend_class_entry *fe_scope, const zend_type *fe_type_ptr, + zend_class_entry *proto_scope, const zend_type *proto_type_ptr); + +static inheritance_status zend_is_type_subtype_of_associated_type( + zend_class_entry *concrete_scope, + const zend_type *concrete_type_ptr, + zend_class_entry *associated_type_scope, + const zend_type *associated_type_ptr +) { + const zend_type associated_type = *associated_type_ptr; + + ZEND_ASSERT(CG(bound_associated_types) && "Have associated type"); + ZEND_ASSERT(ZEND_TYPE_HAS_NAME(associated_type)); + + zend_string *associated_type_name = ZEND_TYPE_NAME(associated_type); + const zend_type *bound_type_ptr = zend_hash_find_ptr(CG(bound_associated_types), associated_type_name); + if (bound_type_ptr == NULL) { + const zend_type *constraint = zend_hash_find_ptr(associated_type_scope->associated_types, associated_type_name); + ZEND_ASSERT(constraint != NULL); + /* Check that the provided type is a subtype of the constraint */ + const inheritance_status status = zend_perform_covariant_type_check( + concrete_scope, concrete_type_ptr, + associated_type_scope, constraint); + if (status != INHERITANCE_SUCCESS) { + return status; + } + + /* Loosing const qualifier here is OK because this hashtable never frees or does anything with the value */ + zend_hash_add_new_ptr(CG(bound_associated_types), associated_type_name, (void*)concrete_type_ptr); + return INHERITANCE_SUCCESS; + } else { + /* Associated type must be invariant */ + const inheritance_status sub_type_status = zend_perform_covariant_type_check( + concrete_scope, concrete_type_ptr, associated_type_scope, bound_type_ptr); + const inheritance_status super_type_status = zend_perform_covariant_type_check( + associated_type_scope, bound_type_ptr, concrete_scope, concrete_type_ptr); + + if (sub_type_status != super_type_status) { + return INHERITANCE_ERROR; + } else { + return sub_type_status; + } + } +} + +ZEND_API inheritance_status zend_perform_covariant_type_check( + zend_class_entry *fe_scope, const zend_type *fe_type_ptr, + zend_class_entry *proto_scope, const zend_type *proto_type_ptr) { + const zend_type fe_type = *fe_type_ptr; + const zend_type proto_type = *proto_type_ptr; ZEND_ASSERT(ZEND_TYPE_IS_SET(fe_type) && ZEND_TYPE_IS_SET(proto_type)); /* Apart from void, everything is trivially covariant to the mixed type. @@ -684,6 +736,17 @@ ZEND_API inheritance_status zend_perform_covariant_type_check( return INHERITANCE_SUCCESS; } + /* If we check for concrete return type */ + if (ZEND_TYPE_IS_ASSOCIATED(proto_type)) { + return zend_is_type_subtype_of_associated_type( + fe_scope, fe_type_ptr, proto_scope, proto_type_ptr); + } + /* If we check for concrete parameter type */ + if (ZEND_TYPE_IS_ASSOCIATED(fe_type)) { + return zend_is_type_subtype_of_associated_type( + proto_scope, proto_type_ptr, fe_scope, fe_type_ptr); + } + /* Builtin types may be removed, but not added */ uint32_t fe_type_mask = ZEND_TYPE_PURE_MASK(fe_type); uint32_t proto_type_mask = ZEND_TYPE_PURE_MASK(proto_type); @@ -713,7 +776,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check( early_exit_status = ZEND_TYPE_IS_INTERSECTION(proto_type) ? INHERITANCE_ERROR : INHERITANCE_SUCCESS; inheritance_status status = zend_is_intersection_subtype_of_type( - fe_scope, fe_type, proto_scope, proto_type); + fe_scope, fe_type_ptr, proto_scope, proto_type_ptr); if (status == early_exit_status) { return status; @@ -733,7 +796,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check( /* Union has an intersection type as it's member */ if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { status = zend_is_intersection_subtype_of_type( - fe_scope, *single_type, proto_scope, proto_type); + fe_scope, single_type, proto_scope, proto_type_ptr); } else { zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type); if (!fe_class_name) { @@ -741,7 +804,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check( } status = zend_is_class_subtype_of_type( - fe_scope, fe_class_name, proto_scope, proto_type); + fe_scope, fe_class_name, proto_scope, proto_type_ptr); } if (status == early_exit_status) { @@ -763,7 +826,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check( } static inheritance_status zend_do_perform_arg_type_hint_check( - zend_class_entry *fe_scope, zend_arg_info *fe_arg_info, + zend_class_entry *fe_scope, const zend_arg_info *fe_arg_info, zend_class_entry *proto_scope, zend_arg_info *proto_arg_info) /* {{{ */ { if (!ZEND_TYPE_IS_SET(fe_arg_info->type) || ZEND_TYPE_PURE_MASK(fe_arg_info->type) == MAY_BE_ANY) { @@ -779,7 +842,7 @@ static inheritance_status zend_do_perform_arg_type_hint_check( /* Contravariant type check is performed as a covariant type check with swapped * argument order. */ return zend_perform_covariant_type_check( - proto_scope, proto_arg_info->type, fe_scope, fe_arg_info->type); + proto_scope, &proto_arg_info->type, fe_scope, &fe_arg_info->type); } /* }}} */ @@ -881,7 +944,7 @@ static inheritance_status zend_do_perform_implementation_check( } local_status = zend_perform_covariant_type_check( - fe_scope, fe->common.arg_info[-1].type, proto_scope, proto->common.arg_info[-1].type); + fe_scope, &fe->common.arg_info[-1].type, proto_scope, &proto->common.arg_info[-1].type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (local_status == INHERITANCE_ERROR @@ -1297,10 +1360,10 @@ static inheritance_status full_property_types_compatible( /* Perform a covariant type check in both directions to determined invariance. */ inheritance_status status1 = variance == PROP_CONTRAVARIANT ? INHERITANCE_SUCCESS : zend_perform_covariant_type_check( - child_info->ce, child_info->type, parent_info->ce, parent_info->type); + child_info->ce, &child_info->type, parent_info->ce, &parent_info->type); inheritance_status status2 = variance == PROP_COVARIANT ? INHERITANCE_SUCCESS : zend_perform_covariant_type_check( - parent_info->ce, parent_info->type, child_info->ce, child_info->type); + parent_info->ce, &parent_info->type, child_info->ce, &child_info->type); if (status1 == INHERITANCE_SUCCESS && status2 == INHERITANCE_SUCCESS) { return INHERITANCE_SUCCESS; } @@ -1357,7 +1420,7 @@ static inheritance_status verify_property_type_compatibility( && (!child_info->hooks || !child_info->hooks[ZEND_PROPERTY_HOOK_SET])) { zend_type set_type = parent_info->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type; inheritance_status result = zend_perform_covariant_type_check( - parent_info->ce, set_type, child_info->ce, child_info->type); + parent_info->ce, &set_type, child_info->ce, &child_info->type); if ((result == INHERITANCE_ERROR && throw_on_error) || (result == INHERITANCE_UNRESOLVED && throw_on_unresolved)) { emit_set_hook_type_error(child_info, parent_info); } @@ -1645,7 +1708,7 @@ static inheritance_status class_constant_types_compatible(const zend_class_const return INHERITANCE_ERROR; } - return zend_perform_covariant_type_check(child->ce, child->type, parent->ce, parent->type); + return zend_perform_covariant_type_check(child->ce, &child->type, parent->ce, &parent->type); } static bool do_inherit_constant_check( @@ -1791,7 +1854,7 @@ ZEND_API inheritance_status zend_verify_property_hook_variance(const zend_proper { ZEND_ASSERT(prop_info->hooks && prop_info->hooks[ZEND_PROPERTY_HOOK_SET] == func); - zend_arg_info *value_arg_info = &func->op_array.arg_info[0]; + const zend_arg_info *value_arg_info = &func->op_array.arg_info[0]; if (!ZEND_TYPE_IS_SET(value_arg_info->type)) { return INHERITANCE_SUCCESS; } @@ -1801,7 +1864,7 @@ ZEND_API inheritance_status zend_verify_property_hook_variance(const zend_proper } zend_class_entry *ce = prop_info->ce; - return zend_perform_covariant_type_check(ce, prop_info->type, ce, value_arg_info->type); + return zend_perform_covariant_type_check(ce, &prop_info->type, ce, &value_arg_info->type); } #ifdef ZEND_OPCACHE_SHM_REATTACHMENT @@ -2168,6 +2231,34 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry * ZEND_INHERITANCE_RESET_CHILD_OVERRIDE; } + if (iface->associated_types) { + const uint32_t num_associated_types = zend_hash_num_elements(iface->associated_types); + if (ce->ce_flags & ZEND_ACC_INTERFACE) { + const bool persistent = ce->type == ZEND_INTERNAL_CLASS; + if (ce->associated_types) { + zend_string *associated_type_name; + zend_type *associated_type_ptr; + ZEND_HASH_FOREACH_STR_KEY_PTR(iface->associated_types, associated_type_name, associated_type_ptr) { + if (zend_hash_exists(ce->associated_types, associated_type_name)) { + zend_error_noreturn(E_ERROR, + "Cannot redeclare associated type %s in interface %s inherited from interface %s", + ZSTR_VAL(associated_type_name), ZSTR_VAL(ce->name), ZSTR_VAL(iface->name)); + } + /* Deep copy the type information */ + zend_type_copy_ctor(associated_type_ptr, /* use_arena */ !persistent, /* persistent */ persistent); + zend_hash_add_new_mem(ce->associated_types, associated_type_name, associated_type_ptr, sizeof(*associated_type_ptr)); + } ZEND_HASH_FOREACH_END(); + } else { + ce->associated_types = pemalloc(sizeof(HashTable), persistent); + zend_hash_init(ce->associated_types, num_associated_types, NULL, NULL, false); + zend_hash_copy(ce->associated_types, iface->associated_types, NULL); + } + return; + } + HashTable *ht = emalloc(sizeof(HashTable)); + zend_hash_init(ht, num_associated_types, NULL, NULL, false); + CG(bound_associated_types) = ht; + } ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&iface->constants_table, key, c) { do_inherit_iface_constant(key, c, ce, iface); } ZEND_HASH_FOREACH_END(); @@ -2189,6 +2280,11 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry * if (iface->num_interfaces) { zend_do_inherit_interfaces(ce, iface); } + if (CG(bound_associated_types)) { + zend_hash_destroy(CG(bound_associated_types)); + efree(CG(bound_associated_types)); + CG(bound_associated_types) = NULL; + } } /* }}} */ @@ -2777,8 +2873,8 @@ static bool do_trait_constant_check( emit_incompatible_trait_constant_error(ce, existing_constant, trait_constant, name, traits, current_trait); return false; } else if (ZEND_TYPE_IS_SET(trait_constant->type)) { - inheritance_status status1 = zend_perform_covariant_type_check(ce, existing_constant->type, traits[current_trait], trait_constant->type); - inheritance_status status2 = zend_perform_covariant_type_check(traits[current_trait], trait_constant->type, ce, existing_constant->type); + inheritance_status status1 = zend_perform_covariant_type_check(ce, &existing_constant->type, traits[current_trait], &trait_constant->type); + inheritance_status status2 = zend_perform_covariant_type_check(traits[current_trait], &trait_constant->type, ce, &existing_constant->type); if (status1 == INHERITANCE_ERROR || status2 == INHERITANCE_ERROR) { emit_incompatible_trait_constant_error(ce, existing_constant, trait_constant, name, traits, current_trait); return false; diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 9483a83b4e955..bcbbe7415e923 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -167,6 +167,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_TRAIT "'trait'" %token T_INTERFACE "'interface'" %token T_ENUM "'enum'" +%token T_TYPE "'type'" %token T_EXTENDS "'extends'" %token T_IMPLEMENTS "'implements'" %token T_NAMESPACE "'namespace'" @@ -286,6 +287,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type function_name non_empty_member_modifiers %type property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body %type optional_parameter_list +%type associated_type %type returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers %type method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers @@ -661,6 +663,13 @@ enum_case_expr: | '=' expr { $$ = $2; } ; +associated_type: + T_TYPE name ':' type_expr_without_static ';' + { $$ = zend_ast_create(ZEND_AST_ASSOCIATED_TYPE, $2, $4); } + | T_TYPE name ';' + { $$ = zend_ast_create(ZEND_AST_ASSOCIATED_TYPE, $2, NULL); } +; + extends_from: %empty { $$ = NULL; } | T_EXTENDS class_name { $$ = $2; } @@ -962,6 +971,7 @@ attributed_class_statement: { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $12, $2, $5, zend_ast_get_str($4), $7, NULL, $11, $9, NULL); CG(extra_fn_flags) = $10; } | enum_case { $$ = $1; } + | associated_type { $$ = $1; } ; class_statement: diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 4c883b81c5f7d..abb816f83c5cb 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1545,6 +1545,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN_WITH_IDENT(T_INTERFACE); } +"type" { + RETURN_TOKEN_WITH_IDENT(T_TYPE); +} + "trait" { RETURN_TOKEN_WITH_IDENT(T_TRAIT); } diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index b25152ec1248b..d58c1fa6425b9 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -351,6 +351,10 @@ ZEND_API void destroy_zend_class(zval *zv) zend_hash_release(ce->attributes); } + if (ce->associated_types) { + zend_hash_release(ce->associated_types); + } + if (ce->num_interfaces > 0 && !(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)) { uint32_t i; @@ -527,6 +531,9 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->attributes) { zend_hash_release(ce->attributes); } + if (ce->associated_types) { + zend_hash_release(ce->associated_types); + } free(ce); break; } diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 7676a1d42a5f4..9046f4b3f4aed 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -142,14 +142,15 @@ typedef struct { zend_type types[1]; } zend_type_list; -#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 25 -#define _ZEND_TYPE_MASK ((1u << 25) - 1) +#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 26 +#define _ZEND_TYPE_MASK ((1u << 26) - 1) /* Only one of these bits may be set. */ +#define _ZEND_TYPE_ASSOCIATED_BIT (1u << 25) #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|_ZEND_TYPE_LITERAL_NAME_BIT) +#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT|_ZEND_TYPE_ASSOCIATED_BIT) /* For BC behaviour with iterable type */ #define _ZEND_TYPE_ITERABLE_BIT (1u << 21) /* Whether the type list is arena allocated */ @@ -167,7 +168,7 @@ typedef struct { (((t).type_mask & _ZEND_TYPE_MASK) != 0) /* If a type is complex it means it's either a list with a union or intersection, - * or the void pointer is a class name */ + * the void pointer is a class name, or the type is an associated type (which implies it is a name) */ #define ZEND_TYPE_IS_COMPLEX(t) \ ((((t).type_mask) & _ZEND_TYPE_KIND_MASK) != 0) @@ -180,6 +181,9 @@ typedef struct { #define ZEND_TYPE_HAS_LIST(t) \ ((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0) +#define ZEND_TYPE_IS_ASSOCIATED(t) \ + ((((t).type_mask) & _ZEND_TYPE_ASSOCIATED_BIT) != 0) + #define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \ ((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0) diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index a1e131032bcfb..c53ee96c6226b 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -105,6 +105,7 @@ char *get_token_type_name(int token_type) case T_TRAIT: return "T_TRAIT"; case T_INTERFACE: return "T_INTERFACE"; case T_ENUM: return "T_ENUM"; + case T_TYPE: return "T_TYPE"; case T_EXTENDS: return "T_EXTENDS"; case T_IMPLEMENTS: return "T_IMPLEMENTS"; case T_NAMESPACE: return "T_NAMESPACE"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index c1e1fd254dfaa..065453981f223 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -402,6 +402,11 @@ * @cvalue T_ENUM */ const T_ENUM = UNKNOWN; +/** + * @var int + * @cvalue T_TYPE + */ +const T_TYPE = UNKNOWN; /** * @var int * @cvalue T_EXTENDS diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index 9c488d19f1890..1571daf0cda58 100644 --- a/ext/tokenizer/tokenizer_data_arginfo.h +++ b/ext/tokenizer/tokenizer_data_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 19d25d22098f46283b517352cbb302db962b50fd */ + * Stub hash: ba2791ef99a630b81f49a3251f3824d7d4858176 */ static void register_tokenizer_data_symbols(int module_number) { @@ -83,6 +83,7 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_TRAIT", T_TRAIT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_INTERFACE", T_INTERFACE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_ENUM", T_ENUM, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_TYPE", T_TYPE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_EXTENDS", T_EXTENDS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_IMPLEMENTS", T_IMPLEMENTS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_NAMESPACE", T_NAMESPACE, CONST_PERSISTENT);