From 796e14f38e1ff45ee91f8f9448b9e5fb4189ad2e Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Tue, 22 Nov 2022 08:03:38 -0800 Subject: [PATCH] Identifier.scope includes scope id and range Refactors `Identifier.scope` to be a `ReactiveScope` object with an id and range. This gives us a place to later add a list of dependencies for the scope. --- compiler/forget/src/HIR/HIR.ts | 18 +- .../src/HIR/InferReactiveScopeVariables.ts | 53 +++-- compiler/forget/src/HIR/PrintHIR.ts | 2 +- compiler/forget/src/HIR/ScopeAnalysis.ts | 186 ------------------ 4 files changed, 32 insertions(+), 227 deletions(-) delete mode 100644 compiler/forget/src/HIR/ScopeAnalysis.ts diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index d72cbdf1e3..ac55b84ea7 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -53,17 +53,6 @@ export type ReactFunction = { scopes: Map; }; -/** - * Each scope has a set of inputs (which is loosely defined as stages of analysis - * may refine the set of inputs), a set of outputs (values it will produce), and - * a set of instructions to produce the outputs from the inputs. - */ -export type ReactiveScope = { - inputs: Set; - outputs: Set; - instructions: HIR; -}; - /** * A function declaration including its path */ @@ -297,7 +286,7 @@ export type Identifier = { mutableRange: MutableRange; // The ID of the reactive scope which will compute this value. Multiple variables may have // the same scope id. - scope: ScopeId | null; + scope: ReactiveScope | null; }; /** @@ -325,6 +314,11 @@ export enum Effect { Mutate = "mutate", } +export type ReactiveScope = { + id: ScopeId; + range: MutableRange; +}; + /** * Simulated opaque type for BlockIds to prevent using normal numbers as block ids * accidentally. diff --git a/compiler/forget/src/HIR/InferReactiveScopeVariables.ts b/compiler/forget/src/HIR/InferReactiveScopeVariables.ts index 9c84db2e36..dae71b54bf 100644 --- a/compiler/forget/src/HIR/InferReactiveScopeVariables.ts +++ b/compiler/forget/src/HIR/InferReactiveScopeVariables.ts @@ -12,8 +12,8 @@ import { Instruction, makeInstructionId, makeScopeId, - MutableRange, Place, + ReactiveScope, ScopeId, } from "./HIR"; import { eachInstructionOperand } from "./visitors"; @@ -66,11 +66,11 @@ import { eachInstructionOperand } from "./visitors"; export function inferReactiveScopeVariables(fn: HIRFunction) { // Represents the set of reactive scopes as disjoint sets of identifiers // that mutate together. - const scopes = new DisjointSet(); + const scopeIdentifiers = new DisjointSet(); for (const [_, block] of fn.body.blocks) { for (const phi of block.phis) { const operands: Array = [phi.id, ...phi.operands.values()]; - scopes.union(operands); + scopeIdentifiers.union(operands); } for (const instr of block.instructions) { @@ -89,18 +89,14 @@ export function inferReactiveScopeVariables(fn: HIRFunction) { } } if (operands.length !== 0) { - scopes.union(operands); + scopeIdentifiers.union(operands); } } } // Maps each scope (by its identifying member) to a ScopeId value - const scopeIds: Map = new Map(); - // Store the mutable range and set of identifiers for each scope - const scopeVariables: Map< - ScopeId, - { range: MutableRange; variables: Set } - > = new Map(); + const scopes: Map = new Map(); + const scopeVariables: Map> = new Map(); /** * Iterate over all the identifiers and assign a unique ScopeId @@ -110,21 +106,14 @@ export function inferReactiveScopeVariables(fn: HIRFunction) { * build a MutableRange that describes the span of mutations * across all identifiers in each scope. */ - scopes.forEach((identifier, groupIdentifier) => { - let scopeId = scopeIds.get(groupIdentifier); - if (scopeId == null) { - scopeId = makeScopeId(scopeIds.size); - scopeIds.set(groupIdentifier, scopeId); - } - identifier.scope = scopeId; - - let scope = scopeVariables.get(scopeId); + scopeIdentifiers.forEach((identifier, groupIdentifier) => { + let scope = scopes.get(groupIdentifier); if (scope === undefined) { scope = { - range: { ...identifier.mutableRange }, - variables: new Set(), + id: makeScopeId(scopes.size), + range: identifier.mutableRange, }; - scopeVariables.set(scopeId, scope); + scopes.set(groupIdentifier, scope); } else { scope.range.start = makeInstructionId( Math.min(scope.range.start, identifier.mutableRange.start) @@ -133,14 +122,22 @@ export function inferReactiveScopeVariables(fn: HIRFunction) { Math.max(scope.range.end, identifier.mutableRange.end) ); } - scope.variables.add(identifier); + identifier.scope = scope; + + let vars = scopeVariables.get(scope); + if (vars === undefined) { + vars = new Set(); + scopeVariables.set(scope, vars); + } + vars.add(identifier); }); - // Update all the identifiers for each scope now that we know - // the scope's full range. - for (const [_, scope] of scopeVariables) { - for (const identifier of scope.variables) { - identifier.mutableRange = scope.range; + // Copy scope ranges to identifier ranges: not strictly required but this is useful + // for visualization + for (const [scope, vars] of scopeVariables) { + for (const identifier of vars) { + identifier.mutableRange.start = scope.range.start; + identifier.mutableRange.end = scope.range.end; } } } diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index 6db64aa41f..4c2d929699 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -297,7 +297,7 @@ export function printPlace(place: Place): string { export function printIdentifier(id: Identifier): string { return `${id.name ?? ""}\$${id.id}${ - id.scope !== null ? `_@${id.scope}` : "" + id.scope !== null ? `_@${id.scope.id}` : "" }`; } diff --git a/compiler/forget/src/HIR/ScopeAnalysis.ts b/compiler/forget/src/HIR/ScopeAnalysis.ts deleted file mode 100644 index cb8c582416..0000000000 --- a/compiler/forget/src/HIR/ScopeAnalysis.ts +++ /dev/null @@ -1,186 +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 { - HIRFunction, - makeScopeId, - ReactFunction, - ReactiveScope, - ScopeId, -} from "./HIR"; - -// import * as t from "@babel/types"; - -/** - * Converts a function in standard HIR form into a reactive function, breaking down - * the function's body to determine a set of minimal scopes which define computation - * of sub-expressions in the input. A separate phase can then reconstruct an HIR - * function given the reactive function using various heuristics for codegen. - * - * ## algorithm - * We want to find minimal sets of instructions which build up values. This - * involves determining which values "construct together". Values that construct - * together derive from: - * * mutable accesses that may capture one value into another: - * `x = y`, `foo(x, y)` - * The intuition is that because a mutable reference can be captured, we have - * to assume that further modifications of one value may affect the other and - * vice versa, so their construction must be grouped together. - * - * * constructing independent values across the same control flow path: - * `let x = ..., y = ...; if (cond) {x.a = ...; y.b = ...; }` - * The intuition here is that we don't want to repeat control-flow constructs, - * (esp loops) so values that are constructed across multiple basic blocks - * get grouped together. - * - * Once we've grouped values that "construct together", we can then create one - * scope per group. we iterate back over the CFG, and for each instruction/terminal - * we figure out which group that belongs to, based on the set of values its creating, - * and push it onto the appropriate group's CFG. - * - * as an extra optimization to avoid creating scopes for primitives, we can try to - * infer that instructions that produce primitives don't get their own scope. - * so in `tmp1 = a * b; tmp2 = tmp1 * 3; tmp3 = tmp2 > 10`, we can say that tmp1/2/3 - * are all primitives, so they should be grouped together even though they don't - * mutate together, and even though there's no control flow. - * - * we'll also need use-def analysis (or similar) to avoid reassignment of variables - * causing overly large grouping of scopes. we really care about the *values* that - * are being constructed together, not the variables. so `foo(x, y)` conjoins x and y, - * but if we later `x = {}; x.a = 5`, that later assignment and modification should *not* - * conjoin with `y`. for now, we accept that these cases will be treated as conjoined and - * grouped together. - */ -export default function analyzeScopes(fn: HIRFunction): ReactFunction { - // naive but trivially correct version - const returnScopeId = makeScopeId(0); - const scopes: Map = new Map(); - scopes.set(returnScopeId, { - inputs: new Set([...fn.params]), - outputs: new Set(), - instructions: fn.body, - }); - return { - loc: fn.loc, - id: fn.id, - params: fn.params, - returnScope: returnScopeId, - scopes, - }; -} - -/** - * ```javascript - * function Component({items}) { - * const renderedItems = []; - * const seen = new Set(); - * for (const item of items) { - * renderedItems.push(
{item}
); - * seen.add(item); - * } - * return ; - * } - * ``` - * Iterate over the IR in tree order (similar to codegen) - single pass with recursion. - * Build a stack of control points and mutable values, associate mutable values with - * control points that occur between each other. control points also naturally group - * together, ie for a continue within a loop. - * - * - * Ideas toward an algorithm: - * - Track the lifetime for which each variable (value, really) is mutable. - * - Values with overlapping mutable lifetimes are conjoined ("memoize together"). - * - Keep a stack of mutated values which we can walk. But *also* store block terminals in this stack. - * When you walk back up the stack to find previous mutations of a value, add all the terminals - * along the way until finding it as dependencies. eg set.union(mutValue, terminal). - * Two values that mutate across the same control points will union with the same terminal, - * and get conjoined. - * - * Eg in the following, `renderedItems` and `seen` have overlapping mutable lifetimes: - * - * ```javascript - * function Component({items, maxItems}) { - * const renderedItems = []; // new-mutable renderedItems - * const seen = new Set(); // new-mutable seen - * const max = Math.max(0, maxItems); // new-mutable max; read-frozen maxItems - * for (const item of items) { // read-frozen items; control point - * if (item == null || seen.has(item)) { // mutable seen; read-frozen item; control point - * continue; // control point - * } - * - * seen.add(item); // mutable seen; read-frozen item - * // finding prev `mut seen` hops the above control points - * renderedItems.push(
{item}
); // mutable renderedItems; read-frozen item - * // finding prev `mut renderedItems` hops the above control points - * if (renderedItems.length >= max) { // read-frozen max; read-mutable renderedItems - * break; - * } - * } - * const count = renderedItems.length; // read-frozen renderedItems - * return

