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