From d8e1e1939e815f19db19b7aee7e74b54695be8ce Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 5 Apr 2025 17:07:11 +0100 Subject: [PATCH 01/11] Zend: Use pointer to zend_type for variance checks --- Zend/zend_inheritance.c | 58 +++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 090b1049418d..15497a84c71d 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,11 @@ 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) { + 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. @@ -713,7 +719,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 +739,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 +747,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 +769,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 +785,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 +887,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 +1303,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 +1363,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 +1651,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 +1797,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 +1807,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 @@ -2777,8 +2783,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; From 5644090e2ce4e487b02fda6e25cd557d9385377c Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 5 Apr 2025 00:54:54 +0100 Subject: [PATCH 02/11] Add T_TYPE token --- .../associated/associated_001.phpt | 17 +++++++++++++++++ Zend/zend_language_parser.y | 1 + Zend/zend_language_scanner.l | 4 ++++ ext/tokenizer/tokenizer_data.c | 1 + ext/tokenizer/tokenizer_data.stub.php | 5 +++++ ext/tokenizer/tokenizer_data_arginfo.h | 3 ++- 6 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/type_declarations/associated/associated_001.phpt 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 000000000000..47a5a7b23604 --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_001.phpt @@ -0,0 +1,17 @@ +--TEST-- +Associated types basic +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected token "type", expecting "function" in %s on line %d diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 9483a83b4e95..5a315498f449 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'" diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 4c883b81c5f7..abb816f83c5c 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/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index a1e131032bcf..c53ee96c6226 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 c1e1fd254dfa..065453981f22 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 9c488d19f189..1571daf0cda5 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); From bcbda7ea7cee3923dad3e6f5a3acaf43f2e38c05 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 5 Apr 2025 01:36:21 +0100 Subject: [PATCH 03/11] Support minimal compilation of associated type --- .../associated/associated_001.phpt | 2 +- .../associated/associated_type_in_class.phpt | 13 +++++++++++++ .../associated/associated_type_in_trait.phpt | 13 +++++++++++++ .../associated/repeated_associated_type.phpt | 17 +++++++++++++++++ Zend/zend_ast.c | 4 ++++ Zend/zend_ast.h | 1 + Zend/zend_compile.c | 17 +++++++++++++++++ Zend/zend_language_parser.y | 7 +++++++ 8 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/type_declarations/associated/associated_type_in_class.phpt create mode 100644 Zend/tests/type_declarations/associated/associated_type_in_trait.phpt create mode 100644 Zend/tests/type_declarations/associated/repeated_associated_type.phpt diff --git a/Zend/tests/type_declarations/associated/associated_001.phpt b/Zend/tests/type_declarations/associated/associated_001.phpt index 47a5a7b23604..395ca814df84 100644 --- a/Zend/tests/type_declarations/associated/associated_001.phpt +++ b/Zend/tests/type_declarations/associated/associated_001.phpt @@ -14,4 +14,4 @@ class C implements I { ?> --EXPECTF-- -Parse error: syntax error, unexpected token "type", expecting "function" in %s on line %d +Fatal error: Declaration of C::foo(string $param): string must be compatible with I::foo(T $param): T 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 000000000000..d767621409d6 --- /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 000000000000..6e944f47fcdc --- /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/repeated_associated_type.phpt b/Zend/tests/type_declarations/associated/repeated_associated_type.phpt new file mode 100644 index 000000000000..47a5a7b23604 --- /dev/null +++ b/Zend/tests/type_declarations/associated/repeated_associated_type.phpt @@ -0,0 +1,17 @@ +--TEST-- +Associated types basic +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected token "type", expecting "function" in %s on line %d diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 2551f876d446..2c6f19d0f25d 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2342,6 +2342,10 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio APPEND_NODE_1("break"); case ZEND_AST_CONTINUE: APPEND_NODE_1("continue"); + case ZEND_AST_ASSOCIATED_TYPE: + smart_str_appends(str, "type "); + zend_ast_export_name(str, ast->child[0], 0, indent); + break; /* 2 child nodes */ case ZEND_AST_DIM: diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 9348c35f6cc0..cba4782c9bec 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -99,6 +99,7 @@ enum _zend_ast_kind { ZEND_AST_POST_DEC, ZEND_AST_YIELD_FROM, ZEND_AST_CLASS_NAME, + ZEND_AST_ASSOCIATED_TYPE, ZEND_AST_GLOBAL, ZEND_AST_UNSET, diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3fa4c3959cb4..531c01aa4aa1 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9020,6 +9020,20 @@ static void zend_compile_use_trait(zend_ast *ast) /* {{{ */ } /* }}} */ +static void zend_compile_associated_type(zend_ast *ast) { + zend_class_entry *ce = CG(active_class_entry); + + 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_ast *name_ast = ast->child[0]; + zend_string *name = zend_ast_get_str(name_ast); + ZEND_ASSERT(name != NULL); + // TODO add associated type to CE +} + static void zend_compile_implements(zend_ast *ast) /* {{{ */ { zend_ast_list *list = zend_ast_get_list(ast); @@ -11586,6 +11600,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_language_parser.y b/Zend/zend_language_parser.y index 5a315498f449..809210e3601d 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -287,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 @@ -662,6 +663,11 @@ enum_case_expr: | '=' expr { $$ = $2; } ; +associated_type: + T_TYPE name ';' + { $$ = zend_ast_create(ZEND_AST_ASSOCIATED_TYPE, $2); } +; + extends_from: %empty { $$ = NULL; } | T_EXTENDS class_name { $$ = $2; } @@ -963,6 +969,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: From 79ee6e896878173abe85fd6584318df82508cf1d Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 5 Apr 2025 02:16:15 +0100 Subject: [PATCH 04/11] Associate type to CE and minimal duplicate check --- .../associated/multiple_associated_type.phpt | 20 +++++++++++++++++ .../associated/repeated_associated_type.phpt | 5 +++-- Zend/zend.h | 3 +++ Zend/zend_compile.c | 22 ++++++++++++++++--- Zend/zend_opcode.c | 7 ++++++ 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 Zend/tests/type_declarations/associated/multiple_associated_type.phpt 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 000000000000..c3d7e7fa8c1b --- /dev/null +++ b/Zend/tests/type_declarations/associated/multiple_associated_type.phpt @@ -0,0 +1,20 @@ +--TEST-- +Multiple associated types +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of C::set(int $key, string $value): void must be compatible with I::set(K $key, V $value): void 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 index 47a5a7b23604..be65c60ef776 100644 --- a/Zend/tests/type_declarations/associated/repeated_associated_type.phpt +++ b/Zend/tests/type_declarations/associated/repeated_associated_type.phpt @@ -1,9 +1,10 @@ --TEST-- -Associated types basic +Repeated associated type --FILE-- --EXPECTF-- -Parse error: syntax error, unexpected token "type", expecting "function" in %s on line %d +Fatal error: Cannot have two associated types with the same name "T" in %s on line %d diff --git a/Zend/zend.h b/Zend/zend.h index 0cf1faeb653f..cff81284740c 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_compile.c b/Zend/zend_compile.c index 531c01aa4aa1..ab90800358a4 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2072,6 +2072,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; @@ -9020,18 +9021,33 @@ static void zend_compile_use_trait(zend_ast *ast) /* {{{ */ } /* }}} */ + + 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_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_ast *name_ast = ast->child[0]; - zend_string *name = zend_ast_get_str(name_ast); ZEND_ASSERT(name != NULL); - // TODO add associated type to CE + 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, NULL, 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)); + } + zval tmp; + ZVAL_UNDEF(&tmp); + zend_hash_add_new(associated_types, name, &tmp); } static void zend_compile_implements(zend_ast *ast) /* {{{ */ diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index b25152ec1248..d58c1fa6425b 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; } From 05199272ef799f591cf48575680c80d5f9e03ccb Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 5 Apr 2025 03:45:25 +0100 Subject: [PATCH 05/11] Prevent associated type from being in a union or intersection type --- ...iated_type_cannot_be_in_intersection1.phpt | 13 ++++++++++++ ...iated_type_cannot_be_in_intersection2.phpt | 13 ++++++++++++ .../associated_type_cannot_be_in_union1.phpt | 13 ++++++++++++ .../associated_type_cannot_be_in_union2.phpt | 13 ++++++++++++ .../associated_type_cannot_be_in_union3.phpt | 13 ++++++++++++ Zend/zend_compile.c | 20 +++++++++++++++---- Zend/zend_types.h | 12 +++++++---- 7 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection1.phpt create mode 100644 Zend/tests/type_declarations/associated/associated_type_cannot_be_in_intersection2.phpt create mode 100644 Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union1.phpt create mode 100644 Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union2.phpt create mode 100644 Zend/tests/type_declarations/associated/associated_type_cannot_be_in_union3.phpt 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 000000000000..0ea969b68c49 --- /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 000000000000..f44dab1a1e7f --- /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 000000000000..7a4e1809a4d5 --- /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 000000000000..904f428d0992 --- /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 000000000000..a40109bb5ebe --- /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/zend_compile.c b/Zend/zend_compile.c index ab90800358a4..f39ebe3c38b5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6966,9 +6966,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"); } @@ -6998,10 +7000,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); @@ -7009,14 +7015,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"); } } @@ -7044,7 +7050,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); } } } @@ -7193,6 +7199,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"); } @@ -7275,6 +7284,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)) { diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 7676a1d42a5f..9046f4b3f4ae 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) From a529f5b47c494b3ab100d5084bd8fe91aea7a066 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 5 Apr 2025 04:14:03 +0100 Subject: [PATCH 06/11] Support invariant bound types for associated types --- .../associated/associated_001.phpt | 23 +++++-- .../associated/multiple_associated_type.phpt | 62 +++++++++++++++++-- Zend/zend.c | 1 + Zend/zend_compile.c | 2 + Zend/zend_globals.h | 2 + Zend/zend_inheritance.c | 57 +++++++++++++++++ 6 files changed, 139 insertions(+), 8 deletions(-) diff --git a/Zend/tests/type_declarations/associated/associated_001.phpt b/Zend/tests/type_declarations/associated/associated_001.phpt index 395ca814df84..f3b6d6cafaa8 100644 --- a/Zend/tests/type_declarations/associated/associated_001.phpt +++ b/Zend/tests/type_declarations/associated/associated_001.phpt @@ -8,10 +8,25 @@ interface I { public function foo(T $param): T; } -class C implements I { - public function foo(string $param): string {} +class CS implements I { + public function foo(string $param): string { + return $param . '!'; + } } +class CI implements I { + public function foo(int $param): int { + return $param + 42; + } +} + +$cs = new CS(); +var_dump($cs->foo("Hello")); + +$ci = new CI(); +var_dump($ci->foo(5)); + ?> ---EXPECTF-- -Fatal error: Declaration of C::foo(string $param): string must be compatible with I::foo(T $param): T in %s on line %d +--EXPECT-- +string(6) "Hello!" +int(47) diff --git a/Zend/tests/type_declarations/associated/multiple_associated_type.phpt b/Zend/tests/type_declarations/associated/multiple_associated_type.phpt index c3d7e7fa8c1b..aae8ca4dda87 100644 --- a/Zend/tests/type_declarations/associated/multiple_associated_type.phpt +++ b/Zend/tests/type_declarations/associated/multiple_associated_type.phpt @@ -10,11 +10,65 @@ interface I { public function get(K $key): V; } -class C implements I { - public function set(int $key, string $value): void {} - public function get(int $key): string {} +class C1 implements I { + public array $a = []; + public function set(int $key, string $value): void { + $this->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-- -Fatal error: Declaration of C::set(int $key, string $value): void must be compatible with I::set(K $key, V $value): void in %s on line %d +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/zend.c b/Zend/zend.c index 2d8a0f455f8b..edec7ba67563 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_compile.c b/Zend/zend_compile.c index f39ebe3c38b5..896df97db4c5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -463,6 +463,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 +492,7 @@ void shutdown_compiler(void) /* {{{ */ CG(unlinked_uses) = NULL; } CG(current_linking_class) = NULL; + ZEND_ASSERT(CG(bound_associated_types) == NULL); } /* }}} */ diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 079bfb99cacc..dc1a06f52c53 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 15497a84c71d..f70eb2b72281 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -675,6 +675,42 @@ static inheritance_status zend_is_intersection_subtype_of_type( return early_exit_status == INHERITANCE_ERROR ? INHERITANCE_SUCCESS : INHERITANCE_ERROR; } +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); + +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) { + /* 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) @@ -690,6 +726,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); @@ -2174,6 +2221,11 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry * ZEND_INHERITANCE_RESET_CHILD_OVERRIDE; } + if (iface->associated_types) { + HashTable *ht = emalloc(sizeof(HashTable)); + zend_hash_init(ht, zend_hash_num_elements(iface->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(); @@ -2195,6 +2247,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; + } } /* }}} */ From 4e745195f482acbb85674f2105cfcf8d157531a3 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 6 Apr 2025 15:52:37 +0100 Subject: [PATCH 07/11] Add true global for mixed type --- Zend/zend_compile.c | 2 ++ Zend/zend_compile.h | 1 + 2 files changed, 3 insertions(+) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 896df97db4c5..1fe74c2e89a3 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); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index db087bdd6003..1397bec88bcc 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 From 339709e14f761d2cc18330c056a54440acb025a8 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 6 Apr 2025 16:41:35 +0100 Subject: [PATCH 08/11] Store zend_type ptr in associated types HT --- Zend/zend_compile.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 1fe74c2e89a3..2ff942dfdcb6 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9037,7 +9037,10 @@ 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? +} static void zend_compile_associated_type(zend_ast *ast) { zend_class_entry *ce = CG(active_class_entry); @@ -9054,16 +9057,15 @@ static void zend_compile_associated_type(zend_ast *ast) { 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, NULL, 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)); } - zval tmp; - ZVAL_UNDEF(&tmp); - zend_hash_add_new(associated_types, name, &tmp); + + zend_hash_add_new_ptr(associated_types, name, (void*) &zend_mixed_type); } static void zend_compile_implements(zend_ast *ast) /* {{{ */ From 4c5a19a3737dc3a14fc7af3ea6a15147c2b7ea1a Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 7 Apr 2025 03:16:26 +0100 Subject: [PATCH 09/11] Add parser support for constraint type --- .../associated_type_with_constraint.phpt | 32 +++++++++++++++++++ ...ssociated_type_with_constraint_failed.phpt | 16 ++++++++++ Zend/zend_ast.c | 14 +++++--- Zend/zend_ast.h | 2 +- Zend/zend_language_parser.y | 6 ++-- 5 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 Zend/tests/type_declarations/associated/associated_type_with_constraint.phpt create mode 100644 Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt 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 000000000000..9e855edd7edb --- /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 000000000000..eae1adbe259e --- /dev/null +++ b/Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt @@ -0,0 +1,16 @@ +--TEST-- +Associated type with a constraint that is not satisfied +--FILE-- + +--EXPECT-- diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 2c6f19d0f25d..1eab1cbcc60a 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2342,10 +2342,6 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio APPEND_NODE_1("break"); case ZEND_AST_CONTINUE: APPEND_NODE_1("continue"); - case ZEND_AST_ASSOCIATED_TYPE: - smart_str_appends(str, "type "); - zend_ast_export_name(str, ast->child[0], 0, indent); - break; /* 2 child nodes */ case ZEND_AST_DIM: @@ -2668,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 cba4782c9bec..81f86f0623f8 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -99,7 +99,6 @@ enum _zend_ast_kind { ZEND_AST_POST_DEC, ZEND_AST_YIELD_FROM, ZEND_AST_CLASS_NAME, - ZEND_AST_ASSOCIATED_TYPE, ZEND_AST_GLOBAL, ZEND_AST_UNSET, @@ -155,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_language_parser.y b/Zend/zend_language_parser.y index 809210e3601d..bcbbe7415e92 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -664,8 +664,10 @@ enum_case_expr: ; associated_type: - T_TYPE name ';' - { $$ = zend_ast_create(ZEND_AST_ASSOCIATED_TYPE, $2); } + 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: From e9c28b11bcf5693e0c242bc290f959bd7a8417a2 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 7 Apr 2025 04:04:20 +0100 Subject: [PATCH 10/11] Add variance checking for constrained associated type --- ...ssociated_type_with_constraint_failed.phpt | 3 +- Zend/zend_compile.c | 42 ++++++++++++++++++- Zend/zend_inheritance.c | 10 +++++ 3 files changed, 52 insertions(+), 3 deletions(-) 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 index eae1adbe259e..50f81abcbe83 100644 --- a/Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt +++ b/Zend/tests/type_declarations/associated/associated_type_with_constraint_failed.phpt @@ -13,4 +13,5 @@ class C implements I { } ?> ---EXPECT-- +--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/zend_compile.c b/Zend/zend_compile.c index 2ff942dfdcb6..94dd993cc3f9 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -494,7 +494,12 @@ void shutdown_compiler(void) /* {{{ */ CG(unlinked_uses) = NULL; } CG(current_linking_class) = NULL; - ZEND_ASSERT(CG(bound_associated_types) == 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; + } } /* }}} */ @@ -1436,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; @@ -1459,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); } @@ -9040,12 +9067,18 @@ 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) { @@ -9065,7 +9098,12 @@ static void zend_compile_associated_type(zend_ast *ast) { "Cannot have two associated types with the same name \"%s\"", ZSTR_VAL(name)); } - zend_hash_add_new_ptr(associated_types, name, (void*) &zend_mixed_type); + 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) /* {{{ */ diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index f70eb2b72281..7118fc8d42c8 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -693,6 +693,16 @@ static inheritance_status zend_is_type_subtype_of_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; From cc039c9c5f613c5d6f56a4dfed98f2bac2bbc711 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 7 Apr 2025 21:18:08 +0100 Subject: [PATCH 11/11] Add basic support for extending interfaces with associated type --- ..._interface_associated_type_redeclared.phpt | 23 ++++++++++++++++ .../extended_interface_associated_types.phpt | 22 ++++++++++++++++ ...tended_interface_new_associated_types.phpt | 26 +++++++++++++++++++ Zend/zend_inheritance.c | 25 +++++++++++++++++- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/type_declarations/associated/extended_interface_associated_type_redeclared.phpt create mode 100644 Zend/tests/type_declarations/associated/extended_interface_associated_types.phpt create mode 100644 Zend/tests/type_declarations/associated/extended_interface_new_associated_types.phpt 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 000000000000..7501eed0e912 --- /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 000000000000..107df2e23ed5 --- /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 000000000000..65a8f6f51576 --- /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/zend_inheritance.c b/Zend/zend_inheritance.c index 7118fc8d42c8..6cee659eab9b 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2232,8 +2232,31 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry * } 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, zend_hash_num_elements(iface->associated_types), NULL, NULL, false); + 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) {