{count} Items

{renderedItems}
; // read-frozen renderedItems - * } - * - * function Component({items, maxItems}) { - * // scope 0 (inputs: maxItems, outputs: max) - * const c_maxItems = ...; - * let max = c_max_items ? Math.max(0, maxItems) : ...; - * const c_max = ...; - * - * // scope 1 (inputs: items, max, ouputs: renderedItems, seen, count) - * const c_items = ...; - * let renderedItems; - * let seen; - * let count; - * if (c_max || c_items) { - * renderedItems = ....; - * seen = ...; - * for (const item of items) { - * if (item == null || seen.has(item)) { // read-mutable seen; read-frozen item; control point - * continue; // control point - * } - * seen.add(item); // mut-mutable seen; read-frozen item - * renderedItems.push(
{item}
); // mut-mutable renderedItems; read-frozen item - * if (renderedItems.length >= max) { // read-frozen max; read-mutable renderedItems - * break; - * } - * } - * count = renderedItems.length; - * } else { - * // populate from cache - * } - * - * // scope 2 (inputs: count, ouputs: div) - * const c_count = ...; - * let h1; - * if (c_count) { - * h1 =

{count}

- * } // else from cache - * - * // scope 3 (inputs: h1, renderedItems, outputs: outer div) - * const c_h1 = ...; - * const c_renderedItems = ...; - * let ret; - * if (c_h1 || c_renderedItems) { - * ret =
{h1}{renderedItems}
; // read-frozen renderedItems - * } // else from cache - * return ret; - * } - * ``` - */ - -function analyze(fn: HIRFunction): ReactFunction { - const returnScopeId = makeScopeId(0); - const scopes: Map = new Map(); - - return { - loc: fn.loc, - id: fn.id, - params: fn.params, - returnScope: returnScopeId, - scopes, - }; -}