24
} from "../HIR/visitors";
25
26
/**
27
- * Removes SSA form by creating unique variable declarations for the versions of each variables.
28
- * - Versions of a variable that do not flow into a phi node are each assigned their own const variable
29
- * declaration.
30
- * - If multiple versions of a variable flow into some phi node, then all those versions are reassigned to
31
- * the phi version. A single declaration is created at the same block scope as the phi, prior to any of the
32
- * assignments.
27
+ * Removes SSA form by converting all phis into explicit bindings and assignments. There are two main categories
28
+ * of phis:
29
+ *
30
+ * ## Reassignments (operands are independently memoizable)
31
+ *
32
+ * These are phis that occur after some high-level control flow such as an if, switch, or loop. These phis are rewritten
33
+ * to add a new `let` binding for the phi id prior to the control flow node (ie prior to the if/switch),
34
+ * and to add a reassignment to that let binding in each of the phi's predecessors.
35
+ *
36
+ * Example:
37
+ *
38
+ * ```javascript
39
+ * // Input
40
+ * let x1 = null;
41
+ * if (a) {
42
+ * x2 = b;
43
+ * } else {
44
+ * x3 = c;
45
+ * }
46
+ * x4 = phi(x2, x3);
47
+ * return x4;
48
+ *
49
+ * // Output
50
+ * const x1 = null;
51
+ * let x4; // synthesized binding for the phi identifier
52
+ * if (a) {
53
+ * x2 = b;
54
+ * x4 = x2;; // sythesized assignment to the phi identifier
55
+ * } else {
56
+ * x3 = c;
57
+ * x4 = x3; // synthesized assignment
58
+ * }
59
+ * // phi removed
60
+ * return x4;
61
+ * ```
62
+ *
63
+ * ## Rewrites (operands are not independently memoizable)
64
+ *
65
+ * Phis that occur inside loop constructs cannot use the reassignment strategy, because there isn't an appropriate place
66
+ * to add the new let binding. Instead, we select a single "canonical" id for these phis which is the operand that is
67
+ * defined first. Then, all assignments and references for any of the phi ir and operands are rewritten to reference
68
+ * the canonical id instead.
69
+ *
70
+ * Example:
71
+ *
72
+ * ```javascript
73
+ * // Input
74
+ * for (
75
+ * let i1 = 0;
76
+ * { i2 = phi(i1, i2); i2 < 10 }; // note the phi in the test block
77
+ * i2 += 1
78
+ * ) { ... }
79
+ *
80
+ * // Output
81
+ * for (
82
+ * let i1 = 0; // i1 is defined first, so it becomes the canonical id
83
+ * i1 < 10; // rewritten to canonical id
84
+ * i1 += 1 // rewritten to canonical id
85
+ * )
86
+ * ```
87
*/
88
export function leaveSSA(fn: HIRFunction) {
35
- // Maps identifiers that appear as a phi or phi operand to a single canonical identifier
36
- // for all instances.
37
- const variableMapping: Map<Identifier, Identifier> = new Map();
38
- const hasDeclaration: Set<Identifier> = new Set();
89
+ // For "memoizable" phis (see docblock), this maps the original identifiers to the identifier they
90
+ // should be reassigned to. The keys are phi operands, and the values are the phi id to which
91
+ // they are being explicitly reassigned.
92
+ const reassignments: Map<Identifier, Identifier> = new Map();
93
+
94
+ // For non-memoizable phis, this maps original identifiers to the identifier they should be
95
+ // *rewritten* to. The keys are the original identifiers, and the value will be _either_ the
96
+ // phi id or, more typically, the operand that was defined prior to the phi.
97
+ const rewrites: Map<Identifier, Identifier> = new Map();
98
99
for (const [, block] of fn.body.blocks) {
100
invariant(
102
"Expected all phis to be cleared by predecessors"
103
);
104
46
- // Identifiers (from phis) that *may* need a new `let` declaration created. If the original
47
- // variable declaration flows into the phi, then we can reuse its declaration - this is
48
- // discovered during iteration of instructions.
49
- const needsDeclaration: Set<Identifier> = new Set();
50
-
105
// Find any phi nodes which need a variable declaration in the current block
106
// This includes phis in fallthrough nodes, or blocks that form part of control flow
107
// such as for or while (and later if/switch).
54
- const phis: Array<Phi> = [];
108
+ const reassignmentPhis: Array<Phi> = [];
109
+ const rewritePhis: Array<Phi> = [];
110
const terminal = block.terminal;
111
if (
112
(terminal.kind === "if" ||
116
terminal.fallthrough !== null
117
) {
118
const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
64
- phis.push(...fallthrough.phis);
119
+ reassignmentPhis.push(...fallthrough.phis);
120
fallthrough.phis.clear();
121
}
122
if (terminal.kind === "while" || terminal.kind === "for") {
123
const test = fn.body.blocks.get(terminal.test)!;
69
- phis.push(...test.phis);
124
+ rewritePhis.push(...test.phis);
125
test.phis.clear();
126
127
const loop = fn.body.blocks.get(terminal.loop)!;
73
- phis.push(...loop.phis);
128
+ rewritePhis.push(...loop.phis);
129
loop.phis.clear();
130
}
131
if (terminal.kind === "for") {
132
const init = fn.body.blocks.get(terminal.init)!;
78
- phis.push(...init.phis);
133
+ rewritePhis.push(...init.phis);
134
init.phis.clear();
135
136
const update = fn.body.blocks.get(terminal.update)!;
82
- phis.push(...update.phis);
137
+ rewritePhis.push(...update.phis);
138
update.phis.clear();
139
140
// find declarations in the for init
141
for (const instr of init.instructions) {
142
if (instr.lvalue !== null && instr.lvalue.place.memberPath === null) {
88
- hasDeclaration.add(instr.lvalue.place.identifier);
143
+ // hasDeclaration.add(instr.lvalue.place.identifier);
144
}
145
}
146
}
147
93
- // For each phi, determine a canonical identifier to use for versions of the variable
94
- // that appear in the phi (as its output id and operands). If this is the first time
95
- // we're seeing the phi id, then we may need to generate a new variable declaration
96
- // Note that there can be multiple phi nodes for the same variable, we capture the
97
- // outermost scope by visiting predecessor blocks first.
98
- for (const phi of phis) {
99
- let canonicalId = variableMapping.get(phi.id);
148
+ for (const phi of reassignmentPhis) {
149
+ // In some cases one of the phi operands can be defined *before* the let binding
150
+ // we will generate. For example, a variable that is only rebound in one branch of
151
+ // an if but not another. In this case we populate the let binding with this initial
152
+ // value rather than generate an extra assignment.
153
+ let initOperand: Identifier | null = null;
154
+
155
+ // Determine the canonical id to use for this phi. In general this is the phi id,
156
+ // but if one phi flows into another as an operand, this will be the final phi.
157
+ let canonicalId = reassignments.get(phi.id);
158
+ if (canonicalId === undefined) {
159
+ canonicalId = phi.id;
160
+ canonicalId.mutableRange.start = Math.min(
161
+ canonicalId.mutableRange.start,
162
+ terminal.id
163
+ ) as InstructionId;
164
+ reassignments.set(phi.id, canonicalId);
165
+ }
166
+
167
+ // all versions of the variable need to be remapped to the canonical id
168
+ // also extend the mutable range of the canonical id based on the min/max
169
+ // of the ranges of its operands
170
+ let start = canonicalId.mutableRange.start as number;
171
+ let end = canonicalId.mutableRange.end as number;
172
+ for (const [, operand] of phi.operands) {
173
+ start = Math.min(start, operand.mutableRange.start);
174
+ end = Math.max(end, operand.mutableRange.end);
175
+ reassignments.set(operand, canonicalId);
176
+
177
+ if (operand.mutableRange.start < terminal.id) {
178
+ invariant(
179
+ initOperand === null,
180
+ "A phi cannot have two operands initialized before its declaration"
181
+ );
182
+ initOperand = operand;
183
+ }
184
+ }
185
+ canonicalId.mutableRange.start = makeInstructionId(start);
186
+ canonicalId.mutableRange.end = makeInstructionId(end);
187
+
188
+ // If this phi id is the canonical id we need to generate a let binding for it
189
+ // (otherwise, it means this phi merges into some other phi which already generated
190
+ // a binding
191
+ if (canonicalId === phi.id) {
192
+ const instr: Instruction = {
193
+ // NOTE: reuse the terminal id since these lets must be scoped with the terminal anyway.
194
+ // the mutable range of this canonical id must by definition span from the binding (before
195
+ // the if) to the phi, so it's safe to reuse the terminal's id.
196
+ id: block.terminal.id,
197
+ lvalue: {
198
+ place: {
199
+ kind: "Identifier",
200
+ memberPath: null,
201
+ identifier: canonicalId,
202
+ effect: Effect.Mutate,
203
+ loc: GeneratedSource,
204
+ },
205
+ kind: InstructionKind.Let,
206
+ },
207
+ value:
208
+ initOperand !== null
209
+ ? {
210
+ kind: "Identifier",
211
+ memberPath: null,
212
+ identifier: initOperand,
213
+ effect: Effect.Read,
214
+ loc: GeneratedSource,
215
+ }
216
+ : {
217
+ kind: "Primitive",
218
+ // TODO: consider leaving the variable uninitialized rather than explicitly undefined.
219
+ value: undefined,
220
+ loc: GeneratedSource,
221
+ },
222
+ loc: GeneratedSource,
223
+ };
224
+ block.instructions.push(instr);
225
+ }
226
+
227
+ // Generate an assignment in each predecessor
228
+ for (const [predecessor, operand] of phi.operands) {
229
+ if (operand === initOperand) {
230
+ continue;
231
+ }
232
+ const instr: Instruction = {
233
+ id: predecessor.terminal.id,
234
+ lvalue: {
235
+ place: {
236
+ kind: "Identifier",
237
+ memberPath: null,
238
+ identifier: canonicalId,
239
+ effect: Effect.Mutate,
240
+ loc: GeneratedSource,
241
+ },
242
+ kind: InstructionKind.Reassign,
243
+ },
244
+ value: {
245
+ kind: "Identifier",
246
+ memberPath: null,
247
+ identifier: operand,
248
+ effect: Effect.Read,
249
+ loc: GeneratedSource,
250
+ },
251
+ loc: GeneratedSource,
252
+ };
253
+ predecessor.instructions.push(instr);
254
+ }
255
+ }
256
+
257
+ // Similar logic for rewrite phis that occur in loops, except that instead of a new let binding
258
+ // we pick one of the operands as the canonical id, and rewrite all references to the other
259
+ // operands and the phi to reference this canonical id.
260
+ for (const phi of rewritePhis) {
261
+ let canonicalId = rewrites.get(phi.id);
262
if (canonicalId === undefined) {
101
- // Determine a new canonical id. We use the id/operand whose id is lowest,
102
- // which ensures that _if_ the original variable declaration is one of the
103
- // options we'll choose it and can reuse the declaration.
263
canonicalId = phi.id;
264
for (const [, operand] of phi.operands) {
106
- let canonicalOperand = variableMapping.get(operand) ?? operand;
265
+ let canonicalOperand = rewrites.get(operand) ?? operand;
266
if (canonicalOperand.id < canonicalId.id) {
267
canonicalId = canonicalOperand;
268
}
271
canonicalId.mutableRange.start,
272
terminal.id
273
) as InstructionId;
115
- variableMapping.set(phi.id, canonicalId);
116
- if (!hasDeclaration.has(canonicalId)) {
117
- needsDeclaration.add(canonicalId);
118
- }
274
+ rewrites.set(phi.id, canonicalId);
275
}
276
277
// all versions of the variable need to be remapped to the canonical id
282
for (const [, operand] of phi.operands) {
283
start = Math.min(start, operand.mutableRange.start);
284
end = Math.max(end, operand.mutableRange.end);
129
- variableMapping.set(operand, canonicalId);
285
+ rewrites.set(operand, canonicalId);
286
}
287
canonicalId.mutableRange.start = makeInstructionId(start);
288
canonicalId.mutableRange.end = makeInstructionId(end);
289
}
290
135
- // Visit instructions and rewrite identifiers based on the variable mapping
136
- // updated above.
291
+ // Finally, iterate the instructions and perform any rewrites as well as converting
292
+ // SSA variables to `const` where possible
293
for (const instr of block.instructions) {
294
const { lvalue, value } = instr;
295
if (lvalue !== null) {
140
- updatePlace(lvalue.place, variableMapping);
141
- if (lvalue.place.memberPath === null) {
142
- if (!variableMapping.has(lvalue.place.identifier)) {
143
- // This variable does not flow into a phi, therefore there
144
- // is no reassignment. Convert the declaration to a const.
145
- lvalue.kind = InstructionKind.Const;
146
- } else if (
147
- variableMapping.get(lvalue.place.identifier) ===
148
- lvalue.place.identifier
149
- ) {
150
- // This is an existing declaration we can reuse as the canonical declaration for its
151
- // phi. Note, the declaration must already be a `let` or else it would be invalid to
152
- // reassign the variable in the first place.
153
- needsDeclaration.delete(lvalue.place.identifier);
154
- }
155
- hasDeclaration.add(lvalue.place.identifier);
296
+ rewritePlace(lvalue.place, rewrites);
297
+ if (
298
+ lvalue.kind !== InstructionKind.Const &&
299
+ lvalue.place.memberPath === null &&
300
+ !rewrites.has(lvalue.place.identifier) &&
301
+ (!reassignments.has(lvalue.place.identifier) ||
302
+ reassignments.get(lvalue.place.identifier) !==
303
+ lvalue.place.identifier)
304
+ ) {
305
+ // Convert individual SSA reassignments into const declarations
306
+ // otherwise the code would be invalid, since the SSA identifiers
307
+ // aren't otherwise declared.
308
+ // TODO @josephsavona: do this in EnterSSA instead?
309
+ lvalue.kind = InstructionKind.Const;
310
}
311
}
158
- for (const operand of eachInstructionValueOperand(value)) {
159
- updatePlace(operand, variableMapping);
312
+ for (const operand of eachInstructionValueOperand(instr.value)) {
313
+ rewritePlace(operand, rewrites);
314
}
315
}
162
-
316
for (const operand of eachTerminalOperand(terminal)) {
164
- operand.identifier =
165
- variableMapping.get(operand.identifier) ?? operand.identifier;
166
- }
167
-
168
- // Generate new let declarations for any remaining phi variables
169
- for (const identifier of needsDeclaration) {
170
- const instr: Instruction = {
171
- // NOTE: reuse the terminal id since these lets must be scoped with the terminal anyway
172
- // the only reason they exist is that there is a scope that will span the control flow.
173
- id: block.terminal.id,
174
- lvalue: {
175
- place: {
176
- kind: "Identifier",
177
- memberPath: null,
178
- identifier,
179
- effect: Effect.Mutate,
180
- loc: GeneratedSource,
181
- },
182
- kind: InstructionKind.Let,
183
- },
184
- value: {
185
- kind: "Primitive",
186
- value: undefined,
187
- loc: GeneratedSource,
188
- },
189
- loc: GeneratedSource,
190
- };
191
- block.instructions.push(instr);
317
+ rewritePlace(operand, rewrites);
318
}
319
}
320
}
321
196
-function updatePlace(
197
- place: Place,
198
- variableMapping: Map<Identifier, Identifier>
199
-) {
322
+// Rewrite @param place's identifier based on the given rewrite mapping, if the identifier
323
+// is present. Also expands the mutable range of the target identifier to include the
324
+// place's range.
325
+function rewritePlace(place: Place, rewrites: Map<Identifier, Identifier>) {
326
const prevIdentifier = place.identifier;
201
- const nextIdentifier = variableMapping.get(prevIdentifier);
327
+ const nextIdentifier = rewrites.get(prevIdentifier);
328
if (nextIdentifier === undefined || nextIdentifier === prevIdentifier) {
329
return;
330
}