Coverage Report

Created: 2026-06-02 06:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/Zend/Optimizer/scdf.c
Line
Count
Source
1
/*
2
   +----------------------------------------------------------------------+
3
   | Zend Engine, Sparse Conditional Data Flow Propagation Framework      |
4
   +----------------------------------------------------------------------+
5
   | Copyright © The PHP Group and Contributors.                          |
6
   +----------------------------------------------------------------------+
7
   | This source file is subject to the Modified BSD License that is      |
8
   | bundled with this package in the file LICENSE, and is available      |
9
   | through the World Wide Web at <https://www.php.net/license/>.        |
10
   |                                                                      |
11
   | SPDX-License-Identifier: BSD-3-Clause                                |
12
   +----------------------------------------------------------------------+
13
   | Authors: Nikita Popov <nikic@php.net>                                |
14
   +----------------------------------------------------------------------+
15
*/
16
17
#include "Optimizer/zend_optimizer_internal.h"
18
#include "Optimizer/scdf.h"
19
20
/* This defines a generic framework for sparse conditional dataflow propagation. The algorithm is
21
 * based on "Sparse conditional constant propagation" by Wegman and Zadeck. We're using a
22
 * generalized implementation as described in chapter 8.3 of the SSA book.
23
 *
24
 * Every SSA variable is associated with an element on a finite-height lattice, those value can only
25
 * ever be lowered during the operation of the algorithm. If a value is lowered all instructions and
26
 * phis using that value need to be reconsidered (this is done by adding the variable to a
27
 * worklist). For phi functions the result is computed by applying the meet operation to the
28
 * operands. This continues until a fixed point is reached.
29
 *
30
 * The algorithm is control-flow sensitive: All blocks except the start block are initially assumed
31
 * to be unreachable. When considering a branch instruction, we determine the feasible successors
32
 * based on the current state of the variable lattice. If a new edge becomes feasible we either have
33
 * to mark the successor block executable and consider all instructions in it, or, if the target is
34
 * already executable, we only have to reconsider the phi functions (as we only consider phi
35
 * operands which are associated with a feasible edge).
36
 *
37
 * The generic framework requires the definition of three functions:
38
 * * visit_instr() should recompute the lattice values of all SSA variables defined by an
39
 *   instruction.
40
 * * visit_phi() should recompute the lattice value of the SSA variable defined by the phi. While
41
 *   doing this it should only consider operands for which scfg_is_edge_feasible() returns true.
42
 * * get_feasible_successors() should determine the feasible successors for a branch instruction.
43
 *   Note that this callback only needs to handle conditional branches (with two successors).
44
 */
