From beb6e1431ca53b6f20f69e40f1dbb95bbdc5095a Mon Sep 17 00:00:00 2001 From: Joseph Savona Date: Tue, 11 Oct 2022 11:32:49 -0700 Subject: [PATCH] [new-arch] Improve reference-kind analysis This implements an alternative approach to reference kind inference in the new architecture based on feedback. Here, we track an environment that maps top-level identifiers (IdentifierId) to the "kind" of value stored: immutable, mutable, frozen, or maybe-frozen. We then do a forward data flow analysis updating this environment based on the semantics of each instruction combined with the types of values present. For example a reference of a value in a "mutable" position is inferred as readonly if the value is known to be frozen or immutable. Similarly, a usage of a reference in a "freeze" position is inferred as a freeze if the value is not yet definitively frozen, and inferred as readonly if the value is already frozen. When multiple control paths converge we merge the previous and new incoming environments, and only reprocess the block if the environment changed relative to the previous value. This has some noticeable benefits over the previous version: * We now infer precisely where `makeReadOnly()` calls need to be inserted, aka points where a value needs to be frozen may not yet be frozen. * We track immutable values and can infer their usage as readonly rather than mutable. * The system handles aliasing by representing values as distinct from variables, so that we can handle situations such as: ```javascript const a = []; // env: {a: value0; value0: mutable} const b = a; // env: {a: value0, b: value0; value0: mutable} freeze(a); // env: {a: value0, b: value0; value0: frozen} mayMutate(b); // ordinarily inferred as a mutable reference, but we know its readonly ``` --- compiler/forget/src/HIR/BuildDefUseGraph.ts | 26 +- compiler/forget/src/HIR/HIR.ts | 9 +- compiler/forget/src/HIR/InferMutability.ts | 676 ----------------- .../src/HIR/InferReferenceCapability.ts | 705 ++++++++++++++++++ compiler/forget/src/HIR/ScopeAnalysis.ts | 2 +- .../__tests__/fixtures/hir-svg/component.svg | 484 ++++++------ .../src/__tests__/fixtures/hir-svg/switch.svg | 194 ++--- .../src/__tests__/fixtures/hir/call.expect.md | 14 +- .../fixtures/hir/component.expect.md | 58 +- .../fixtures/hir/conditional-break.expect.md | 68 +- .../hir/conditional-on-mutable.expect.md | 30 +- .../fixtures/hir/constructor.expect.md | 14 +- .../fixtures/hir/frozen-after-alias.expect.md | 70 ++ .../fixtures/hir/frozen-after-alias.js | 9 + .../fixtures/hir/hook-call.expect.md | 16 +- .../hir/hooks-freeze-arguments.expect.md | 71 ++ .../fixtures/hir/hooks-freeze-arguments.js | 10 + ...reeze-possibly-mutable-arguments.expect.md | 97 +++ ...hooks-freeze-possibly-mutable-arguments.js | 17 + .../hir/independent-across-if.expect.md | 10 +- .../fixtures/hir/independent.expect.md | 8 +- .../hir/interdependent-across-if.expect.md | 10 +- .../fixtures/hir/interdependent.expect.md | 8 +- .../hir/property-assignment.expect.md | 14 +- .../hir/reassignment-conditional.expect.md | 18 +- .../fixtures/hir/reassignment.expect.md | 16 +- .../__tests__/fixtures/hir/simple.expect.md | 16 +- .../hir/switch-non-final-default.expect.md | 26 +- .../__tests__/fixtures/hir/switch.expect.md | 26 +- compiler/forget/src/__tests__/hir-test.ts | 36 +- compiler/forget/yarn.lock | 44 +- 31 files changed, 1557 insertions(+), 1245 deletions(-) delete mode 100644 compiler/forget/src/HIR/InferMutability.ts create mode 100644 compiler/forget/src/HIR/InferReferenceCapability.ts create mode 100644 compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.js create mode 100644 compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.js create mode 100644 compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.js diff --git a/compiler/forget/src/HIR/BuildDefUseGraph.ts b/compiler/forget/src/HIR/BuildDefUseGraph.ts index e31c4835ca..04bef729f8 100644 --- a/compiler/forget/src/HIR/BuildDefUseGraph.ts +++ b/compiler/forget/src/HIR/BuildDefUseGraph.ts @@ -22,8 +22,8 @@ import { mapTerminalSuccessors } from "./HIRBuilder"; import { printMixedHIR, printPlace } from "./PrintHIR"; const HOOKS: Map = new Map([ - ["useState", { kind: "State", capability: Capability.Frozen }], - ["useRef", { kind: "Ref", capability: Capability.Frozen }], + ["useState", { kind: "State", capability: Capability.Freeze }], + ["useRef", { kind: "Ref", capability: Capability.Freeze }], ]); type HookKind = { kind: "State" } | { kind: "Ref" } | { kind: "Custom" }; @@ -86,7 +86,7 @@ export default function buildDefUseGraph(fn: HIRFunction): Array { memberPath: null, value: param, path: null as any, // TODO - capability: Capability.Frozen, + capability: Capability.Freeze, }; preambleBuilder.init(place, null, true); } @@ -131,7 +131,7 @@ export default function buildDefUseGraph(fn: HIRFunction): Array { // Vertices derived from a frozen value are also frozen if ( vertex.place !== null && - vertex.place.capability === Capability.Frozen + vertex.place.capability === Capability.Freeze ) { flowFrozennessForwards(vertex, 0); } @@ -157,7 +157,7 @@ function flowFrozennessForwards(vertex: Vertex, epoch: number) { } vertex.epoch = epoch; if (vertex.place !== null) { - vertex.place.capability = Capability.Frozen; + vertex.place.capability = Capability.Freeze; } for (const outgoing of vertex.outgoing) { flowFrozennessForwards(outgoing, epoch); @@ -260,7 +260,7 @@ function buildInputsOutputsForBlock( let valueCapability = Capability.Readonly; switch (instrValue.kind) { case "BinaryExpression": { - valueCapability = Capability.Frozen; + valueCapability = Capability.Freeze; builder.reference(instrValue.left, instrValue, Capability.Readonly); builder.reference(instrValue.right, instrValue, Capability.Readonly); break; @@ -301,7 +301,7 @@ function buildInputsOutputsForBlock( break; } case "UnaryExpression": { - valueCapability = Capability.Frozen; // TODO check that value must be a primitive, or make conditional based on the operator + 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; } @@ -310,13 +310,13 @@ function buildInputsOutputsForBlock( break; } case "JsxExpression": { - builder.reference(instrValue.tag, instrValue, Capability.Frozen); + builder.reference(instrValue.tag, instrValue, Capability.Freeze); for (const [_prop, value] of Object.entries(instrValue.props)) { - builder.reference(value, instrValue, Capability.Frozen); + builder.reference(value, instrValue, Capability.Freeze); } if (instrValue.children !== null) { for (const child of instrValue.children) { - builder.reference(child, instrValue, Capability.Frozen); + builder.reference(child, instrValue, Capability.Freeze); } } break; @@ -354,7 +354,7 @@ function buildInputsOutputsForBlock( builder.reference( block.terminal.value, block.terminal, - Capability.Frozen + Capability.Freeze ); break; } @@ -363,7 +363,7 @@ function buildInputsOutputsForBlock( builder.reference( block.terminal.value, block.terminal, - Capability.Frozen + Capability.Freeze ); } break; @@ -416,7 +416,7 @@ function parseHookCall(place: Place): Hook | null { if (hook != null) { return hook; } - return { kind: "Custom", capability: Capability.Frozen }; + return { kind: "Custom", capability: Capability.Freeze }; } type BlockResult = { diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index d2432ab19d..5bdfb38ac6 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -221,12 +221,11 @@ export type Identifier = { export enum Capability { // Default value: not allowed after lifetime inference Unknown = "", - // The value is known to be deeply, permanently mutable at this point. - Frozen = "frozen", - // The value is not modified at this point or thereafter, but is not guaranteed to be - // frozen. It would be safe to freeze the value at this point. + // The value is made frozen at this point. + Freeze = "freeze", + // The value is not modified at this point. Readonly = "readonly", - // The value is mutated at this point or at some later point. + // The value is modified at this point. Mutable = "mutable", } diff --git a/compiler/forget/src/HIR/InferMutability.ts b/compiler/forget/src/HIR/InferMutability.ts deleted file mode 100644 index dff3bd6dcc..0000000000 --- a/compiler/forget/src/HIR/InferMutability.ts +++ /dev/null @@ -1,676 +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 invariant from "invariant"; -import { assertExhaustive } from "../Common/utils"; -import { - BasicBlock, - BlockId, - Capability, - HIRFunction, - IdentifierId, - Instruction, - InstructionValue, - Place, - Terminal, -} from "./HIR"; -import { printMixedHIR, printPlace } from "./PrintHIR"; - -const HOOKS: Map = new Map([ - ["useState", { kind: "State", capability: Capability.Frozen }], - ["useRef", { kind: "Ref", capability: Capability.Frozen }], -]); - -type HookKind = { kind: "State" } | { kind: "Ref" } | { kind: "Custom" }; -type Hook = HookKind & { capability: Capability }; - -/** - * For every usage of a value, infers whether the value is accessed as frozen, mutable, - * or readonly: - * - Frozen: the value is known to be deeply immutable. This usage cannot alter the value, - * and the value cannot change after this usage. The usage can therefore safely memoize - * based on the value. - * - Mutable: the value may be modified by this or a subsequent usage. - * - Readonly: the value is not modified by this or any subsequent usage, but was previously - * mutable. Thus the first usage of a value as readonly (after a previous mutable usage) - * is a point at which the value could safely frozen. - * - * In general usages default to be inferred as readonly (for operations that read) or mutable - * (for operations that may write). Usages can only be inferred as frozen when: - * - The value is derived from a reactive input: props, a hook argument. Derived here means - * that the value is itself a reactive input (`props`) or statically known to be extracted - * purely from a reactive input (`props.a`, `props.a[1]` etc). - * - The value is "moved" into a reactive ouput, ie is captured into a component/hook return - * value. - * - The value is known to be a primitive (boolean, number, string, null, or undefined). This - * is only the case when the value is a constant primitive *or* is the result of an - * operation that, under JS semantics, must produce a primitive. Examples are binary - * expressions. - * - * ## Algorithm - * Fixpoint iteration of the CFG to construct use-use chains (graphs) in which nodes - * are usages of a particular value and the required capability (frozen/read/write), - * and each "usage" node points to predecessor node(s). Iteration stops once no new - * edges/nodes need to be added to the graph. - * - * After construction, walk the graph to propagate frozenness forward and mutability - * backward: - * - If a usage of a value is frozen, then any subsequent usage of that value must be - * frozen as well. Flow the frozenness forward through the graph, following only - * use chains (outoing edges, but not captures). - * - If a usage of a vale is mutable, then any prior readonly references of that value - * must be marked as mutable as well (since there is a subsequent mutation). Again, - * usages only remain readonly if there is no subsequent mutation. - * - Finally, any value that is *initialized* as readonly and never modified (ie, that is - * still readonly in the graph) can be marked frozen (and all of its usages as frozen), - * since it is never ever modified. - */ -export default function inferLifetimes(fn: HIRFunction): Map { - const graph = UseGraph.empty(); - for (const param of fn.params) { - const place: Place = { - kind: "Identifier", - memberPath: null, - value: param, - path: null as any, // TODO - capability: Capability.Frozen, - }; - graph.init(place, null); - } - - const queue: Array = [{ block: fn.body.entry, graph }]; - const blockMemory: Map = new Map(); - while (queue.length !== 0) { - const { block: blockId, graph: inputGraph } = queue.shift()!; - const prevGraph = blockMemory.get(blockId); - let nextGraph = null; - if (prevGraph == null) { - blockMemory.set(blockId, inputGraph); - nextGraph = inputGraph.snapshot(); - } else { - const merged = prevGraph.snapshot(); - const hasChanges = merged.merge(inputGraph); - if (!hasChanges) { - continue; - } - blockMemory.set(blockId, merged); - nextGraph = prevGraph.snapshot(); - } - - const block = fn.body.blocks.get(blockId)!; - inferBlock(nextGraph, block); - - const terminal = block.terminal; - switch (terminal.kind) { - case "throw": { - nextGraph.reference(terminal.value, terminal, Capability.Frozen); - break; - } - case "return": { - if (terminal.value !== null) { - nextGraph.reference(terminal.value, terminal, Capability.Frozen); - } - break; - } - case "goto": { - queue.push({ block: terminal.block, graph: nextGraph }); - break; - } - case "if": { - nextGraph.reference(terminal.test, terminal, Capability.Readonly); - queue.push({ block: terminal.consequent, graph: nextGraph }); - queue.push({ block: terminal.alternate, graph: nextGraph }); - break; - } - case "switch": { - nextGraph.reference(terminal.test, terminal, Capability.Readonly); - for (const case_ of terminal.cases) { - if (case_.test !== null) { - nextGraph.reference(case_.test, terminal, Capability.Readonly); - } - queue.push({ block: case_.block, graph: nextGraph.snapshot() }); - } - break; - } - default: { - assertExhaustive(terminal, "Unexpected terminal kind"); - } - } - } - - // perform inference over the constructed graph and return it - return analyzeGraph(graph); -} - -type QueueEntry = { - block: BlockId; - graph: UseGraph; -}; - -function analyzeGraph(graph: UseGraph): Map { - const vertices = graph.build(); - let epoch = 0; - - // First flow frozen values forward through the graph - for (const vertex of vertices.values()) { - if (vertex.place.capability === Capability.Frozen) { - flowFrozennessForwards(vertex, epoch++); - } - } - // Then flow mutability backward through the graph - for (const vertex of vertices.values()) { - if (vertex.place.capability === Capability.Mutable) { - flowMutabilityBackwards(vertex, epoch++); - } - } - // Finally, any *entry* nodes that are readonly are now known to never be mutated, - // so convert them to frozen (and flow that frozenness forward) - for (const vertex of vertices.values()) { - invariant( - vertex.place.capability !== Capability.Unknown, - "Expected capability to have been inferred as frozen, readonly, or mutable" - ); - if ( - vertex.place.capability === Capability.Readonly && - vertex.incoming.size === 0 - ) { - flowFrozennessForwards(vertex, epoch); - } - } - 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; - vertex.place.capability = Capability.Frozen; - for (const outgoing of vertex.outgoing) { - flowFrozennessForwards(outgoing, epoch); - } -} - -/** - * If a value is known to be mutable, any previous readonly references must be mutable too. - */ -function flowMutabilityBackwards(vertex: Vertex, epoch: number) { - if (vertex.epoch === epoch) { - return; - } - vertex.epoch = epoch; - if (vertex.place.capability === Capability.Frozen) { - return; - } - vertex.place.capability = Capability.Mutable; - for (const incoming of vertex.incoming) { - flowMutabilityBackwards(incoming, epoch); - } - for (const [id, capture] of vertex.captures) { - flowMutabilityBackwards(capture, epoch); - } -} - -function inferBlock(graph: UseGraph, block: BasicBlock) { - for (const instr of block.instructions) { - if (instr.lvalue !== null) { - if (instr.lvalue.place.memberPath == null) { - graph.init(instr.lvalue.place, instr); - } else { - graph.reference(instr.lvalue.place, instr, Capability.Mutable); - } - } - const instrValue = instr.value; - let valueCapability = Capability.Readonly; - switch (instrValue.kind) { - case "BinaryExpression": { - valueCapability = Capability.Frozen; - graph.reference(instrValue.left, instrValue, Capability.Readonly); - graph.reference(instrValue.right, instrValue, Capability.Readonly); - break; - } - case "ArrayExpression": { - for (const element of instrValue.elements) { - graph.reference(element, instrValue, Capability.Readonly); - if (instr.lvalue !== null) { - graph.capture(instr.lvalue.place, instr, element); - } - } - break; - } - case "NewExpression": { - graph.reference(instrValue.callee, instrValue, Capability.Mutable); - let prevArg: Place | null = null; - for (const arg of instrValue.args) { - graph.reference(arg, instrValue, Capability.Mutable); - if (instr.lvalue !== null) { - graph.capture(instr.lvalue.place, instr, arg); - } - if (prevArg !== null) { - graph.capture(prevArg, instr, arg); - } - prevArg = arg; - } - break; - } - case "CallExpression": { - let capability = Capability.Mutable; - const hook = parseHookCall(instrValue.callee); - if (hook !== null) { - capability = hook.capability; - valueCapability = hook.capability; - } - graph.reference(instrValue.callee, instrValue, capability); - let prevArg: Place | null = null; - for (const arg of instrValue.args) { - graph.reference(arg, instrValue, capability); - if (instr.lvalue !== null) { - graph.capture(instr.lvalue.place, instr, arg); - } - if (prevArg !== null) { - graph.capture(prevArg, instr, arg); - } - prevArg = arg; - } - 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)) { - graph.reference(value, instrValue, Capability.Readonly); - if (instr.lvalue !== null) { - graph.capture(instr.lvalue.place, instr, value); - } - } - } - break; - } - case "UnaryExpression": { - valueCapability = Capability.Frozen; // TODO check that value must be a primitive, or make conditional based on the operator - graph.reference(instrValue.value, instrValue, Capability.Readonly); - break; - } - case "OtherStatement": { - // TODO: handle other statement kinds - break; - } - case "JsxExpression": { - graph.reference(instrValue.tag, instrValue, Capability.Readonly); - for (const [_prop, value] of Object.entries(instrValue.props)) { - graph.reference(value, instrValue, Capability.Readonly); - if (instr.lvalue !== null) { - graph.capture(instr.lvalue.place, instr, value); - } - } - if (instrValue.children !== null) { - for (const child of instrValue.children) { - graph.reference(child, instrValue, Capability.Readonly); - if (instr.lvalue !== null) { - graph.capture(instr.lvalue.place, instr, child); - } - } - } - break; - } - case "JSXText": - case "Primitive": { - valueCapability = Capability.Frozen; - break; - } - case "Identifier": { - graph.reference(instrValue, instrValue, Capability.Readonly); - if (instr.lvalue !== null) { - graph.assign(instr.lvalue.place, instr, instrValue); - } - valueCapability = instrValue.capability; - break; - } - default: { - assertExhaustive(instrValue, "Unexpected instruction kind"); - } - } - if (instr.lvalue !== null) { - instr.lvalue.place.capability = valueCapability; - } - } -} - -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.Frozen }; -} - -/** - * A graph of usages of references within a program. - */ -class UseGraph { - /** - * Represents the last usage for each top-level identifier (by IdentifierId) - * in the program. So after `let x = []`, there will be an entry mapping - * `x` to a vertex. A subsequent `x.y` reference will update the mapping - * for `x` to point to a new vertex, with the previous `let x = []` as - * an incoming edge. Note again that vertices are always created based - * on the top-level identifier id and ignore member paths. - */ - #nodes: Map; - - /** - * A mapping of places to vertices, allowing repeated operations against - * the same place (eg referencing, assigning, etc) to update the single - * vertex for that place. - * - * NOTE: this algorithm and data structure relies on `Place` instances - * being unique, hence BuildHIR is careful to clone Place instances rather - * than use structural sharing. - */ - #vertices: Map; - - static empty(): UseGraph { - return new UseGraph(new Map(), new Map()); - } - - constructor(nodes: Map, vertices: Map) { - this.#nodes = nodes; - this.#vertices = vertices; - } - - /** - * Lookup the current vertex for a given identifier. - */ - lookup(id: IdentifierId): Vertex | null { - return this.#nodes.get(id) ?? null; - } - - /** - * 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 | null) { - let vertex = this.#vertices.get(place); - if (vertex == null) { - vertex = new Vertex(place, instr); - this.#vertices.set(place, vertex); - } - this.#nodes.set(place.value.id, 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) { - this.init(target, instr); - if (value.capability === Capability.Frozen) { - target.capability = Capability.Frozen; - } - this.capture(target, instr, value); - } - - /** - * Represents data flow in which two places capture references to each other, - * such that a mutation of one place may affect data accessible via the other - * place or vice versa. This includes assignment (`x = y`) but also things like - * array or object construction, where `x = [y]` means that modifying either - * `x` or `y` _could_ be visible through either refernece (eg `x[0].foo = ...`) - * would modify `y`. - */ - capture(target: Place, instr: Instruction, value: Place) { - const targetVertex = this.#vertices.get(target)!; - const valueVertex = this.#vertices.get(value)!; - - if ( - targetVertex.place.capability === Capability.Frozen || - valueVertex.place.capability === Capability.Frozen - ) { - return; - } - - targetVertex.captures.set(value.value.id, valueVertex); - valueVertex.captures.set(target.value.id, 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 - ) { - place.capability = capability; - let vertex = this.#vertices.get(place); - if (vertex == null) { - vertex = new Vertex(place, instr); - this.#vertices.set(place, vertex); - } - - let prev = this.#nodes.get(place.value.id); - if (prev != null) { - vertex.incoming.add(prev); - prev.outgoing.add(vertex); - - for (const [id, _vertex] of prev.captures) { - const capture = this.#nodes.get(id)!; - if (capture.captures.has(place.value.id)) { - vertex.captures.set(id, capture); - } - } - - if (prev.place.capability === Capability.Frozen) { - vertex.place.capability = Capability.Frozen; - } - } - this.#nodes.set(place.value.id, vertex); - } - - /** - * Returns a snapshot of the graph that can be used to represent different - * control flows paths, and which later may merge together (see `merge()`). - * - * The snapshot contains a distinct mapping of the most recent vertex per - * identifier, but *shares* the mapping of places to vertices (vertices are - * stable across different control flow paths). - * - * The intent is that fixpoint iteration relies on updating the `#nodes` - * mapping of identifiers to most recent vertex, but the underlying graph - * of vertices is intended to be shared across control flow paths. - */ - snapshot(): UseGraph { - const nodes = new Map(this.#nodes); - return new UseGraph(nodes, this.#vertices); - } - - /** - * Giver some @param other use graph that shares an ancestor with @param this, - * merges the graphs together and returns whether there were any changes to - * the underlying edges (if any new edges were added). - * - * Note that @param this and @param other must share an ancestor, ie one must - * be derived from a `snapshot()` of the other, or they must both be derived - * from a `snapshot()` of the same instance. - */ - merge(other: UseGraph): boolean { - let hasChange = false; - for (const [id, newIncoming] of other.#nodes) { - const prevIncoming = this.#nodes.get(id); - if (prevIncoming == null || newIncoming === prevIncoming) { - continue; - } - for (const prevOutgoing of prevIncoming.outgoing) { - hasChange = - hasChange || - !prevOutgoing.incoming.has(newIncoming) || - !newIncoming.outgoing.has(prevOutgoing); - prevOutgoing.incoming.add(newIncoming); - newIncoming.outgoing.add(prevOutgoing); - } - } - return hasChange; - } - - /** - * Returns the mapping of places to vertices. - */ - build(): Map { - 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 { - /** - * A set of vertices whose values are captured into this vertex, keyed - * by the identifier of the captured value. This represents any form - * of capturing, including: - * - direct assignment: `x = y`, `x.y = y`, `x[0] = y` all captures - * x into y and vice-versa. - * - composition of objects into arrays, object, jsx: `{x}{y}`, - * `{k1: x, k2: y}`, and `[x, y]` all capture x and y into the outer - * value (jsx, object, array, respectively). - * - Mutation that could cause values to take references into each other: - * `foo(x, y)` could modify x and/or y in a way that they are assigned - * into each other (eg it could internally do `x.y = y`, `y.x = x` etc). - * - * Note that the representation only stores *one* vertex for each identifier, - * so eg `y.x = x` the `y` vertex can only store a capture of *one* vertex - * for `x`. If there are multiple potentially prior usages of x when this - * occurs, we have to take care that they are both represnted as captured: - * - * ```javascript - * let x; - * if (cond) { - * x = [1]; - * } else { - * x = [2] - * } - * y = x; - * ``` - * The final line must reflect that y captures *either* of the two possible - * values of `x`. This is achieved by *first* using `x`, *then* capturing. - * The usage will create a single new `x` vertex in the final scope, to which - * there are two incoming edges. Then the capture of `x` into `y` references - * that single x vertex with two incoming edges. Thus the data model can capture - * this case, but the order of construction is important. - * - * The result is a graph such as: - * - * [x @ let x] -> [ x @ x = [1] ] -> [ x @ y = x] - * └--> [ x @ x = [2] ] ---┘ ↑ - * | capture - * ↓ - * [ y @ y - x] - * - * Note that it's possible to walk from `y` to both of the possible x values. - */ - captures: Map = new Map(); - - /** - * 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; - - /** - * The instruction where the reference occurs (for debugging) - */ - instr: Instruction | InstructionValue | Terminal | null; - - /** - * 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, - instr: Instruction | InstructionValue | Terminal | null - ) { - this.place = place; - this.instr = instr; - } -} - -/** - * Prints the graph into GraphViz DOT format. - * https://graphviz.org/doc/info/lang.html - */ -export function printGraph(vertices: Map): string { - const output = []; - - // Maps vertices to short string names. Note that multiple vertex instances - // can share the same Place, so we can't use the place (identifier/path etc). - // Instead we create an auto-incrementing index. - const indices: Map = new Map(); - function identify(vertex: Vertex): string { - let id = indices.get(vertex); - if (id == null) { - id = `v${indices.size}`; - indices.set(vertex, id); - } - return id; - } - - for (const [place, vertex] of vertices) { - const vertexId = identify(vertex); - output.push( - `${vertexId} [ label="${vertexId} (${printPlace(vertex.place)} @ ${ - vertex.instr - ? printMixedHIR(vertex.instr).replaceAll('"', '\\"') - : "" - })", shape="box" ]` - ); - for (const outgoing of vertex.outgoing) { - const outgoingId = identify(outgoing); - output.push(`${vertexId} -> ${outgoingId}`); - } - for (const [id, capture] of vertex.captures) { - const captureId = identify(capture); - output.push(`${captureId} -> ${vertexId} [ label="capture" ]`); - } - } - const lines = output.map((line) => " " + line); - lines.unshift("digraph InferMutability {"); - lines.push("}"); - return lines.join("\n"); -} diff --git a/compiler/forget/src/HIR/InferReferenceCapability.ts b/compiler/forget/src/HIR/InferReferenceCapability.ts new file mode 100644 index 0000000000..c086761b16 --- /dev/null +++ b/compiler/forget/src/HIR/InferReferenceCapability.ts @@ -0,0 +1,705 @@ +/** + * 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 } from "./PrintHIR"; + +/** + * For every usage of a value in the given function, infers the capability or action + * taken at that reference. Each reference is inferred as exactly one of: + * - freeze: this usage freezes the value, ie converts it to frozen. This is only inferred + * when the value *may* not already be frozen. + * - frozen: the value is known to already be "owned" by React and is therefore already + * frozen (permanently and transitively immutable). + * - immutable: the value is not owned by React, but is known to be an immutable value + * that therefore cannot ever change. + * - readonly: the value is not frozen or immutable, 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 or immutable, 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. + * + * Internally, the inference tracks the approximate type of value held by each variable, + * and iterates over the control flow graph. The inferred capability of reach reference is + * a combination of the operation performed (ie, assignment into an object mutably uses the + * object; an if condition reads the condition) and the type of the value. The types of values + * are: + * - frozen: can be any type so long as the value is known to be owned by React, permanently + * and transitively immutable + * - maybe-frozen: the value may or may not be frozen, conditionally depending on control flow. + * - immutable: a type with value semantics: primitives, records/tuples when standardized. + * - mutable: a type with reference semantics eg array, object, class instance, etc. + * + * When control flow paths converge the types of values are merged together, with the value + * types forming a lattice to ensure convergence. + */ +export default function inferReferenceCapability(fn: HIRFunction) { + // Initial environment contains function params + // TODO: include module declarations here as well + const initialEnvironment = Environment.empty(); + const id: Place = { + kind: "Identifier", + memberPath: null, + value: fn.id as any, + path: null as any, // TODO + capability: Capability.Freeze, + }; + const value: InstructionValue = { + kind: "Primitive", + path: null as any, // TODO + value: undefined, + }; + initialEnvironment.initialize(value, ValueKind.Frozen); + initialEnvironment.define(id, value); + + for (const param of fn.params) { + const place: Place = { + kind: "Identifier", + memberPath: null, + value: param, + path: null as any, // TODO + capability: Capability.Freeze, + }; + const value: InstructionValue = { + kind: "Primitive", + path: null as any, // TODO + value: undefined, + }; + initialEnvironment.initialize(value, ValueKind.Frozen); + initialEnvironment.define(place, value); + } + + // Queue of blocks to visit, with block and the incoming Environment value + const queue: Array = [ + { blockId: fn.body.entry, environment: initialEnvironment }, + ]; + // Map of blocks to the last incoming environment that was processed + const environmentsByBlock: Map = new Map(); + + while (queue.length !== 0) { + const { blockId, environment: incomingEnvironment } = queue.shift()!; + + let previousEnvironment = environmentsByBlock.get(blockId); + let nextEnvironment: Environment | null = null; + if (previousEnvironment === undefined) { + // If no previous environment, save the incoming environment and + // infer the block with this environment + environmentsByBlock.set(blockId, incomingEnvironment); + nextEnvironment = incomingEnvironment.clone(); + } else { + // If there's a previous environment, merge the previous/new incoming + // environments. If there are no changes, then the block can be skipped. + // otherwise save the merged environment and revisit the block with it. + const mergedEnvironment = previousEnvironment.merge(incomingEnvironment); + if (mergedEnvironment !== null) { + environmentsByBlock.set(blockId, mergedEnvironment); + nextEnvironment = mergedEnvironment.clone(); + } else { + continue; + } + } + + const environment = nextEnvironment; // rebind to preserve the non-null refinement + const block = fn.body.blocks.get(blockId)!; + inferBlock(environment, block); + + // TODO: add a `forEachTerminalSuccessor` helper, we don't actually want the result + // here + const _ = mapTerminalSuccessors( + block.terminal, + (nextBlockId, isFallthrough) => { + if (!isFallthrough) { + queue.push({ blockId: nextBlockId, environment }); + } + return nextBlockId; + } + ); + } +} + +type QueueEntry = { + blockId: BlockId; + environment: Environment; +}; + +/** + * Maintains a mapping of top-level variables to the kind of value they hold + */ +class Environment { + // The kind of reach value, based on its allocation site + #values: Map; + // The set of values pointed to by each identifier. This is a set + // to accomodate phi points (where a variable may have different + // values from different control flow paths). + #variables: Map>; + + constructor( + values: Map, + variables: Map> + ) { + this.#values = values; + this.#variables = variables; + } + + static empty(): Environment { + return new Environment(new Map(), new Map()); + } + + /** + * (Re)initializes a @param value with its default @param kind. + */ + initialize( + value: InstructionValue, + kind: ValueKind, + instr: Instruction | null = null + ) { + invariant( + value.kind !== "Identifier" || value.memberPath !== null, + "Expected all top-level identifiers to be defined as variables, not values" + ); + this.#values.set(value, kind); + } + + /** + * Lookup the kind of the given @param value. + */ + kind(place: Place): ValueKind { + const values = this.#variables.get(place.value.id); + invariant( + values != null, + `Expected value kind to be initialized at '${String(place.path)}'` + ); + let mergedKind: ValueKind | null = null; + for (const value of values) { + const kind = this.#values.get(value)!; + mergedKind = mergedKind !== null ? mergeValues(mergedKind, kind) : kind; + } + invariant(mergedKind !== null, "Expected at least value"); + return mergedKind; + } + + /** + * Updates the value at @param place to point to the same value as @param value. + */ + alias(place: Place, value: Place) { + const values = this.#variables.get(value.value.id); + invariant( + values != null, + `Expected value to be populated at '${String(value.path)}' in '${String( + value.path.parentPath + )}'` + ); + this.#variables.set(place.value.id, new Set(values)); + } + + /** + * Defines (initializing or updating) a variable with a specific kind of value. + */ + define(place: Place, value: InstructionValue) { + invariant( + place.memberPath === null, + "Expected a top-level identifier, not a member path" + ); + invariant( + this.#values.has(value), + `Expected value to be initialized at '${String(value.path)}' in '${String( + value.path?.parentPath + )}'` + ); + this.#variables.set(place.value.id, new Set([value])); + } + + /** + * Records that a given Place was accessed with the given kind and: + * - Updates the capability of @param place based on the kind of value + * and the kind of reference (@param effectKind). + * - Updates the value kind to reflect the effect of the reference. + * + * Notably, a mutable reference is downgraded to readonly if the + * value unless the value is known to be mutable. + * + * Similarly, a freeze reference is converted to readonly if the + * value is already frozen or is immutable. + */ + reference(place: Place, effectKind: EffectKind) { + const values = this.#variables.get(place.value.id); + if (values === undefined) { + place.capability = + effectKind === EffectKind.Write + ? Capability.Mutable + : Capability.Readonly; + return; + } + let capability: Capability | null = null; + switch (effectKind) { + case EffectKind.Freeze: { + values.forEach((value) => { + const valueKind = this.#values.get(value)!; + if ( + valueKind === ValueKind.Mutable || + valueKind === ValueKind.MaybeFrozen + ) { + this.#values.set(value, ValueKind.Frozen); + capability = Capability.Freeze; + } + }); + capability = capability ?? Capability.Readonly; + break; + } + case EffectKind.Write: { + let maybeFrozen = false; + let maybeMutable = false; + values.forEach((value) => { + const valueKind = this.#values.get(value)!; + if ( + valueKind === ValueKind.Frozen || + valueKind === ValueKind.MaybeFrozen + ) { + maybeFrozen = true; + } else if (valueKind === ValueKind.Mutable) { + maybeMutable = true; + } + }); + capability = maybeFrozen + ? Capability.Readonly + : maybeMutable + ? Capability.Mutable + : Capability.Readonly; + break; + } + case EffectKind.Read: { + capability = Capability.Readonly; + break; + } + default: { + assertExhaustive( + effectKind, + `Unexpected reference kind '${effectKind as any as string}'` + ); + } + } + invariant(capability !== null, "Expected capability to be set"); + place.capability = capability; + } + + /** + * Combine the contents of @param this and @param other, returning a new + * instance with the combined changes _if_ there are any changes, or + * returning null if no changes would occur. Changes include: + * - new entries in @param other that did not exist in @param this + * - entries whose values differ in @param this and @param other, + * and where joining the values produces a different value than + * what was in @param this. + * + * Note that values are joined using a lattice operation to ensure + * termination. + */ + merge(other: Environment): Environment | null { + let nextValues: Map | null = null; + let nextVariables: Map> | null = null; + + for (const [id, thisValue] of this.#values) { + const otherValue = other.#values.get(id); + if (otherValue !== undefined) { + const mergedValue = mergeValues(thisValue, otherValue); + if (mergedValue !== thisValue) { + nextValues = nextValues ?? new Map(this.#values); + nextValues.set(id, mergedValue); + } + } + } + for (const [id, otherValue] of other.#values) { + if (this.#values.has(id)) { + // merged above + continue; + } + nextValues = nextValues ?? new Map(this.#values); + nextValues.set(id, otherValue); + } + + for (const [id, thisValues] of this.#variables) { + const otherValues = other.#variables.get(id); + if (otherValues !== undefined) { + let mergedValues: Set | null = null; + for (const otherValue of otherValues) { + if (!thisValues.has(otherValue)) { + mergedValues = mergedValues ?? new Set(thisValues); + mergedValues.add(otherValue); + } + } + if (mergedValues !== null) { + nextVariables = nextVariables ?? new Map(this.#variables); + nextVariables.set(id, mergedValues); + } + } + } + for (const [id, otherValues] of other.#variables) { + if (this.#variables.has(id)) { + continue; + } + nextVariables = nextVariables ?? new Map(this.#variables); + nextVariables.set(id, new Set(otherValues)); + } + + if (nextVariables === null && nextValues === null) { + return null; + } else { + return new Environment( + nextValues ?? new Map(this.#values), + nextVariables ?? new Map(this.#variables) + ); + } + } + + /** + * Returns a copy of this environment. + * TODO: consider using persistent data structures to make + * clone cheaper. + */ + clone(): Environment { + return new Environment(new Map(this.#values), new Map(this.#variables)); + } + + /** + * For debugging purposes, dumps the environment to a plain + * object so that it can printed as JSON. + */ + debug(): any { + const result: any = { values: {}, variables: {} }; + const objects: Map = new Map(); + function identify(value: InstructionValue): number { + let id = objects.get(value); + if (id == null) { + id = objects.size; + objects.set(value, id); + } + return id; + } + for (const [value, kind] of this.#values) { + const id = identify(value); + result.values[id] = { kind, value: printMixedHIR(value) }; + } + for (const [variable, values] of this.#variables) { + result.variables[variable] = [...values].map(identify); + } + return result; + } +} + +/** + * Joins two values using the following rules: + * == Effect Transitions == + * + * Freezing an immutable value has not effect: + * ┌───────────────┐ + * │ │ + * ▼ │ Freeze + * ┌──────────────────────────┐ │ + * │ Immutable │──┘ + * └──────────────────────────┘ + * + * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen + * value has no effect: + * ┌───────────────┐ + * ┌─────────────────────────┐ Freeze │ │ + * │ MaybeFrozen │────┐ ▼ │ Freeze + * └─────────────────────────┘ │ ┌──────────────────────────┐ │ + * ├────▶│ Frozen │──┘ + * │ └──────────────────────────┘ + * ┌─────────────────────────┐ │ + * │ Mutable │────┘ + * └─────────────────────────┘ + * + * == Join Lattice == + * - immutable | frozen => frozen + * - frozen | mutable => maybe-frozen + * - | maybe-frozen => maybe-frozen + * + * ┌──────────────────────────┐ + * │ Immutable │───┐ + * └──────────────────────────┘ │ + * │ ┌─────────────────────────┐ + * ├───▶│ Frozen │──┐ + * ┌──────────────────────────┐ │ └─────────────────────────┘ │ + * │ Frozen │───┤ │ ┌─────────────────────────┐ + * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │ + * │ ┌─────────────────────────┐ │ └─────────────────────────┘ + * ├───▶│ MaybeFrozen │──┘ + * ┌──────────────────────────┐ │ └─────────────────────────┘ + * │ Mutable │───┘ + * └──────────────────────────┘ + */ +function mergeValues(a: ValueKind, b: ValueKind): ValueKind { + if (a === b) { + return a; + } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) { + return ValueKind.MaybeFrozen; + // after this a and b differ and neither are MaybeFrozen + } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) { + if (a === ValueKind.Frozen || b === ValueKind.Frozen) { + // frozen | mutable + return ValueKind.MaybeFrozen; + } else { + // mutable | immutable + return ValueKind.Mutable; + } + } else { + // frozen | immutable + return ValueKind.Frozen; + } +} + +/** + * Distinguish between different kinds of values relevant to inference purposes: + * see the main docblock for the module for details. + */ +enum ValueKind { + MaybeFrozen = "MaybeFrozen", + Frozen = "Frozen", + Immutable = "Immutable", + Mutable = "Mutable", +} + +/** + * Distinguish between different kinds of references. + */ +enum EffectKind { + Write = "Write", + Read = "Read", + Freeze = "Freeze", +} + +/** + * Iterates over the given @param block, defining variables and + * recording references on the @param env according to JS semantics. + */ +function inferBlock(env: Environment, block: BasicBlock) { + for (const instr of block.instructions) { + const instrValue = instr.value; + let valueKind: ValueKind; + switch (instrValue.kind) { + case "BinaryExpression": { + valueKind = ValueKind.Immutable; + env.reference(instrValue.left, EffectKind.Read); + env.reference(instrValue.right, EffectKind.Read); + break; + } + case "ArrayExpression": { + valueKind = ValueKind.Mutable; + for (const element of instrValue.elements) { + env.reference(element, EffectKind.Read); + } + break; + } + case "NewExpression": { + valueKind = ValueKind.Mutable; + env.reference(instrValue.callee, EffectKind.Write); + for (const arg of instrValue.args) { + env.reference(arg, EffectKind.Write); + } + break; + } + case "CallExpression": { + let effectKind = EffectKind.Write; + valueKind = ValueKind.Mutable; + const hook = parseHookCall(instrValue.callee); + if (hook !== null) { + effectKind = hook.effectKind; + valueKind = hook.valueKind; + } + env.reference(instrValue.callee, effectKind); + for (const arg of instrValue.args) { + env.reference(arg, effectKind); + } + break; + } + case "ObjectExpression": { + valueKind = ValueKind.Mutable; + // Object construction captures but does not modify the key/property values + if (instrValue.properties !== null) { + for (const [_key, value] of Object.entries(instrValue.properties)) { + env.reference(value, EffectKind.Read); + } + } + break; + } + case "UnaryExpression": { + // TODO check that value must be a primitive, or make conditional based on the operator + valueKind = ValueKind.Immutable; + env.reference(instrValue.value, EffectKind.Read); + break; + } + case "OtherStatement": { + // TODO: handle other statement kinds + valueKind = ValueKind.Mutable; + break; + } + case "JsxExpression": { + valueKind = ValueKind.Frozen; + env.reference(instrValue.tag, EffectKind.Freeze); + for (const [_prop, value] of Object.entries(instrValue.props)) { + env.reference(value, EffectKind.Freeze); + } + if (instrValue.children !== null) { + for (const child of instrValue.children) { + env.reference(child, EffectKind.Freeze); + } + } + break; + } + case "JSXText": + case "Primitive": { + valueKind = ValueKind.Immutable; + break; + } + case "Identifier": { + env.reference(instrValue, EffectKind.Read); + const lvalue = instr.lvalue; + if (lvalue !== null) { + lvalue.place.capability = Capability.Mutable; + if ( + lvalue.place.memberPath === null && + instrValue.memberPath === null + ) { + // direct aliasing: `a = b`; + env.alias(lvalue.place, instrValue); + } else if (lvalue.place.memberPath === null) { + // redefine lvalue: `a = b.c.d` + env.initialize(instrValue, env.kind(instrValue)); + env.define(lvalue.place, instrValue); + } else if (instrValue.memberPath === null) { + // no-op: `a.b.c = d` + env.reference(lvalue.place, EffectKind.Write); + } else { + // no-op: `a.b.c = d.e.f` + env.reference(lvalue.place, EffectKind.Write); + } + } + continue; + } + default: { + assertExhaustive(instrValue, "Unexpected instruction kind"); + } + } + env.initialize(instrValue, valueKind, instr); + if (instr.lvalue !== null) { + if (instr.lvalue.place.memberPath === null) { + env.define(instr.lvalue.place, instrValue); + } else { + env.reference(instr.lvalue.place, EffectKind.Write); + } + instr.lvalue.place.capability = Capability.Mutable; + } + } + switch (block.terminal.kind) { + case "throw": { + env.reference(block.terminal.value, EffectKind.Freeze); + break; + } + case "return": { + if (block.terminal.value !== null) { + env.reference(block.terminal.value, EffectKind.Freeze); + } + break; + } + case "if": { + env.reference(block.terminal.test, EffectKind.Read); + break; + } + case "switch": { + for (const case_ of block.terminal.cases) { + if (case_.test !== null) { + env.reference(case_.test, EffectKind.Read); + } + } + break; + } + case "goto": { + break; + } + default: { + assertExhaustive( + block.terminal, + `Unexpected terminal kind '${(block.terminal as any as Terminal).kind}'` + ); + } + } +} + +const GLOBALS: Map = new Map([ + ["Map", ValueKind.Mutable], + ["Set", ValueKind.Mutable], + ["Math.max", ValueKind.Immutable], +]); + +const HOOKS: Map = new Map([ + [ + "useState", + { + kind: "State", + effectKind: EffectKind.Freeze, + valueKind: ValueKind.Frozen, + }, + ], + [ + "useRef", + { + kind: "Ref", + effectKind: EffectKind.Read, + valueKind: ValueKind.Mutable, + }, + ], +]); + +type HookKind = { kind: "State" } | { kind: "Ref" } | { kind: "Custom" }; +type Hook = HookKind & { effectKind: EffectKind; valueKind: ValueKind }; + +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", + effectKind: EffectKind.Freeze, + valueKind: ValueKind.Frozen, + }; +} diff --git a/compiler/forget/src/HIR/ScopeAnalysis.ts b/compiler/forget/src/HIR/ScopeAnalysis.ts index 07c4493203..29ec69fe9b 100644 --- a/compiler/forget/src/HIR/ScopeAnalysis.ts +++ b/compiler/forget/src/HIR/ScopeAnalysis.ts @@ -66,7 +66,7 @@ export default function analyzeScopes(fn: HIRFunction): ReactFunction { kind: "Identifier", value: param, memberPath: null, - capability: Capability.Frozen, + capability: Capability.Freeze, path: null as any, })) ), diff --git a/compiler/forget/src/__tests__/fixtures/hir-svg/component.svg b/compiler/forget/src/__tests__/fixtures/hir-svg/component.svg index 14371d0713..7e95d97dcc 100644 --- a/compiler/forget/src/__tests__/fixtures/hir-svg/component.svg +++ b/compiler/forget/src/__tests__/fixtures/hir-svg/component.svg @@ -4,721 +4,721 @@ - + BuildDefUseGraph - + v0 - -v0 (frozen props$1.items @ frozen props$1.items) <update> + +v0 (freeze props$1.items @ freeze props$1.items) <update> v1 - -v1 (frozen items$2 @ Const frozen items$2 = frozen props$1.items) <assign> + +v1 (freeze items$2 @ Const freeze items$2 = freeze props$1.items) <assign> v0->v1 - - + + v2 - -v2 (frozen props$1.maxItems @ frozen props$1.maxItems) <update> + +v2 (freeze props$1.maxItems @ freeze props$1.maxItems) <update> v0->v2 - - + + v12 - -v12 (frozen items$2 @ If (frozen items$2) then:bb3 else:bb2) <update> + +v12 (freeze items$2 @ If (freeze items$2) then:bb3 else:bb2) <update> v1->v12 - - + + v3 - -v3 (frozen maxItems$3 @ Const frozen maxItems$3 = frozen props$1.maxItems) <assign> + +v3 (freeze maxItems$3 @ Const freeze maxItems$3 = freeze props$1.maxItems) <assign> v2->v3 - - + + v10 - -v10 (frozen maxItems$3 @ Call mutable Math$8.max(mutable $9, frozen maxItems$3)) <update> + +v10 (freeze maxItems$3 @ Call mutable Math$8.max(mutable $9, freeze maxItems$3)) <update> v3->v10 - - + + v4 - -v4 (readonly renderedItems$4 @ Const readonly renderedItems$4 = Array []) <assign> + +v4 (readonly renderedItems$4 @ Const readonly renderedItems$4 = Array []) <assign> v18 - -v18 (readonly renderedItems$4.length @ readonly renderedItems$4.length) <update> + +v18 (readonly renderedItems$4.length @ readonly renderedItems$4.length) <update> v4->v18 - - + + v49 - -v49 (mutable renderedItems$4.push @ Call mutable renderedItems$4.push(mutable $15)) <update> + +v49 (mutable renderedItems$4.push @ Call mutable renderedItems$4.push(mutable $15)) <update> v4->v49 - - + + v62 - -v62 (<join> @ <no-instr>) <update> + +v62 (<join> @ <no-instr>) <update> v4->v62 - - + + v71 - -v71 (<join> @ <no-instr>) <update> + +v71 (<join> @ <no-instr>) <update> v4->v71 - - + + v19 - -v19 (readonly count$17 @ Const readonly count$17 = readonly renderedItems$4.length) <assign> + +v19 (readonly count$17 @ Const readonly count$17 = readonly renderedItems$4.length) <assign> v18->v19 - - + + v34 - -v34 (frozen renderedItems$4 @ JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <update> + +v34 (freeze renderedItems$4 @ JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <update> v18->v34 - - + + v51 - -v51 (readonly renderedItems$4.length @ Binary readonly renderedItems$4.length >= readonly max$7) <update> + +v51 (readonly renderedItems$4.length @ Binary readonly renderedItems$4.length >= readonly max$7) <update> v49->v51 - - + + v62->v18 - - + + v71->v49 - - + + v5 - -v5 (mutable Set$6 @ New mutable Set$6()) <update> + +v5 (mutable Set$6 @ New mutable Set$6()) <update> v6 - -v6 (readonly seen$5 @ Const readonly seen$5 = New mutable Set$6()) <assign> + +v6 (readonly seen$5 @ Const readonly seen$5 = New mutable Set$6()) <assign> v39 - -v39 (mutable seen$5.has @ Call mutable seen$5.has(frozen item$10)) <update> + +v39 (mutable seen$5.has @ Call mutable seen$5.has(freeze item$10)) <update> v6->v39 - - + + v56 - -v56 (<join> @ <no-instr>) <update> + +v56 (<join> @ <no-instr>) <update> v6->v56 - - + + v59 - -v59 (<join> @ <no-instr>) <update> + +v59 (<join> @ <no-instr>) <update> v6->v59 - - + + v39->v56 - - + + v39->v59 - - + + v43 - -v43 (mutable seen$5.add @ Call mutable seen$5.add(frozen item$10)) <update> + +v43 (mutable seen$5.add @ Call mutable seen$5.add(freeze item$10)) <update> v56->v43 - - + + v59->v39 - - + + v7 - -v7 (readonly $9 @ Const readonly $9 = 0) <assign> + +v7 (readonly $9 @ Const readonly $9 = 0) <assign> v9 - -v9 (mutable $9 @ Call mutable Math$8.max(mutable $9, frozen maxItems$3)) <update> + +v9 (mutable $9 @ Call mutable Math$8.max(mutable $9, freeze maxItems$3)) <update> v7->v9 - - + + v8 - -v8 (mutable Math$8.max @ Call mutable Math$8.max(mutable $9, frozen maxItems$3)) <update> + +v8 (mutable Math$8.max @ Call mutable Math$8.max(mutable $9, freeze maxItems$3)) <update> v11 - -v11 (readonly max$7 @ Const readonly max$7 = Call mutable Math$8.max(mutable $9, frozen maxItems$3)) <assign> + +v11 (readonly max$7 @ Const readonly max$7 = Call mutable Math$8.max(mutable $9, freeze maxItems$3)) <assign> v52 - -v52 (readonly max$7 @ Binary readonly renderedItems$4.length >= readonly max$7) <update> + +v52 (readonly max$7 @ Binary readonly renderedItems$4.length >= readonly max$7) <update> v11->v52 - - + + v72 - -v72 (<join> @ <no-instr>) <update> + +v72 (<join> @ <no-instr>) <update> v11->v72 - - + + v52->v72 - - + + v72->v52 - - + + v13 - -v13 (readonly $11 @ Const readonly $11 = null) <assign> + +v13 (readonly $11 @ Const readonly $11 = null) <assign> v15 - -v15 (readonly $11 @ Binary frozen item$10 == readonly $11) <update> + +v15 (readonly $11 @ Binary freeze item$10 == readonly $11) <update> v13->v15 - - + + v14 - -v14 (frozen item$10 @ Binary frozen item$10 == readonly $11) <update> + +v14 (freeze item$10 @ Binary freeze item$10 == readonly $11) <update> v40 - -v40 (frozen item$10 @ Call mutable seen$5.has(frozen item$10)) <update> + +v40 (freeze item$10 @ Call mutable seen$5.has(freeze item$10)) <update> v14->v40 - - + + v57 - -v57 (<join> @ <no-instr>) <update> + +v57 (<join> @ <no-instr>) <update> v14->v57 - - + + v40->v57 - - + + v57->v14 - - + + v44 - -v44 (frozen item$10 @ Call mutable seen$5.add(frozen item$10)) <update> + +v44 (freeze item$10 @ Call mutable seen$5.add(freeze item$10)) <update> v57->v44 - - + + v16 - -v16 (frozen $12 @ Const frozen $12 = Binary frozen item$10 == readonly $11) <assign> + +v16 (freeze $12 @ Const freeze $12 = Binary freeze item$10 == readonly $11) <assign> v17 - -v17 (frozen $12 @ If (frozen $12) then:bb8 else:bb9) <update> + +v17 (freeze $12 @ If (freeze $12) then:bb8 else:bb9) <update> v16->v17 - - + + v38 - -v38 (frozen $13 @ Const frozen $13 = frozen $12) <assign> + +v38 (freeze $13 @ Const freeze $13 = freeze $12) <assign> v17->v38 - - + + v42 - -v42 (frozen $13 @ If (frozen $13) then:bb1 else:bb4) <update> + +v42 (freeze $13 @ If (freeze $13) then:bb1 else:bb4) <update> v38->v42 - - + + v25 - -v25 (frozen count$17 @ JSX <frozen $20>{frozen count$17}{frozen $21}</frozen $20>) <update> + +v25 (freeze count$17 @ JSX <freeze $20>{freeze count$17}{freeze $21}</freeze $20>) <update> v19->v25 - - + + v20 - -v20 (readonly $18 @ Const readonly $18 = "div") <assign> + +v20 (readonly $18 @ Const readonly $18 = "div") <assign> v30 - -v30 (frozen $18 @ JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <update> + +v30 (freeze $18 @ JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <update> v20->v30 - - + + v21 - -v21 (readonly $19 @ Const readonly $19 = " -      ") <assign> + +v21 (readonly $19 @ Const readonly $19 = " +      ") <assign> v31 - -v31 (frozen $19 @ JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <update> + +v31 (freeze $19 @ JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <update> v21->v31 - - + + v22 - -v22 (readonly $20 @ Const readonly $20 = "h1") <assign> + +v22 (readonly $20 @ Const readonly $20 = "h1") <assign> v24 - -v24 (frozen $20 @ JSX <frozen $20>{frozen count$17}{frozen $21}</frozen $20>) <update> + +v24 (freeze $20 @ JSX <freeze $20>{freeze count$17}{freeze $21}</freeze $20>) <update> v22->v24 - - + + v23 - -v23 (readonly $21 @ Const readonly $21 = " Items") <assign> + +v23 (readonly $21 @ Const readonly $21 = " Items") <assign> v26 - -v26 (frozen $21 @ JSX <frozen $20>{frozen count$17}{frozen $21}</frozen $20>) <update> + +v26 (freeze $21 @ JSX <freeze $20>{freeze count$17}{freeze $21}</freeze $20>) <update> v23->v26 - - + + v27 - -v27 (readonly $22 @ Const readonly $22 = JSX <frozen $20>{frozen count$17}{frozen $21}</frozen $20>) <assign> + +v27 (readonly $22 @ Const readonly $22 = JSX <freeze $20>{freeze count$17}{freeze $21}</freeze $20>) <assign> v32 - -v32 (frozen $22 @ JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <update> + +v32 (freeze $22 @ JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <update> v27->v32 - - + + v28 - -v28 (readonly $23 @ Const readonly $23 = " -      ") <assign> + +v28 (readonly $23 @ Const readonly $23 = " +      ") <assign> v33 - -v33 (frozen $23 @ JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <update> + +v33 (freeze $23 @ JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <update> v28->v33 - - + + v29 - -v29 (readonly $24 @ Const readonly $24 = " -    ") <assign> + +v29 (readonly $24 @ Const readonly $24 = " +    ") <assign> v35 - -v35 (frozen $24 @ JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <update> + +v35 (freeze $24 @ JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <update> v29->v35 - - + + v36 - -v36 (readonly $25 @ Const readonly $25 = JSX <frozen $18>{frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24}</frozen $18>) <assign> + +v36 (readonly $25 @ Const readonly $25 = JSX <freeze $18>{freeze $19}{freeze $22}{freeze $23}{freeze renderedItems$4}{freeze $24}</freeze $18>) <assign> v37 - -v37 (frozen $25 @ Return frozen $25) <update> + +v37 (freeze $25 @ Return freeze $25) <update> v36->v37 - - + + v41 - -v41 (readonly $13 @ Const readonly $13 = Call mutable seen$5.has(frozen item$10)) <assign> + +v41 (readonly $13 @ Const readonly $13 = Call mutable seen$5.has(freeze item$10)) <assign> v41->v42 - - + + v43->v56 - - + + v43->v59 - - + + v47 - -v47 (frozen item$10 @ JSX <frozen $14>{frozen item$10}</frozen $14>) <update> + +v47 (freeze item$10 @ JSX <freeze $14>{freeze item$10}</freeze $14>) <update> v44->v47 - - + + v47->v57 - - + + v45 - -v45 (readonly $14 @ Const readonly $14 = "div") <assign> + +v45 (readonly $14 @ Const readonly $14 = "div") <assign> v46 - -v46 (frozen $14 @ JSX <frozen $14>{frozen item$10}</frozen $14>) <update> + +v46 (freeze $14 @ JSX <freeze $14>{freeze item$10}</freeze $14>) <update> v45->v46 - - + + v48 - -v48 (readonly $15 @ Const readonly $15 = JSX <frozen $14>{frozen item$10}</frozen $14>) <assign> + +v48 (readonly $15 @ Const readonly $15 = JSX <freeze $14>{freeze item$10}</freeze $14>) <assign> v50 - -v50 (mutable $15 @ Call mutable renderedItems$4.push(mutable $15)) <update> + +v50 (mutable $15 @ Call mutable renderedItems$4.push(mutable $15)) <update> v48->v50 - - + + v51->v18 - - + + v51->v62 - - + + v51->v71 - - + + v53 - -v53 (frozen $16 @ Const frozen $16 = Binary readonly renderedItems$4.length >= readonly max$7) <assign> + +v53 (freeze $16 @ Const freeze $16 = Binary readonly renderedItems$4.length >= readonly max$7) <assign> v54 - -v54 (frozen $16 @ If (frozen $16) then:bb2 else:bb1) <update> + +v54 (freeze $16 @ If (freeze $16) then:bb2 else:bb1) <update> v53->v54 - - + + v55 - -v55 (frozen props$1 @ <no-instr>) <assign> + +v55 (freeze props$1 @ <no-instr>) <assign> v55->v0 - - + + diff --git a/compiler/forget/src/__tests__/fixtures/hir-svg/switch.svg b/compiler/forget/src/__tests__/fixtures/hir-svg/switch.svg index 9c213d390c..6022b483e5 100644 --- a/compiler/forget/src/__tests__/fixtures/hir-svg/switch.svg +++ b/compiler/forget/src/__tests__/fixtures/hir-svg/switch.svg @@ -4,70 +4,70 @@ - + BuildDefUseGraph - + v0 - -v0 (readonly x$2 @ Let readonly x$2 = Array []) <assign> + +v0 (readonly x$2 @ Let readonly x$2 = Array []) <assign> v7 - -v7 (mutable x$2.push @ Call mutable x$2.push(frozen props$1.p2)) <update> + +v7 (mutable x$2.push @ Call mutable x$2.push(freeze props$1.p2)) <update> v0->v7 - - + + v12 - -v12 (readonly x$2 @ readonly x$2) <update> + +v12 (readonly x$2 @ readonly x$2) <update> v0->v12 - - + + v15 - -v15 (frozen x$2 @ JSX <frozen Component$0 data={frozen x$2} ></frozen Component$0>) <update> + +v15 (freeze x$2 @ JSX <freeze Component$0 data={freeze x$2} ></freeze Component$0>) <update> v0->v15 - - + + v9 - -v9 (mutable x$2.push @ Call mutable x$2.push(frozen props$1.p3)) <update> + +v9 (mutable x$2.push @ Call mutable x$2.push(freeze props$1.p3)) <update> v7->v9 - - + + v12->v15 - - + + @@ -78,224 +78,224 @@ v12->v13 - - + + v1 - -v1 (readonly y$3 @ Let readonly y$3 = undefined) <assign> + +v1 (readonly y$3 @ Let readonly y$3 = undefined) <assign> v17 - -v17 (mutable y$3.push @ Call mutable y$3.push(frozen props$1.p4)) <update> + +v17 (mutable y$3.push @ Call mutable y$3.push(freeze props$1.p4)) <update> v1->v17 - - + + v20 - -v20 (frozen y$3 @ JSX <frozen Component$0 data={frozen y$3} >{frozen child$6}</frozen Component$0>) <update> + +v20 (freeze y$3 @ JSX <freeze Component$0 data={freeze y$3} >{freeze child$6}</freeze Component$0>) <update> v17->v20 - - + + v2 - -v2 (readonly $4 @ Const readonly $4 = false) <assign> + +v2 (readonly $4 @ Const readonly $4 = false) <assign> v6 - -v6 (readonly $4 @ Switch (frozen props$1.p0);   Case readonly $5: bb4;   Case readonly $4: bb2;   Default: bb1) <update> + +v6 (readonly $4 @ Switch (freeze props$1.p0);   Case readonly $5: bb4;   Case readonly $4: bb2;   Default: bb1) <update> v2->v6 - - + + v3 - -v3 (readonly $5 @ Const readonly $5 = true) <assign> + +v3 (readonly $5 @ Const readonly $5 = true) <assign> v5 - -v5 (readonly $5 @ Switch (frozen props$1.p0);   Case readonly $5: bb4;   Case readonly $4: bb2;   Default: bb1) <update> + +v5 (readonly $5 @ Switch (freeze props$1.p0);   Case readonly $5: bb4;   Case readonly $4: bb2;   Default: bb1) <update> v3->v5 - - + + v4 - -v4 (frozen props$1.p0 @ Switch (frozen props$1.p0);   Case readonly $5: bb4;   Case readonly $4: bb2;   Default: bb1) <update> + +v4 (freeze props$1.p0 @ Switch (freeze props$1.p0);   Case readonly $5: bb4;   Case readonly $4: bb2;   Default: bb1) <update> v8 - -v8 (frozen props$1.p2 @ Call mutable x$2.push(frozen props$1.p2)) <update> + +v8 (freeze props$1.p2 @ Call mutable x$2.push(freeze props$1.p2)) <update> v4->v8 - - + + v18 - -v18 (frozen props$1.p4 @ Call mutable y$3.push(frozen props$1.p4)) <update> + +v18 (freeze props$1.p4 @ Call mutable y$3.push(freeze props$1.p4)) <update> v4->v18 - - + + v25 - -v25 (<join> @ <no-instr>) <update> + +v25 (<join> @ <no-instr>) <update> v4->v25 - - + + v10 - -v10 (frozen props$1.p3 @ Call mutable x$2.push(frozen props$1.p3)) <update> + +v10 (freeze props$1.p3 @ Call mutable x$2.push(freeze props$1.p3)) <update> v8->v10 - - + + v25->v18 - - + + v9->v12 - - + + v10->v25 - - + + v11 - -v11 (readonly y$3 @ Reassign readonly y$3 = Array []) <assign> + +v11 (readonly y$3 @ Reassign readonly y$3 = Array []) <assign> v13->v17 - - + + v14 - -v14 (frozen Component$0 @ JSX <frozen Component$0 data={frozen x$2} ></frozen Component$0>) <update> + +v14 (freeze Component$0 @ JSX <freeze Component$0 data={freeze x$2} ></freeze Component$0>) <update> v19 - -v19 (frozen Component$0 @ JSX <frozen Component$0 data={frozen y$3} >{frozen child$6}</frozen Component$0>) <update> + +v19 (freeze Component$0 @ JSX <freeze Component$0 data={freeze y$3} >{freeze child$6}</freeze Component$0>) <update> v14->v19 - - + + v16 - -v16 (readonly child$6 @ Const readonly child$6 = JSX <frozen Component$0 data={frozen x$2} ></frozen Component$0>) <assign> + +v16 (readonly child$6 @ Const readonly child$6 = JSX <freeze Component$0 data={freeze x$2} ></freeze Component$0>) <assign> v21 - -v21 (frozen child$6 @ JSX <frozen Component$0 data={frozen y$3} >{frozen child$6}</frozen Component$0>) <update> + +v21 (freeze child$6 @ JSX <freeze Component$0 data={freeze y$3} >{freeze child$6}</freeze Component$0>) <update> v16->v21 - - + + v22 - -v22 (readonly $7 @ Const readonly $7 = JSX <frozen Component$0 data={frozen y$3} >{frozen child$6}</frozen Component$0>) <assign> + +v22 (readonly $7 @ Const readonly $7 = JSX <freeze Component$0 data={freeze y$3} >{freeze child$6}</freeze Component$0>) <assign> v23 - -v23 (frozen $7 @ Return frozen $7) <update> + +v23 (freeze $7 @ Return freeze $7) <update> v22->v23 - - + + v24 - -v24 (frozen props$1 @ <no-instr>) <assign> + +v24 (freeze props$1 @ <no-instr>) <assign> v24->v4 - - + + diff --git a/compiler/forget/src/__tests__/fixtures/hir/call.expect.md b/compiler/forget/src/__tests__/fixtures/hir/call.expect.md index 05808f35db..8f53259353 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/call.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/call.expect.md @@ -34,15 +34,15 @@ function foo$0() { ``` bb0: - Const readonly a$2 = Array [] - Const readonly b$3 = Object { } + Const mutable a$2 = Array [] + Const mutable b$3 = Object { } Call mutable foo$4(mutable a$2, mutable b$3) - Const readonly $6 = "div" - Let readonly _$5 = JSX + Const mutable $6 = "div" + Let mutable _$5 = JSX Call mutable foo$4(mutable b$3) - Const readonly $7 = "div" - Const readonly $8 = JSX - Return frozen $8 + Const mutable $7 = "div" + Const mutable $8 = JSX + Return readonly $8 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/component.expect.md b/compiler/forget/src/__tests__/fixtures/hir/component.expect.md index 279301828d..95a1623d6b 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/component.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/component.expect.md @@ -35,45 +35,45 @@ function Component(props) { ``` bb0: - Const frozen items$2 = frozen props$1.items - Const frozen maxItems$3 = frozen props$1.maxItems - Const readonly renderedItems$4 = Array [] - Const readonly seen$5 = New mutable Set$6() - Const readonly $9 = 0 - Const readonly max$7 = Call mutable Math$8.max(mutable $9, frozen maxItems$3) + Const mutable items$2 = readonly props$1.items + Const mutable maxItems$3 = readonly props$1.maxItems + Const mutable renderedItems$4 = Array [] + Const mutable seen$5 = New mutable Set$6() + Const mutable $9 = 0 + Const mutable max$7 = Call mutable Math$8.max(readonly $9, readonly maxItems$3) Goto bb1 bb1: - If (frozen items$2) then:bb3 else:bb2 + If (readonly items$2) then:bb3 else:bb2 bb3: - Const readonly $11 = null - Const frozen $12 = Binary frozen item$10 == readonly $11 - If (frozen $12) then:bb8 else:bb9 + Const mutable $11 = null + Const mutable $12 = Binary readonly item$10 == readonly $11 + If (readonly $12) then:bb8 else:bb9 bb2: - Const readonly count$17 = readonly renderedItems$4.length - Const readonly $18 = "div" - Const readonly $19 = "\n " - Const readonly $20 = "h1" - Const readonly $21 = " Items" - Const readonly $22 = JSX {frozen count$17}{frozen $21} - Const readonly $23 = "\n " - Const readonly $24 = "\n " - Const readonly $25 = JSX {frozen $19}{frozen $22}{frozen $23}{frozen renderedItems$4}{frozen $24} - Return frozen $25 + Const mutable count$17 = readonly renderedItems$4.length + Const mutable $18 = "div" + Const mutable $19 = "\n " + Const mutable $20 = "h1" + Const mutable $21 = " Items" + Const mutable $22 = JSX {freeze count$17}{readonly $21} + Const mutable $23 = "\n " + Const mutable $24 = "\n " + Const mutable $25 = JSX {readonly $19}{readonly $22}{readonly $23}{freeze renderedItems$4}{readonly $24} + Return readonly $25 bb8: - Const frozen $13 = frozen $12 + Const mutable $13 = readonly $12 Goto bb7 bb9: - Const readonly $13 = Call mutable seen$5.has(frozen item$10) + Const mutable $13 = Call mutable seen$5.has(mutable item$10) Goto bb7 bb7: - If (frozen $13) then:bb1 else:bb4 + If (readonly $13) then:bb1 else:bb4 bb4: - Call mutable seen$5.add(frozen item$10) - Const readonly $14 = "div" - Const readonly $15 = JSX {frozen item$10} - Call mutable renderedItems$4.push(mutable $15) - Const frozen $16 = Binary readonly renderedItems$4.length >= readonly max$7 - If (frozen $16) then:bb2 else:bb1 + Call mutable seen$5.add(mutable item$10) + Const mutable $14 = "div" + Const mutable $15 = JSX {readonly item$10} + Call mutable renderedItems$4.push(readonly $15) + Const mutable $16 = Binary readonly renderedItems$4.length >= readonly max$7 + If (readonly $16) then:bb2 else:bb1 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md b/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md index 703b59f405..f656d1c9af 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md @@ -78,15 +78,15 @@ function Component(props) { ``` bb0: - Const readonly a_DEBUG$2 = Array [] - Call mutable a_DEBUG$2.push(frozen props$1.a) - If (frozen props$1.b) then:bb2 else:bb1 + Const mutable a_DEBUG$2 = Array [] + Call mutable a_DEBUG$2.push(readonly props$1.a) + If (readonly props$1.b) then:bb2 else:bb1 bb2: - Const readonly $3 = null - Return frozen $3 + Const mutable $3 = null + Return readonly $3 bb1: - Call mutable a_DEBUG$2.push(frozen props$1.d) - Return frozen a_DEBUG$2 + Call mutable a_DEBUG$2.push(readonly props$1.d) + Return freeze a_DEBUG$2 ``` ## Code @@ -107,15 +107,15 @@ function Component$0(props$1) { ``` bb0: - Const readonly a$2 = Array [] - Call mutable a$2.push(frozen props$1.a) - If (frozen props$1.b) then:bb2 else:bb1 + Const mutable a$2 = Array [] + Call mutable a$2.push(readonly props$1.a) + If (readonly props$1.b) then:bb2 else:bb1 bb2: - Call mutable a$2.push(frozen props$1.c) + Call mutable a$2.push(readonly props$1.c) Goto bb1 bb1: - Call mutable a$2.push(frozen props$1.d) - Return frozen a$2 + Call mutable a$2.push(readonly props$1.d) + Return freeze a$2 ``` ## Code @@ -137,16 +137,16 @@ function Component$0(props$1) { ``` bb0: - Const readonly a$2 = Array [] - Call mutable a$2.push(frozen props$1.a) - If (frozen props$1.b) then:bb2 else:bb1 + Const mutable a$2 = Array [] + Call mutable a$2.push(readonly props$1.a) + If (readonly props$1.b) then:bb2 else:bb1 bb2: - Call mutable a$2.push(frozen props$1.c) - Const readonly $3 = null - Return frozen $3 + Call mutable a$2.push(readonly props$1.c) + Const mutable $3 = null + Return readonly $3 bb1: - Call mutable a$2.push(frozen props$1.d) - Return frozen a$2 + Call mutable a$2.push(readonly props$1.d) + Return freeze a$2 ``` ## Code @@ -168,15 +168,15 @@ function Component$0(props$1) { ``` bb0: - Const readonly a$2 = Array [] - Call mutable a$2.push(frozen props$1.a) - If (frozen props$1.b) then:bb2 else:bb1 + Const mutable a$2 = Array [] + Call mutable a$2.push(readonly props$1.a) + If (readonly props$1.b) then:bb2 else:bb1 bb2: - Call mutable a$2.push(frozen props$1.c) - Return frozen a$2 + Call mutable a$2.push(readonly props$1.c) + Return freeze a$2 bb1: - Call mutable a$2.push(frozen props$1.d) - Return frozen a$2 + Call mutable a$2.push(readonly props$1.d) + Return freeze a$2 ``` ## Code @@ -198,14 +198,14 @@ function Component$0(props$1) { ``` bb0: - Const readonly a$2 = Array [] - Call mutable a$2.push(frozen props$1.a) - If (frozen props$1.b) then:bb1 else:bb2 + Const mutable a$2 = Array [] + Call mutable a$2.push(readonly props$1.a) + If (readonly props$1.b) then:bb1 else:bb2 bb1: - Call mutable a$2.push(frozen props$1.d) - Return frozen a$2 + Call mutable a$2.push(readonly props$1.d) + Return freeze a$2 bb2: - Call mutable a$2.push(frozen props$1.c) + Call mutable a$2.push(readonly props$1.c) Goto bb1 ``` diff --git a/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md b/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md index 2cc637df05..c9cdd582d0 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/conditional-on-mutable.expect.md @@ -67,20 +67,20 @@ function mayMutate() {} ``` bb0: - Const readonly a$2 = Array [] - Const readonly b$3 = Array [] + Const mutable a$2 = Array [] + Const mutable b$3 = Array [] If (readonly b$3) then:bb2 else:bb1 bb2: - Call mutable a$2.push(frozen props$1.p0) + Call mutable a$2.push(readonly props$1.p0) Goto bb1 bb1: - If (frozen props$1.p1) then:bb4 else:bb3 + If (readonly props$1.p1) then:bb4 else:bb3 bb4: - Call mutable b$3.push(frozen props$1.p2) + Call mutable b$3.push(readonly props$1.p2) Goto bb3 bb3: - Const readonly $5 = JSX - Return frozen $5 + Const mutable $5 = JSX + Return readonly $5 ``` ## Code @@ -105,21 +105,21 @@ function Component$0(props$1) { ``` bb0: - Const readonly a$2 = Array [] - Const readonly b$3 = Array [] - Const readonly $5 = Call mutable mayMutate$4(mutable b$3) + Const mutable a$2 = Array [] + Const mutable b$3 = Array [] + Const mutable $5 = Call mutable mayMutate$4(mutable b$3) If (readonly $5) then:bb2 else:bb1 bb2: - Call mutable a$2.push(frozen props$1.p0) + Call mutable a$2.push(readonly props$1.p0) Goto bb1 bb1: - If (frozen props$1.p1) then:bb4 else:bb3 + If (readonly props$1.p1) then:bb4 else:bb3 bb4: - Call mutable b$3.push(frozen props$1.p2) + Call mutable b$3.push(readonly props$1.p2) Goto bb3 bb3: - Const readonly $7 = JSX - Return frozen $7 + Const mutable $7 = JSX + Return readonly $7 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/constructor.expect.md b/compiler/forget/src/__tests__/fixtures/hir/constructor.expect.md index 045650a9ef..82330df8a8 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/constructor.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/constructor.expect.md @@ -34,15 +34,15 @@ function Foo$0() { ``` bb0: - Const readonly a$2 = Array [] - Const readonly b$3 = Object { } + Const mutable a$2 = Array [] + Const mutable b$3 = Object { } New mutable Foo$4(mutable a$2, mutable b$3) - Const readonly $6 = "div" - Let readonly _$5 = JSX + Const mutable $6 = "div" + Let mutable _$5 = JSX New mutable Foo$4(mutable b$3) - Const readonly $7 = "div" - Const readonly $8 = JSX - Return frozen $8 + Const mutable $7 = "div" + Const mutable $8 = JSX + Return readonly $8 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.expect.md b/compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.expect.md new file mode 100644 index 0000000000..ae649abbae --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.expect.md @@ -0,0 +1,70 @@ + +## Input + +```javascript +function Component() { + const a = []; + const b = a; + useFreeze(a); + foo(b); // should be readonly, value is guaranteed frozen via alias +} + +function useFreeze() {} +function foo(x) {} + +``` + +## HIR + +``` +bb0: + Const mutable a$1 = Array [] + Const mutable b$2 = readonly a$1 + Call readonly useFreeze$3(freeze a$1) + Call mutable foo$4(readonly b$2) + Return +``` + +## Code + +```javascript +function Component$0() { + const a$1 = []; + const b$2 = a$1; + useFreeze$3(a$1); + foo$4(b$2); + return; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function useFreeze$0() { + return; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function foo$0(x$1) { + return; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.js b/compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.js new file mode 100644 index 0000000000..0f240a4205 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/frozen-after-alias.js @@ -0,0 +1,9 @@ +function Component() { + const a = []; + const b = a; + useFreeze(a); + foo(b); // should be readonly, value is guaranteed frozen via alias +} + +function useFreeze() {} +function foo(x) {} diff --git a/compiler/forget/src/__tests__/fixtures/hir/hook-call.expect.md b/compiler/forget/src/__tests__/fixtures/hir/hook-call.expect.md index 7ac2ad574c..d0a1dfe012 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/hook-call.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/hook-call.expect.md @@ -53,14 +53,14 @@ function foo$0() { ``` bb0: - Const readonly x$2 = Array [] - Const frozen y$3 = Call frozen useFreeze$4(frozen x$2) - Call mutable foo$5(frozen y$3, frozen x$2) - Const readonly $6 = "\n " - Const readonly $7 = "\n " - Const readonly $8 = "\n " - Const readonly $9 = JSX {frozen $6}{frozen x$2}{frozen $7}{frozen y$3}{frozen $8} - Return frozen $9 + Const mutable x$2 = Array [] + Const mutable y$3 = Call readonly useFreeze$4(freeze x$2) + Call mutable foo$5(readonly y$3, readonly x$2) + Const mutable $6 = "\n " + Const mutable $7 = "\n " + Const mutable $8 = "\n " + Const mutable $9 = JSX {readonly $6}{readonly x$2}{readonly $7}{readonly y$3}{readonly $8} + Return readonly $9 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.expect.md b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.expect.md new file mode 100644 index 0000000000..8d2954776b --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +function Component() { + const a = []; + useFreeze(a); // should freeze + useFreeze(a); // should be readonly + call(a); // should be readonly + return a; +} + +function useFreeze(x) {} +function call(x) {} + +``` + +## HIR + +``` +bb0: + Const mutable a$1 = Array [] + Call readonly useFreeze$2(freeze a$1) + Call readonly useFreeze$2(readonly a$1) + Call mutable call$3(readonly a$1) + Return readonly a$1 +``` + +## Code + +```javascript +function Component$0() { + const a$1 = []; + useFreeze$2(a$1); + useFreeze$2(a$1); + call$3(a$1); + return a$1; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function useFreeze$0(x$1) { + return; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function call$0(x$1) { + return; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.js b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.js new file mode 100644 index 0000000000..cdb1fce7e1 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-arguments.js @@ -0,0 +1,10 @@ +function Component() { + const a = []; + useFreeze(a); // should freeze + useFreeze(a); // should be readonly + call(a); // should be readonly + return a; +} + +function useFreeze(x) {} +function call(x) {} diff --git a/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.expect.md b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.expect.md new file mode 100644 index 0000000000..4d8ee08023 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.expect.md @@ -0,0 +1,97 @@ + +## Input + +```javascript +function Component(props) { + const cond = props.cond; + const x = props.x; + let a; + if (cond) { + a = x; + } else { + a = []; + } + useFreeze(a); // should freeze, value *may* be mutable + useFreeze(a); // should be readonly + call(a); // should be readonly + return a; +} + +function useFreeze(x) {} +function call(x) {} + +``` + +## HIR + +``` +bb0: + Const mutable cond$2 = readonly props$1.cond + Const mutable x$3 = readonly props$1.x + Let mutable a$4 = undefined + If (readonly cond$2) then:bb2 else:bb3 +bb2: + Reassign mutable a$4 = readonly x$3 + Goto bb1 +bb3: + Reassign mutable a$4 = Array [] + Goto bb1 +bb1: + Call readonly useFreeze$5(freeze a$4) + Call readonly useFreeze$5(readonly a$4) + Call mutable call$6(readonly a$4) + Return readonly a$4 +``` + +## Code + +```javascript +function Component$0(props$1) { + const cond$2 = props$1.cond; + const x$3 = props$1.x; + let a$4 = undefined; + if (cond$2) { + a$4 = x$3; + ("<>"); + } else { + a$4 = []; + ("<>"); + } + useFreeze$5(a$4); + useFreeze$5(a$4); + call$6(a$4); + return a$4; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function useFreeze$0(x$1) { + return; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function call$0(x$1) { + return; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.js b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.js new file mode 100644 index 0000000000..bcf51ccc4e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/hooks-freeze-possibly-mutable-arguments.js @@ -0,0 +1,17 @@ +function Component(props) { + const cond = props.cond; + const x = props.x; + let a; + if (cond) { + a = x; + } else { + a = []; + } + useFreeze(a); // should freeze, value *may* be mutable + useFreeze(a); // should be readonly + call(a); // should be readonly + return a; +} + +function useFreeze(x) {} +function call(x) {} diff --git a/compiler/forget/src/__tests__/fixtures/hir/independent-across-if.expect.md b/compiler/forget/src/__tests__/fixtures/hir/independent-across-if.expect.md index ffb2213e6c..0c54e9f6a4 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/independent-across-if.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/independent-across-if.expect.md @@ -97,16 +97,16 @@ function Foo$0() { ``` bb0: - Const readonly a$2 = Call mutable compute$3(frozen props$1.a) - Const readonly b$4 = Call mutable compute$3(frozen props$1.b) - If (frozen props$1.c) then:bb2 else:bb1 + Const mutable a$2 = Call mutable compute$3(readonly props$1.a) + Const mutable b$4 = Call mutable compute$3(readonly props$1.b) + If (readonly props$1.c) then:bb2 else:bb1 bb2: Call mutable mutate$5(mutable a$2) Call mutable mutate$5(mutable b$4) Goto bb1 bb1: - Const readonly $7 = JSX - Return frozen $7 + Const mutable $7 = JSX + Return readonly $7 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/independent.expect.md b/compiler/forget/src/__tests__/fixtures/hir/independent.expect.md index 19ee176fee..6fc689dfc8 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/independent.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/independent.expect.md @@ -28,10 +28,10 @@ function Foo() {} ``` bb0: - Const readonly a$2 = Call mutable compute$3(frozen props$1.a) - Const readonly b$4 = Call mutable compute$3(frozen props$1.b) - Const readonly $6 = JSX - Return frozen $6 + Const mutable a$2 = Call mutable compute$3(readonly props$1.a) + Const mutable b$4 = Call mutable compute$3(readonly props$1.b) + Const mutable $6 = JSX + Return readonly $6 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/interdependent-across-if.expect.md b/compiler/forget/src/__tests__/fixtures/hir/interdependent-across-if.expect.md index b5d8dc4883..bab5171d68 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/interdependent-across-if.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/interdependent-across-if.expect.md @@ -76,15 +76,15 @@ function Foo$0() { ``` bb0: - Const readonly a$2 = Call mutable compute$3(frozen props$1.a) - Const readonly b$4 = Call mutable compute$3(frozen props$1.b) - If (frozen props$1.c) then:bb2 else:bb1 + Const mutable a$2 = Call mutable compute$3(readonly props$1.a) + Const mutable b$4 = Call mutable compute$3(readonly props$1.b) + If (readonly props$1.c) then:bb2 else:bb1 bb2: Call mutable foo$5(mutable a$2, mutable b$4) Goto bb1 bb1: - Const readonly $7 = JSX - Return frozen $7 + Const mutable $7 = JSX + Return readonly $7 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/interdependent.expect.md b/compiler/forget/src/__tests__/fixtures/hir/interdependent.expect.md index 43ac1bc3d8..307a2735ce 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/interdependent.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/interdependent.expect.md @@ -28,11 +28,11 @@ function Foo() {} ``` bb0: - Const readonly a$2 = Call mutable compute$3(frozen props$1.a) - Const readonly b$4 = Call mutable compute$3(frozen props$1.b) + Const mutable a$2 = Call mutable compute$3(readonly props$1.a) + Const mutable b$4 = Call mutable compute$3(readonly props$1.b) Call mutable foo$5(mutable a$2, mutable b$4) - Const readonly $7 = JSX - Return frozen $7 + Const mutable $7 = JSX + Return readonly $7 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/property-assignment.expect.md b/compiler/forget/src/__tests__/fixtures/hir/property-assignment.expect.md index a7cb076b9e..0e9198fa54 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/property-assignment.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/property-assignment.expect.md @@ -17,13 +17,13 @@ function Component(props) { ``` bb0: - Const readonly x$2 = Object { } - Const readonly y$3 = Array [] - Reassign readonly x$2.y = readonly y$3 - Const readonly child$4 = JSX - Call mutable x$2.y.push(frozen props$1.p0) - Const readonly $5 = JSX {frozen child$4} - Return frozen $5 + Const mutable x$2 = Object { } + Const mutable y$3 = Array [] + Reassign mutable x$2.y = readonly y$3 + Const mutable child$4 = JSX + Call mutable x$2.y.push(readonly props$1.p0) + Const mutable $5 = JSX {readonly child$4} + Return readonly $5 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/reassignment-conditional.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reassignment-conditional.expect.md index 57cd445f04..82726cf705 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/reassignment-conditional.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/reassignment-conditional.expect.md @@ -23,18 +23,18 @@ function Component(props) { ``` bb0: - Let readonly x$2 = Array [] - Call mutable x$2.push(frozen props$1.p0) - Let readonly y$3 = readonly x$2 - If (frozen props$1.p1) then:bb2 else:bb1 + Let mutable x$2 = Array [] + Call mutable x$2.push(readonly props$1.p0) + Let mutable y$3 = readonly x$2 + If (readonly props$1.p1) then:bb2 else:bb1 bb2: - Reassign readonly x$2 = Array [] + Reassign mutable x$2 = Array [] Goto bb1 bb1: - Let readonly _$4 = JSX - Call mutable y$3.push(frozen props$1.p2) - Const readonly $5 = JSX - Return frozen $5 + Let mutable _$4 = JSX + Call readonly y$3.push(readonly props$1.p2) + Const mutable $5 = JSX + Return readonly $5 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/reassignment.expect.md b/compiler/forget/src/__tests__/fixtures/hir/reassignment.expect.md index dcdd0301dd..16c8f07747 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/reassignment.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/reassignment.expect.md @@ -21,14 +21,14 @@ function Component(props) { ``` bb0: - Let readonly x$2 = Array [] - Call mutable x$2.push(frozen props$1.p0) - Let readonly y$3 = readonly x$2 - Reassign readonly x$2 = Array [] - Let readonly _$4 = JSX - Call mutable y$3.push(frozen props$1.p1) - Const readonly $5 = JSX - Return frozen $5 + Let mutable x$2 = Array [] + Call mutable x$2.push(readonly props$1.p0) + Let mutable y$3 = readonly x$2 + Reassign mutable x$2 = Array [] + Let mutable _$4 = JSX + Call mutable y$3.push(readonly props$1.p1) + Const mutable $5 = JSX + Return readonly $5 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple.expect.md b/compiler/forget/src/__tests__/fixtures/hir/simple.expect.md index e1fb624c18..9c61008af3 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/simple.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/simple.expect.md @@ -15,16 +15,16 @@ function foo(x, y) { ``` bb0: - If (frozen x$1) then:bb2 else:bb1 + If (readonly x$1) then:bb2 else:bb1 bb2: - Const readonly $3 = false - Const readonly $4 = Call mutable foo$0(mutable $3, frozen y$2) - Return frozen $4 + Const mutable $3 = false + Const mutable $4 = Call readonly foo$0(readonly $3, readonly y$2) + Return freeze $4 bb1: - Const readonly $5 = 10 - Const frozen $6 = Binary frozen y$2 * readonly $5 - Const readonly $7 = Array [frozen $6] - Return frozen $7 + Const mutable $5 = 10 + Const mutable $6 = Binary readonly y$2 * readonly $5 + Const mutable $7 = Array [readonly $6] + Return freeze $7 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md b/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md index d02e9e8166..9f783f2078 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md @@ -32,27 +32,27 @@ function Component(props) { ``` bb0: - Let readonly x$2 = Array [] - Let readonly y$3 = undefined - Const readonly $4 = false - Const readonly $5 = true - Const readonly $6 = 1 - Switch (frozen props$1.p0) + Let mutable x$2 = Array [] + Let mutable y$3 = undefined + Const mutable $4 = false + Const mutable $5 = true + Const mutable $6 = 1 + Switch ( props$1.p0) Case readonly $6: bb1 Case readonly $5: bb6 Default: bb1 Case readonly $4: bb2 bb1: - Const readonly child$7 = JSX - Call mutable y$3.push(frozen props$1.p4) - Const readonly $8 = JSX {frozen child$7} - Return frozen $8 + Const mutable child$7 = JSX + Call readonly y$3.push(readonly props$1.p4) + Const mutable $8 = JSX {readonly child$7} + Return readonly $8 bb6: - Call mutable x$2.push(frozen props$1.p2) - Reassign readonly y$3 = Array [] + Call mutable x$2.push(readonly props$1.p2) + Reassign mutable y$3 = Array [] Goto bb1 bb2: - Reassign readonly y$3 = readonly x$2 + Reassign mutable y$3 = readonly x$2 Goto bb1 ``` diff --git a/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md b/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md index f267daf383..49527e1174 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/switch.expect.md @@ -28,27 +28,27 @@ function Component(props) { ``` bb0: - Let readonly x$2 = Array [] - Let readonly y$3 = undefined - Const readonly $4 = false - Const readonly $5 = true - Switch (frozen props$1.p0) + Let mutable x$2 = Array [] + Let mutable y$3 = undefined + Const mutable $4 = false + Const mutable $5 = true + Switch ( props$1.p0) Case readonly $5: bb4 Case readonly $4: bb2 Default: bb1 bb4: - Call mutable x$2.push(frozen props$1.p2) - Call mutable x$2.push(frozen props$1.p3) - Reassign readonly y$3 = Array [] + Call mutable x$2.push(readonly props$1.p2) + Call mutable x$2.push(readonly props$1.p3) + Reassign mutable y$3 = Array [] Goto bb2 bb2: - Reassign readonly y$3 = readonly x$2 + Reassign mutable y$3 = readonly x$2 Goto bb1 bb1: - Const readonly child$6 = JSX - Call mutable y$3.push(frozen props$1.p4) - Const readonly $7 = JSX {frozen child$6} - Return frozen $7 + Const mutable child$6 = JSX + Call readonly y$3.push(readonly props$1.p4) + Const mutable $7 = JSX {readonly child$6} + Return readonly $7 ``` ## Code diff --git a/compiler/forget/src/__tests__/hir-test.ts b/compiler/forget/src/__tests__/hir-test.ts index e36437ced1..9a8bd19d41 100644 --- a/compiler/forget/src/__tests__/hir-test.ts +++ b/compiler/forget/src/__tests__/hir-test.ts @@ -10,15 +10,14 @@ import generate from "@babel/generator"; import * as parser from "@babel/parser"; import traverse from "@babel/traverse"; -import { graphviz, wasmFolder } from "@hpcc-js/wasm"; -import { existsSync, unlinkSync, writeFileSync } from "fs"; +import { wasmFolder } from "@hpcc-js/wasm"; import invariant from "invariant"; import path from "path"; import prettier from "prettier"; -import buildDefUseGraph, { printGraph } from "../HIR/BuildDefUseGraph"; import { lower } from "../HIR/BuildHIR"; import codegen from "../HIR/Codegen"; import { HIRFunction } from "../HIR/HIR"; +import inferReferenceCapability from "../HIR/InferReferenceCapability"; import printHIR from "../HIR/PrintHIR"; import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures"; @@ -45,22 +44,23 @@ describe("React Forget (HIR version)", () => { FunctionDeclaration: { enter(nodePath) { const ir: HIRFunction = lower(nodePath); - const lifetimeGraph = buildDefUseGraph(ir); + inferReferenceCapability(ir); + // const lifetimeGraph = buildDefUseGraph(ir); const textHIR = printHIR(ir.body); - const textLifetimeGraph = printGraph(lifetimeGraph); - const graphvizFile = path.join( - __dirname, - "fixtures", - "hir-svg", - file + ".svg" - ); - if (input.indexOf("@Out DefUseGraph") !== -1) { - graphviz.layout(textLifetimeGraph, "svg", "dot").then((svg) => { - writeFileSync(graphvizFile, svg); - }); - } else if (existsSync(graphvizFile)) { - unlinkSync(graphvizFile); - } + // const textLifetimeGraph = printGraph(lifetimeGraph); + // const graphvizFile = path.join( + // __dirname, + // "fixtures", + // "hir-svg", + // file + ".svg" + // ); + // if (input.indexOf("@Out DefUseGraph") !== -1) { + // graphviz.layout(textLifetimeGraph, "svg", "dot").then((svg) => { + // writeFileSync(graphvizFile, svg); + // }); + // } else if (existsSync(graphvizFile)) { + // unlinkSync(graphvizFile); + // } const ast = codegen(ir); const text = prettier.format( diff --git a/compiler/forget/yarn.lock b/compiler/forget/yarn.lock index 5d45b60fcd..297d6129f5 100644 --- a/compiler/forget/yarn.lock +++ b/compiler/forget/yarn.lock @@ -63,7 +63,7 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/generator@7.2.0", "@babel/generator@^7.1.6", "@babel/generator@^7.19.3", "@babel/generator@^7.2.0", "@babel/generator@^7.7.2": +"@babel/generator@7.2.0", "@babel/generator@^7.1.6", "@babel/generator@^7.19.4", "@babel/generator@^7.2.0", "@babel/generator@^7.7.2": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.0.tgz#eaf3821fa0301d9d4aef88e63d4bcc19b73ba16c" integrity sha512-BA75MVfRlFQG2EZgFYIwyT1r6xSkwfP2bdkY/kLZusEYWiJs4xCowab/alaEaT0wSvmVuXGqiefeBlP+7V1yKg== @@ -75,11 +75,11 @@ trim-right "^1.0.1" "@babel/generator@^7.19.0": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.3.tgz#d7f4d1300485b4547cb6f94b27d10d237b42bf59" - integrity sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ== + version "7.19.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.5.tgz#da3f4b301c8086717eee9cab14da91b1fa5dcca7" + integrity sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg== dependencies: - "@babel/types" "^7.19.3" + "@babel/types" "^7.19.4" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -203,6 +203,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -241,11 +246,16 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.1.tgz#6f6d6c2e621aad19a92544cc217ed13f1aac5b4c" integrity sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A== -"@babel/parser@^7.18.6", "@babel/parser@^7.19.3": +"@babel/parser@^7.18.6": version "7.19.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a" integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ== +"@babel/parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.4.tgz#03c4339d2b8971eb3beca5252bafd9b9f79db3dc" + integrity sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -465,18 +475,18 @@ lodash "^4.17.10" "@babel/traverse@^7.19.1": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.3.tgz#3a3c5348d4988ba60884e8494b0592b2f15a04b4" - integrity sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ== + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.4.tgz#f117820e18b1e59448a6c1fa9d0ff08f7ac459a8" + integrity sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.3" + "@babel/generator" "^7.19.4" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.19.3" - "@babel/types" "^7.19.3" + "@babel/parser" "^7.19.4" + "@babel/types" "^7.19.4" debug "^4.1.0" globals "^11.1.0" @@ -506,12 +516,12 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" -"@babel/types@^7.19.3": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.3.tgz#fc420e6bbe54880bce6779ffaf315f5e43ec9624" - integrity sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw== +"@babel/types@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7" + integrity sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw== dependencies: - "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0"