mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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.
This commit is contained in:
@@ -53,17 +53,6 @@ export type ReactFunction = {
|
||||
scopes: Map<ScopeId, ReactiveScope>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<Place>;
|
||||
outputs: Set<Place>;
|
||||
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.
|
||||
|
||||
@@ -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<Identifier>();
|
||||
const scopeIdentifiers = new DisjointSet<Identifier>();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const operands: Array<Identifier> = [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<Identifier, ScopeId> = new Map();
|
||||
// Store the mutable range and set of identifiers for each scope
|
||||
const scopeVariables: Map<
|
||||
ScopeId,
|
||||
{ range: MutableRange; variables: Set<Identifier> }
|
||||
> = new Map();
|
||||
const scopes: Map<Identifier, ReactiveScope> = new Map();
|
||||
const scopeVariables: Map<ReactiveScope, Set<Identifier>> = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}` : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ScopeId, ReactiveScope> = 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(<div>{item}</div>);
|
||||
* seen.add(item);
|
||||
* }
|
||||
* return <Child items={renderedItems} seen={seen} />;
|
||||
* }
|
||||
* ```
|
||||
* 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(<div>{item}</div>); // 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 <div><h1>{count} Items</h1>{renderedItems}</div>; // 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(<div>{item}</div>); // 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 = <h1>{count}</h1>
|
||||
* } // 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 = <div>{h1}{renderedItems}</div>; // read-frozen renderedItems
|
||||
* } // else from cache
|
||||
* return ret;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
function analyze(fn: HIRFunction): ReactFunction {
|
||||
const returnScopeId = makeScopeId(0);
|
||||
const scopes: Map<ScopeId, ReactiveScope> = new Map();
|
||||
|
||||
return {
|
||||
loc: fn.loc,
|
||||
id: fn.id,
|
||||
params: fn.params,
|
||||
returnScope: returnScopeId,
|
||||
scopes,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user