Coverage Report

Created: 2026-03-31 06:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/spirv-tools/source/opt/propagator.h
Line
Count
Source
1
// Copyright (c) 2017 Google Inc.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
#ifndef SOURCE_OPT_PROPAGATOR_H_
16
#define SOURCE_OPT_PROPAGATOR_H_
17
18
#include <functional>
19
#include <queue>
20
#include <set>
21
#include <unordered_map>
22
#include <unordered_set>
23
#include <utility>
24
#include <vector>
25
26
#include "source/opt/ir_context.h"
27
#include "source/opt/module.h"
28
#include "source/opt/pass.h"
29
30
namespace spvtools {
31
namespace opt {
32
33
// Represents a CFG control edge.
34
struct Edge {
35
2.68M
  Edge(BasicBlock* b1, BasicBlock* b2) : source(b1), dest(b2) {
36
2.68M
    assert(source && "CFG edges cannot have a null source block.");
37
2.68M
    assert(dest && "CFG edges cannot have a null destination block.");
38
2.68M
  }
39
  BasicBlock* source;
40
  BasicBlock* dest;
41
19.4M
  bool operator<(const Edge& o) const {
42
19.4M
    return std::make_pair(source->id(), dest->id()) <
43
19.4M
           std::make_pair(o.source->id(), o.dest->id());
44
19.4M
  }
45
};
46
47
// This class implements a generic value propagation algorithm based on the
48
// conditional constant propagation algorithm proposed in
49
//
50
//      Constant propagation with conditional branches,
51
//      Wegman and Zadeck, ACM TOPLAS 13(2):181-210.
52
//
53
//      A Propagation Engine for GCC
54
//      Diego Novillo, GCC Summit 2005
55
//      http://ols.fedoraproject.org/GCC/Reprints-2005/novillo-Reprint.pdf
56
//
57
// The purpose of this implementation is to act as a common framework for any
58
// transformation that needs to propagate values from statements producing new
59
// values to statements using those values.  Simulation proceeds as follows:
60
//
61
// 1- Initially, all edges of the CFG are marked not executable and the CFG
62
//    worklist is seeded with all the statements in the entry basic block.
63
//
64
// 2- Every instruction I is simulated by calling a pass-provided function
65
//    |visit_fn|. This function is responsible for three things:
66
//
67
//    (a) Keep a value table of interesting values.  This table maps SSA IDs to
68
//        their values.  For instance, when implementing constant propagation,
69
//        given a store operation 'OpStore %f %int_3', |visit_fn| should assign
70
//        the value 3 to the table slot for %f.
71
//
72
//        In general, |visit_fn| will need to use the value table to replace its
73
//        operands, fold the result and decide whether a new value needs to be
74
//        stored in the table. |visit_fn| should only create a new mapping in
75
//        the value table if all the operands in the instruction are known and
76
//        present in the value table.
77
//
78
//    (b) Return a status indicator to direct the propagator logic.  Once the
79
//        instruction is simulated, the propagator needs to know whether this
80
//        instruction produced something interesting.  This is indicated via
81
//        |visit_fn|'s return value:
82
//
83
//         SSAPropagator::kNotInteresting: Instruction I produces nothing of
84
//             interest and does not affect any of the work lists.  The
85
//             propagator will visit the statement again if any of its operands
86
//             produce an interesting value in the future.
87
//
88
//             |visit_fn| should always return this value when it is not sure
89
//             whether the instruction will produce an interesting value in the
90
//             future or not.  For instance, for constant propagation, an OpIAdd
91
//             instruction may produce a constant if its two operands are
92
//             constant, but the first time we visit the instruction, we still
93
//             may not have its operands in the value table.
94
//
95
//         SSAPropagator::kVarying: The value produced by I cannot be determined
96
//             at compile time.  Further simulation of I is not required.  The
97
//             propagator will not visit this instruction again.  Additionally,
98
//             the propagator will add all the instructions at the end of SSA
99
//             def-use edges to be simulated again.
100
//
101
//             If I is a basic block terminator, it will mark all outgoing edges
102
//             as executable so they are traversed one more time.  Eventually
103
//             the kVarying attribute will be spread out to all the data and
104
//             control dependents for I.
105
//
106
//             It is important for propagation to use kVarying as a bottom value
107
//             for the propagation lattice.  It should never be possible for an
108
//             instruction to return kVarying once and kInteresting on a second
109
//             visit.  Otherwise, propagation would not stabilize.
110
//
111
//         SSAPropagator::kInteresting: Instruction I produces a value that can
112
//             be computed at compile time.  In this case, |visit_fn| should
113
//             create a new mapping between I's result ID and the produced
114
//             value.  Much like the kNotInteresting case, the propagator will
115
//             visit this instruction again if any of its operands changes.
116
//             This is useful when the statement changes from one interesting
117
//             state to another.
118
//
119
//    (c) For conditional branches, |visit_fn| may decide which edge to take out
120
//        of I's basic block.  For example, if the operand for an OpSwitch is
121
//        known to take a specific constant value, |visit_fn| should figure out
122
//        the destination basic block and pass it back by setting the second
123
//        argument to |visit_fn|.
124
//
125
//    At the end of propagation, values in the value table are guaranteed to be
126
//    stable and can be replaced in the IR.
127
//
128
// 3- The propagator keeps two work queues.  Instructions are only added to
129
//    these queues if they produce an interesting or varying value. None of this
130
//    should be handled by |visit_fn|. The propagator keeps track of this
131
//    automatically (see SSAPropagator::Simulate for implementation).
132
//
133
//      CFG blocks: contains the queue of blocks to be simulated.
134
//             Blocks are added to this queue if their incoming edges are
135
//             executable.
136
//
137
//      SSA Edges: An SSA edge is a def-use edge between a value-producing
138
//              instruction and its use instruction.  The SSA edges list
139
//              contains the statements at the end of a def-use edge that need
140
//              to be re-visited when an instruction produces a kVarying or
141
//              kInteresting result.
142
//
143
// 4- Simulation terminates when all work queues are drained.
144
//
145
//
146
// EXAMPLE: Basic constant store propagator.
147
//
148
// Suppose we want to propagate all constant assignments of the form "OpStore
149
// %id %cst" where "%id" is some variable and "%cst" an OpConstant.  The
150
// following code builds a table |values| where every id that was assigned a
151
// constant value is mapped to the constant value it was assigned.
152
//
153
//   auto ctx = BuildModule(...);
154
//   std::map<uint32_t, uint32_t> values;
155
//   const auto visit_fn = [&ctx, &values](Instruction* instr,
156
//                                         BasicBlock** dest_bb) {
157
//     if (instr->opcode() == spv::Op::OpStore) {
158
//       uint32_t rhs_id = instr->GetSingleWordOperand(1);
159
//       Instruction* rhs_def = ctx->get_def_use_mgr()->GetDef(rhs_id);
160
//       if (rhs_def->opcode() == spv::Op::OpConstant) {
161
//         uint32_t val = rhs_def->GetSingleWordOperand(2);
162
//         values[rhs_id] = val;
163
//         return SSAPropagator::kInteresting;
164
//       }
165
//     }
166
//     return SSAPropagator::kVarying;
167
//   };
168
//   SSAPropagator propagator(ctx.get(), &cfg, visit_fn);
169
//   propagator.Run(&fn);
170
//
171
// Given the code:
172
//
173
//       %int_4 = OpConstant %int 4
174
//       %int_3 = OpConstant %int 3
175
//       %int_1 = OpConstant %int 1
176
//                OpStore %x %int_4
177
//                OpStore %y %int_3
178
//                OpStore %z %int_1
179
//
180
// After SSAPropagator::Run returns, the |values| map will contain the entries:
181
// values[%x] = 4, values[%y] = 3, and, values[%z] = 1.
182
class SSAPropagator {
183
 public:
184
  // Lattice values used for propagation. See class documentation for
185
  // a description.
186
  enum PropStatus { kNotInteresting, kInteresting, kVarying, kFailed };
187
188
  using VisitFunction = std::function<PropStatus(Instruction*, BasicBlock**)>;
189
190
  SSAPropagator(IRContext* context, const VisitFunction& visit_fn)
191
17.5k
      : ctx_(context), visit_fn_(visit_fn) {}
192
193
  // Runs the propagator on function |fn|. Returns true if changes were made to
194
  // the function. Otherwise, it returns false. The user should check
195
  // IRContext::id_overflow() to see if there was an error caused by reaching
196
  // the max id.
197
  bool Run(Function* fn);
198
199
  // Returns true if the |i|th argument for |phi| comes through a CFG edge that
200
  // has been marked executable. |i| should be an index value accepted by
201
  // Instruction::GetSingleWordOperand.
202
  bool IsPhiArgExecutable(Instruction* phi, uint32_t i) const;
203
204
  // Returns true if |inst| has a recorded status. This will be true once |inst|
205
  // has been simulated once.
206
5.25M
  bool HasStatus(Instruction* inst) const { return statuses_.count(inst); }
207
208
  // Returns the current propagation status of |inst|. Assumes
209
  // |HasStatus(inst)| returns true.
210
2.70M
  PropStatus Status(Instruction* inst) const {
211
2.70M
    return statuses_.find(inst)->second;
212
2.70M
  }
213
214
  // Records the propagation status |status| for |inst|. Returns true if the
215
  // status for |inst| has changed or set was set for the first time.
216
  bool SetStatus(Instruction* inst, PropStatus status);
217
218
 private:
219
  // Initialize processing.
220
  void Initialize(Function* fn);
221
222
  // Simulate the execution |block| by calling |visit_fn_| on every instruction
223
  // in it.
224
  Pass::Status Simulate(BasicBlock* block);
225
226
  // Simulate the execution of |instr| by replacing all the known values in
227
  // every operand and determining whether the result is interesting for
228
  // propagation. This invokes the callback function |visit_fn_| to determine
229
  // the value computed by |instr|.
230
  Pass::Status Simulate(Instruction* instr);
231
232
  // Returns true if |instr| should be simulated again.
233
4.71M
  bool ShouldSimulateAgain(Instruction* instr) const {
234
4.71M
    return do_not_simulate_.find(instr) == do_not_simulate_.end();
235
4.71M
  }
236
237
  // Add |instr| to the set of instructions not to simulate again.
238
2.07M
  void DontSimulateAgain(Instruction* instr) { do_not_simulate_.insert(instr); }
239
240
  // Returns true if |block| has been simulated already.
241
3.51M
  bool BlockHasBeenSimulated(BasicBlock* block) const {
242
3.51M
    return simulated_blocks_.find(block) != simulated_blocks_.end();
243
3.51M
  }
244
245
  // Marks block |block| as simulated.
246
355k
  void MarkBlockSimulated(BasicBlock* block) {
247
355k
    simulated_blocks_.insert(block);
248
355k
  }
249
250
  // Marks |edge| as executable.  Returns false if the edge was already marked
251
  // as executable.
252
934k
  bool MarkEdgeExecutable(const Edge& edge) {
253
934k
    return executable_edges_.insert(edge).second;
254
934k
  }
255
256
  // Returns true if |edge| has been marked as executable.
257
1.04M
  bool IsEdgeExecutable(const Edge& edge) const {
258
1.04M
    return executable_edges_.find(edge) != executable_edges_.end();
259
1.04M
  }
260
261
  // Returns a pointer to the def-use manager for |ctx_|.
262
4.04M
  analysis::DefUseManager* get_def_use_mgr() const {
263
4.04M
    return ctx_->get_def_use_mgr();
264
4.04M
  }
265
266
  // If the CFG edge |e| has not been executed, this function adds |e|'s
267
  // destination block to the work list.
268
  void AddControlEdge(const Edge& e);
269
270
  // Adds all the instructions that use the result of |instr| to the SSA edges
271
  // work list. If |instr| produces no result id, this does nothing.
272
  void AddSSAEdges(Instruction* instr);
273
274
  // IR context to use.
275
  IRContext* ctx_;
276
277
  // Function that visits instructions during simulation. The output of this
278
  // function is used to determine if the simulated instruction produced a value
279
  // interesting for propagation. The function is responsible for keeping
280
  // track of interesting values by storing them in some user-provided map.
281
  VisitFunction visit_fn_;
282
283
  // SSA def-use edges to traverse. Each entry is a destination statement for an
284
  // SSA def-use edge as returned by |def_use_manager_|.
285
  std::queue<Instruction*> ssa_edge_uses_;
286
287
  // Blocks to simulate.
288
  std::queue<BasicBlock*> blocks_;
289
290
  // Blocks simulated during propagation.
291
  std::unordered_set<BasicBlock*> simulated_blocks_;
292
293
  // Set of instructions that should not be simulated again because they have
294
  // been found to be in the kVarying state.
295
  std::unordered_set<Instruction*> do_not_simulate_;
296
297
  // Map between a basic block and its predecessor edges.
298
  // TODO(dnovillo): Move this to CFG and always build them. Alternately,
299
  // move it to IRContext and build CFG preds/succs on-demand.
300
  std::unordered_map<BasicBlock*, std::vector<Edge>> bb_preds_;
301
302
  // Map between a basic block and its successor edges.
303
  // TODO(dnovillo): Move this to CFG and always build them. Alternately,
304
  // move it to IRContext and build CFG preds/succs on-demand.
305
  std::unordered_map<BasicBlock*, std::vector<Edge>> bb_succs_;
306
307
  // Set of executable CFG edges.
308
  std::set<Edge> executable_edges_;
309
310
  // Tracks instruction propagation status.
311
  std::unordered_map<Instruction*, SSAPropagator::PropStatus> statuses_;
312
};
313
314
std::ostream& operator<<(std::ostream& str,
315
                         const SSAPropagator::PropStatus& status);
316
317
}  // namespace opt
318
}  // namespace spvtools
319
320
#endif  // SOURCE_OPT_PROPAGATOR_H_