Commit 5b589114723 for php

commit 5b589114723057c37accb154a26d857b3e0a3fc7
Author: Nora Dossche <7771979+ndossche@users.noreply.github.com>
Date:   Tue Nov 10 11:17:12 2026 +0100

    Implement fast-path static property access in JIT (#25042)

    * Factor out jit_called_scope()

    This includes the fix from GH-25041.

    * Implement fast-path static:: property access in JIT

    This is common for OOP applications, in particularly noticed in the
    Laravel framework.

    * Test

    * Adapt test

diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index d5d61c3289b..1b95e9e2e26 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 7e110e6361f..0f50d6915b3 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;
@@ -16377,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;
@@ -16424,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*)));
@@ -16499,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 e8705794281..01d1ff50811 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)) {
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 00000000000..07a2957765a
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_static_prop_002.phpt
@@ -0,0 +1,112 @@
+--TEST--
+FETCH_STATIC_PROP_* with static:: must respect the called scope
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit_hot_loop=1
+opcache.jit_hot_func=1
+opcache.jit_hot_return=1
+opcache.jit_hot_side_exit=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+
+trait T {
+    public static $t = 'trait';
+
+    public function getT() {
+        return static::$t;
+    }
+}
+
+class A {
+    use T;
+    public static $v = 'A';
+    public static array $arr = [];
+    public static int $typed;
+
+    public static function getStatic() {
+        return static::$v;
+    }
+
+    public function getInstance() {
+        return static::$v;
+    }
+
+    public function getClosure() {
+        return function () {
+            return static::$v;
+        };
+    }
+
+    public static function getStaticClosure() {
+        return static fn () => 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,BBBBtraitB1,AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1
+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)