1 package jdk.incubator.code.analysis;
  2 
  3 import jdk.incubator.code.Block;
  4 import jdk.incubator.code.CodeElement;
  5 import jdk.incubator.code.CopyContext;
  6 import jdk.incubator.code.Op;
  7 import jdk.incubator.code.OpTransformer;
  8 import jdk.incubator.code.Value;
  9 import jdk.incubator.code.op.CoreOp;
 10 import java.util.ArrayList;
 11 import java.util.Collections;
 12 import java.util.HashMap;
 13 import java.util.HashSet;
 14 import java.util.LinkedHashSet;
 15 import java.util.List;
 16 import java.util.Map;
 17 import java.util.Objects;
 18 import java.util.SequencedSet;
 19 import java.util.Set;
 20 
 21 /**
 22  * This is an implementation of SSA construction based on
 23  * <a href="https://doi.org/10.1007/978-3-642-37051-9">
 24  * Simple end Efficient Construction of Static Single Assignment Form (pp 102-122)
 25  * </a>.
 26  * <p>
 27  * This implementation contains some adaptions, notably:
 28  * <ul>
 29  *     <li>Adapt to block parameters rather than phi functions.</li>
 30  *     <li>Adapt to work with multiple bodies.</li>
 31  * </ul>
 32  */
 33 public class SSAConstruction implements OpTransformer {
 34 
 35     private final Map<CoreOp.VarOp, Map<Block, Val>> currentDef = new HashMap<>();
 36     private final Set<Block> sealedBlocks = new HashSet<>();
 37     private final Map<Block, Map<CoreOp.VarOp, Phi>> incompletePhis = new HashMap<>();
 38 
 39     // according to the algorithm:
 40     // "As only filled blocks may have successors, predecessors are always filled."
 41     // In consequence, this means that only filled predecessors should be considered
 42     // when recursively searching for a definition
 43     private final Map<Block, SequencedSet<Block>> predecessors = new HashMap<>();
 44     // as we can't modify the graph at the same time as we analyze it,
 45     // we need to store which load op needs to remapped to which value
 46     private final Map<CoreOp.VarAccessOp.VarLoadOp, Val> loads = new HashMap<>();
 47     private final Map<Block, List<Phi>> additionalParameters = new HashMap<>();
 48     // as we look up definitions during the actual transformation again,
 49     // we might encounter deleted phis.
 50     // we use this set to be able to correct that during transformation
 51     private final Set<Phi> deletedPhis = new HashSet<>();
 52 
 53     public static <O extends Op> O transform(O nestedOp) {
 54         SSAConstruction construction = new SSAConstruction();
 55         construction.prepare(nestedOp);
 56         @SuppressWarnings("unchecked")
 57         O temp = (O) nestedOp.transform(CopyContext.create(), construction);
 58         return temp;
 59     }
 60 
 61     private SSAConstruction() {
 62     }
 63 
 64     private void prepare(Op nestedOp) {
 65         nestedOp.traverse(null, CodeElement.opVisitor((_, op) -> {
 66             switch (op) {
 67                 case CoreOp.VarAccessOp.VarLoadOp load -> {
 68                     Val val = readVariable(load.varOp(), load.parentBlock());
 69                     registerLoad(load, val);
 70                 }
 71                 case CoreOp.VarAccessOp.VarStoreOp store ->
 72                         writeVariable(store.varOp(), store.parentBlock(), new Holder(store.storeOperand()));
 73                 case CoreOp.VarOp initialStore -> {
 74                     Val val = initialStore.isUninitialized()
 75                             ? Uninitialized.VALUE
 76                             : new Holder(initialStore.initOperand());
 77                     writeVariable(initialStore, initialStore.parentBlock(), val);
 78                 }
 79                 case Op.Terminating _ -> {
 80                     Block block = op.parentBlock();
 81                     // handle the sealing, i.e. only now make this block a predecessor of its successors
 82                     for (Block.Reference successor : block.successors()) {
 83                         Block successorBlock = successor.targetBlock();
 84                         Set<Block> blocks = this.predecessors.computeIfAbsent(successorBlock, _ -> new LinkedHashSet<>());
 85                         blocks.add(block);
 86                         // if this was the last predecessor added to successorBlock, seal it
 87                         if (blocks.size() == successorBlock.predecessors().size()) {
 88                             sealBlock(successorBlock);
 89                         }
 90                     }
 91                 }
 92                 default -> {
 93                 }
 94             }
 95             return null;
 96         }));
 97     }
 98 
 99     private void registerLoad(CoreOp.VarAccessOp.VarLoadOp load, Val val) {
100         this.loads.put(load, val);
101         if (val instanceof Phi phi) {
102             phi.users.add(load);
103         }
104     }
105 
106     private void writeVariable(CoreOp.VarOp variable, Block block, Val value) {
107         this.currentDef.computeIfAbsent(variable, _ -> new HashMap<>()).put(block, value);
108     }
109 
110     private Val readVariable(CoreOp.VarOp variable, Block block) {
111         Val value = this.currentDef.getOrDefault(variable, Map.of()).get(block);
112         if (value == null
113             // deleted Phi, this is an old reference
114             // due to our 2-step variant of the original algorithm, we might encounter outdated definitions
115             // when we read to prepare block arguments
116             || value instanceof Phi phi && this.deletedPhis.contains(phi)) {
117             return readVariableRecursive(variable, block);
118         }
119         return value;
120     }
121 
122     private Val readVariableRecursive(CoreOp.VarOp variable, Block block) {
123         Val value;
124         if (!block.isEntryBlock() && !this.sealedBlocks.contains(block)) {
125             Phi phi = new Phi(variable, block);
126             value = phi;
127             this.incompletePhis.computeIfAbsent(block, _ -> new HashMap<>()).put(variable, phi);
128             this.additionalParameters.computeIfAbsent(block, _ -> new ArrayList<>()).add(phi);
129         } else if (block.isEntryBlock() && variable.ancestorBody() != block.parentBody()) {
130             // we are in an entry block but didn't find a definition yet
131             Block enclosingBlock = block.parent().parent().parent();
132             assert enclosingBlock != null : "def not found in entry block, with no enclosing block";
133             value = readVariable(variable, enclosingBlock);
134         } else if (this.predecessors.get(block).size() == 1) {
135             value = readVariable(variable, this.predecessors.get(block).getFirst());
136         } else {
137             Phi param = new Phi(variable, block);
138             writeVariable(variable, block, param);
139             value = addPhiOperands(variable, param);
140             // To go from Phis to block parameters, we remember that we produced a Phi here.
141             // This means that edges to this block need to pass a value via parameter
142             if (value == param) {
143                 this.additionalParameters.computeIfAbsent(block, _ -> new ArrayList<>()).add(param);
144             }
145         }
146         writeVariable(variable, block, value); // cache value for this variable + block
147         return value;
148     }
149 
150     private Val addPhiOperands(CoreOp.VarOp variable, Phi value) {
151         for (Block pred : this.predecessors.getOrDefault(value.block(), Collections.emptySortedSet())) {
152             value.appendOperand(readVariable(variable, pred));
153         }
154         return tryRemoveTrivialPhi(value);
155     }
156 
157     private Val tryRemoveTrivialPhi(Phi phi) {
158         Val same = null;
159         for (Val op : phi.operands()) {
160             if (op == same || op == phi) {
161                 continue;
162             }
163             if (same != null) {
164                 return phi;
165             }
166             same = op;
167         }
168         // we shouldn't have phis without operands (other than itself)
169         assert same != null : "phi without different operands";
170         List<Phi> phiUsers = phi.replaceBy(same, this);
171         List<Phi> phis = this.additionalParameters.get(phi.block());
172         if (phis != null) {
173             phis.remove(phi);
174         }
175         for (Phi user : phiUsers) {
176             tryRemoveTrivialPhi(user);
177         }
178         return same;
179     }
180 
181     private void sealBlock(Block block) {
182         this.incompletePhis.getOrDefault(block, Map.of()).forEach(this::addPhiOperands);
183         this.sealedBlocks.add(block);
184     }
185 
186     // only used during transformation
187 
188     private Value resolveValue(CopyContext context, Val val) {
189         return switch (val) {
190             case Uninitialized _ -> throw new IllegalStateException("Uninitialized variable");
191             case Holder holder -> context.getValueOrDefault(holder.value(), holder.value());
192             case Phi phi -> {
193                 List<Phi> phis = this.additionalParameters.get(phi.block());
194                 int additionalParameterIndex = phis.indexOf(phi);
195                 assert additionalParameterIndex >= 0 : "phi not in parameters " + phi;
196                 int index = additionalParameterIndex + phi.block().parameters().size();
197                 Block.Builder b = context.getBlock(phi.block());
198                 yield b.parameters().get(index);
199             }
200         };
201     }
202 
203     @Override
204     public Block.Builder apply(Block.Builder block, Op op) {
205         Block originalBlock = op.parentBlock();
206         CopyContext context = block.context();
207         switch (op) {
208             case CoreOp.VarAccessOp.VarLoadOp load -> {
209                 Val val = this.loads.get(load);
210                 context.mapValue(load.result(), resolveValue(context, val));
211             }
212             case CoreOp.VarOp _, CoreOp.VarAccessOp.VarStoreOp _ -> {
213             }
214             case Op.Terminating _ -> {
215                 // make sure outgoing branches are corrected
216                 for (Block.Reference successor : originalBlock.successors()) {
217                     Block successorBlock = successor.targetBlock();
218                     List<Phi> successorParams = this.additionalParameters.getOrDefault(successorBlock, List.of());
219                     List<Value> additionalParams = successorParams.stream()
220                             .map(phi -> readVariable(phi.variable, originalBlock))
221                             .map(val -> resolveValue(context, val))
222                             .toList();
223                     List<Value> values = context.getValues(successor.arguments());
224                     values.addAll(additionalParams);
225                     Block.Builder successorBlockBuilder = context.getBlock(successorBlock);
226                     context.mapSuccessor(successor, successorBlockBuilder.successor(values));
227                 }
228                 block.op(op);
229             }
230             default -> block.op(op);
231         }
232         return block;
233     }
234 
235     @Override
236     public void apply(Block.Builder block, Block b) {
237         // add the required additional parameters to this block
238         boolean isEntry = b.isEntryBlock();
239         for (Phi phi : this.additionalParameters.getOrDefault(b, List.of())) {
240             if (isEntry) {
241                 // Phis in entry blocks denote captured values. Do not add as param but make sure
242                 // the original value is used
243                 assert phi.operands().size() == 1 : "entry block phi with multiple operands";
244                 CopyContext context = block.context();
245                 context.mapValue(resolveValue(context, phi), resolveValue(context, phi.operands().getFirst()));
246             } else {
247                 block.parameter(phi.variable.varValueType());
248             }
249         }
250 
251         // actually visit ops in this block
252         OpTransformer.super.apply(block, b);
253     }
254 
255     sealed interface Val {
256     }
257 
258     record Holder(Value value) implements Val {
259     }
260 
261     enum Uninitialized implements Val {
262         VALUE;
263     }
264 
265     record Phi(CoreOp.VarOp variable, Block block, List<Val> operands, Set<Object> users) implements Val {
266         Phi(CoreOp.VarOp variable, Block block) {
267             this(variable, block, new ArrayList<>(), new HashSet<>());
268         }
269 
270         void appendOperand(Val val) {
271             this.operands.add(val);
272             if (val instanceof Phi phi) { // load op uses are added separately
273                 phi.users.add(this);
274             }
275         }
276 
277         @Override
278         public boolean equals(Object obj) {
279             return this == obj;
280         }
281 
282         @Override
283         public int hashCode() {
284             return Objects.hash(this.variable, this.block);
285         }
286 
287         public List<Phi> replaceBy(Val same, SSAConstruction construction) {
288             List<Phi> usingPhis = new ArrayList<>();
289             for (Object user : this.users) {
290                 if (user == this) {
291                     continue;
292                 }
293                 if (same instanceof Phi samePhi) {
294                     samePhi.users.add(user);
295                 }
296                 switch (user) {
297                     case Phi phi -> {
298                         int i = phi.operands.indexOf(this);
299                         assert i >= 0 : "use does not have this as operand";
300                         phi.operands.set(i, same);
301                         usingPhis.add(phi);
302                     }
303                     case CoreOp.VarAccessOp.VarLoadOp load -> construction.loads.put(load, same);
304                     default -> throw new UnsupportedOperationException(user + ":" + user.getClass());
305                 }
306             }
307             if (same instanceof Phi samePhi) {
308                 samePhi.users.remove(this);
309             }
310             construction.currentDef.get(this.variable).put(this.block, same);
311             construction.deletedPhis.add(this); // we might not replace all current defs, so mark this phi as deleted
312             this.users.clear();
313             return usingPhis;
314         }
315 
316         @Override
317         public String toString() {
318             return "Phi[" + variable.varName() + "(" + block.index() + ")," + "operands: " + operands.size() + "}";
319         }
320     }
321 }