main
ts 426 lines 13.3 KB
Raw
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 {
9 BlockId,
10 Environment,
11 getHookKind,
12 HIRFunction,
13 Identifier,
14 IdentifierId,
15 Instruction,
16 InstructionKind,
17 InstructionValue,
18 ObjectPattern,
19 } from '../HIR';
20 import {
21 eachInstructionValueOperand,
22 eachPatternOperand,
23 eachTerminalOperand,
24 } from '../HIR/visitors';
25 import {assertExhaustive, retainWhere} from '../Utils/utils';
26
27 /*
28 * Implements dead-code elimination, eliminating instructions whose values are unused.
29 *
30 * Note that unreachable blocks are already pruned during HIR construction.
31 */
32 export function deadCodeElimination(fn: HIRFunction): void {
33 /**
34 * Phase 1: Find/mark all referenced identifiers
35 * Usages may be visited AFTER declarations if there are circular phi / data dependencies
36 * between blocks, so we wait to sweep until after fixed point iteration is complete
37 */
38 const state = findReferencedIdentifiers(fn);
39
40 /**
41 * Phase 2: Prune / sweep unreferenced identifiers and instructions
42 * as possible (subject to HIR structural constraints)
43 */
44 for (const [, block] of fn.body.blocks) {
45 for (const phi of block.phis) {
46 if (!state.isIdOrNameUsed(phi.place.identifier)) {
47 block.phis.delete(phi);
48 }
49 }
50 retainWhere(block.instructions, instr =>
51 state.isIdOrNameUsed(instr.lvalue.identifier),
52 );
53 // Rewrite retained instructions
54 for (let i = 0; i < block.instructions.length; i++) {
55 const isBlockValue =
56 block.kind !== 'block' && i === block.instructions.length - 1;
57 if (!isBlockValue) {
58 rewriteInstruction(block.instructions[i], state);
59 }
60 }
61 }
62
63 /**
64 * Constant propagation and DCE may have deleted or rewritten instructions
65 * that reference context variables.
66 */
67 retainWhere(fn.context, contextVar =>
68 state.isIdOrNameUsed(contextVar.identifier),
69 );
70 }
71
72 class State {
73 env: Environment;
74 named: Set<string> = new Set();
75 identifiers: Set<IdentifierId> = new Set();
76
77 constructor(env: Environment) {
78 this.env = env;
79 }
80
81 // Mark the identifier as being referenced (not dead code)
82 reference(identifier: Identifier): void {
83 this.identifiers.add(identifier.id);
84 if (identifier.name !== null) {
85 this.named.add(identifier.name.value);
86 }
87 }
88
89 /*
90 * Check if any version of the given identifier is used somewhere.
91 * This checks both for usage of this specific identifer id (ssa id)
92 * and (for named identifiers) for any usages of that identifier name.
93 */
94 isIdOrNameUsed(identifier: Identifier): boolean {
95 return (
96 this.identifiers.has(identifier.id) ||
97 (identifier.name !== null && this.named.has(identifier.name.value))
98 );
99 }
100
101 /*
102 * Like `used()`, but only checks for usages of this specific identifier id
103 * (ssa id).
104 */
105 isIdUsed(identifier: Identifier): boolean {
106 return this.identifiers.has(identifier.id);
107 }
108
109 get count(): number {
110 return this.identifiers.size;
111 }
112 }
113
114 function findReferencedIdentifiers(fn: HIRFunction): State {
115 /*
116 * If there are no back-edges the algorithm can terminate after a single iteration
117 * of the blocks
118 */
119 const hasLoop = hasBackEdge(fn);
120 const reversedBlocks = [...fn.body.blocks.values()].reverse();
121
122 const state = new State(fn.env);
123 let size = state.count;
124 do {
125 size = state.count;
126
127 /*
128 * Iterate blocks in postorder (successors before predecessors, excepting loops)
129 * to visit usages before declarations
130 */
131 for (const block of reversedBlocks) {
132 for (const operand of eachTerminalOperand(block.terminal)) {
133 state.reference(operand.identifier);
134 }
135
136 for (let i = block.instructions.length - 1; i >= 0; i--) {
137 const instr = block.instructions[i]!;
138 const isBlockValue =
139 block.kind !== 'block' && i === block.instructions.length - 1;
140
141 if (isBlockValue) {
142 /**
143 * The last instr of a value block is never eligible for pruning,
144 * as that's the block's value. Pessimistically consider all operands
145 * as used to avoid rewriting the last instruction
146 */
147 state.reference(instr.lvalue.identifier);
148 for (const place of eachInstructionValueOperand(instr.value)) {
149 state.reference(place.identifier);
150 }
151 } else if (
152 state.isIdOrNameUsed(instr.lvalue.identifier) ||
153 !pruneableValue(instr.value, state)
154 ) {
155 state.reference(instr.lvalue.identifier);
156
157 if (instr.value.kind === 'StoreLocal') {
158 /*
159 * If this is a Let/Const declaration, mark the initializer as referenced
160 * only if the ssa'ed lval is also referenced
161 */
162 if (
163 instr.value.lvalue.kind === InstructionKind.Reassign ||
164 state.isIdUsed(instr.value.lvalue.place.identifier)
165 ) {
166 state.reference(instr.value.value.identifier);
167 }
168 } else {
169 for (const operand of eachInstructionValueOperand(instr.value)) {
170 state.reference(operand.identifier);
171 }
172 }
173 }
174 }
175 for (const phi of block.phis) {
176 if (state.isIdOrNameUsed(phi.place.identifier)) {
177 for (const [_pred, operand] of phi.operands) {
178 state.reference(operand.identifier);
179 }
180 }
181 }
182 }
183 } while (state.count > size && hasLoop);
184 return state;
185 }
186
187 function rewriteInstruction(instr: Instruction, state: State): void {
188 if (instr.value.kind === 'Destructure') {
189 // Remove unused lvalues
190 switch (instr.value.lvalue.pattern.kind) {
191 case 'ArrayPattern': {
192 /*
193 * For arrays, we can prune items prior to the end by replacing
194 * them with a hole. Items at the end can simply be dropped.
195 */
196 let lastEntryIndex = 0;
197 const items = instr.value.lvalue.pattern.items;
198 for (let i = 0; i < items.length; i++) {
199 const item = items[i];
200 if (item.kind === 'Identifier') {
201 if (!state.isIdOrNameUsed(item.identifier)) {
202 items[i] = {kind: 'Hole'};
203 } else {
204 lastEntryIndex = i;
205 }
206 } else if (item.kind === 'Spread') {
207 if (!state.isIdOrNameUsed(item.place.identifier)) {
208 items[i] = {kind: 'Hole'};
209 } else {
210 lastEntryIndex = i;
211 }
212 }
213 }
214 items.length = lastEntryIndex + 1;
215 break;
216 }
217 case 'ObjectPattern': {
218 /*
219 * For objects we can prune any unused properties so long as there is no used rest element
220 * (`const {x, ...y} = z`). If a rest element exists and is used, then nothing can be pruned
221 * because it would change the set of properties which are copied into the rest value.
222 * In the `const {x, ...y} = z` example, removing the `x` property would mean that `y` now
223 * has an `x` property, changing the semantics.
224 */
225 let nextProperties: ObjectPattern['properties'] | null = null;
226 for (const property of instr.value.lvalue.pattern.properties) {
227 if (property.kind === 'ObjectProperty') {
228 if (state.isIdOrNameUsed(property.place.identifier)) {
229 nextProperties ??= [];
230 nextProperties.push(property);
231 }
232 } else {
233 if (state.isIdOrNameUsed(property.place.identifier)) {
234 nextProperties = null;
235 break;
236 }
237 }
238 }
239 if (nextProperties !== null) {
240 instr.value.lvalue.pattern.properties = nextProperties;
241 }
242 break;
243 }
244 default: {
245 assertExhaustive(
246 instr.value.lvalue.pattern,
247 `Unexpected pattern kind '${
248 (instr.value.lvalue.pattern as any).kind
249 }'`,
250 );
251 }
252 }
253 } else if (instr.value.kind === 'StoreLocal') {
254 if (
255 instr.value.lvalue.kind !== InstructionKind.Reassign &&
256 !state.isIdUsed(instr.value.lvalue.place.identifier)
257 ) {
258 /*
259 * This is a const/let declaration where the variable is accessed later,
260 * but where the value is always overwritten before being read. Ie the
261 * initializer value is never read. We rewrite to a DeclareLocal so
262 * that the initializer value can be DCE'd
263 */
264 instr.value = {
265 kind: 'DeclareLocal',
266 lvalue: instr.value.lvalue,
267 type: instr.value.type,
268 loc: instr.value.loc,
269 };
270 }
271 }
272 }
273
274 /*
275 * Returns true if it is safe to prune an instruction with the given value.
276 * Functions which may have side-
277 */
278 function pruneableValue(value: InstructionValue, state: State): boolean {
279 switch (value.kind) {
280 case 'DeclareLocal': {
281 // Declarations are pruneable only if the named variable is never read later
282 return !state.isIdOrNameUsed(value.lvalue.place.identifier);
283 }
284 case 'StoreLocal': {
285 if (value.lvalue.kind === InstructionKind.Reassign) {
286 // Reassignments can be pruned if the specific instance being assigned is never read
287 return !state.isIdUsed(value.lvalue.place.identifier);
288 }
289 // Declarations are pruneable only if the named variable is never read later
290 return !state.isIdOrNameUsed(value.lvalue.place.identifier);
291 }
292 case 'Destructure': {
293 let isIdOrNameUsed = false;
294 let isIdUsed = false;
295 for (const place of eachPatternOperand(value.lvalue.pattern)) {
296 if (state.isIdUsed(place.identifier)) {
297 isIdOrNameUsed = true;
298 isIdUsed = true;
299 } else if (state.isIdOrNameUsed(place.identifier)) {
300 isIdOrNameUsed = true;
301 }
302 }
303 if (value.lvalue.kind === InstructionKind.Reassign) {
304 // Reassignments can be pruned if the specific instance being assigned is never read
305 return !isIdUsed;
306 } else {
307 // Otherwise pruneable only if none of the identifiers are read from later
308 return !isIdOrNameUsed;
309 }
310 }
311 case 'PostfixUpdate':
312 case 'PrefixUpdate': {
313 // Updates are pruneable if the specific instance instance being assigned is never read
314 return !state.isIdUsed(value.lvalue.identifier);
315 }
316 case 'Debugger': {
317 // explicitly retain debugger statements to not break debugging workflows
318 return false;
319 }
320 case 'CallExpression':
321 case 'MethodCall': {
322 if (state.env.outputMode === 'ssr') {
323 const calleee =
324 value.kind === 'CallExpression' ? value.callee : value.property;
325 const hookKind = getHookKind(state.env, calleee.identifier);
326 switch (hookKind) {
327 case 'useState':
328 case 'useReducer':
329 case 'useRef': {
330 // unused refs can be removed
331 return true;
332 }
333 }
334 }
335 return false;
336 }
337 case 'Await':
338 case 'ComputedDelete':
339 case 'ComputedStore':
340 case 'PropertyDelete':
341 case 'PropertyStore':
342 case 'StoreGlobal': {
343 /*
344 * Mutating instructions are not safe to prune.
345 * TODO: we could be more precise and make this conditional on whether
346 * any arguments are actually modified
347 */
348 return false;
349 }
350 case 'NewExpression':
351 case 'UnsupportedNode':
352 case 'TaggedTemplateExpression': {
353 // Potentially safe to prune, since they should just be creating new values
354 return false;
355 }
356 case 'GetIterator':
357 case 'NextPropertyOf':
358 case 'IteratorNext': {
359 /*
360 * Technically a IteratorNext/NextPropertyOf will never be unused because it's
361 * always used later by another StoreLocal or Destructure instruction, but conceptually
362 * we can't prune
363 */
364 return false;
365 }
366 case 'LoadContext':
367 case 'DeclareContext':
368 case 'StoreContext': {
369 return false;
370 }
371 case 'StartMemoize':
372 case 'FinishMemoize': {
373 /**
374 * This instruction is used by the @enablePreserveExistingMemoizationGuarantees feature
375 * to preserve information about memoization semantics in the original code. We can't
376 * DCE without losing the memoization guarantees.
377 */
378 return false;
379 }
380 case 'RegExpLiteral':
381 case 'MetaProperty':
382 case 'LoadGlobal':
383 case 'ArrayExpression':
384 case 'BinaryExpression':
385 case 'ComputedLoad':
386 case 'ObjectMethod':
387 case 'FunctionExpression':
388 case 'LoadLocal':
389 case 'JsxExpression':
390 case 'JsxFragment':
391 case 'JSXText':
392 case 'ObjectExpression':
393 case 'Primitive':
394 case 'PropertyLoad':
395 case 'TemplateLiteral':
396 case 'TypeCastExpression':
397 case 'UnaryExpression': {
398 // Definitely safe to prune since they are read-only
399 return true;
400 }
401 default: {
402 assertExhaustive(
403 value,
404 `Unexepcted value kind \`${(value as any).kind}\``,
405 );
406 }
407 }
408 }
409
410 export function hasBackEdge(fn: HIRFunction): boolean {
411 return findBlocksWithBackEdges(fn).size > 0;
412 }
413
414 export function findBlocksWithBackEdges(fn: HIRFunction): Set<BlockId> {
415 const visited = new Set<BlockId>();
416 const blocks = new Set<BlockId>();
417 for (const [blockId, block] of fn.body.blocks) {
418 for (const predId of block.preds) {
419 if (!visited.has(predId)) {
420 blocks.add(blockId);
421 }
422 }
423 visited.add(blockId);
424 }
425 return blocks;
426 }