From 323bef7e0a899469647d3b580093eac0f1fc9427 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Wed, 26 Aug 2026 21:30:38 +0200 Subject: [PATCH 1/2] Optimizer: resolve arg/prop/return class hints via op_array scope --- Zend/Optimizer/zend_inference.c | 47 +++++++++++++++++--------------- Zend/Optimizer/zend_inference.h | 3 +- ext/opcache/jit/zend_jit_trace.c | 10 ++++--- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index effff75e00f2..5d6866d0160c 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -2375,8 +2375,9 @@ static uint32_t zend_convert_type_declaration_mask(uint32_t type_mask) { return result_mask; } -static uint32_t zend_convert_type(const zend_script *script, zend_type type, zend_class_entry **pce) -{ +static uint32_t zend_convert_type( + const zend_script *script, const zend_op_array *op_array, + zend_type type, zend_class_entry **pce) { if (pce) { *pce = NULL; } @@ -2393,8 +2394,7 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen * we use a plain object type for class unions. */ if (ZEND_TYPE_HAS_NAME(type)) { zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(type)); - // TODO: Pass through op_array. - *pce = zend_optimizer_get_class_entry(script, NULL, lcname); + *pce = zend_optimizer_get_class_entry(script, op_array, lcname); zend_string_release_ex(lcname, 0); } } @@ -2405,9 +2405,10 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen return tmp; } -ZEND_API uint32_t zend_fetch_arg_info_type(const zend_script *script, const zend_arg_info *arg_info, zend_class_entry **pce) -{ - return zend_convert_type(script, arg_info->type, pce); +ZEND_API uint32_t zend_fetch_arg_info_type( + const zend_script *script, const zend_op_array *op_array, + const zend_arg_info *arg_info, zend_class_entry **pce) { + return zend_convert_type(script, op_array, arg_info->type, pce); } static const zend_property_info *lookup_prop_info(const zend_class_entry *ce, zend_string *name, zend_class_entry *scope) { @@ -2496,8 +2497,9 @@ static const zend_property_info *zend_fetch_static_prop_info(const zend_script * return prop_info; } -static uint32_t zend_fetch_prop_type(const zend_script *script, const zend_property_info *prop_info, zend_class_entry **pce) -{ +static uint32_t zend_fetch_prop_type( + const zend_script *script, const zend_op_array *op_array, + const zend_property_info *prop_info, zend_class_entry **pce) { if (!prop_info) { if (pce) { *pce = NULL; @@ -2505,7 +2507,7 @@ static uint32_t zend_fetch_prop_type(const zend_script *script, const zend_prope return MAY_BE_ANY | MAY_BE_ARRAY_KEY_ANY | MAY_BE_ARRAY_OF_ANY | MAY_BE_ARRAY_OF_REF | MAY_BE_RC1 | MAY_BE_RCN; } - return zend_convert_type(script, prop_info->type, pce); + return zend_convert_type(script, op_array, prop_info->type, pce); } static bool result_may_be_separated(const zend_ssa *ssa, const zend_ssa_op *ssa_op) @@ -2755,7 +2757,7 @@ static zend_always_inline zend_result _zend_update_type_info( if (opline->opcode == ZEND_ASSIGN_OBJ_OP) { prop_info = zend_fetch_prop_info(op_array, ssa, opline, ssa_op); orig = t1; - t1 = zend_fetch_prop_type(script, prop_info, NULL); + t1 = zend_fetch_prop_type(script, op_array, prop_info, NULL); t2 = OP1_DATA_INFO(); } else if (opline->opcode == ZEND_ASSIGN_DIM_OP) { if (t1 & MAY_BE_ARRAY_OF_REF) { @@ -2766,7 +2768,7 @@ static zend_always_inline zend_result _zend_update_type_info( t2 = OP1_DATA_INFO(); } else if (opline->opcode == ZEND_ASSIGN_STATIC_PROP_OP) { prop_info = zend_fetch_static_prop_info(script, op_array, ssa, opline); - t1 = zend_fetch_prop_type(script, prop_info, NULL); + t1 = zend_fetch_prop_type(script, op_array, prop_info, NULL); t2 = OP1_DATA_INFO(); } else { if (t1 & MAY_BE_REF) { @@ -2828,7 +2830,7 @@ static zend_always_inline zend_result _zend_update_type_info( } else if (opline->opcode == ZEND_ASSIGN_OBJ_OP) { /* The return value must also satisfy the property type */ if (prop_info) { - t1 = zend_fetch_prop_type(script, prop_info, &ce); + t1 = zend_fetch_prop_type(script, op_array, prop_info, &ce); if ((t1 & (MAY_BE_LONG|MAY_BE_DOUBLE)) == MAY_BE_LONG && (tmp & (MAY_BE_LONG|MAY_BE_DOUBLE)) == MAY_BE_DOUBLE) { /* DOUBLE may be auto-converted to LONG */ @@ -2847,7 +2849,7 @@ static zend_always_inline zend_result _zend_update_type_info( } else if (opline->opcode == ZEND_ASSIGN_STATIC_PROP_OP) { /* The return value must also satisfy the property type */ if (prop_info) { - t1 = zend_fetch_prop_type(script, prop_info, &ce); + t1 = zend_fetch_prop_type(script, op_array, prop_info, &ce); if ((t1 & (MAY_BE_LONG|MAY_BE_DOUBLE)) == MAY_BE_LONG && (tmp & (MAY_BE_LONG|MAY_BE_DOUBLE)) == MAY_BE_DOUBLE) { /* DOUBLE may be auto-converted to LONG */ @@ -3043,7 +3045,7 @@ static zend_always_inline zend_result _zend_update_type_info( } if (ssa_op->result_def >= 0) { // TODO: If there is no __set we might do better - tmp = zend_fetch_prop_type(script, + tmp = zend_fetch_prop_type(script, op_array, zend_fetch_prop_info(op_array, ssa, opline, ssa_op), &ce); UPDATE_SSA_TYPE(tmp, ssa_op->result_def); if (ce) { @@ -3314,7 +3316,7 @@ static zend_always_inline zend_result _zend_update_type_info( zend_arg_info *arg_info = &op_array->arg_info[opline->op1.num-1]; ce = NULL; - tmp = zend_fetch_arg_info_type(script, arg_info, &ce); + tmp = zend_fetch_arg_info_type(script, op_array, arg_info, &ce); if (ZEND_ARG_SEND_MODE(arg_info)) { tmp |= MAY_BE_REF; ce = NULL; @@ -3791,7 +3793,7 @@ static zend_always_inline zend_result _zend_update_type_info( } if (opline->op1_type == IS_UNUSED || (t1 & MAY_BE_OBJECT)) { const zend_property_info *prop_info = zend_fetch_prop_info(op_array, ssa, opline, ssa_op); - tmp |= zend_fetch_prop_type(script, prop_info, &ce); + tmp |= zend_fetch_prop_type(script, op_array, prop_info, &ce); if (opline->opcode != ZEND_FETCH_OBJ_R && opline->opcode != ZEND_FETCH_OBJ_IS) { tmp |= MAY_BE_REF | MAY_BE_INDIRECT; if ((opline->extended_value & ZEND_FETCH_OBJ_FLAGS) == ZEND_FETCH_DIM_WRITE) { @@ -3832,7 +3834,7 @@ static zend_always_inline zend_result _zend_update_type_info( case ZEND_FETCH_STATIC_PROP_W: case ZEND_FETCH_STATIC_PROP_UNSET: case ZEND_FETCH_STATIC_PROP_FUNC_ARG: - tmp = zend_fetch_prop_type(script, + tmp = zend_fetch_prop_type(script, op_array, zend_fetch_static_prop_info(script, op_array, ssa, opline), &ce); if (opline->opcode != ZEND_FETCH_STATIC_PROP_R && opline->opcode != ZEND_FETCH_STATIC_PROP_IS) { @@ -3926,7 +3928,7 @@ static zend_always_inline zend_result _zend_update_type_info( UPDATE_SSA_TYPE(MAY_BE_RC1|MAY_BE_RCN|MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY, ssa_op->result_def); break; } - UPDATE_SSA_TYPE(zend_convert_type(script, cc->type, &ce), ssa_op->result_def); + UPDATE_SSA_TYPE(zend_convert_type(script, op_array, cc->type, &ce), ssa_op->result_def); if (ce) { UPDATE_SSA_OBJ_TYPE(ce, /* is_instanceof */ true, ssa_op->result_def); } @@ -3974,7 +3976,7 @@ static zend_always_inline zend_result _zend_update_type_info( ce = NULL; } else { zend_arg_info *ret_info = op_array->arg_info - 1; - tmp = zend_fetch_arg_info_type(script, ret_info, &ce); + tmp = zend_fetch_arg_info_type(script, op_array, ret_info, &ce); if ((tmp & MAY_BE_NULL) && opline->op1_type == IS_CV) { tmp |= MAY_BE_UNDEF; } @@ -4030,7 +4032,7 @@ static zend_always_inline zend_result _zend_update_type_info( if (ssa_op->result_def >= 0) { const zend_property_info *prop_info = zend_fetch_static_prop_info(script, op_array, ssa, opline); zend_class_entry *prop_ce; - tmp = zend_fetch_prop_type(script, prop_info, &prop_ce); + tmp = zend_fetch_prop_type(script, op_array, prop_info, &prop_ce); /* Internal objects may result in essentially anything. */ if (tmp & MAY_BE_OBJECT) { goto unknown_opcode; @@ -4529,7 +4531,8 @@ uint32_t zend_get_return_info_from_signature_only( (use_tentative_return_info || !ZEND_ARG_TYPE_IS_TENTATIVE(func->common.arg_info - 1)) ) { const zend_arg_info *ret_info = func->common.arg_info - 1; - type = zend_fetch_arg_info_type(script, ret_info, ce); + const zend_op_array *op_array = (func->common.type != ZEND_INTERNAL_FUNCTION) ? (const zend_op_array *) func : NULL; + type = zend_fetch_arg_info_type(script, op_array, ret_info, ce); *ce_is_instanceof = ce != NULL; } else { type = MAY_BE_ANY | MAY_BE_ARRAY_KEY_ANY | MAY_BE_ARRAY_OF_ANY | MAY_BE_ARRAY_OF_REF diff --git a/Zend/Optimizer/zend_inference.h b/Zend/Optimizer/zend_inference.h index e6b4207c7494..160c52fd2294 100644 --- a/Zend/Optimizer/zend_inference.h +++ b/Zend/Optimizer/zend_inference.h @@ -224,7 +224,8 @@ ZEND_API uint32_t zend_array_element_type(uint32_t t1, uint8_t op_type, bool wri ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline, const zend_ssa_op* ssa_op, int var, zend_ssa_range *tmp); ZEND_API uint32_t zend_fetch_arg_info_type( - const zend_script *script, const zend_arg_info *arg_info, zend_class_entry **pce); + const zend_script *script, const zend_op_array *op_array, + const zend_arg_info *arg_info, zend_class_entry **pce); ZEND_API void zend_init_func_return_info( const zend_op_array *op_array, const zend_script *script, zend_ssa_var_info *ret); uint32_t zend_get_return_info_from_signature_only( diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index e1836b826107..238a60144eab 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -1720,7 +1720,7 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin if (op_array->arg_info && i < trace_buffer[1].opline - op_array->opcodes) { zend_arg_info *arg_info = &op_array->arg_info[i]; zend_class_entry *ce; - uint32_t tmp = zend_fetch_arg_info_type(script, arg_info, &ce); + uint32_t tmp = zend_fetch_arg_info_type(script, op_array, arg_info, &ce); if (ZEND_ARG_SEND_MODE(arg_info)) { tmp |= MAY_BE_REF; @@ -2182,7 +2182,7 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin arg_info = &frame->call->func->op_array.arg_info[opline->op2.num - 1]; if (ZEND_TYPE_IS_SET(arg_info->type)) { zend_class_entry *ce; - uint32_t tmp = zend_fetch_arg_info_type(script, arg_info, &ce); + uint32_t tmp = zend_fetch_arg_info_type(script, &frame->call->func->op_array, arg_info, &ce); info &= tmp; if (!info) { break; @@ -2521,7 +2521,7 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin if (op_array->arg_info) { zend_arg_info *arg_info = &op_array->arg_info[i]; zend_class_entry *ce; - uint32_t tmp = zend_fetch_arg_info_type(script, arg_info, &ce); + uint32_t tmp = zend_fetch_arg_info_type(script, op_array, arg_info, &ce); if (ZEND_ARG_SEND_MODE(arg_info)) { tmp |= MAY_BE_REF; @@ -2688,7 +2688,9 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin zend_class_entry *ce; const zend_function *func = p->func; zend_arg_info *ret_info = func->common.arg_info - 1; - uint32_t ret_type = zend_fetch_arg_info_type(NULL, ret_info, &ce); + const zend_op_array *callee_op_array = + (func->common.type != ZEND_INTERNAL_FUNCTION) ? &func->op_array : NULL; + uint32_t ret_type = zend_fetch_arg_info_type(script, callee_op_array, ret_info, &ce); ssa_var_info[ssa_ops[idx-1].result_def].type &= ret_type; } From e359b003b92e6259ef1ef89d1dcd5b316cc7f5d9 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Fri, 16 Oct 2026 22:56:23 +0200 Subject: [PATCH 2/2] Optimizer: add test, drop unused class lookups in trace JIT The previous commit passes the op_array through zend_convert_type(), which used to call zend_optimizer_get_class_entry() with a NULL op_array. This lets a class hint naming the current class resolve via op_array->scope when that class is unlinked in the script, i.e. it is conditionally declared or its parent is not known at compile time. self/parent are unaffected: they are substituted at compile time wherever the scope is known. The fallback is sound because code executing in scope X implies that X is the class declared under that name. --- UPGRADING.INTERNALS | 4 ++ Zend/Optimizer/zend_inference.c | 2 +- ext/opcache/jit/zend_jit_trace.c | 8 +-- .../opt/type_inference_unlinked_scope.phpt | 68 +++++++++++++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 ext/opcache/tests/opt/type_inference_unlinked_scope.phpt diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 4c5d74b49e9c..080346272576 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -47,6 +47,10 @@ PHP 8.7 INTERNALS UPGRADE NOTES - The INIT_CLASS_ENTRY_INIT_METHODS() macro, which was previously used as part of handling disabled classes, was removed following the removal of the `disable_classes` INI setting in PHP 8.5. +- zend_fetch_arg_info_type() now takes an additional const zend_op_array* + parameter after the script. It is used to resolve class names against the + op_array's scope and filename. NULL may be passed if no op_array is + available. ======================== 2. Build system changes diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index 5d6866d0160c..a22718ad0f0c 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -4531,7 +4531,7 @@ uint32_t zend_get_return_info_from_signature_only( (use_tentative_return_info || !ZEND_ARG_TYPE_IS_TENTATIVE(func->common.arg_info - 1)) ) { const zend_arg_info *ret_info = func->common.arg_info - 1; - const zend_op_array *op_array = (func->common.type != ZEND_INTERNAL_FUNCTION) ? (const zend_op_array *) func : NULL; + const zend_op_array *op_array = ZEND_USER_CODE(func->type) ? &func->op_array : NULL; type = zend_fetch_arg_info_type(script, op_array, ret_info, ce); *ce_is_instanceof = ce != NULL; } else { diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 238a60144eab..9848810224c6 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -2181,8 +2181,7 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin ZEND_ASSERT(frame->call->func->op_array.arg_info); arg_info = &frame->call->func->op_array.arg_info[opline->op2.num - 1]; if (ZEND_TYPE_IS_SET(arg_info->type)) { - zend_class_entry *ce; - uint32_t tmp = zend_fetch_arg_info_type(script, &frame->call->func->op_array, arg_info, &ce); + uint32_t tmp = zend_fetch_arg_info_type(script, NULL, arg_info, NULL); info &= tmp; if (!info) { break; @@ -2685,12 +2684,9 @@ static zend_ssa *zend_jit_trace_build_tssa(zend_jit_trace_rec *trace_buffer, uin opline->opcode == ZEND_DO_FCALL_BY_NAME); if (opline->result_type != IS_UNDEF) { - zend_class_entry *ce; const zend_function *func = p->func; zend_arg_info *ret_info = func->common.arg_info - 1; - const zend_op_array *callee_op_array = - (func->common.type != ZEND_INTERNAL_FUNCTION) ? &func->op_array : NULL; - uint32_t ret_type = zend_fetch_arg_info_type(script, callee_op_array, ret_info, &ce); + uint32_t ret_type = zend_fetch_arg_info_type(NULL, NULL, ret_info, NULL); ssa_var_info[ssa_ops[idx-1].result_def].type &= ret_type; } diff --git a/ext/opcache/tests/opt/type_inference_unlinked_scope.phpt b/ext/opcache/tests/opt/type_inference_unlinked_scope.phpt new file mode 100644 index 000000000000..1166ac4951c1 --- /dev/null +++ b/ext/opcache/tests/opt/type_inference_unlinked_scope.phpt @@ -0,0 +1,68 @@ +--TEST-- +Type inference resolves class hints naming the unlinked declaring class +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.opt_debug_level=0x400000 +opcache.preload= +zend_test.observer.enabled=0 +--EXTENSIONS-- +opcache +--FILE-- +x; } + } +} + +class B extends C { + public int $y = 1; + public function f(B $b) { return $b->y; } +} + +class C {} + +?> +--EXPECTF-- + +$_main: + ; (lines=3, args=0, vars=0, tmps=0, ssa_vars=0, no_loops) + ; (after dfa pass) + ; %s:1-18 + ; return [long] RANGE[1..1] +BB0: + ; start exit lines=[0-2] + ; level=0 +0000 DECLARE_CLASS string("a") +0001 DECLARE_CLASS_DELAYED string("b") string("c") +0002 RETURN int(1) + +A::f: + ; (lines=3, args=1, vars=1, tmps=1, ssa_vars=3, no_loops) + ; (after dfa pass) + ; %s:6-6 + ; return [long] + ; #0.CV0($a) NOVAL [undef] +BB0: + ; start exit lines=[0-2] + ; level=0 +0000 #1.CV0($a) [object (A)] = RECV 1 +0001 #2.T1 [long] = FETCH_OBJ_R #1.CV0($a) [object (A)] string("x") +0002 RETURN #2.T1 [long] + +B::f: + ; (lines=3, args=1, vars=1, tmps=1, ssa_vars=3, no_loops) + ; (after dfa pass) + ; %s:12-12 + ; return [long] + ; #0.CV0($b) NOVAL [undef] +BB0: + ; start exit lines=[0-2] + ; level=0 +0000 #1.CV0($b) [object (instanceof B)] = RECV 1 +0001 #2.T1 [long] = FETCH_OBJ_R #1.CV0($b) [object (instanceof B)] string("y") +0002 RETURN #2.T1 [long]