1 /*
2 * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package jdk.incubator.code.analysis;
27
28 import jdk.incubator.code.*;
29 import jdk.incubator.code.dialect.core.CoreOp;
30
31 import java.util.*;
32 import java.util.stream.Collectors;
33
34 /**
35 * An implementation of SSA construction based on
36 * "Efficiently Computing Static Single Assignment Form and the Control Dependence Graph" by Ron Cytron et. al.
37 * <p>
38 * This implementation contains some adaptions, notably:
39 * <ul>
40 * <li>Adapt to block parameters rather than phi functions.</li>
41 * <li>Adapt to work with multiple bodies.</li>
42 * </ul>
43 */
44 final class SSACytron {
45 private SSACytron() {
46 }
47
48 /**
49 * Applies an SSA transformation to an operation with bodies, replacing operations that declare variables and
50 * access them with the use of values they depend on or additional block parameters.
51 * <p>
52 * The operation should first be in lowered form before applying this transformation.
53 * <p>
54 * Note: this implementation does not currently work correctly when a variable is stored to within an exception
55 * region and read from outside as a result of catching an exception. In such cases a complete transformation may be
56 * not possible and such variables will need to be retained.
57 *
58 * @param nestedOp the operation with bodies
59 * @return the transformed operation
60 * @param <T> the operation type
61 */
62 static <T extends Op & Op.Nested> T transform(T nestedOp) {
63 Map<Block, Set<CoreOp.VarOp>> joinPoints = new HashMap<>();
64 Map<CoreOp.VarAccessOp.VarLoadOp, Object> loadValues = new HashMap<>();
65 Map<Block.Reference, List<Object>> joinSuccessorValues = new HashMap<>();
66
67 Map<Body, Boolean> visited = new HashMap<>();
68 Map<Block, Map<CoreOp.VarOp, Block.Parameter>> joinBlockArguments = new HashMap<>();
69 @SuppressWarnings("unchecked")
70 T ssaOp = (T) nestedOp.transform(CopyContext.create(), (block, op) -> {
71 // Compute join points and value mappings for body
72 visited.computeIfAbsent(op.ancestorBody(), b -> {
73 findJoinPoints(b, joinPoints);
74 variableToValue(b, joinPoints, loadValues, joinSuccessorValues);
75 return true;
76 });
77
78 if (op instanceof CoreOp.VarOp || op instanceof CoreOp.VarAccessOp) {
79 // Drop var operations
80 if (op instanceof CoreOp.VarAccessOp.VarLoadOp vl) {
81 // Replace result of load
82 Object loadValue = loadValues.get(vl);
83 CopyContext cc = block.context();
84 Value v = loadValue instanceof VarOpBlockArgument vba
85 ? joinBlockArguments.get(vba.b()).get(vba.vop())
86 : cc.getValue((Value) loadValue);
87 cc.mapValue(op.result(), v);
88 }
89 } else if (op instanceof Op.Terminating) {
90 for (Block.Reference s : op.successors()) {
91 List<Object> joinValues = joinSuccessorValues.get(s);
92 // Successor has join values
93 if (joinValues != null) {
94 CopyContext cc = block.context();
95
96 // Lazily append target block arguments
97 joinBlockArguments.computeIfAbsent(s.targetBlock(), b -> {
98 Block.Builder bb = cc.getBlock(b);
99 return joinPoints.get(b).stream().collect(Collectors.toMap(
100 varOp -> varOp,
101 varOp -> bb.parameter(varOp.varValueType())));
102 });
103
104 // Append successor arguments
105 List<Value> values = new ArrayList<>();
106 for (Object o : joinValues) {
107 Value v = o instanceof VarOpBlockArgument vba
108 ? joinBlockArguments.get(vba.b()).get(vba.vop())
109 : cc.getValue((Value) o);
110 values.add(v);
111 }
112
113 // Map successor with append arguments
114 List<Value> toArgs = cc.getValues(s.arguments());
115 toArgs.addAll(values);
116 Block.Reference toS = cc.getBlock(s.targetBlock()).successor(toArgs);
117 cc.mapSuccessor(s, toS);
118 }
119 }
120
121 block.op(op);
122 } else {
123 block.op(op);
124 }
125
126 return block;
127 });
128 return ssaOp;
129 }
130
131 record VarOpBlockArgument(Block b, CoreOp.VarOp vop) {
132 }
133
134 enum Uninitialized {
135 UNINITIALIZED;
136 }
137
138 // @@@ Check for var uses in exception regions
139 // A variable cannot be converted to SAA form if the variable is stored
140 // to in an exception region and accessed from an associated catch region
141
142 static void variableToValue(Body body,
143 Map<Block, Set<CoreOp.VarOp>> joinPoints,
144 Map<CoreOp.VarAccessOp.VarLoadOp, Object> loadValues,
145 Map<Block.Reference, List<Object>> joinSuccessorValues) {
146 Map<CoreOp.VarOp, Deque<Object>> variableStack = new HashMap<>();
147 Node top = buildDomTree(body.entryBlock(), body.immediateDominators());
148 variableToValue(top, variableStack, joinPoints, loadValues, joinSuccessorValues);
149 }
150
151 /**
152 * Replaces usages of a variable with the corresponding value, from a given block node in the dominator tree.
153 * <p>
154 * The result of a {@code VarLoadOp} for variable, {@code V} say the result of a {@code VarOp} operation,
155 * is replaced with the value passed as an operand to the immediately dominating {@code VarStoreOp} that operates
156 * on {@code V}, or a block argument representing the equivalent of a phi-value of {@code V}.
157 * After which, any related {@code VarOp}, {@code VarLoadOp}, or {@code VarStoreOp} operations are removed.
158 *
159 * @param n the node in the dominator tree
160 * @param variableStack the variable stack
161 * @param joinPoints the join points
162 * @implNote See "Efficiently Computing Static Single Assignment Form and the Control Dependence Graph" by Ron Cytron et. al.
163 * Section 5.2 and Figure 12.
164 */
165 static void variableToValue(Node n,
166 Map<CoreOp.VarOp, Deque<Object>> variableStack,
167 Map<Block, Set<CoreOp.VarOp>> joinPoints,
168 Map<CoreOp.VarAccessOp.VarLoadOp, Object> loadValues,
169 Map<Block.Reference, List<Object>> joinSuccessorValues) {
170 int size = n.b().ops().size();
171
172 // Check if V is associated with block argument (phi)
173 // Push argument onto V's stack
174 {
175 Set<CoreOp.VarOp> varOps = joinPoints.get(n.b());
176 if (varOps != null) {
177 varOps.forEach(v -> {
178 assert variableStack.containsKey(v);
179 variableStack.get(v).push(new VarOpBlockArgument(n.b(), v));
180 });
181 }
182 }
183
184 {
185 for (int i = 0; i < size - 1; i++) {
186 Op op = n.b().ops().get(i);
187
188 if (op instanceof CoreOp.VarOp varOp) {
189 // Initial value assigned to variable
190 Object current = varOp.isUninitialized()
191 ? Uninitialized.UNINITIALIZED
192 : op.operands().get(0);
193 assert !variableStack.containsKey(varOp);
194 variableStack.computeIfAbsent(varOp, _ -> new ArrayDeque<>())
195 .push(current);
196 } else if (op instanceof CoreOp.VarAccessOp.VarStoreOp storeOp) {
197 // Value assigned to variable
198 Value current = op.operands().get(1);
199 variableStack.get(storeOp.varOp()).push(current);
200 } else if (op instanceof CoreOp.VarAccessOp.VarLoadOp loadOp &&
201 loadOp.varOp().ancestorBody() == op.ancestorBody()) {
202 Object to = peekAtCurrentVariable(variableStack, loadOp.varOp());
203 loadValues.put(loadOp, to);
204 } else if (op instanceof Op.Nested) {
205 // Traverse descendant variable loads for variables
206 // declared in the block's parent body
207 op.traverse(null, (_, ce) -> {
208 if (ce instanceof CoreOp.VarAccessOp.VarLoadOp loadOp &&
209 loadOp.varOp().ancestorBody() == op.ancestorBody()) {
210 Object to = peekAtCurrentVariable(variableStack, loadOp.varOp());
211 loadValues.put(loadOp, to);
212 }
213 return null;
214 });
215 }
216 }
217
218 // Add successor args for joint points
219 for (Block.Reference succ : n.b().successors()) {
220 Set<CoreOp.VarOp> varOps = joinPoints.get(succ.targetBlock());
221 if (varOps != null) {
222 List<Object> joinValues = varOps.stream()
223 .map(vop -> peekAtCurrentVariable(variableStack, vop)).toList();
224 joinSuccessorValues.put(succ, joinValues);
225 }
226 }
227
228 // The result of a VarOp, a variable value, can only be used in VarStoreOp and VarLoadOp
229 // therefore there is no need to check existing successor arguments
230 }
231
232 // Traverse children of dom tree
233 for (Node y : n.children()) {
234 variableToValue(y, variableStack, joinPoints, loadValues, joinSuccessorValues);
235 }
236
237 // Pop off values for variables
238 {
239 Set<CoreOp.VarOp> varOps = joinPoints.get(n.b());
240 if (varOps != null) {
241 varOps.forEach(v -> {
242 variableStack.get(v).pop();
243 });
244 }
245
246 for (int i = 0; i < size - 1; i++) {
247 Op op = n.b().ops().get(i);
248
249 if (op instanceof CoreOp.VarOp varOp) {
250 variableStack.get(varOp).pop();
251 } else if (op instanceof CoreOp.VarAccessOp.VarStoreOp storeOp) {
252 variableStack.get(storeOp.varOp()).pop();
253 }
254 }
255 }
256 }
257
258 static Object peekAtCurrentVariable(Map<CoreOp.VarOp, Deque<Object>> variableStack, CoreOp.VarOp vop) {
259 Object to = variableStack.get(vop).peek();
260 return throwIfUninitialized(vop, to);
261 }
262
263 static Object throwIfUninitialized(CoreOp.VarOp vop, Object to) {
264 if (to instanceof Uninitialized) {
265 throw new IllegalStateException("Loading from uninitialized variable: " + vop);
266 }
267 return to;
268 }
269
270 /**
271 * Finds the join points of a body.
272 * <p>
273 * A join point is a block that is in the dominance frontier of one or more predecessors, that make one or more
274 * stores to variables (using the {@code VarStoreOp} operation on the result of a {@code VarOp} operation).
275 * The join point contains the set variables ({@code VarOp} operations) that are stored to.
276 * <p>
277 * A variable of a joint point indicates that a block argument may need to be added to the join point's block
278 * when converting variables to SSA form. Different values of a variable may occur at different control flow
279 * paths at the join point. The block argument represents the convergence of multiple values for the same
280 * variable, where a predecessor assigns to the block argument.
281 * (Block arguments are equivalent to phi-values, or phi-nodes, used in other representations.)
282 *
283 * @param body the body.
284 * @param joinPoints the returned join points.
285 * @implNote See "Efficiently Computing Static Single Assignment Form and the Control Dependence Graph" by Ron Cytron et. al.
286 * Section 5.1 and Figure 11.
287 */
288 static void findJoinPoints(Body body, Map<Block, Set<CoreOp.VarOp>> joinPoints) {
289 Map<Block, Set<Block>> df = body.dominanceFrontier();
290 Map<CoreOp.VarOp, Set<Block>> a = findVarStores(body);
291
292 int iterCount = 0;
293 int[] hasAlready = new int[body.blocks().size()];
294 int[] work = new int[body.blocks().size()];
295
296 Deque<Block> w = new ArrayDeque<>();
297
298 for (CoreOp.VarOp v : a.keySet()) {
299 iterCount++;
300
301 for (Block x : a.get(v)) {
302 work[x.index()] = iterCount;
303 w.push(x);
304 }
305
306 while (!w.isEmpty()) {
307 Block x = w.pop();
308
309 for (Block y : df.getOrDefault(x, Set.of())) {
310 if (hasAlready[y.index()] < iterCount) {
311 // Only add to the join points if y is dominated by the var's block
312 if (y.isDominatedBy(v.ancestorBlock())) {
313 joinPoints.computeIfAbsent(y, _k -> new LinkedHashSet<>()).add(v);
314 }
315 hasAlready[y.index()] = iterCount;
316
317 if (work[y.index()] < iterCount) {
318 work[y.index()] = iterCount;
319 w.push(y);
320 }
321 }
322 }
323 }
324 }
325 }
326
327 // Returns map of variable to blocks that contain stores to the variables declared in the body
328 // Throws ISE if a descendant store operation is encountered
329 // @@@ Compute map for whole tree, then traverse keys with filter
330 static Map<CoreOp.VarOp, Set<Block>> findVarStores(Body r) {
331 return r.traverse(new LinkedHashMap<>(), CodeElement.opVisitor((stores, op) -> {
332 if (op instanceof CoreOp.VarAccessOp.VarStoreOp storeOp) {
333 if (storeOp.varOp().ancestorBody() != storeOp.ancestorBody()) {
334 throw new IllegalStateException("Descendant variable store operation");
335 }
336 if (storeOp.varOp().ancestorBody() == r) {
337 stores.computeIfAbsent(storeOp.varOp(), _v -> new LinkedHashSet<>()).add(storeOp.ancestorBlock());
338 }
339 }
340 return stores;
341 }));
342 }
343
344 record Node(Block b, Set<Node> children) {
345 }
346
347 static Node buildDomTree(Block entryBlock, Map<Block, Block> idoms) {
348 Map<Block, Node> tree = new HashMap<>();
349 for (Map.Entry<Block, Block> e : idoms.entrySet()) {
350 Block id = e.getValue();
351 Block b = e.getKey();
352
353 Node parent = tree.computeIfAbsent(id, _k -> new Node(_k, new HashSet<>()));
354 if (b == entryBlock) {
355 continue;
356 }
357
358 Node child = tree.computeIfAbsent(b, _k -> new Node(_k, new HashSet<>()));
359 parent.children.add(child);
360 }
361 return tree.get(entryBlock);
362 }
363 }