@samitouri / QOS-React / commits / 94adcf91b1

[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 -}