Skip to content

Commit 15b87fd

Browse files
committed
Optimizer: prove return types for unlinked classes via names
Signed-off-by: azjezz <azjezz@protonmail.com>
1 parent 15c01db commit 15b87fd

3 files changed

Lines changed: 180 additions & 25 deletions

File tree

Zend/Optimizer/dfa_pass.c

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -252,45 +252,64 @@ static void zend_ssa_remove_nops(zend_op_array *op_array, zend_ssa *ssa, zend_op
252252
free_alloca(shiftlist, use_heap);
253253
}
254254

255+
/* Returns true if every instance of ce1 (or a subclass) is provably an instance
256+
* of the target class. target_ce is the resolved class entry for the target
257+
* when available; it may be NULL for a class declared in the same compilation
258+
* unit that is not yet linked, in which case we fall back to matching
259+
* target_lc against ce1's own declared parent and interfaces. */
255260
static bool safe_instanceof(
256261
const zend_class_entry *ce1,
257-
const zend_class_entry *ce2,
262+
const zend_class_entry *target_ce,
263+
const zend_string *target_lc,
258264
const zend_script *script,
259265
const zend_op_array *op_array
260266
) {
261-
if (ce1 == ce2) {
267+
if (target_ce && ce1 == target_ce) {
268+
return true;
269+
}
270+
if (zend_string_equals_ci(ce1->name, target_lc)) {
262271
return true;
263272
}
264273
if (ce1->ce_flags & ZEND_ACC_LINKED) {
265-
return instanceof_function(ce1, ce2);
274+
return target_ce && instanceof_function(ce1, target_ce);
266275
}
267276

268-
/* TODO Handle unlinked parents ike in unlinked_instanceof()? */
269-
270-
if (ce1->num_interfaces) {
271-
uint32_t i;
277+
for (uint32_t i = 0; i < ce1->num_interfaces; i++) {
278+
const zend_class_entry *iface;
272279
if (ce1->ce_flags & ZEND_ACC_RESOLVED_INTERFACES) {
273280
/* Unlike the normal instanceof_function(), we have to perform a recursive
274281
* check here, as the parent interfaces might not have been fully copied yet. */
275-
for (i = 0; i < ce1->num_interfaces; i++) {
276-
if (safe_instanceof(ce1->interfaces[i], ce2, script, op_array)) {
277-
return true;
278-
}
279-
}
282+
iface = ce1->interfaces[i];
280283
} else {
281-
for (i = 0; i < ce1->num_interfaces; i++) {
282-
const zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, ce1->interface_names[i].lc_name);
283-
if (!ce) {
284-
continue;
285-
}
286-
/* Avoid recursing if class implements itself. */
287-
if (ce == ce1) {
288-
continue;
289-
}
290-
if (safe_instanceof(ce, ce2, script, op_array)) {
291-
return true;
292-
}
284+
if (zend_string_equals_ci(ce1->interface_names[i].lc_name, target_lc)) {
285+
return true;
293286
}
287+
288+
iface = zend_optimizer_get_class_entry(script, op_array, ce1->interface_names[i].lc_name);
289+
}
290+
291+
/* Skip if unresolvable/not-yet-copied, or the class implements itself. */
292+
if (!iface || iface == ce1) {
293+
continue;
294+
}
295+
296+
if (safe_instanceof(iface, target_ce, target_lc, script, op_array)) {
297+
return true;
298+
}
299+
}
300+
301+
/* ce1 is unlinked here (the linked case returned above), so the
302+
* parent/parent_name union holds the as-yet-unresolved parent name. */
303+
if (ce1->parent_name) {
304+
if (zend_string_equals_ci(ce1->parent_name, target_lc)) {
305+
return true;
306+
}
307+
308+
zend_string *parent_lc = zend_string_tolower(ce1->parent_name);
309+
const zend_class_entry *parent = zend_optimizer_get_class_entry(script, op_array, parent_lc);
310+
zend_string_release(parent_lc);
311+
if (parent && parent != ce1 && safe_instanceof(parent, target_ce, target_lc, script, op_array)) {
312+
return true;
294313
}
295314
}
296315

@@ -313,8 +332,8 @@ static inline bool can_elide_list_type(
313332
if (ZEND_TYPE_HAS_NAME(*single_type)) {
314333
zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type));
315334
const zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname);
335+
bool result = use_info->ce && safe_instanceof(use_info->ce, ce, lcname, script, op_array);
316336
zend_string_release(lcname);
317-
bool result = ce && safe_instanceof(use_info->ce, ce, script, op_array);
318337
if (result == !is_intersection) {
319338
return result;
320339
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
--TEST--
2+
Return type check elision in the optimizer for an interface reached through a (resolvable) implemented interface and $this
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.opt_debug_level=0x30000
8+
--EXTENSIONS--
9+
opcache
10+
--FILE--
11+
<?php
12+
if (true) {
13+
class D implements IteratorAggregate {
14+
public function getIterator(): Iterator {
15+
throw new Exception();
16+
}
17+
public function foo(): Traversable {
18+
return $this;
19+
}
20+
}
21+
}
22+
?>
23+
--EXPECTF--
24+
$_main:
25+
; (lines=3, args=0, vars=0, tmps=0)
26+
; (before optimizer)
27+
; %s:1-13
28+
; return [] RANGE[0..0]
29+
0000 JMPZ bool(true) 0002
30+
0001 DECLARE_CLASS string("d")
31+
0002 RETURN int(1)
32+
33+
D::getIterator:
34+
; (lines=5, args=0, vars=0, tmps=2)
35+
; (before optimizer)
36+
; %s:4-6
37+
; return [] RANGE[0..0]
38+
0000 T0 = NEW 0 string("Exception")
39+
0001 DO_FCALL
40+
0002 THROW T0
41+
0003 VERIFY_RETURN_TYPE
42+
0004 RETURN null
43+
LIVE RANGES:
44+
0: 0001 - 0002 (new)
45+
46+
D::foo:
47+
; (lines=5, args=0, vars=0, tmps=1)
48+
; (before optimizer)
49+
; %s:7-9
50+
; return [] RANGE[0..0]
51+
0000 T0 = FETCH_THIS
52+
0001 VERIFY_RETURN_TYPE T0
53+
0002 RETURN T0
54+
0003 VERIFY_RETURN_TYPE
55+
0004 RETURN null
56+
LIVE RANGES:
57+
0: 0001 - 0002 (tmp/var)
58+
59+
$_main:
60+
; (lines=2, args=0, vars=0, tmps=0)
61+
; (after optimizer)
62+
; %s:1-13
63+
0000 DECLARE_CLASS string("d")
64+
0001 RETURN int(1)
65+
66+
D::getIterator:
67+
; (lines=3, args=0, vars=0, tmps=1)
68+
; (after optimizer)
69+
; %s:4-6
70+
0000 T0 = NEW 0 string("Exception")
71+
0001 DO_FCALL
72+
0002 THROW T0
73+
LIVE RANGES:
74+
0: 0001 - 0002 (new)
75+
76+
D::foo:
77+
; (lines=2, args=0, vars=0, tmps=1)
78+
; (after optimizer)
79+
; %s:7-9
80+
0000 T0 = FETCH_THIS
81+
0001 RETURN T0
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Return type check elision in the optimizer for an interface reached through a (resolvable) parent class and $this
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.opt_debug_level=0x30000
8+
--EXTENSIONS--
9+
opcache
10+
--FILE--
11+
<?php
12+
if (true) {
13+
class C extends ArrayObject {
14+
public function foo(): Traversable {
15+
return $this;
16+
}
17+
}
18+
}
19+
?>
20+
--EXPECTF--
21+
$_main:
22+
; (lines=3, args=0, vars=0, tmps=0)
23+
; (before optimizer)
24+
; %s:1-10
25+
; return [] RANGE[0..0]
26+
0000 JMPZ bool(true) 0002
27+
0001 DECLARE_CLASS string("c") string("arrayobject")
28+
0002 RETURN int(1)
29+
30+
C::foo:
31+
; (lines=5, args=0, vars=0, tmps=1)
32+
; (before optimizer)
33+
; %s:4-6
34+
; return [] RANGE[0..0]
35+
0000 T0 = FETCH_THIS
36+
0001 VERIFY_RETURN_TYPE T0
37+
0002 RETURN T0
38+
0003 VERIFY_RETURN_TYPE
39+
0004 RETURN null
40+
LIVE RANGES:
41+
0: 0001 - 0002 (tmp/var)
42+
43+
$_main:
44+
; (lines=2, args=0, vars=0, tmps=0)
45+
; (after optimizer)
46+
; %s:1-10
47+
0000 DECLARE_CLASS string("c") string("arrayobject")
48+
0001 RETURN int(1)
49+
50+
C::foo:
51+
; (lines=2, args=0, vars=0, tmps=1)
52+
; (after optimizer)
53+
; %s:4-6
54+
0000 T0 = FETCH_THIS
55+
0001 RETURN T0

0 commit comments

Comments
 (0)