45
46
#if 0
47
#define DEBUG_PRINT(...) fprintf(stderr, __VA_ARGS__)
48
#else
49
#define DEBUG_PRINT(...)
50
#endif
51
52
0
void scdf_mark_edge_feasible(scdf_ctx *scdf, int from, int to) {
53
0
  uint32_t edge = scdf_edge(&scdf->ssa->cfg, from, to);
54
55
0
  if (zend_bitset_in(scdf->feasible_edges, edge)) {
56
    /* We already handled this edge */
57
0
    return;
58
0
  }
59
60
0
  DEBUG_PRINT("Marking edge %d->%d feasible\n", from, to);
61
0
  zend_bitset_incl(scdf->feasible_edges, edge);
62
63
0
  if (!zend_bitset_in(scdf->executable_blocks, to)) {
64
0
    if (!zend_bitset_in(scdf->block_worklist, to)) {
65
0
      DEBUG_PRINT("Adding block %d to worklist\n", to);
66
0
    }
67
0
    zend_bitset_incl(scdf->block_worklist, to);
68
0
  } else {
69
    /* Block is already executable, only a new edge became feasible.
70
     * Reevaluate phi nodes to account for changed source operands. */
71
0
    const zend_ssa_block *ssa_block = &scdf->ssa->blocks[to];
72
0
    for (const zend_ssa_phi *phi = ssa_block->phis; phi; phi = phi->next) {
73
0
      zend_bitset_excl(scdf->phi_var_worklist, phi->ssa_var);
74
0
      scdf->handlers.visit_phi(scdf, phi);
75
0
    }
76
0
  }
77
0
}
78
79
1
void scdf_init(zend_optimizer_ctx *ctx, scdf_ctx *scdf, zend_op_array *op_array, zend_ssa *ssa) {
80
1
  scdf->op_array = op_array;
81
1
  scdf->ssa = ssa;
82
83
1
  scdf->instr_worklist_len = zend_bitset_len(op_array->last);
84
1
  scdf->phi_var_worklist_len = zend_bitset_len(ssa->vars_count);
85
1
  scdf->block_worklist_len = zend_bitset_len(ssa->cfg.blocks_count);
86
87
1
  scdf->instr_worklist = zend_arena_calloc(&ctx->arena,
88
1
    scdf->instr_worklist_len + scdf->phi_var_worklist_len + 2 * scdf->block_worklist_len + zend_bitset_len(ssa->cfg.edges_count),
89
1
    sizeof(zend_ulong));
90
91
1
  scdf->phi_var_worklist = scdf->instr_worklist + scdf->instr_worklist_len;
92
1
  scdf->block_worklist = scdf->phi_var_worklist + scdf->phi_var_worklist_len;
93
1
  scdf->executable_blocks = scdf->block_worklist + scdf->block_worklist_len;
94
1
  scdf->feasible_edges = scdf->executable_blocks + scdf->block_worklist_len;
95
96
1
  zend_bitset_incl(scdf->block_worklist, 0);
97
1
  zend_bitset_incl(scdf->executable_blocks, 0);
98
1
}
99
100
1
void scdf_solve(scdf_ctx *scdf, const char *name) {
101
1
  const zend_ssa *ssa = scdf->ssa;
102
1
  DEBUG_PRINT("Start SCDF solve (%s)\n", name);
103
2
  while (!zend_bitset_empty(scdf->instr_worklist, scdf->instr_worklist_len)
104
2
    || !zend_bitset_empty(scdf->phi_var_worklist, scdf->phi_var_worklist_len)
105
2
    || !zend_bitset_empty(scdf->block_worklist, scdf->block_worklist_len)
106
1
  ) {
107
1
    int i;
108
1
    while ((i = zend_bitset_pop_first(scdf->phi_var_worklist, scdf->phi_var_worklist_len)) >= 0) {
109
0
      const zend_ssa_phi *phi = ssa->vars[i].definition_phi;
110
0
      ZEND_ASSERT(phi);
111
0
      if (zend_bitset_in(scdf->executable_blocks, phi->block)) {
112
0
        scdf->handlers.visit_phi(scdf, phi);
113
0
      }
114
0
    }
115
116
1
    while ((i = zend_bitset_pop_first(scdf->instr_worklist, scdf->instr_worklist_len)) >= 0) {
117
0
      int block_num = ssa->cfg.map[i];
118
0
      if (zend_bitset_in(scdf->executable_blocks, block_num)) {
119
0
        zend_basic_block *block = &ssa->cfg.blocks[block_num];
120
0
        zend_op *opline = &scdf->op_array->opcodes[i];
121
0
        zend_ssa_op *ssa_op = &ssa->ops[i];
122
0
        if (opline->opcode == ZEND_OP_DATA) {
123
0
          opline--;
124
0
          ssa_op--;
125
0
        }
126
0
        scdf->handlers.visit_instr(scdf, opline, ssa_op);
127
0
        if (i == block->start + block->len - 1) {
128
0
          if (block->successors_count == 1) {
129
0
            scdf_mark_edge_feasible(scdf, block_num, block->successors[0]);
130
0
          } else if (block->successors_count > 1) {
131
0
            scdf->handlers.mark_feasible_successors(scdf, block_num, block, opline, ssa_op);
132
0
          }
133
0
        }
134
0
      }
135
0
    }
136
137
2
    while ((i = zend_bitset_pop_first(scdf->block_worklist, scdf->block_worklist_len)) >= 0) {
138
      /* This block is now live. Interpret phis and instructions in it. */
139
1
      zend_basic_block *block = &ssa->cfg.blocks[i];
140
1
      const zend_ssa_block *ssa_block = &ssa->blocks[i];
141
142
1
      DEBUG_PRINT("Pop block %d from worklist\n", i);
143
1
      zend_bitset_incl(scdf->executable_blocks, i);
144
145
1
      for (const zend_ssa_phi *phi = ssa_block->phis; phi; phi = phi->next) {
146
0
        zend_bitset_excl(scdf->phi_var_worklist, phi->ssa_var);
147
0
        scdf->handlers.visit_phi(scdf, phi);
148
0
      }
149
150
1
      if (block->len == 0) {
151
        /* Zero length blocks don't have a last instruction that would normally do this */
152
0
        scdf_mark_edge_feasible(scdf, i, block->successors[0]);
153
1
      } else {
154
1
        zend_op *opline = NULL;
155
1
        uint32_t j, end = block->start + block->len;
156
2
        for (j = block->start; j < end; j++) {
157
1
          opline = &scdf->op_array->opcodes[j];
158
1
          zend_bitset_excl(scdf->instr_worklist, j);
159
1
          if (opline->opcode != ZEND_OP_DATA) {
160
1
            scdf->handlers.visit_instr(scdf, opline, &ssa->ops[j]);
161
1
          }
162
1
        }
163
1
        if (block->successors_count == 1) {
164
0
          scdf_mark_edge_feasible(scdf, i, block->successors[0]);
165
1
        } else if (block->successors_count > 1) {
166
0
          ZEND_ASSERT(opline && "Should have opline in non-empty block");
167
0
          if (opline->opcode == ZEND_OP_DATA) {
168
0
            opline--;
169
0
            j--;
170
0
          }
171
0
          scdf->handlers.mark_feasible_successors(scdf, i, block, opline, &ssa->ops[j-1]);
172
0
        }
173
1
      }
174
1
    }
175
1
  }
176
1
}
177
178
/* If a live range starts in a reachable block and ends in an unreachable block, we should
179
 * not eliminate the latter. While it cannot be reached, the FREE opcode of the loop var
180
 * is necessary for the correctness of temporary compaction. */
