From b373a1f6ea21157d1efaec4fd3b4fd7cecaff766 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 4 May 2026 21:14:46 +0100 Subject: [PATCH 1/4] Do not emit ZEND_VERIFY_RETURN_TYPE if we return $this and return value is static --- Zend/zend_compile.c | 11 +++++++ ...de_static_verify_return_type_for_this.phpt | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 ext/opcache/tests/opt/elide_static_verify_return_type_for_this.phpt diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index a96af71aa900..0d0ad47d2cf2 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2696,6 +2696,17 @@ static void zend_emit_return_type_check( /* we don't need run-time check */ return; } + + /* If return type contains static and we are returning $this + * (determined by checking if the previous opcode is ZEND_FETCH_THIS) + * then we don't need to check the return type */ + if (expr && ZEND_TYPE_CONTAINS_CODE(type, IS_STATIC)) { + const zend_op_array *op_array = CG(active_op_array); + zend_op previous = op_array->opcodes[op_array->last-1]; + if (previous.opcode == ZEND_FETCH_THIS) { + return; + } + } opline = zend_emit_op(NULL, ZEND_VERIFY_RETURN_TYPE, expr, NULL); if (expr && expr->op_type == IS_CONST) { diff --git a/ext/opcache/tests/opt/elide_static_verify_return_type_for_this.phpt b/ext/opcache/tests/opt/elide_static_verify_return_type_for_this.phpt new file mode 100644 index 000000000000..05c62646854a --- /dev/null +++ b/ext/opcache/tests/opt/elide_static_verify_return_type_for_this.phpt @@ -0,0 +1,33 @@ +--TEST-- +Return type check elision +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.opt_debug_level=0x20000 +opcache.preload= +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECTF-- +$_main: + ; (lines=1, args=0, vars=0, tmps=0) + ; (after optimizer) + ; %s:1-10 +0000 RETURN int(1) + +C::returnStatic: + ; (lines=2, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s:4-6 +0000 T0 = FETCH_THIS +0001 RETURN T0 From bf784bd01866a6725afaecd7e4357dc0c08da982 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 5 May 2026 00:39:24 +0100 Subject: [PATCH 2/4] Elide ZEND_VERIFY_RETURN_TYPE for directly provable instances of $this --- Zend/zend_compile.c | 59 +++++++++-- ...lide_self_verify_return_type_for_this.phpt | 100 ++++++++++++++++++ 2 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 ext/opcache/tests/opt/elide_self_verify_return_type_for_this.phpt diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0d0ad47d2cf2..6add78a1ceee 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2642,6 +2642,53 @@ static void zend_compile_memoized_expr(znode *result, zend_ast *expr, uint32_t t } /* }}} */ +static bool zend_is_this_instance_of_name(const zend_string *type_name) +{ + if (zend_string_equals_ci(CG(active_class_entry)->name, type_name)) { + return true; + } + if (zend_string_equals_ci(type_name, ZSTR_KNOWN(ZEND_STR_SELF))) { + return true; + } + if (zend_string_equals_ci(type_name, ZSTR_KNOWN(ZEND_STR_PARENT))) { + return true; + } + + ZEND_ASSERT((CG(active_class_entry)->ce_flags & ZEND_ACC_LINKED) == 0); + if (CG(active_class_entry)->num_interfaces) { + for (uint32_t i = 0; i < CG(active_class_entry)->num_interfaces; i++) { + if (zend_string_equals_ci(CG(active_class_entry)->interface_names[i].lc_name, type_name)) { + return true; + } + } + } + const zend_string *parent_name = CG(active_class_entry)->parent_name; + if (parent_name && zend_string_equals_ci(parent_name, type_name)) { + return true; + } + + return false; +} + +static bool zend_is_this_valid_for_return_type(zend_type type) +{ + if (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_OBJECT|MAY_BE_STATIC)) { + return true; + } + + const zend_type *single_type; + ZEND_TYPE_FOREACH(type, single_type) { + if (ZEND_TYPE_HAS_NAME(*single_type)) { + const zend_string *name = ZEND_TYPE_NAME(*single_type); + if (zend_is_this_instance_of_name(name)) { + return true; + } + } + } ZEND_TYPE_FOREACH_END(); + + return false; +} + static void zend_emit_return_type_check( znode *expr, const zend_arg_info *return_info, bool implicit) /* {{{ */ { @@ -2696,16 +2743,14 @@ static void zend_emit_return_type_check( /* we don't need run-time check */ return; } - + /* If return type contains static and we are returning $this * (determined by checking if the previous opcode is ZEND_FETCH_THIS) * then we don't need to check the return type */ - if (expr && ZEND_TYPE_CONTAINS_CODE(type, IS_STATIC)) { - const zend_op_array *op_array = CG(active_op_array); - zend_op previous = op_array->opcodes[op_array->last-1]; - if (previous.opcode == ZEND_FETCH_THIS) { - return; - } + const zend_op_array *op_array = CG(active_op_array); + zend_op previous = op_array->opcodes[op_array->last-1]; + if (expr && previous.opcode == ZEND_FETCH_THIS && zend_is_this_valid_for_return_type(type)) { + return; } opline = zend_emit_op(NULL, ZEND_VERIFY_RETURN_TYPE, expr, NULL); diff --git a/ext/opcache/tests/opt/elide_self_verify_return_type_for_this.phpt b/ext/opcache/tests/opt/elide_self_verify_return_type_for_this.phpt new file mode 100644 index 000000000000..70971cd84ffe --- /dev/null +++ b/ext/opcache/tests/opt/elide_self_verify_return_type_for_this.phpt @@ -0,0 +1,100 @@ +--TEST-- +Return type check elision +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.opt_debug_level=0x20000 +opcache.preload= +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECTF-- +$_main: + ; (lines=5, args=0, vars=0, tmps=0) + ; (after optimizer) + ; %s:1-39 +0000 DECLARE_CLASS string("c4") +0001 DECLARE_CLASS string("i2") +0002 DECLARE_CLASS string("c5") +0003 DECLARE_CLASS_DELAYED string("c6") string("c5") +0004 RETURN int(1) + +C1::foo: + ; (lines=2, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s:4-6 +0000 T0 = FETCH_THIS +0001 RETURN T0 + +C3::foo: + ; (lines=2, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s:11-13 +0000 T0 = FETCH_THIS +0001 RETURN T0 + +C4::foo: + ; (lines=2, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s:19-21 +0000 T0 = FETCH_THIS +0001 RETURN T0 + +C5::foo: + ; (lines=3, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s:27-29 +0000 T0 = FETCH_THIS +0001 VERIFY_RETURN_TYPE T0 +0002 RETURN T0 +LIVE RANGES: + 0: 0001 - 0002 (tmp/var) + +C6::foo: + ; (lines=3, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s:33-35 +0000 T0 = FETCH_THIS +0001 VERIFY_RETURN_TYPE T0 +0002 RETURN T0 +LIVE RANGES: + 0: 0001 - 0002 (tmp/var) From ccf90d5bf932b21991402d9729117f0a12561750 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 5 May 2026 01:33:44 +0100 Subject: [PATCH 3/4] Fix opcode check --- Zend/zend_compile.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6add78a1ceee..ef97cb35d279 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2748,8 +2748,9 @@ static void zend_emit_return_type_check( * (determined by checking if the previous opcode is ZEND_FETCH_THIS) * then we don't need to check the return type */ const zend_op_array *op_array = CG(active_op_array); - zend_op previous = op_array->opcodes[op_array->last-1]; - if (expr && previous.opcode == ZEND_FETCH_THIS && zend_is_this_valid_for_return_type(type)) { + if (expr && op_array->last >= 1 + && op_array->opcodes[op_array->last-1].opcode == ZEND_FETCH_THIS + && zend_is_this_valid_for_return_type(type)) { return; } From cf008aa2ead77fa92e9ff6dfd5f6c7ead0001edd Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 5 May 2026 02:06:27 +0100 Subject: [PATCH 4/4] Fix unbound closure --- Zend/tests/return_types/025_2.phpt | 14 ++++++++++++++ Zend/zend_compile.c | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 Zend/tests/return_types/025_2.phpt diff --git a/Zend/tests/return_types/025_2.phpt b/Zend/tests/return_types/025_2.phpt new file mode 100644 index 000000000000..50683c5c1267 --- /dev/null +++ b/Zend/tests/return_types/025_2.phpt @@ -0,0 +1,14 @@ +--TEST-- +Return type of self is allowed in closure but $this return value must be checked as closure might not be bound to a class +--FILE-- +getMessage(), PHP_EOL; +} +?> +--EXPECT-- +Error: Using $this when not in object context diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ef97cb35d279..b0adeeed3366 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2672,6 +2672,11 @@ static bool zend_is_this_instance_of_name(const zend_string *type_name) static bool zend_is_this_valid_for_return_type(zend_type type) { + /* Closures can be bound to a class scope, however it might not and this must type error */ + if (CG(active_op_array)->fn_flags & ZEND_ACC_CLOSURE) { + return false; + } + if (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_OBJECT|MAY_BE_STATIC)) { return true; }