From 94adcf91b146504e6765029689a441d6e91ca998 Mon Sep 17 00:00:00 2001 From: Joseph Savona Date: Tue, 11 Oct 2022 12:00:45 -0700 Subject: [PATCH] [new-arch] Cleanup unused analysis (#651) --- compiler/forget/src/HIR/BuildDefUseGraph.ts | 644 -------------------- 1 file changed, 644 deletions(-) delete mode 100644 compiler/forget/src/HIR/BuildDefUseGraph.ts diff --git a/compiler/forget/src/HIR/BuildDefUseGraph.ts b/compiler/forget/src/HIR/BuildDefUseGraph.ts deleted file mode 100644 index 04bef729f8..0000000000 --- a/compiler/forget/src/HIR/BuildDefUseGraph.ts +++ /dev/null @@ -1,644 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { assertExhaustive } from "../Common/utils"; -import { invariant } from "../CompilerError"; -import { - BasicBlock, - BlockId, - Capability, - HIRFunction, - IdentifierId, - Instruction, - InstructionValue, - Place, - Terminal, -} from "./HIR"; -import { mapTerminalSuccessors } from "./HIRBuilder"; -import { printMixedHIR, printPlace } from "./PrintHIR"; - -const HOOKS: Map = new Map([ - ["useState", { kind: "State", capability: Capability.Freeze }], - ["useRef", { kind: "Ref", capability: Capability.Freeze }], -]); - -type HookKind = { kind: "State" } | { kind: "Ref" } | { kind: "Custom" }; -type Hook = HookKind & { capability: Capability }; - -/** - * For every usage of a value in the given function, infers whether that usage - * is frozen, readonly, or mutable: - * - frozen: the value is known to be "owned" by React and is therefore permanently - * and transitively immutable. - * - readonly: the value is not frozen, but this usage of the value does not modify it. - * the value may be mutated by a subsequent reference. Examples include referencing - * the operands of a binary expression, or referencing the items/properties of an - * array or object literal. - * - mutable: the value is not frozen and this usage *may* modify it. Examples include - * passing a value to as a function argument or assigning into an object. - * - * Note that the inference follows variable assignment, so assigning a frozen value - * to a different value will infer usages of the other variable as frozen as well. - * - * The inference assumes that the code follows the rules of React: - * - React function arguments are frozen (component props, hook arguments). - * - Hook arguments are frozen at the point the hook is invoked. - * - React function return values are frozen at the point of being returned, - * thus the return value of a hook call is frozen. - * - JSX represents invocation of a React function (the component) and - * therefore all values passed to JSX become frozen at the point the JSX - * is created. - * - * ## Algorithm - * - * The algorithm creates a "use-use" graph in which each usage of a variable links to - * the previous (incoming) and subsequent (outgoing) usages of that variable. The primary - * purpose of this graph is to perform inference of usages that are frozen vs not, including - * accounting for control-flow and reassignment, and to update the input *in place* to - * annotate `Place` with the appropriate capability. The set of vertexs is returned for - * debugging purposes. - * - * - First create a mapping of the first and last usages of each top-level identifier - * in each block, in isolation (ie without considering control-flow paths between - * blocks). - * - Then iterate over the blocks in control-flow order and link the last usage of - * identifiers in predecssor blocks with the first usage in successor blocks. - * - Then find all vertices corresponding to frozen usage of a value, and propagate - * that "frozenness" forward to all subsequent usages of that value. - */ -export default function buildDefUseGraph(fn: HIRFunction): Array { - const graph = new UseGraph(); - - const blockResults: Map = new Map(); - for (const [blockId, block] of fn.body.blocks) { - const blockResult = buildInputsOutputsForBlock(graph, block); - blockResults.set(blockId, blockResult); - } - - const preambleBuilder = new BlockResultBuilder(graph); - for (const param of fn.params) { - const place: Place = { - kind: "Identifier", - memberPath: null, - value: param, - path: null as any, // TODO - capability: Capability.Freeze, - }; - preambleBuilder.init(place, null, true); - } - const preambleResult = preambleBuilder.build(); - - // Iterate over the CFG linking outputs of predecssor blocks to the inputs - // of successor blocks, stopping once all links have been established. - const queue: Array<{ - blockId: BlockId; - prevResult: BlockResult; - }> = [{ blockId: fn.body.entry, prevResult: preambleResult }]; - while (queue.length !== 0) { - const { blockId, prevResult } = queue.shift()!; - - // Link the previous block's outputs to the next block's inputs - const blockResult = blockResults.get(blockId)!; - const hasChange = linkPreviousNextBlock(graph, prevResult, blockResult); - - // If there were changes to this block's outgoing edges, update any - // successor blocks - if (hasChange || blockId === fn.body.entry) { - const block = fn.body.blocks.get(blockId)!; - // TODO: add a forEachSuccessor helper, this maps the terminal unnecessarily - const _ = mapTerminalSuccessors( - block.terminal, - (blockId, isFallthrough) => { - if (!isFallthrough) { - queue.push({ blockId, prevResult: blockResult }); - } - return blockId; - } - ); - } - } - const vertices = graph.build(); - for (const vertex of vertices) { - invariant( - vertex.place === null || vertex.place.capability !== Capability.Unknown, - "Expected all vertices to have a capability inferred" - ); - - // Vertices derived from a frozen value are also frozen - if ( - vertex.place !== null && - vertex.place.capability === Capability.Freeze - ) { - flowFrozennessForwards(vertex, 0); - } - // Join nodes are created before we know if they will be consumed, - // prune join nodes without any outgoing edges to aid visualization. - // note that this is not required for correctness. - if (vertex.place === null && vertex.outgoing.size === 0) { - for (const incoming of vertex.incoming) { - incoming.outgoing.delete(vertex); - } - vertex.incoming.clear(); - } - } - return vertices; -} - -/** - * Once a value is known to be frozen, all usages forward of that point must be frozen too. - */ -function flowFrozennessForwards(vertex: Vertex, epoch: number) { - if (vertex.epoch === epoch) { - return; - } - vertex.epoch = epoch; - if (vertex.place !== null) { - vertex.place.capability = Capability.Freeze; - } - for (const outgoing of vertex.outgoing) { - flowFrozennessForwards(outgoing, epoch); - } -} - -/** - * Link the last usages from a predecessor block to the first usages in a successor block, - * and propagate any values used in the predecessor but _unused_ in the successor. - */ -function linkPreviousNextBlock( - graph: UseGraph, - prevBlock: BlockResult, - nextBlock: BlockResult -): boolean { - // are there any changes to the *outgoing* edges of `nextBlock`? - let hasChanges = false; - - // for each first usage of next block, link it to last usage from prev block. - for (const [id, nextVertex] of nextBlock.firstUsage) { - if (nextVertex.isReassignment) { - // nextVertex was a full reassignment, do not attach an edge. - continue; - } - const prevVertex = prevBlock.lastUsage.get(id); - if (prevVertex != null && prevVertex !== nextVertex) { - // Note: don't update hasChanges here bc these do not affect the *outgoing* edges - prevVertex.outgoing.add(nextVertex); - nextVertex.incoming.add(prevVertex); - } - } - // for each last usage in the prev block that was not already linked in the above - // loop, add an outgoing edge to pass-through the data. the value may be used by - // a later block. - for (const [id, prevVertex] of prevBlock.lastUsage) { - if (nextBlock.firstUsage.has(id)) { - // already handled in the above loop - continue; - } - const nextVertex = nextBlock.lastUsage.get(id); - if (nextVertex == null) { - // First time propagating a value for this id: use the prev vertex from - // the predecessor as the output of the successor - hasChanges = true; - nextBlock.lastUsage.set(id, prevVertex); - } else if (nextVertex === prevVertex) { - // already propagated - continue; - } else { - let joinVertex; - if (nextVertex.place === null) { - // A join vertex was already created, link the successor value to it - joinVertex = nextVertex; - } else { - // A normal vertex was propagated through on the previous visit, but - // now there is a different value that needs to be propagated through. - // swap the node for a "join" vertex, adding the previous node as an - // input to that node. - joinVertex = graph.join(); - nextVertex.outgoing.add(joinVertex); - joinVertex.incoming.add(nextVertex); - nextBlock.lastUsage.set(id, joinVertex); - } - // the incoming node may itself be a join vertex, add its inputs to - // avoid linking join vertices to other join vertices. - if (prevVertex.place === null) { - for (const prev of prevVertex.incoming) { - hasChanges = - hasChanges || - !joinVertex.incoming.has(prev) || - !prev.outgoing.has(joinVertex); - joinVertex.incoming.add(prev); - prev.outgoing.add(joinVertex); - } - } else { - hasChanges = - hasChanges || - !joinVertex.incoming.has(prevVertex) || - !prevVertex.outgoing.has(joinVertex); - joinVertex.incoming.add(prevVertex); - prevVertex.outgoing.add(joinVertex); - } - } - } - return hasChanges; -} - -/** - * Iterates over a single basic block and constructs a mapping of the first and last usages - * of each Place referenced in that block. - */ -function buildInputsOutputsForBlock( - graph: UseGraph, - block: BasicBlock -): BlockResult { - const builder = new BlockResultBuilder(graph); - - for (const instr of block.instructions) { - const instrValue = instr.value; - let valueCapability = Capability.Readonly; - switch (instrValue.kind) { - case "BinaryExpression": { - valueCapability = Capability.Freeze; - builder.reference(instrValue.left, instrValue, Capability.Readonly); - builder.reference(instrValue.right, instrValue, Capability.Readonly); - break; - } - case "ArrayExpression": { - for (const element of instrValue.elements) { - builder.reference(element, instrValue, Capability.Readonly); - } - break; - } - case "NewExpression": { - builder.reference(instrValue.callee, instrValue, Capability.Mutable); - for (const arg of instrValue.args) { - builder.reference(arg, instrValue, Capability.Mutable); - } - break; - } - case "CallExpression": { - let capability = Capability.Mutable; - const hook = parseHookCall(instrValue.callee); - if (hook !== null) { - capability = hook.capability; - valueCapability = hook.capability; - } - builder.reference(instrValue.callee, instrValue, capability); - for (const arg of instrValue.args) { - builder.reference(arg, instrValue, capability); - } - break; - } - case "ObjectExpression": { - // Object construction captures but does not modify the key/property values - if (instrValue.properties !== null) { - for (const [_key, value] of Object.entries(instrValue.properties)) { - builder.reference(value, instrValue, Capability.Readonly); - } - } - break; - } - case "UnaryExpression": { - valueCapability = Capability.Freeze; // TODO check that value must be a primitive, or make conditional based on the operator - builder.reference(instrValue.value, instrValue, Capability.Readonly); - break; - } - case "OtherStatement": { - // TODO: handle other statement kinds - break; - } - case "JsxExpression": { - builder.reference(instrValue.tag, instrValue, Capability.Freeze); - for (const [_prop, value] of Object.entries(instrValue.props)) { - builder.reference(value, instrValue, Capability.Freeze); - } - if (instrValue.children !== null) { - for (const child of instrValue.children) { - builder.reference(child, instrValue, Capability.Freeze); - } - } - break; - } - case "JSXText": - case "Primitive": { - valueCapability = Capability.Readonly; - break; - } - case "Identifier": { - builder.reference(instrValue, instrValue, Capability.Readonly); - valueCapability = instrValue.capability; - if (instr.lvalue !== null && instr.lvalue.place.memberPath === null) { - builder.assign(instr.lvalue.place, instr, instrValue); - instr.lvalue.place.capability = valueCapability; - continue; - } - break; - } - default: { - assertExhaustive(instrValue, "Unexpected instruction kind"); - } - } - if (instr.lvalue !== null) { - if (instr.lvalue.place.memberPath == null) { - builder.init(instr.lvalue.place, instr, true); - instr.lvalue.place.capability = valueCapability; - } else { - builder.reference(instr.lvalue.place, instr, valueCapability); - } - } - } - switch (block.terminal.kind) { - case "throw": { - builder.reference( - block.terminal.value, - block.terminal, - Capability.Freeze - ); - break; - } - case "return": { - if (block.terminal.value !== null) { - builder.reference( - block.terminal.value, - block.terminal, - Capability.Freeze - ); - } - break; - } - case "if": { - builder.reference( - block.terminal.test, - block.terminal, - Capability.Readonly - ); - break; - } - case "switch": { - builder.reference( - block.terminal.test, - block.terminal, - Capability.Readonly - ); - for (const case_ of block.terminal.cases) { - if (case_.test !== null) { - builder.reference(case_.test, block.terminal, Capability.Readonly); - } - } - break; - } - case "goto": { - break; - } - default: { - assertExhaustive( - block.terminal, - `Unexpected terminal kind '${(block.terminal as any as Terminal).kind}'` - ); - } - } - - return builder.build(); -} - -function parseHookCall(place: Place): Hook | null { - if (place.memberPath !== null) { - // Hook calls must be statically resolved - return null; - } - const name = place.value.name; - if (name === null || !name.match(/^_?use/)) { - return null; - } - const hook = HOOKS.get(name); - if (hook != null) { - return hook; - } - return { kind: "Custom", capability: Capability.Freeze }; -} - -type BlockResult = { - firstUsage: Map; - lastUsage: Map; -}; - -class BlockResultBuilder { - #firstUsage: Map = new Map(); - #lastUsage: Map = new Map(); - #graph: UseGraph; - - constructor(graph: UseGraph) { - this.#graph = graph; - } - - build(): BlockResult { - return { - firstUsage: this.#firstUsage, - lastUsage: this.#lastUsage, - }; - } - - /** - * Represents assignment of a value to a Place. Unlike with `reference()`, - * this does *not* establish an edge between this usage of the place and - * previous usages, because the value is not the same. - */ - init( - place: Place, - instr: Instruction | InstructionValue | Terminal | null, - isReassignment: boolean - ): Vertex { - const id = place.value.id; - const vertex = this.#graph.init(place, instr, isReassignment); - if (!this.#firstUsage.has(id)) { - this.#firstUsage.set(id, vertex); - } - this.#lastUsage.set(id, vertex); - return vertex; - } - - /** - * Represents assigning a (new) value to @param target via the given @param instr, - * with @param value as the value being assigned. - * - * This breaks the use chain, such that this and subsequent usages of @param target - * are not associated to previous usages. This replicates SSA semantics, conceptually - * @param target is a new place now. - */ - assign(target: Place, instr: Instruction, value: Place) { - const targetVertex = this.init(target, instr, true); - const valueVertex = this.#graph.get(value)!; - targetVertex.incoming.add(valueVertex); - valueVertex.outgoing.add(targetVertex); - } - - /** - * Represents a reference (usage of) a Place. This establishes an edge - * with this usage and the previous usage - */ - reference( - place: Place, - instr: Instruction | InstructionValue | Terminal, - capability: Capability - ) { - const id = place.value.id; - place.capability = capability; - const prevVertex = this.#lastUsage.get(id); - const vertex = this.init(place, instr, false); - - if (prevVertex != null) { - prevVertex.outgoing.add(vertex); - vertex.incoming?.add(prevVertex); - } - } -} - -class IdGenerator { - #value: number = 0; - - next(): number { - return this.#value++; - } -} - -class UseGraph { - #idGenerator: IdGenerator = new IdGenerator(); - #verticesByPlace: Map = new Map(); - #vertices: Array = []; - - get(place: Place): Vertex | null { - return this.#verticesByPlace.get(place) ?? null; - } - - init( - place: Place, - instr: Instruction | InstructionValue | Terminal | null, - isReassignment: boolean - ): Vertex { - let vertex = this.#verticesByPlace.get(place); - if (vertex == null) { - vertex = new Vertex(place, instr, this.#idGenerator, isReassignment); - this.#verticesByPlace.set(place, vertex); - this.#vertices.push(vertex); - } - return vertex; - } - - join(): Vertex { - const vertex = new Vertex(null, null, this.#idGenerator, false); - this.#vertices.push(vertex); - return vertex; - } - - build(): Array { - return this.#vertices; - } -} - -/** - * Represents a distinct usage of a `Place` within a program. Note that - * vertices are only created for top-level identifiers, not for points - * within an object. So both `x` and `x.y` create vertices for `x`, - * though this is enforced by the _construction_ of vertices in UseGraph, - * not the implementation of Vertex itself. - */ -class Vertex { - /** - * the set of vertices where *this* place was previously referenced - */ - incoming: Set = new Set(); - - /** - * the set of vertices where *this* place is subseqently referenced - */ - outgoing: Set = new Set(); - - /** - * The place that is using the value. - */ - place: Place | null; - - /** - * The instruction where the reference occurs (for debugging) - */ - instr: Instruction | InstructionValue | Terminal | null; - - /** - * Does this vertex represent an assignment? If yes, incoming - * represents the set of values that are *assigned* to this vertex, - * and not previous usages. - */ - isReassignment: boolean; - - /** - * Unique identifier for this vertex (for debugging) - */ - id: string; - - /** - * The last epoch in which this vertex was visited. This is used during - * data-flow analysis post construction of the graph to ensure termination - * by avoiding revisiting the same nodes in a given pass. - */ - epoch: number | null = null; - - constructor( - place: Place | null, - instr: Instruction | InstructionValue | Terminal | null, - idGenerator: IdGenerator, - isReassignment: boolean - ) { - this.place = place; - this.instr = instr; - this.id = `v${idGenerator.next()}`; - this.isReassignment = isReassignment; - } -} - -/** - * Prints the graph into GraphViz DOT format. - * https://graphviz.org/doc/info/lang.html - */ -export function printGraph(vertices: Array): string { - const output = []; - - for (const vertex of vertices) { - if ( - vertex.place === null && - vertex.incoming.size === 0 && - vertex.outgoing.size === 0 - ) { - continue; - } - const vertexId = vertex.id; - output.push( - `${vertexId} [ label="${vertexId} (${ - vertex.place ? printPlace(vertex.place) : "" - } @ ${ - vertex.instr - ? printMixedHIR(vertex.instr).replaceAll('"', '\\"') - : "" - }) ${vertex.isReassignment ? "" : ""}", shape="box" ]` - ); - for (const outgoing of vertex.outgoing) { - const outgoingId = outgoing.id; - output.push(`${vertexId} -> ${outgoingId}`); - } - } - const lines = output.map((line) => " " + line); - lines.unshift("digraph BuildDefUseGraph {"); - lines.push("}"); - return lines.join("\n"); -} - -function mapToJson(map: Map): { - [key: string]: string; -} { - const result: { [key: string]: string } = {}; - for (const [id, vertex] of map) { - result[id] = vertex.id; - } - return result; -}