181
static bool is_live_loop_var_free(
182
0
    const scdf_ctx *scdf, const zend_op *opline, const zend_ssa_op *ssa_op) {
183
0
  if (!zend_optimizer_is_loop_var_free(opline)) {
184
0
    return false;
185
0
  }
186
187
0
  int var = ssa_op->op1_use;
188
0
  if (var < 0) {
189
0
    return false;
190
0
  }
191
192
0
  const zend_ssa_var *ssa_var = &scdf->ssa->vars[var];
193
0
  uint32_t def_block;
194
0
  if (ssa_var->definition >= 0) {
195
0
    def_block = scdf->ssa->cfg.map[ssa_var->definition];
196
0
  } else {
197
0
    def_block = ssa_var->definition_phi->block;
198
0
  }
199
0
  return zend_bitset_in(scdf->executable_blocks, def_block);
200
0
}
201
202
0
static bool kept_alive_by_loop_var_free(const scdf_ctx *scdf, const zend_basic_block *block) {
203
0
  const zend_op_array *op_array = scdf->op_array;
204
0
  const zend_cfg *cfg = &scdf->ssa->cfg;
205
0
  if (!(cfg->flags & ZEND_FUNC_FREE_LOOP_VAR)) {
206
0
    return false;
207
0
  }
208
209
0
  for (uint32_t i = block->start; i < block->start + block->len; i++) {
210
0
    if (is_live_loop_var_free(scdf, &op_array->opcodes[i], &scdf->ssa->ops[i])) {
211
0
      return true;
212
0
    }
213
0
  }
214
0
  return false;
215
0
}
216
217
0
static uint32_t cleanup_loop_var_free_block(const scdf_ctx *scdf, const zend_basic_block *block) {
218
0
  zend_ssa *ssa = scdf->ssa;
219
0
  const zend_op_array *op_array = scdf->op_array;
220
0
  const zend_cfg *cfg = &ssa->cfg;
221
0
  int block_num = block - cfg->blocks;
222
0
  uint32_t removed_ops = 0;
223
224
  /* Removes phi nodes */
225
0
  for (zend_ssa_phi *phi = ssa->blocks[block_num].phis; phi; phi = phi->next) {
226
0
    zend_ssa_remove_uses_of_var(ssa, phi->ssa_var);
227
0
    zend_ssa_remove_phi(ssa, phi);
228
0
  }
229
230
0
  for (uint32_t i = block->start; i < block->start + block->len; i++) {
231
0
    zend_op *opline = &op_array->opcodes[i];
232
0
    zend_ssa_op *ssa_op = &scdf->ssa->ops[i];
233
0
    if (opline->opcode == ZEND_NOP
234
0
     || is_live_loop_var_free(scdf, opline, ssa_op)) {
235
0
      continue;
236
0
    }
237
238
    /* While we have to preserve the loop var free, we can still remove other instructions
239
     * in the block. */
240
0
    zend_ssa_remove_defs_of_instr(ssa, ssa_op);
241
0
    zend_ssa_remove_instr(ssa, opline, ssa_op);
242
0
    removed_ops++;
243
0
  }
244
245
0
  zend_ssa_remove_block_from_cfg(ssa, block_num);
246
247
0
  return removed_ops;
248
0
}
249
250
/* Removes unreachable blocks. This will remove both the instructions (and phis) in the
251
 * blocks, as well as remove them from the successor / predecessor lists and mark them
252
 * unreachable. Blocks already marked unreachable are not removed. */
253
1
uint32_t scdf_remove_unreachable_blocks(const scdf_ctx *scdf) {
254
1
  zend_ssa *ssa = scdf->ssa;
255
1
  uint32_t removed_ops = 0;
256
2
  for (uint32_t i = 0; i < ssa->cfg.blocks_count; i++) {
257
1
    const zend_basic_block *block = &ssa->cfg.blocks[i];
258
1
    if (!zend_bitset_in(scdf->executable_blocks, i) && (block->flags & ZEND_BB_REACHABLE)) {
259
0
      if (!kept_alive_by_loop_var_free(scdf, block)) {
260
0
        removed_ops += block->len;
261
0
        zend_ssa_remove_block(scdf->op_array, ssa, i);
262
0
      } else {
263
0
        removed_ops += cleanup_loop_var_free_block(scdf, block);
264
0
      }
265
0
    }
266
1
  }
267
1
  return removed_ops;
268
1
}