Commit 1053403d9aa for php.net
commit 1053403d9aa48e64616ca6284d2cba7deb34671e
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date: Fri Sep 4 18:00:56 2026 +0200
JIT: Record fake init calls for subtraces (fixes array_map optimization) (#23450)
The array_map optimization may emit loops inside an INIT_FCALL-DO_FCALL
sequence, which was not possible before. JIT doesn't expect that and forgets
about pending calls when starting a subtrace for the loop.
Fix by recording fake init calls in zend_jit_trace_subtrace().
diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c
index b01c3aaac62..688d523b254 100644
--- a/ext/opcache/jit/zend_jit_vm_helpers.c
+++ b/ext/opcache/jit/zend_jit_vm_helpers.c
@@ -680,11 +680,20 @@ static int zend_jit_trace_record_fake_init_call(zend_execute_data *call, zend_ji
return zend_jit_trace_record_fake_init_call_ex(call, trace_buffer, idx, is_megamorphic, 0);
}
-static int zend_jit_trace_subtrace(zend_jit_trace_rec *trace_buffer, int start, int end, uint8_t event, const zend_op_array *op_array, const zend_op *opline)
+static int zend_jit_trace_subtrace(zend_execute_data *call, zend_jit_trace_rec *trace_buffer, int start, int end, uint8_t event, const zend_op_array *op_array, const zend_op *opline)
{
int idx;
TRACE_START(ZEND_JIT_TRACE_START, event, op_array, opline);
+ if (call) {
+ idx = zend_jit_trace_record_fake_init_call(call, trace_buffer, idx, 0);
+ if (idx < 0) {
+ return idx;
+ }
+ }
+ if (idx + (end - start) >= JIT_G(max_trace_length) - 2) {
+ return -1;
+ }
memmove(trace_buffer + idx, trace_buffer + start, (end - start) * sizeof(zend_jit_trace_rec));
return idx + (end - start);
}
@@ -1352,8 +1361,13 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
if (opline == last_loop_opline
&& level == last_loop_level) {
- idx = zend_jit_trace_subtrace(trace_buffer,
+ int ret = zend_jit_trace_subtrace(EX(call), trace_buffer,
last_loop, idx, ZEND_JIT_TRACE_START_LOOP, op_array, opline);
+ if (ret < 0) {
+ stop = ZEND_JIT_TRACE_STOP_TOO_LONG;
+ break;
+ }
+ idx = ret;
start = ZEND_JIT_TRACE_START_LOOP;
stop = ZEND_JIT_TRACE_STOP_LOOP;
ret_level = 0;
diff --git a/ext/opcache/tests/jit/array_map_loop_in_call_region.phpt b/ext/opcache/tests/jit/array_map_loop_in_call_region.phpt
new file mode 100644
index 00000000000..acdb969e972
--- /dev/null
+++ b/ext/opcache/tests/jit/array_map_loop_in_call_region.phpt
@@ -0,0 +1,40 @@
+--TEST--
+JIT: array_map() foreach optimization: loop trace inside a call region must not clobber EX(call)
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+
+class C {
+ static function f($x) { return $x + 1; }
+}
+
+function sink(array $a) { return array_sum($a); }
+
+/* array_map() is compiled to a foreach loop, so both loop headers sit between
+ * the INIT_FCALL of sink() and its DO_UCALL. The side trace recorded at the
+ * exit of the first loop runs into the header of the second one and is turned
+ * into a loop trace rooted there; that conversion must not lose track of the
+ * sink() frame that is still under construction, otherwise DO_FCALL of the
+ * callback stores NULL into EX(call) and the following SEND_VAL crashes.
+ *
+ * $o::f(...) is used so that INIT_STATIC_METHOD_CALL falls back to the VM
+ * handler, which is what makes the JIT emit the EX(call) store at DO_FCALL. */
+function test(array $a, $o) {
+ $n = sink(array_map($o::f(...), $a));
+ $n += sink(array_map($o::f(...), $a));
+ return $n;
+}
+
+$a = [1, 2, 3];
+$o = new C();
+
+for ($i = 0; $i < 2; $i++) {
+ $r = test($a, $o);
+}
+
+var_dump($r);
+
+?>
+--EXPECT--
+int(18)