From 3d861783214793cb671f9eaa384b044a6867c8cb Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:16:57 +0100 Subject: [PATCH 1/4] Factor out jit_called_scope() This includes the fix from GH-25041. --- ext/opcache/jit/zend_jit_ir.c | 42 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 7e110e6361fc..500fbced68bf 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -9322,6 +9322,31 @@ static int zend_jit_init_method_call(zend_jit_ctx *jit, return 1; } +/* Called scope of the current frame, as used by static:: and inherited by self::/parent:: calls. */ +static ir_ref jit_called_scope(zend_jit_ctx *jit, const zend_op_array *op_array) +{ + if (op_array->fn_flags & ZEND_ACC_STATIC) { + // JIT: Z_CE(EX(This)) + return ir_LOAD_A(jit_EX(This.value.ref)); + } else if (op_array->scope + && op_array->function_name + && !(op_array->fn_flags & ZEND_ACC_CLOSURE)) { + // JIT: Z_OBJCE(EX(This)) + return ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce))); + } else { + /* Closures can be unbound, file/eval inherits This from the existing frame's scope. */ + ir_ref if_object, values = IR_UNUSED; + + if_object = ir_IF(ir_EQ(jit_Z_TYPE_ref(jit, jit_EX(This)), ir_CONST_U8(IS_OBJECT))); + ir_IF_TRUE(if_object); + ir_END_PHI_list(values, + ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce)))); + ir_IF_FALSE(if_object); + ir_END_PHI_list(values, ir_LOAD_A(jit_EX(This.value.ref))); + return ir_PHI_list(values); + } +} + static int zend_jit_init_static_method_call(zend_jit_ctx *jit, const zend_op *opline, uint32_t b, @@ -9453,22 +9478,7 @@ static int zend_jit_init_static_method_call(zend_jit_ctx *jit, if (opline->op1_type == IS_UNUSED && ((opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT || (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF)) { - if (op_array->fn_flags & ZEND_ACC_STATIC) { - scope_ref = ir_LOAD_A(jit_EX(This.value.ref)); - } else if ((op_array->fn_flags & ZEND_ACC_CLOSURE) || !op_array->function_name) { - /* Closures can be unbound, file/eval inherits This from the existing frame's scope. */ - ir_ref if_object, values = IR_UNUSED; - - if_object = ir_IF(ir_EQ(jit_Z_TYPE_ref(jit, jit_EX(This)), ir_CONST_U8(IS_OBJECT))); - ir_IF_TRUE(if_object); - ir_END_PHI_list(values, - ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce)))); - ir_IF_FALSE(if_object); - ir_END_PHI_list(values, ir_LOAD_A(jit_EX(This.value.ref))); - scope_ref = ir_PHI_list(values); - } else { - scope_ref = ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce))); - } + scope_ref = jit_called_scope(jit, op_array); } if (!zend_jit_push_call_frame(jit, opline, op_array, func, false, false, checked_stack, func_ref, scope_ref)) { return 0; From 2875fa224e5aae2cae546751f8e34f21580964fc Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:17:30 +0100 Subject: [PATCH 2/4] Implement fast-path static:: property access in JIT This is common for OOP applications, in particularly noticed in the Laravel framework. --- ext/opcache/jit/zend_jit.c | 3 ++- ext/opcache/jit/zend_jit_ir.c | 20 +++++++++++++++++++- ext/opcache/jit/zend_jit_trace.c | 3 ++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index d5d61c3289bb..1b95e9e2e26f 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -2535,7 +2535,8 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op && (opline->op2_type == IS_CONST || (opline->op2_type == IS_UNUSED && ((opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF - || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT))))) { + || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT + || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC))))) { break; } if (!zend_jit_fetch_static_prop(&ctx, opline, op_array)) { diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 500fbced68bf..0f50d6915b39 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -16387,11 +16387,14 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline, uint32_t cache_slot = opline->extended_value & ~ZEND_FETCH_OBJ_FLAGS; uint32_t flags; ir_ref ref, ref2, if_cached, fast_path, cold_path, prop_info_ref, if_typed, if_def; + ir_ref slow_inputs = IR_UNUSED; int fetch_type; zend_property_info *known_prop_info = NULL; zend_class_entry *ce; + bool is_static = opline->op2_type == IS_UNUSED + && (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC; - ce = zend_get_known_class(op_array, opline, opline->op2_type, opline->op2); + ce = is_static ? NULL : zend_get_known_class(op_array, opline, opline->op2_type, opline->op2); if (ce) { zval *zv = RT_CONSTANT(opline, opline->op1); zend_string *prop_name; @@ -16434,6 +16437,17 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline, default: ZEND_UNREACHABLE(); } + if (is_static) { + /* The cache slot is polymorphic on the called scope. */ + // JIT: if (CACHED_PTR(cache_slot) == called_scope) + ir_ref if_scope = ir_IF(ir_EQ( + ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot)), + jit_called_scope(jit, op_array))); + ir_IF_FALSE_cold(if_scope); + ir_END_list(slow_inputs); + ir_IF_TRUE(if_scope); + } + // JIT: result = CACHED_PTR(cache_slot + sizeof(void *)); ref = ir_LOAD_A( ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot + sizeof(void*))); @@ -16509,6 +16523,10 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline, fast_path = ir_END(); ir_IF_FALSE_cold(if_cached); + if (slow_inputs) { + ir_END_list(slow_inputs); + ir_MERGE_list(slow_inputs); + } jit_SET_EX_OPLINE(jit, opline); ref2 = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_fetch_static_property), jit_FP(jit), ir_CONST_I32(fetch_type)); zend_jit_check_exception_undef_result(jit, opline); diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index e87057942817..01d1ff50811f 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -6212,7 +6212,8 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer, && (opline->op2_type == IS_CONST || (opline->op2_type == IS_UNUSED && ((opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF - || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT))))) { + || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT + || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC))))) { break; } if (!zend_jit_fetch_static_prop(&ctx, opline, op_array)) { From 138bd26b14b88f96945c71dc705b1a3a84d579ce Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:34:24 +0100 Subject: [PATCH 3/4] Test --- .../tests/jit/fetch_static_prop_002.phpt | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 ext/opcache/tests/jit/fetch_static_prop_002.phpt diff --git a/ext/opcache/tests/jit/fetch_static_prop_002.phpt b/ext/opcache/tests/jit/fetch_static_prop_002.phpt new file mode 100644 index 000000000000..090bb9d0d588 --- /dev/null +++ b/ext/opcache/tests/jit/fetch_static_prop_002.phpt @@ -0,0 +1,113 @@ +--TEST-- +FETCH_STATIC_PROP_* with static:: must respect the called scope +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_hot_loop=1 +opcache.jit_hot_func=1 +opcache.jit_hot_return=1 +opcache.jit_hot_side_exit=1 +--EXTENSIONS-- +opcache +--FILE-- + static::$v; + } + + public static function push($x) { + static::$arr[] = $x; + } + + public static function has() { + return isset(static::$v); + } + + public static function getTyped() { + return static::$typed; + } +} + +// Used for the shared cache slot testing +class B extends A { + public static $v = 'B'; + public static array $arr = []; + public static $t = 'traitB'; +} + +$out = []; +for ($i = 0; $i < 8; $i++) { + $o = $i % 2 ? new B : new A; + $c = $i % 2 ? 'B' : 'A'; + $out[] = $c::getStatic() . $o->getInstance() . ($o->getClosure())() . ($c::getStaticClosure())() . $o->getT() . (int) $c::has(); + $c::push($i); +} +echo implode(',', $out), "\n"; +print_r(A::$arr); +echo "\n"; +print_r(B::$arr); +echo "\n"; + +for ($i = 0; $i < 2; $i++) { + try { + A::getTyped(); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } +} +A::$typed = 42; +var_dump(A::getTyped(), B::getTyped()); + +?> +--EXPECT-- +AAAAtrait1,BBBAtraitB1,AAAAtrait1,BBBAtraitB1,AAAAtrait1,BBBAtraitB1,AAAAtrait1,BBBAtraitB1 +Array +( + [0] => 0 + [1] => 2 + [2] => 4 + [3] => 6 +) + +Array +( + [0] => 1 + [1] => 3 + [2] => 5 + [3] => 7 +) + +Typed static property A::$typed must not be accessed before initialization +Typed static property A::$typed must not be accessed before initialization +int(42) +int(42) From ff5d78949f7e3d2b72e8deda18ea4c5d2cb3900d Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:50:16 +0100 Subject: [PATCH 4/4] Adapt test --- ext/opcache/tests/jit/fetch_static_prop_002.phpt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/opcache/tests/jit/fetch_static_prop_002.phpt b/ext/opcache/tests/jit/fetch_static_prop_002.phpt index 090bb9d0d588..07a2957765ac 100644 --- a/ext/opcache/tests/jit/fetch_static_prop_002.phpt +++ b/ext/opcache/tests/jit/fetch_static_prop_002.phpt @@ -3,7 +3,6 @@ FETCH_STATIC_PROP_* with static:: must respect the called scope --INI-- opcache.enable=1 opcache.enable_cli=1 -opcache.jit=tracing opcache.jit_hot_loop=1 opcache.jit_hot_func=1 opcache.jit_hot_return=1 @@ -90,7 +89,7 @@ var_dump(A::getTyped(), B::getTyped()); ?> --EXPECT-- -AAAAtrait1,BBBAtraitB1,AAAAtrait1,BBBAtraitB1,AAAAtrait1,BBBAtraitB1,AAAAtrait1,BBBAtraitB1 +AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1 Array ( [0] => 0