[new-arch] Cleanup unused analysis (#651)
Joseph Savona committed
Oct 11, 2022 at 12:00 UTC
94adcf91b146504e6765029689a441d6e91ca998
1 file changed
-644
compiler/forget/src/HIR/BuildDefUseGraph.ts
deleted
-644
@@ -1,644 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its 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 { assertExhaustive } from "../Common/utils";
9
-import { invariant } from "../CompilerError";
10
-import {
11
- BasicBlock,
12
- BlockId,
13
- Capability,
14
- HIRFunction,
15
- IdentifierId,
16
- Instruction,
17
- InstructionValue,
18
- Place,
19
- Terminal,
20
-} from "./HIR";
21
-import { mapTerminalSuccessors } from "./HIRBuilder";
22
-import { printMixedHIR, printPlace } from "./PrintHIR";
23
-
24
-const HOOKS: Map<string, Hook> = new Map([
25
- ["useState", { kind: "State", capability: Capability.Freeze }],
26
- ["useRef", { kind: "Ref", capability: Capability.Freeze }],
27
-]);
28
-
29
-type HookKind = { kind: "State" } | { kind: "Ref" } | { kind: "Custom" };
30
-type Hook = HookKind & { capability: Capability };
31
-
32
-/**
33
- * For every usage of a value in the given function, infers whether that usage
34
- * is frozen, readonly, or mutable:
35
- * - frozen: the value is known to be "owned" by React and is therefore permanently
36
- * and transitively immutable.
37
- * - readonly: the value is not frozen, but this usage of the value does not modify it.
38
- * the value may be mutated by a subsequent reference. Examples include referencing
39
- * the operands of a binary expression, or referencing the items/properties of an
40
- * array or object literal.
41
- * - mutable: the value is not frozen and this usage *may* modify it. Examples include
42
- * passing a value to as a function argument or assigning into an object.
43
- *
44
- * Note that the inference follows variable assignment, so assigning a frozen value
45
- * to a different value will infer usages of the other variable as frozen as well.
46
- *
47
- * The inference assumes that the code follows the rules of React:
48
- * - React function arguments are frozen (component props, hook arguments).
49
- * - Hook arguments are frozen at the point the hook is invoked.
50
- * - React function return values are frozen at the point of being returned,
51
- * thus the return value of a hook call is frozen.
52
- * - JSX represents invocation of a React function (the component) and
53
- * therefore all values passed to JSX become frozen at the point the JSX
54
- * is created.
55
- *
56
- * ## Algorithm
57
- *
58
- * The algorithm creates a "use-use" graph in which each usage of a variable links to
59
- * the previous (incoming) and subsequent (outgoing) usages of that variable. The primary
60
- * purpose of this graph is to perform inference of usages that are frozen vs not, including
61
- * accounting for control-flow and reassignment, and to update the input *in place* to
62
- * annotate `Place` with the appropriate capability. The set of vertexs is returned for
63
- * debugging purposes.
64
- *
65
- * - First create a mapping of the first and last usages of each top-level identifier
66
- * in each block, in isolation (ie without considering control-flow paths between
67
- * blocks).
68
- * - Then iterate over the blocks in control-flow order and link the last usage of
69
- * identifiers in predecssor blocks with the first usage in successor blocks.
70
- * - Then find all vertices corresponding to frozen usage of a value, and propagate
71
- * that "frozenness" forward to all subsequent usages of that value.
72
- */
73
-export default function buildDefUseGraph(fn: HIRFunction): Array<Vertex> {
74
- const graph = new UseGraph();
75
-
76
- const blockResults: Map<BlockId, BlockResult> = new Map();
77
- for (const [blockId, block] of fn.body.blocks) {
78
- const blockResult = buildInputsOutputsForBlock(graph, block);
79
- blockResults.set(blockId, blockResult);
80
- }
81
-
82
- const preambleBuilder = new BlockResultBuilder(graph);
83
- for (const param of fn.params) {
84
- const place: Place = {
85
- kind: "Identifier",
86
- memberPath: null,
87
- value: param,
88
- path: null as any, // TODO
89
- capability: Capability.Freeze,
90
- };
91
- preambleBuilder.init(place, null, true);
92
- }
93
- const preambleResult = preambleBuilder.build();
94
-
95
- // Iterate over the CFG linking outputs of predecssor blocks to the inputs
96
- // of successor blocks, stopping once all links have been established.
97
- const queue: Array<{
98
- blockId: BlockId;
99
- prevResult: BlockResult;
100
- }> = [{ blockId: fn.body.entry, prevResult: preambleResult }];
101
- while (queue.length !== 0) {
102
- const { blockId, prevResult } = queue.shift()!;
103
-
104
- // Link the previous block's outputs to the next block's inputs
105
- const blockResult = blockResults.get(blockId)!;
106
- const hasChange = linkPreviousNextBlock(graph, prevResult, blockResult);
107
-
108
- // If there were changes to this block's outgoing edges, update any
109
- // successor blocks
110
- if (hasChange || blockId === fn.body.entry) {
111
- const block = fn.body.blocks.get(blockId)!;
112
- // TODO: add a forEachSuccessor helper, this maps the terminal unnecessarily
113
- const _ = mapTerminalSuccessors(
114
- block.terminal,
115
- (blockId, isFallthrough) => {
116
- if (!isFallthrough) {
117
- queue.push({ blockId, prevResult: blockResult });
118
- }
119
- return blockId;
120
- }
121
- );
122
- }
123
- }
124
- const vertices = graph.build();
125
- for (const vertex of vertices) {
126
- invariant(
127
- vertex.place === null || vertex.place.capability !== Capability.Unknown,
128
- "Expected all vertices to have a capability inferred"
129
- );
130
-
131
- // Vertices derived from a frozen value are also frozen
132
- if (
133
- vertex.place !== null &&
134
- vertex.place.capability === Capability.Freeze
135
- ) {
136
- flowFrozennessForwards(vertex, 0);
137
- }
138
- // Join nodes are created before we know if they will be consumed,
139
- // prune join nodes without any outgoing edges to aid visualization.
140
- // note that this is not required for correctness.
141
- if (vertex.place === null && vertex.outgoing.size === 0) {
142
- for (const incoming of vertex.incoming) {
143
- incoming.outgoing.delete(vertex);
144
- }
145
- vertex.incoming.clear();
146
- }
147
- }
148
- return vertices;
149
-}
150
-
151
-/**
152
- * Once a value is known to be frozen, all usages forward of that point must be frozen too.
153
- */
154
-function flowFrozennessForwards(vertex: Vertex, epoch: number) {
155
- if (vertex.epoch === epoch) {
156
- return;
157
- }
158
- vertex.epoch = epoch;
159
- if (vertex.place !== null) {
160
- vertex.place.capability = Capability.Freeze;
161
- }
162
- for (const outgoing of vertex.outgoing) {
163
- flowFrozennessForwards(outgoing, epoch);
164
- }
165
-}
166
-
167
-/**
168
- * Link the last usages from a predecessor block to the first usages in a successor block,
169
- * and propagate any values used in the predecessor but _unused_ in the successor.
170
- */
171
-function linkPreviousNextBlock(
172
- graph: UseGraph,
173
- prevBlock: BlockResult,
174
- nextBlock: BlockResult
175
-): boolean {
176
- // are there any changes to the *outgoing* edges of `nextBlock`?
177
- let hasChanges = false;
178
-
179
- // for each first usage of next block, link it to last usage from prev block.
180
- for (const [id, nextVertex] of nextBlock.firstUsage) {
181
- if (nextVertex.isReassignment) {
182
- // nextVertex was a full reassignment, do not attach an edge.
183
- continue;
184
- }
185
- const prevVertex = prevBlock.lastUsage.get(id);
186
- if (prevVertex != null && prevVertex !== nextVertex) {
187
- // Note: don't update hasChanges here bc these do not affect the *outgoing* edges
188
- prevVertex.outgoing.add(nextVertex);
189
- nextVertex.incoming.add(prevVertex);
190
- }
191
- }
192
- // for each last usage in the prev block that was not already linked in the above
193
- // loop, add an outgoing edge to pass-through the data. the value may be used by
194
- // a later block.
195
- for (const [id, prevVertex] of prevBlock.lastUsage) {
196
- if (nextBlock.firstUsage.has(id)) {
197
- // already handled in the above loop
198
- continue;
199
- }
200
- const nextVertex = nextBlock.lastUsage.get(id);
201
- if (nextVertex == null) {
202
- // First time propagating a value for this id: use the prev vertex from
203
- // the predecessor as the output of the successor
204
- hasChanges = true;
205
- nextBlock.lastUsage.set(id, prevVertex);
206
- } else if (nextVertex === prevVertex) {
207
- // already propagated
208
- continue;
209
- } else {
210
- let joinVertex;
211
- if (nextVertex.place === null) {
212
- // A join vertex was already created, link the successor value to it
213
- joinVertex = nextVertex;
214
- } else {
215
- // A normal vertex was propagated through on the previous visit, but
216
- // now there is a different value that needs to be propagated through.
217
- // swap the node for a "join" vertex, adding the previous node as an
218
- // input to that node.
219
- joinVertex = graph.join();
220
- nextVertex.outgoing.add(joinVertex);
221
- joinVertex.incoming.add(nextVertex);
222
- nextBlock.lastUsage.set(id, joinVertex);
223
- }
224
- // the incoming node may itself be a join vertex, add its inputs to
225
- // avoid linking join vertices to other join vertices.
226
- if (prevVertex.place === null) {
227
- for (const prev of prevVertex.incoming) {
228
- hasChanges =
229
- hasChanges ||
230
- !joinVertex.incoming.has(prev) ||
231
- !prev.outgoing.has(joinVertex);
232
- joinVertex.incoming.add(prev);
233
- prev.outgoing.add(joinVertex);
234
- }
235
- } else {
236
- hasChanges =
237
- hasChanges ||
238
- !joinVertex.incoming.has(prevVertex) ||
239
- !prevVertex.outgoing.has(joinVertex);
240
- joinVertex.incoming.add(prevVertex);
241
- prevVertex.outgoing.add(joinVertex);
242
- }
243
- }
244
- }
245
- return hasChanges;
246
-}
247
-
248
-/**
249
- * Iterates over a single basic block and constructs a mapping of the first and last usages
250
- * of each Place referenced in that block.
251
- */
252
-function buildInputsOutputsForBlock(
253
- graph: UseGraph,
254
- block: BasicBlock
255
-): BlockResult {
256
- const builder = new BlockResultBuilder(graph);
257
-
258
- for (const instr of block.instructions) {
259
- const instrValue = instr.value;
260
- let valueCapability = Capability.Readonly;
261
- switch (instrValue.kind) {
262
- case "BinaryExpression": {
263
- valueCapability = Capability.Freeze;
264
- builder.reference(instrValue.left, instrValue, Capability.Readonly);
265
- builder.reference(instrValue.right, instrValue, Capability.Readonly);
266
- break;
267
- }
268
- case "ArrayExpression": {
269
- for (const element of instrValue.elements) {
270
- builder.reference(element, instrValue, Capability.Readonly);
271
- }
272
- break;
273
- }
274
- case "NewExpression": {
275
- builder.reference(instrValue.callee, instrValue, Capability.Mutable);
276
- for (const arg of instrValue.args) {
277
- builder.reference(arg, instrValue, Capability.Mutable);
278
- }
279
- break;
280
- }
281
- case "CallExpression": {
282
- let capability = Capability.Mutable;
283
- const hook = parseHookCall(instrValue.callee);
284
- if (hook !== null) {
285
- capability = hook.capability;
286
- valueCapability = hook.capability;
287
- }
288
- builder.reference(instrValue.callee, instrValue, capability);
289
- for (const arg of instrValue.args) {
290
- builder.reference(arg, instrValue, capability);
291
- }
292
- break;
293
- }
294
- case "ObjectExpression": {
295
- // Object construction captures but does not modify the key/property values
296
- if (instrValue.properties !== null) {
297
- for (const [_key, value] of Object.entries(instrValue.properties)) {
298
- builder.reference(value, instrValue, Capability.Readonly);
299
- }
300
- }
301
- break;
302
- }
303
- case "UnaryExpression": {
304
- valueCapability = Capability.Freeze; // TODO check that value must be a primitive, or make conditional based on the operator
305
- builder.reference(instrValue.value, instrValue, Capability.Readonly);
306
- break;
307
- }
308
- case "OtherStatement": {
309
- // TODO: handle other statement kinds
310
- break;
311
- }
312
- case "JsxExpression": {
313
- builder.reference(instrValue.tag, instrValue, Capability.Freeze);
314
- for (const [_prop, value] of Object.entries(instrValue.props)) {
315
- builder.reference(value, instrValue, Capability.Freeze);
316
- }
317
- if (instrValue.children !== null) {
318
- for (const child of instrValue.children) {
319
- builder.reference(child, instrValue, Capability.Freeze);
320
- }
321
- }
322
- break;
323
- }
324
- case "JSXText":
325
- case "Primitive": {
326
- valueCapability = Capability.Readonly;
327
- break;
328
- }
329
- case "Identifier": {
330
- builder.reference(instrValue, instrValue, Capability.Readonly);
331
- valueCapability = instrValue.capability;
332
- if (instr.lvalue !== null && instr.lvalue.place.memberPath === null) {
333
- builder.assign(instr.lvalue.place, instr, instrValue);
334
- instr.lvalue.place.capability = valueCapability;
335
- continue;
336
- }
337
- break;
338
- }
339
- default: {
340
- assertExhaustive(instrValue, "Unexpected instruction kind");
341
- }
342
- }
343
- if (instr.lvalue !== null) {
344
- if (instr.lvalue.place.memberPath == null) {
345
- builder.init(instr.lvalue.place, instr, true);
346
- instr.lvalue.place.capability = valueCapability;
347
- } else {
348
- builder.reference(instr.lvalue.place, instr, valueCapability);
349
- }
350
- }
351
- }
352
- switch (block.terminal.kind) {
353
- case "throw": {
354
- builder.reference(
355
- block.terminal.value,
356
- block.terminal,
357
- Capability.Freeze
358
- );
359
- break;
360
- }
361
- case "return": {
362
- if (block.terminal.value !== null) {
363
- builder.reference(
364
- block.terminal.value,
365
- block.terminal,
366
- Capability.Freeze
367
- );
368
- }
369
- break;
370
- }
371
- case "if": {
372
- builder.reference(
373
- block.terminal.test,
374
- block.terminal,
375
- Capability.Readonly
376
- );
377
- break;
378
- }
379
- case "switch": {
380
- builder.reference(
381
- block.terminal.test,
382
- block.terminal,
383
- Capability.Readonly
384
- );
385
- for (const case_ of block.terminal.cases) {
386
- if (case_.test !== null) {
387
- builder.reference(case_.test, block.terminal, Capability.Readonly);
388
- }
389
- }
390
- break;
391
- }
392
- case "goto": {
393
- break;
394
- }
395
- default: {
396
- assertExhaustive(
397
- block.terminal,
398
- `Unexpected terminal kind '${(block.terminal as any as Terminal).kind}'`
399
- );
400
- }
401
- }
402
-
403
- return builder.build();
404
-}
405
-
406
-function parseHookCall(place: Place): Hook | null {
407
- if (place.memberPath !== null) {
408
- // Hook calls must be statically resolved
409
- return null;
410
- }
411
- const name = place.value.name;
412
- if (name === null || !name.match(/^_?use/)) {
413
- return null;
414
- }
415
- const hook = HOOKS.get(name);
416
- if (hook != null) {
417
- return hook;
418
- }
419
- return { kind: "Custom", capability: Capability.Freeze };
420
-}
421
-
422
-type BlockResult = {
423
- firstUsage: Map<IdentifierId, Vertex>;
424
- lastUsage: Map<IdentifierId, Vertex>;
425
-};
426
-
427
-class BlockResultBuilder {
428
- #firstUsage: Map<IdentifierId, Vertex> = new Map();
429
- #lastUsage: Map<IdentifierId, Vertex> = new Map();
430
- #graph: UseGraph;
431
-
432
- constructor(graph: UseGraph) {
433
- this.#graph = graph;
434
- }
435
-
436
- build(): BlockResult {
437
- return {
438
- firstUsage: this.#firstUsage,
439
- lastUsage: this.#lastUsage,
440
- };
441
- }
442
-
443
- /**
444
- * Represents assignment of a value to a Place. Unlike with `reference()`,
445
- * this does *not* establish an edge between this usage of the place and
446
- * previous usages, because the value is not the same.
447
- */
448
- init(
449
- place: Place,
450
- instr: Instruction | InstructionValue | Terminal | null,
451
- isReassignment: boolean
452
- ): Vertex {
453
- const id = place.value.id;
454
- const vertex = this.#graph.init(place, instr, isReassignment);
455
- if (!this.#firstUsage.has(id)) {
456
- this.#firstUsage.set(id, vertex);
457
- }
458
- this.#lastUsage.set(id, vertex);
459
- return vertex;
460
- }
461
-
462
- /**
463
- * Represents assigning a (new) value to @param target via the given @param instr,
464
- * with @param value as the value being assigned.
465
- *
466
- * This breaks the use chain, such that this and subsequent usages of @param target
467
- * are not associated to previous usages. This replicates SSA semantics, conceptually
468
- * @param target is a new place now.
469
- */
470
- assign(target: Place, instr: Instruction, value: Place) {
471
- const targetVertex = this.init(target, instr, true);
472
- const valueVertex = this.#graph.get(value)!;
473
- targetVertex.incoming.add(valueVertex);
474
- valueVertex.outgoing.add(targetVertex);
475
- }
476
-
477
- /**
478
- * Represents a reference (usage of) a Place. This establishes an edge
479
- * with this usage and the previous usage
480
- */
481
- reference(
482
- place: Place,
483
- instr: Instruction | InstructionValue | Terminal,
484
- capability: Capability
485
- ) {
486
- const id = place.value.id;
487
- place.capability = capability;
488
- const prevVertex = this.#lastUsage.get(id);
489
- const vertex = this.init(place, instr, false);
490
-
491
- if (prevVertex != null) {
492
- prevVertex.outgoing.add(vertex);
493
- vertex.incoming?.add(prevVertex);
494
- }
495
- }
496
-}
497
-
498
-class IdGenerator {
499
- #value: number = 0;
500
-
501
- next(): number {
502
- return this.#value++;
503
- }
504
-}
505
-
506
-class UseGraph {
507
- #idGenerator: IdGenerator = new IdGenerator();
508
- #verticesByPlace: Map<Place, Vertex> = new Map();
509
- #vertices: Array<Vertex> = [];
510
-
511
- get(place: Place): Vertex | null {
512
- return this.#verticesByPlace.get(place) ?? null;
513
- }
514
-
515
- init(
516
- place: Place,
517
- instr: Instruction | InstructionValue | Terminal | null,
518
- isReassignment: boolean
519
- ): Vertex {
520
- let vertex = this.#verticesByPlace.get(place);
521
- if (vertex == null) {
522
- vertex = new Vertex(place, instr, this.#idGenerator, isReassignment);
523
- this.#verticesByPlace.set(place, vertex);
524
- this.#vertices.push(vertex);
525
- }
526
- return vertex;
527
- }
528
-
529
- join(): Vertex {
530
- const vertex = new Vertex(null, null, this.#idGenerator, false);
531
- this.#vertices.push(vertex);
532
- return vertex;
533
- }
534
-
535
- build(): Array<Vertex> {
536
- return this.#vertices;
537
- }
538
-}
539
-
540
-/**
541
- * Represents a distinct usage of a `Place` within a program. Note that
542
- * vertices are only created for top-level identifiers, not for points
543
- * within an object. So both `x` and `x.y` create vertices for `x`,
544
- * though this is enforced by the _construction_ of vertices in UseGraph,
545
- * not the implementation of Vertex itself.
546
- */
547
-class Vertex {
548
- /**
549
- * the set of vertices where *this* place was previously referenced
550
- */
551
- incoming: Set<Vertex> = new Set();
552
-
553
- /**
554
- * the set of vertices where *this* place is subseqently referenced
555
- */
556
- outgoing: Set<Vertex> = new Set();
557
-
558
- /**
559
- * The place that is using the value.
560
- */
561
- place: Place | null;
562
-
563
- /**
564
- * The instruction where the reference occurs (for debugging)
565
- */
566
- instr: Instruction | InstructionValue | Terminal | null;
567
-
568
- /**
569
- * Does this vertex represent an assignment? If yes, incoming
570
- * represents the set of values that are *assigned* to this vertex,
571
- * and not previous usages.
572
- */
573
- isReassignment: boolean;
574
-
575
- /**
576
- * Unique identifier for this vertex (for debugging)
577
- */
578
- id: string;
579
-
580
- /**
581
- * The last epoch in which this vertex was visited. This is used during
582
- * data-flow analysis post construction of the graph to ensure termination
583
- * by avoiding revisiting the same nodes in a given pass.
584
- */
585
- epoch: number | null = null;
586
-
587
- constructor(
588
- place: Place | null,
589
- instr: Instruction | InstructionValue | Terminal | null,
590
- idGenerator: IdGenerator,
591
- isReassignment: boolean
592
- ) {
593
- this.place = place;
594
- this.instr = instr;
595
- this.id = `v${idGenerator.next()}`;
596
- this.isReassignment = isReassignment;
597
- }
598
-}
599
-
600
-/**
601
- * Prints the graph into GraphViz DOT format.
602
- * https://graphviz.org/doc/info/lang.html
603
- */
604
-export function printGraph(vertices: Array<Vertex>): string {
605
- const output = [];
606
-
607
- for (const vertex of vertices) {
608
- if (
609
- vertex.place === null &&
610
- vertex.incoming.size === 0 &&
611
- vertex.outgoing.size === 0
612
- ) {
613
- continue;
614
- }
615
- const vertexId = vertex.id;
616
- output.push(
617
- `${vertexId} [ label="${vertexId} (${
618
- vertex.place ? printPlace(vertex.place) : "<join>"
619
- } @ ${
620
- vertex.instr
621
- ? printMixedHIR(vertex.instr).replaceAll('"', '\\"')
622
- : "<no-instr>"
623
- }) ${vertex.isReassignment ? "<assign>" : "<update>"}", shape="box" ]`
624
- );
625
- for (const outgoing of vertex.outgoing) {
626
- const outgoingId = outgoing.id;
627
- output.push(`${vertexId} -> ${outgoingId}`);
628
- }
629
- }
630
- const lines = output.map((line) => " " + line);
631
- lines.unshift("digraph BuildDefUseGraph {");
632
- lines.push("}");
633
- return lines.join("\n");
634
-}
635
-
636
-function mapToJson(map: Map<IdentifierId, Vertex>): {
637
- [key: string]: string;
638
-} {
639
- const result: { [key: string]: string } = {};
640
- for (const [id, vertex] of map) {
641
- result[id] = vertex.id;
642
- }
643
- return result;
644
-}