1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
-import {CompilerError} from '../CompilerError';
9
-import {
10
- BasicBlock,
11
- BlockId,
12
- HIRFunction,
13
- Identifier,
14
- InstructionKind,
15
- LValue,
16
- LValuePattern,
17
- Phi,
18
- Place,
19
-} from '../HIR/HIR';
20
-import {printIdentifier, printPlace} from '../HIR/PrintHIR';
21
-import {
22
- eachInstructionLValue,
23
- eachInstructionValueOperand,
24
- eachPatternOperand,
25
- eachTerminalOperand,
26
- eachTerminalSuccessor,
27
- terminalFallthrough,
28
-} from '../HIR/visitors';
29
-
30
-/*
31
- * Removes SSA form by converting all phis into explicit bindings and assignments. There are two main categories
32
- * of phis:
33
- *
34
- * ## Reassignments (operands are independently memoizable)
35
- *
36
- * These are phis that occur after some high-level control flow such as an if, switch, or loop. These phis are rewritten
37
- * to add a new `let` binding for the phi id prior to the control flow node (ie prior to the if/switch),
38
- * and to add a reassignment to that let binding in each of the phi's predecessors.
39
- *
40
- * Example:
41
- *
42
- * ```javascript
43
- * // Input
44
- * let x1 = null;
45
- * if (a) {
46
- * x2 = b;
47
- * } else {
48
- * x3 = c;
49
- * }
50
- * x4 = phi(x2, x3);
51
- * return x4;
52
- *
53
- * // Output
54
- * const x1 = null;
55
- * let x4; // synthesized binding for the phi identifier
56
- * if (a) {
57
- * x2 = b;
58
- * x4 = x2;; // sythesized assignment to the phi identifier
59
- * } else {
60
- * x3 = c;
61
- * x4 = x3; // synthesized assignment
62
- * }
63
- * // phi removed
64
- * return x4;
65
- * ```
66
- *
67
- * ## Rewrites (operands are not independently memoizable)
68
- *
69
- * Phis that occur inside loop constructs cannot use the reassignment strategy, because there isn't an appropriate place
70
- * to add the new let binding. Instead, we select a single "canonical" id for these phis which is the operand that is
71
- * defined first. Then, all assignments and references for any of the phi ir and operands are rewritten to reference
72
- * the canonical id instead.
73
- *
74
- * Example:
75
- *
76
- * ```javascript
77
- * // Input
78
- * for (
79
- * let i1 = 0;
80
- * { i2 = phi(i1, i2); i2 < 10 }; // note the phi in the test block
81
- * i2 += 1
82
- * ) { ... }
83
- *
84
- * // Output
85
- * for (
86
- * let i1 = 0; // i1 is defined first, so it becomes the canonical id
87
- * i1 < 10; // rewritten to canonical id
88
- * i1 += 1 // rewritten to canonical id
89
- * )
90
- * ```
91
- */
92
-export function leaveSSA(fn: HIRFunction): void {
93
- // Maps identifier names to their original declaration.
94
- const declarations: Map<
95
- string,
96
- {lvalue: LValue | LValuePattern; place: Place}
97
- > = new Map();
98
-
99
- for (const param of fn.params) {
100
- let place: Place = param.kind === 'Identifier' ? param : param.place;
101
- if (place.identifier.name !== null) {
102
- declarations.set(place.identifier.name.value, {
103
- lvalue: {
104
- kind: InstructionKind.Let,
105
- place,
106
- },
107
- place,
108
- });
109
- }
110
- }
111
-
112
- /*
113
- * For non-memoizable phis, this maps original identifiers to the identifier they should be
114
- * *rewritten* to. The keys are the original identifiers, and the value will be _either_ the
115
- * phi id or, more typically, the operand that was defined prior to the phi.
116
- */
117
- const rewrites: Map<Identifier, Identifier> = new Map();
118
-
119
- type PhiState = {
120
- phi: Phi;
121
- block: BasicBlock;
122
- };
123
-
124
- const seen = new Set<BlockId>();
125
- const backEdgePhis = new Set<Phi>();
126
- for (const [, block] of fn.body.blocks) {
127
- for (const phi of block.phis) {
128
- for (const [pred] of phi.operands) {
129
- if (!seen.has(pred)) {
130
- backEdgePhis.add(phi);
131
- break;
132
- }
133
- }
134
- }
135
- seen.add(block.id);
136
- }
137
-
138
- for (const [, block] of fn.body.blocks) {
139
- for (const instr of block.instructions) {
140
- /*
141
- * Iterate the instructions and perform any rewrites as well as promoting SSA variables to
142
- * `let` or `reassign` where possible.
143
- */
144
- const {lvalue, value} = instr;
145
- if (value.kind === 'DeclareLocal') {
146
- const name = value.lvalue.place.identifier.name;
147
- if (name !== null) {
148
- CompilerError.invariant(!declarations.has(name.value), {
149
- reason: `Unexpected duplicate declaration`,
150
- description: `Found duplicate declaration for \`${name.value}\``,
151
- loc: value.lvalue.place.loc,
152
- suggestions: null,
153
- });
154
- declarations.set(name.value, {
155
- lvalue: value.lvalue,
156
- place: value.lvalue.place,
157
- });
158
- }
159
- } else if (
160
- value.kind === 'PrefixUpdate' ||
161
- value.kind === 'PostfixUpdate'
162
- ) {
163
- CompilerError.invariant(value.lvalue.identifier.name !== null, {
164
- reason: `Expected update expression to be applied to a named variable`,
165
- description: null,
166
- loc: value.lvalue.loc,
167
- suggestions: null,
168
- });
169
- const originalLVal = declarations.get(
170
- value.lvalue.identifier.name.value,
171
- );
172
- CompilerError.invariant(originalLVal !== undefined, {
173
- reason: `Expected update expression to be applied to a previously defined variable`,
174
- description: null,
175
- loc: value.lvalue.loc,
176
- suggestions: null,
177
- });
178
- originalLVal.lvalue.kind = InstructionKind.Let;
179
- } else if (value.kind === 'StoreLocal') {
180
- if (value.lvalue.place.identifier.name != null) {
181
- const originalLVal = declarations.get(
182
- value.lvalue.place.identifier.name.value,
183
- );
184
- if (
185
- originalLVal === undefined ||
186
- originalLVal.lvalue === value.lvalue // in case this was pre-declared for the `for` initializer
187
- ) {
188
- CompilerError.invariant(
189
- originalLVal !== undefined ||
190
- block.kind === 'block' ||
191
- block.kind === 'catch',
192
- {
193
- reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
194
- description: null,
195
- loc: value.lvalue.place.loc,
196
- suggestions: null,
197
- },
198
- );
199
- declarations.set(value.lvalue.place.identifier.name.value, {
200
- lvalue: value.lvalue,
201
- place: value.lvalue.place,
202
- });
203
- value.lvalue.kind = InstructionKind.Const;
204
- } else {
205
- /*
206
- * This is an instance of the original id, so we need to promote the original declaration
207
- * to a `let` and the current lval to a `reassign`
208
- */
209
- originalLVal.lvalue.kind = InstructionKind.Let;
210
- value.lvalue.kind = InstructionKind.Reassign;
211
- }
212
- } else if (rewrites.has(value.lvalue.place.identifier)) {
213
- value.lvalue.kind = InstructionKind.Const;
214
- }
215
- } else if (value.kind === 'Destructure') {
216
- let kind: InstructionKind | null = null;
217
- for (const place of eachPatternOperand(value.lvalue.pattern)) {
218
- if (place.identifier.name == null) {
219
- CompilerError.invariant(
220
- kind === null || kind === InstructionKind.Const,
221
- {
222
- reason: `Expected consistent kind for destructuring`,
223
- description: `other places were \`${kind}\` but '${printPlace(
224
- place,
225
- )}' is const`,
226
- loc: place.loc,
227
- suggestions: null,
228
- },
229
- );
230
- kind = InstructionKind.Const;
231
- } else {
232
- const originalLVal = declarations.get(place.identifier.name.value);
233
- if (
234
- originalLVal === undefined ||
235
- originalLVal.lvalue === value.lvalue
236
- ) {
237
- CompilerError.invariant(
238
- originalLVal !== undefined || block.kind !== 'value',
239
- {
240
- reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
241
- description: null,
242
- loc: place.loc,
243
- suggestions: null,
244
- },
245
- );
246
- declarations.set(place.identifier.name.value, {
247
- lvalue: value.lvalue,
248
- place,
249
- });
250
- CompilerError.invariant(
251
- kind === null || kind === InstructionKind.Const,
252
- {
253
- reason: `Expected consistent kind for destructuring`,
254
- description: `Other places were \`${kind}\` but '${printPlace(
255
- place,
256
- )}' is const`,
257
- loc: place.loc,
258
- suggestions: null,
259
- },
260
- );
261
- kind = InstructionKind.Const;
262
- } else {
263
- CompilerError.invariant(
264
- kind === null || kind === InstructionKind.Reassign,
265
- {
266
- reason: `Expected consistent kind for destructuring`,
267
- description: `Other places were \`${kind}\` but '${printPlace(
268
- place,
269
- )}' is reassigned`,
270
- loc: place.loc,
271
- suggestions: null,
272
- },
273
- );
274
- kind = InstructionKind.Reassign;
275
- originalLVal.lvalue.kind = InstructionKind.Let;
276
- }
277
- }
278
- }
279
- CompilerError.invariant(kind !== null, {
280
- reason: 'Expected at least one operand',
281
- description: null,
282
- loc: null,
283
- suggestions: null,
284
- });
285
- value.lvalue.kind = kind;
286
- }
287
- rewritePlace(lvalue, rewrites, declarations);
288
- for (const operand of eachInstructionLValue(instr)) {
289
- rewritePlace(operand, rewrites, declarations);
290
- }
291
- for (const operand of eachInstructionValueOperand(instr.value)) {
292
- rewritePlace(operand, rewrites, declarations);
293
- }
294
- }
295
-
296
- const terminal = block.terminal;
297
- for (const operand of eachTerminalOperand(terminal)) {
298
- rewritePlace(operand, rewrites, declarations);
299
- }
300
-
301
- /*
302
- * Find any phi nodes which need a variable declaration in the current block
303
- * This includes phis in fallthrough nodes, or blocks that form part of control flow
304
- * such as for or while (and later if/switch).
305
- */
306
- const reassignmentPhis: Array<PhiState> = [];
307
- const rewritePhis: Array<PhiState> = [];
308
- function pushPhis(phiBlock: BasicBlock): void {
309
- for (const phi of phiBlock.phis) {
310
- if (phi.id.name === null) {
311
- rewritePhis.push({phi, block: phiBlock});
312
- } else {
313
- reassignmentPhis.push({phi, block: phiBlock});
314
- }
315
- }
316
- }
317
- const fallthroughId = terminalFallthrough(terminal);
318
- if (fallthroughId !== null) {
319
- const fallthrough = fn.body.blocks.get(fallthroughId)!;
320
- pushPhis(fallthrough);
321
- }
322
- if (terminal.kind === 'while' || terminal.kind === 'for') {
323
- const test = fn.body.blocks.get(terminal.test)!;
324
- pushPhis(test);
325
-
326
- const loop = fn.body.blocks.get(terminal.loop)!;
327
- pushPhis(loop);
328
- }
329
- if (
330
- terminal.kind === 'for' ||
331
- terminal.kind === 'for-of' ||
332
- terminal.kind === 'for-in'
333
- ) {
334
- let init = fn.body.blocks.get(terminal.init)!;
335
- pushPhis(init);
336
-
337
- // The first block after the end of the init
338
- let initContinuation =
339
- terminal.kind === 'for' ? terminal.test : terminal.loop;
340
- /*
341
- * To avoid generating a let binding for the initializer prior to the loop,
342
- * check to see if the for declares an iterator variable.
343
- */
344
- const queue: Array<BlockId> = [init.id];
345
- while (queue.length !== 0) {
346
- const blockId = queue.shift()!;
347
- if (blockId === initContinuation) {
348
- break;
349
- }
350
- const block = fn.body.blocks.get(blockId)!;
351
- for (const instr of block.instructions) {
352
- if (
353
- instr.value.kind === 'StoreLocal' &&
354
- instr.value.lvalue.kind !== InstructionKind.Reassign
355
- ) {
356
- const value = instr.value;
357
- if (value.lvalue.place.identifier.name !== null) {
358
- const originalLVal = declarations.get(
359
- value.lvalue.place.identifier.name.value,
360
- );
361
- if (originalLVal === undefined) {
362
- declarations.set(value.lvalue.place.identifier.name.value, {
363
- lvalue: value.lvalue,
364
- place: value.lvalue.place,
365
- });
366
- value.lvalue.kind = InstructionKind.Const;
367
- }
368
- }
369
- }
370
- }
371
-
372
- switch (block.terminal.kind) {
373
- case 'maybe-throw': {
374
- queue.push(block.terminal.continuation);
375
- break;
376
- }
377
- case 'goto': {
378
- queue.push(block.terminal.block);
379
- break;
380
- }
381
- case 'branch':
382
- case 'logical':
383
- case 'optional':
384
- case 'ternary':
385
- case 'label': {
386
- for (const successor of eachTerminalSuccessor(block.terminal)) {
387
- queue.push(successor);
388
- }
389
- break;
390
- }
391
- default: {
392
- break;
393
- }
394
- }
395
- }
396
-
397
- if (terminal.kind === 'for' && terminal.update !== null) {
398
- const update = fn.body.blocks.get(terminal.update)!;
399
- pushPhis(update);
400
- }
401
- }
402
-
403
- for (const {phi, block: phiBlock} of reassignmentPhis) {
404
- /*
405
- * In some cases one of the phi operands can be defined *before* the let binding
406
- * we will generate. For example, a variable that is only rebound in one branch of
407
- * an if but not another. In this case we populate the let binding with this initial
408
- * value rather than generate an extra assignment.
409
- */
410
- let initOperand: Identifier | null = null;
411
- for (const [, operand] of phi.operands) {
412
- if (operand.mutableRange.start < terminal.id) {
413
- if (initOperand == null) {
414
- initOperand = operand;
415
- }
416
- }
417
- }
418
-
419
- /*
420
- * If the phi is mutated after its creation, then any values which flow into the phi
421
- * must also have their ranges extended accordingly.
422
- */
423
- const isPhiMutatedAfterCreation: boolean =
424
- phi.id.mutableRange.end >
425
- (phiBlock.instructions.at(0)?.id ?? phiBlock.terminal.id);
426
-
427
- /*
428
- * If we never saw a declaration for this phi, it may have been pruned by DCE, so synthesize
429
- * a new Let binding
430
- */
431
- CompilerError.invariant(phi.id.name != null, {
432
- reason: 'Expected reassignment phis to have a name',
433
- description: null,
434
- loc: null,
435
- suggestions: null,
436
- });
437
- const declaration = declarations.get(phi.id.name.value);
438
- CompilerError.invariant(declaration != null, {
439
- loc: null,
440
- reason: 'Expected a declaration for all variables',
441
- description: `${printIdentifier(phi.id)} in block bb${phiBlock.id}`,
442
- suggestions: null,
443
- });
444
- if (isPhiMutatedAfterCreation) {
445
- /*
446
- * The declaration is not guaranteed to flow into the phi, for example in the case of a variable
447
- * that is reassigned in all control flow paths to a given phi. The original declaration's range
448
- * has to be extended in this case (if the phi is later mutated) since we are reusing the original
449
- * declaration instead of creating a new declaration.
450
- *
451
- * NOTE: this can *only* happen if the original declaration involves an instruction that DCE does
452
- * not prune. Otherwise, the declaration would have been pruned and we'd synthesize a new one.
453
- */
454
- declaration.place.identifier.mutableRange.end = phi.id.mutableRange.end;
455
- }
456
- rewrites.set(phi.id, declaration.place.identifier);
457
- }
458
-
459
- /*
460
- * Similar logic for rewrite phis that occur in loops, except that instead of a new let binding
461
- * we pick one of the operands as the canonical id, and rewrite all references to the other
462
- * operands and the phi to reference this canonical id.
463
- */
464
- for (const {phi} of rewritePhis) {
465
- let canonicalId = rewrites.get(phi.id);
466
- if (canonicalId === undefined) {
467
- canonicalId = phi.id;
468
- for (const [, operand] of phi.operands) {
469
- let canonicalOperand = rewrites.get(operand) ?? operand;
470
- if (canonicalOperand.id < canonicalId.id) {
471
- canonicalId = canonicalOperand;
472
- }
473
- }
474
- rewrites.set(phi.id, canonicalId);
475
-
476
- if (canonicalId.name !== null) {
477
- const declaration = declarations.get(canonicalId.name.value);
478
- if (declaration !== undefined) {
479
- declaration.lvalue.kind = InstructionKind.Let;
480
- }
481
- }
482
- }
483
-
484
- // all versions of the variable need to be remapped to the canonical id
485
- for (const [, operand] of phi.operands) {
486
- rewrites.set(operand, canonicalId);
487
- }
488
- }
489
- }
490
-}
491
-
492
-/*
493
- * Rewrite @param place's identifier based on the given rewrite mapping, if the identifier
494
- * is present. Also expands the mutable range of the target identifier to include the
495
- * place's range.
496
- */
497
-function rewritePlace(
498
- place: Place,
499
- rewrites: Map<Identifier, Identifier>,
500
- declarations: Map<string, {lvalue: LValue | LValuePattern; place: Place}>,
501
-): void {
502
- const prevIdentifier = place.identifier;
503
- const nextIdentifier = rewrites.get(prevIdentifier);
504
-
505
- if (nextIdentifier !== undefined) {
506
- if (nextIdentifier === prevIdentifier) return;
507
- place.identifier = nextIdentifier;
508
- } else if (prevIdentifier.name != null) {
509
- const declaration = declarations.get(prevIdentifier.name.value);
510
- // Only rewrite identifiers that were declared within the function
511
- if (declaration === undefined) return;
512
- const originalIdentifier = declaration.place.identifier;
513
- prevIdentifier.id = originalIdentifier.id;
514
- }
515
-}