Commit e4fbcf8509e for php

commit e4fbcf8509eaf7dd9ecaaddc97c0b3eb353438c9
Author: Nora Dossche <7771979+ndossche@users.noreply.github.com>
Date:   Mon Nov 9 18:41:06 2026 +0100

    Tracing JIT: Implement COALESCE as a guard resulting in a side-trace (#25071)

diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 2ba24806d79..7e110e6361f 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -7597,6 +7597,39 @@ static int zend_jit_identical(zend_jit_ctx   *jit,
 	return 1;
 }

+/* This is only for the tracing JIT.
+ * `taken` is used for the recorded direction in the tracing jit. */
+static int zend_jit_coalesce(zend_jit_ctx *jit, const zend_op *opline, uint32_t op1_info, zend_jit_addr op1_addr, zend_jit_addr res_addr, bool taken, const void *exit_addr)
+{
+	ir_ref ref = jit_ZVAL_ADDR(jit, op1_addr);
+
+	ZEND_ASSERT(opline->op1_type & (IS_TMP_VAR|IS_CV));
+
+	if (opline->op1_type == IS_CV && (op1_info & MAY_BE_REF)) {
+		ref = jit_ZVAL_DEREF_ref(jit, ref);
+		op1_info &= ~MAY_BE_REF;
+	}
+
+	if (exit_addr) {
+		// JIT: if (Z_TYPE_P(value) > IS_NULL)
+		ir_ref cond = ir_GT(jit_Z_TYPE_ref(jit, ref), ir_CONST_U8(IS_NULL));
+
+		if (taken) {
+			ir_GUARD(cond, ir_CONST_ADDR(exit_addr));
+		} else {
+			ir_GUARD_NOT(cond, ir_CONST_ADDR(exit_addr));
+		}
+	}
+
+	if (taken) {
+		// JIT: ZVAL_COPY_VALUE(EX_VAR(opline->result.var), value);
+		// JIT: Z_ADDREF_P(EX_VAR(opline->result.var)) for CV
+		jit_ZVAL_COPY(jit, res_addr, -1, ZEND_ADDR_REF_ZVAL(ref), op1_info & ~(MAY_BE_UNDEF|MAY_BE_NULL), opline->op1_type == IS_CV);
+	}
+
+	return 1;
+}
+
 static int zend_jit_bool_jmpznz(zend_jit_ctx *jit, const zend_op *opline, uint32_t op1_info, zend_jit_addr op1_addr, zend_jit_addr res_addr, uint32_t target_label, uint32_t target_label2, int may_throw, uint8_t branch_opcode, const void *exit_addr, bool warn_undef)
 {
 	uint32_t true_label = -1;
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 4567216075a..e8705794281 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -5761,6 +5761,46 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer,
 							goto jit_failure;
 						}
 						goto done;
+					case ZEND_COALESCE:
+						if (!(opline->op1_type & (IS_TMP_VAR|IS_CV))
+						 /* COALESCE may have two identical successors due to DCE edge case.
+						  * In that case we cannot tell the branch directions apart. */
+						 || OP_JMP_ADDR(opline, opline->op2) == opline + 1) {
+							break;
+						}
+						op1_info = OP1_INFO();
+						CHECK_OP1_TRACE_TYPE();
+						if ((p+1)->op == ZEND_JIT_TRACE_VM || (p+1)->op == ZEND_JIT_TRACE_END) {
+							bool taken;
+							uint32_t refined_type_mask;
+
+							if ((p+1)->opline == OP_JMP_ADDR(opline, opline->op2)) {
+								taken = true;
+								refined_type_mask = op1_info & (MAY_BE_UNDEF|MAY_BE_NULL);
+							} else if ((p+1)->opline == opline + 1) {
+								taken = false;
+								refined_type_mask = op1_info & ((MAY_BE_ANY|MAY_BE_REF) - MAY_BE_NULL);
+							} else {
+								ZEND_UNREACHABLE();
+							}
+							if (!refined_type_mask) {
+								/* unconditional branch */
+								exit_addr = NULL;
+							} else {
+								/* Execute the other direction in VM or side trace. */
+								uint32_t exit_point = zend_jit_trace_get_exit_point(opline, 0);
+								exit_addr = zend_jit_trace_get_exit_addr(exit_point);
+								if (!exit_addr) {
+									goto jit_failure;
+								}
+							}
+							if (!zend_jit_coalesce(&ctx, opline, op1_info, OP1_ADDR(), RES_ADDR(), taken, exit_addr)) {
+								goto jit_failure;
+							}
+							goto done;
+						} else {
+							ZEND_UNREACHABLE();
+						}
 					case ZEND_JMP_FRAMELESS:
 						op1_info = OP1_INFO();
 						ZEND_ASSERT((p+1)->op == ZEND_JIT_TRACE_VM);
diff --git a/ext/opcache/tests/jit/coalesce_001.phpt b/ext/opcache/tests/jit/coalesce_001.phpt
new file mode 100644
index 00000000000..c6ba05c81e2
--- /dev/null
+++ b/ext/opcache/tests/jit/coalesce_001.phpt
@@ -0,0 +1,52 @@
+--TEST--
+JIT COALESCE: guard in both recorded directions and CV reference
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=tracing
+opcache.jit_hot_loop=1
+opcache.jit_hot_side_exit=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+/* Trace recorded with a non-null operand (jump taken), then null hits the guard. */
+function taken(array $values) {
+    $out = [];
+    foreach ($values as $v) {
+        $out[] = $v ?? 'null';
+    }
+    return implode(',', $out);
+}
+
+/* Trace recorded with a null operand (fallthrough), then non-null hits the guard. */
+function not_taken(array $values) {
+    $out = [];
+    foreach ($values as $v) {
+        $out[] = $v ?? 'null';
+    }
+    return implode(',', $out);
+}
+
+/* The CV is a reference, so the operand must be dereferenced. */
+function cv_ref(array $values) {
+    $out = [];
+    $src = null;
+    $r = &$src;
+    foreach ($values as $v) {
+        $src = $v;
+        $out[] = $r ?? 'null';
+    }
+    return implode(',', $out);
+}
+
+echo taken(['a', 'b', 'c', 'd', null, null, 'e', null]), "\n";
+echo not_taken([null, null, null, null, 'a', 'b', null, 'c']), "\n";
+echo cv_ref(['a', 'b', 'c', 'd', null, null, 'e', null]), "\n";
+echo cv_ref([null, null, null, null, 'a', 'b', null, 'c']), "\n";
+?>
+--EXPECT--
+a,b,c,d,null,null,e,null
+null,null,null,null,a,b,null,c
+a,b,c,d,null,null,e,null
+null,null,null,null,a,b,null,c