Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions Zend/Optimizer/dfa_pass.c
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,49 @@ static void zend_ssa_remove_nops(zend_op_array *op_array, zend_ssa *ssa, zend_op
free_alloca(shiftlist, use_heap);
}

static bool safe_instanceof(const zend_class_entry *ce1, const zend_class_entry *ce2) {
static bool safe_instanceof(
const zend_class_entry *ce1,
const zend_class_entry *ce2,
const zend_script *script,
const zend_op_array *op_array
) {
if (ce1 == ce2) {
return true;
}
if (!(ce1->ce_flags & ZEND_ACC_LINKED)) {
/* This case could be generalized, similarly to unlinked_instanceof */
return false;
if (ce1->ce_flags & ZEND_ACC_LINKED) {
return instanceof_function(ce1, ce2);
}

/* TODO Handle unlinked parents ike in unlinked_instanceof()? */

if (ce1->num_interfaces) {
uint32_t i;
if (ce1->ce_flags & ZEND_ACC_RESOLVED_INTERFACES) {
/* Unlike the normal instanceof_function(), we have to perform a recursive
* check here, as the parent interfaces might not have been fully copied yet. */
for (i = 0; i < ce1->num_interfaces; i++) {
if (safe_instanceof(ce1->interfaces[i], ce2, script, op_array)) {
return true;
}
}
Comment on lines +272 to +279
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to test we hit this case? As the tests I added don't.

} else {
for (i = 0; i < ce1->num_interfaces; i++) {
const zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, ce1->interface_names[i].lc_name);
if (!ce) {
continue;
}
/* Avoid recursing if class implements itself. */
if (ce == ce1) {
continue;
}
if (safe_instanceof(ce, ce2, script, op_array)) {
return true;
}
}
}
}
return instanceof_function(ce1, ce2);

return false;
}

static inline bool can_elide_list_type(
Expand All @@ -280,7 +314,7 @@ static inline bool can_elide_list_type(
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
const zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
zend_string_release(lcname);
bool result = ce && safe_instanceof(use_info->ce, ce);
bool result = ce && safe_instanceof(use_info->ce, ce, script, op_array);
if (result == !is_intersection) {
return result;
}
Expand Down
14 changes: 14 additions & 0 deletions Zend/tests/return_types/025_2.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php

$c = function(): self { return $this; };
try {
var_dump($c());
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
Error: Using $this when not in object context
63 changes: 63 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -2642,6 +2642,58 @@ 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)
{
/* 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;
}

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) /* {{{ */
{
Expand Down Expand Up @@ -2697,6 +2749,17 @@ static void zend_emit_return_type_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 */
const zend_op_array *op_array = CG(active_op_array);
if (expr && op_array->last >= 1
&& op_array->opcodes[op_array->last-1].opcode == ZEND_FETCH_THIS
Copy link
Copy Markdown
Member

@iluuu1994 iluuu1994 May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably check that the last opline is the right one, i.e. compare vars of expr and result var of the last opline. Maybe this is guaranteed, but then an assert would at least be useful.

&& zend_is_this_valid_for_return_type(type)) {
ZEND_ASSERT(expr->u.op.opline_num == op_array->last-1);
return;
}

opline = zend_emit_op(NULL, ZEND_VERIFY_RETURN_TYPE, expr, NULL);
if (expr && expr->op_type == IS_CONST) {
opline->result_type = expr->op_type = IS_TMP_VAR;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--TEST--
Return type check elision for direct interface return type and $this in class method when interface extends another one
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.opt_debug_level=0x30000
--EXTENSIONS--
opcache
--FILE--
<?php

interface I1 {}
interface I2 extends I1 {}

class C implements I2 {
public function foo(): I2 {
return $this;
}
}

?>
--EXPECTF--
$_main:
; (lines=3, args=0, vars=0, tmps=0)
; (before optimizer)
; %s:1-13
; return [] RANGE[0..0]
0000 DECLARE_CLASS string("i2")
0001 DECLARE_CLASS string("c")
0002 RETURN int(1)

C::foo:
; (lines=4, args=0, vars=0, tmps=1)
; (before optimizer)
; %s:7-9
; return [] RANGE[0..0]
0000 T0 = FETCH_THIS
0001 RETURN T0
0002 VERIFY_RETURN_TYPE
0003 RETURN null

$_main:
; (lines=3, args=0, vars=0, tmps=0)
; (after optimizer)
; %s:1-13
0000 DECLARE_CLASS string("i2")
0001 DECLARE_CLASS string("c")
0002 RETURN int(1)

C::foo:
; (lines=2, args=0, vars=0, tmps=1)
; (after optimizer)
; %s:7-9
0000 T0 = FETCH_THIS
0001 RETURN T0
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
Return type check elision for direct interface return type and $this in class method
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.opt_debug_level=0x30000
--EXTENSIONS--
opcache
--FILE--
<?php

interface I1 {}

class C implements I1 {
public function foo(): I1 {
return $this;
}
}

?>
--EXPECTF--
$_main:
; (lines=2, args=0, vars=0, tmps=0)
; (before optimizer)
; %s:1-12
; return [] RANGE[0..0]
0000 DECLARE_CLASS string("c")
0001 RETURN int(1)

C::foo:
; (lines=4, args=0, vars=0, tmps=1)
; (before optimizer)
; %s:6-8
; return [] RANGE[0..0]
0000 T0 = FETCH_THIS
0001 RETURN T0
0002 VERIFY_RETURN_TYPE
0003 RETURN null

$_main:
; (lines=2, args=0, vars=0, tmps=0)
; (after optimizer)
; %s:1-12
0000 DECLARE_CLASS string("c")
0001 RETURN int(1)

C::foo:
; (lines=2, args=0, vars=0, tmps=1)
; (after optimizer)
; %s:6-8
0000 T0 = FETCH_THIS
0001 RETURN T0
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--TEST--
Return type check elision for inherited interface return type and $this in class method
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.opt_debug_level=0x30000
--EXTENSIONS--
opcache
--FILE--
<?php
interface I1 {}
interface I2 extends I1 {}

class C implements I2 {
public function foo(): I1 {
return $this;
}
}

?>
--EXPECTF--
$_main:
; (lines=3, args=0, vars=0, tmps=0)
; (before optimizer)
; /home/girgias/Dev/php-src/ext/opcache/tests/opt/verify_return_type/inherited_interface_verify_return_type_for_this_class.php:1-12
; return [] RANGE[0..0]
0000 DECLARE_CLASS string("i2")
0001 DECLARE_CLASS string("c")
0002 RETURN int(1)

C::foo:
; (lines=5, args=0, vars=0, tmps=1)
; (before optimizer)
; /home/girgias/Dev/php-src/ext/opcache/tests/opt/verify_return_type/inherited_interface_verify_return_type_for_this_class.php:6-8
; return [] RANGE[0..0]
0000 T0 = FETCH_THIS
0001 VERIFY_RETURN_TYPE T0
0002 RETURN T0
0003 VERIFY_RETURN_TYPE
0004 RETURN null
LIVE RANGES:
0: 0001 - 0002 (tmp/var)

$_main:
; (lines=3, args=0, vars=0, tmps=0)
; (after optimizer)
; /home/girgias/Dev/php-src/ext/opcache/tests/opt/verify_return_type/inherited_interface_verify_return_type_for_this_class.php:1-12
0000 DECLARE_CLASS string("i2")
0001 DECLARE_CLASS string("c")
0002 RETURN int(1)

C::foo:
; (lines=3, args=0, vars=0, tmps=1)
; (after optimizer)
; /home/girgias/Dev/php-src/ext/opcache/tests/opt/verify_return_type/inherited_interface_verify_return_type_for_this_class.php:6-8
0000 T0 = FETCH_THIS
0001 VERIFY_RETURN_TYPE T0
0002 RETURN T0
LIVE RANGES:
0: 0001 - 0002 (tmp/var)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--TEST--
Return type check elision for inherited interface via class extension return type and $this in class method
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.opt_debug_level=0x30000
--EXTENSIONS--
opcache
--FILE--
<?php
interface I1 {}
interface I2 extends I1 {}

class C1 implements I2 {}

class C2 extends C1 {
public function foo(): I2 {
return $this;
}
}

?>
--EXPECTF--
$_main:
; (lines=4, args=0, vars=0, tmps=0)
; (before optimizer)
; %s:1-14
; return [] RANGE[0..0]
0000 DECLARE_CLASS string("i2")
0001 DECLARE_CLASS string("c1")
0002 DECLARE_CLASS_DELAYED string("c2") string("c1")
0003 RETURN int(1)

C2::foo:
; (lines=5, args=0, vars=0, tmps=1)
; (before optimizer)
; %s:8-10
; return [] RANGE[0..0]
0000 T0 = FETCH_THIS
0001 VERIFY_RETURN_TYPE T0
0002 RETURN T0
0003 VERIFY_RETURN_TYPE
0004 RETURN null
LIVE RANGES:
0: 0001 - 0002 (tmp/var)

$_main:
; (lines=4, args=0, vars=0, tmps=0)
; (after optimizer)
; %s:1-14
0000 DECLARE_CLASS string("i2")
0001 DECLARE_CLASS string("c1")
0002 DECLARE_CLASS_DELAYED string("c2") string("c1")
0003 RETURN int(1)

C2::foo:
; (lines=3, args=0, vars=0, tmps=1)
; (after optimizer)
; %s:8-10
0000 T0 = FETCH_THIS
0001 VERIFY_RETURN_TYPE T0
0002 RETURN T0
LIVE RANGES:
0: 0001 - 0002 (tmp/var)
Loading
Loading