Files
react/compiler/forget/src/ReactiveScopes/InferReactiveIdentifiers.ts
T
Joe Savona b793fe424d Separate eachLValue/eachOperand visitors
This PR starts to clean up our handling of lvalues and rvalues by adding new 
`eachInstructionLValues()` and `mapInstructionLValues()` helpers. Now, 
`eachInstructionOperand()` and `mapInstructionOperands()` only visit true 
rvalues, and the new passes must be used to visit lvalues. This allows us to 
remove the special-casing for StoreLocal and Destructure in most of the passes.
2023-03-06 14:33:52 -08:00

166 lines
5.3 KiB
TypeScript

/**
* 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 { CompilerError } from "../CompilerError";
import {
Effect,
IdentifierId,
isHookType,
ReactiveFunction,
ReactiveInstruction,
} from "../HIR/HIR";
import { eachInstructionLValue } from "../HIR/visitors";
import { assertExhaustive } from "../Utils/utils";
import {
eachReactiveValueOperand,
ReactiveFunctionVisitor,
visitReactiveFunction,
} from "./visitors";
type IdentifierReactivity = Map<IdentifierId, boolean>;
class State {
reactivityMap: IdentifierReactivity = new Map();
temporaries: Map<IdentifierId, IdentifierId> = new Map();
}
class Visitor extends ReactiveFunctionVisitor<State> {
override visitInstruction(instr: ReactiveInstruction, state: State) {
this.traverseInstruction(instr, state);
const lval = instr.lvalue;
if (lval == null) {
return;
}
const { value } = instr;
let hasReactiveInput = false;
// Globals are currently treated as non-reactive this happens implicitly because LoadGlobal
// has no operands which can be registered as reactive.
// Consider adding an option to declare whether a given global can be reactive or not, or
// a more general "treat all globals as reactive" flag.
for (const operand of eachReactiveValueOperand(value)) {
if (operand.effect === Effect.Store) {
continue;
}
const resolvedId: IdentifierId =
state.temporaries.get(operand.identifier.id) ?? operand.identifier.id;
if (state.reactivityMap.get(resolvedId)) {
hasReactiveInput = true;
break;
}
}
if (
!hasReactiveInput &&
instr.value.kind === "CallExpression" &&
isHookType(instr.value.callee.identifier)
) {
// Hooks cannot be memoized. Even if they do not accept any reactive inputs,
// they are not guaranteed to memoize their return value, and their result
// must be assumed to be reactive.
// TODO: use types or an opt-in registry of custom hook information to
// allow treating safe hooks as non-reactive.
hasReactiveInput = true;
}
state.reactivityMap.set(lval.identifier.id, hasReactiveInput);
if (hasReactiveInput) {
for (const lvalue of eachInstructionLValue(instr)) {
state.reactivityMap.set(lvalue.identifier.id, true);
}
// all mutating effects must also be marked as reactive
for (const operand of eachReactiveValueOperand(value)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.Mutate: {
const resolvedId: IdentifierId =
state.temporaries.get(operand.identifier.id) ??
operand.identifier.id;
state.reactivityMap.set(resolvedId, true);
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant("Unexpected unknown effect", operand.loc);
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind '${operand.effect}'`
);
}
}
}
}
if (instr.lvalue !== null) {
if (instr.value.kind === "LoadLocal") {
state.temporaries.set(
instr.lvalue.identifier.id,
instr.value.place.identifier.id
);
}
}
}
}
/**
* Computes a map of {@link Place} -> reactivityMap. A Place is reactive if any
* operant used in its construction is reactive. Sources of reactivity are
* {@link ReactiveFunction.params} and HookCall return values (TODO).
* Free values are currently not populated.
*
* This relies on alias analysis done by InferReactiveScopeVariables, which
* creates reactive scopes for variables that mutate together. If one value
* declared in a scope is Reactive, then the rest are marked as reactive as
* well.
* e.g.
* ```javascript
* function foo(props) {
* let x = {};
* let y = [];
* x.y = y;
* y.push(props.a)
* // references to x are reactive here
* }
* ```
* This an overestimate when two identifiers have overlapping scope, but
* one is not actually reactive. However, since the same ReactiveBlock now
* produces both identifiers, they are effectively both reactive (i.e.
* object creation is not stable)
* e.g.
* ```javascript
* function bar(props) {
* // x and y have overlapping mutableRanges, so they share a ReactiveScope
* // (even though they are not aliased together)
* // technically y has no reactive inputs, but it becomes non-stable due to
* // sharing a ReactiveScopeBlock with x
* let x = {};
* let y = [];
* mutate1(x, props);
* mutate2(y);
* }
* ```
*/
export function inferReactiveIdentifiers(
fn: ReactiveFunction
): Set<IdentifierId> {
const visitor = new Visitor();
const state = new State();
for (const param of fn.params) {
state.reactivityMap.set(param.identifier.id, true);
}
visitReactiveFunction(fn, visitor, state);
const result = new Set<IdentifierId>();
state.reactivityMap.forEach((isReactive, id) => {
if (isReactive) result.add(id);
});
return result;
}