mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
5e51d767d1
AnalyzeFunctions was reusing the `ReactiveScopeDependency` type since it happened to have a convenient shape, but we need to change this type to represent optionality. We now use a locally defined type instead.
ghstack-source-id: e305c6ede4
Pull Request resolved: https://github.com/facebook/react/pull/30811
201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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,
|
|
HIRFunction,
|
|
Identifier,
|
|
IdentifierName,
|
|
LoweredFunction,
|
|
Place,
|
|
isRefOrRefValue,
|
|
makeInstructionId,
|
|
} from '../HIR';
|
|
import {deadCodeElimination} from '../Optimization';
|
|
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
|
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
|
import {logHIRFunction} from '../Utils/logger';
|
|
import {inferMutableContextVariables} from './InferMutableContextVariables';
|
|
import {inferMutableRanges} from './InferMutableRanges';
|
|
import inferReferenceEffects from './InferReferenceEffects';
|
|
|
|
type Dependency = {
|
|
identifier: Identifier;
|
|
path: Array<string>;
|
|
};
|
|
|
|
// Helper class to track indirections such as LoadLocal and PropertyLoad.
|
|
export class IdentifierState {
|
|
properties: Map<Identifier, Dependency> = new Map();
|
|
|
|
resolve(identifier: Identifier): Identifier {
|
|
const resolved = this.properties.get(identifier);
|
|
if (resolved !== undefined) {
|
|
return resolved.identifier;
|
|
}
|
|
return identifier;
|
|
}
|
|
|
|
declareProperty(lvalue: Place, object: Place, property: string): void {
|
|
const objectDependency = this.properties.get(object.identifier);
|
|
let nextDependency: Dependency;
|
|
if (objectDependency === undefined) {
|
|
nextDependency = {identifier: object.identifier, path: [property]};
|
|
} else {
|
|
nextDependency = {
|
|
identifier: objectDependency.identifier,
|
|
path: [...objectDependency.path, property],
|
|
};
|
|
}
|
|
this.properties.set(lvalue.identifier, nextDependency);
|
|
}
|
|
|
|
declareTemporary(lvalue: Place, value: Place): void {
|
|
const resolved: Dependency = this.properties.get(value.identifier) ?? {
|
|
identifier: value.identifier,
|
|
path: [],
|
|
};
|
|
this.properties.set(lvalue.identifier, resolved);
|
|
}
|
|
}
|
|
|
|
export default function analyseFunctions(func: HIRFunction): void {
|
|
const state = new IdentifierState();
|
|
|
|
for (const [_, block] of func.body.blocks) {
|
|
for (const instr of block.instructions) {
|
|
switch (instr.value.kind) {
|
|
case 'ObjectMethod':
|
|
case 'FunctionExpression': {
|
|
lower(instr.value.loweredFunc.func);
|
|
infer(instr.value.loweredFunc, state, func.context);
|
|
break;
|
|
}
|
|
case 'PropertyLoad': {
|
|
state.declareProperty(
|
|
instr.lvalue,
|
|
instr.value.object,
|
|
instr.value.property,
|
|
);
|
|
break;
|
|
}
|
|
case 'ComputedLoad': {
|
|
/*
|
|
* The path is set to an empty string as the path doesn't really
|
|
* matter for a computed load.
|
|
*/
|
|
state.declareProperty(instr.lvalue, instr.value.object, '');
|
|
break;
|
|
}
|
|
case 'LoadLocal':
|
|
case 'LoadContext': {
|
|
if (instr.lvalue.identifier.name === null) {
|
|
state.declareTemporary(instr.lvalue, instr.value.place);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function lower(func: HIRFunction): void {
|
|
analyseFunctions(func);
|
|
inferReferenceEffects(func, {isFunctionExpression: true});
|
|
deadCodeElimination(func);
|
|
inferMutableRanges(func);
|
|
rewriteInstructionKindsBasedOnReassignment(func);
|
|
inferReactiveScopeVariables(func);
|
|
inferMutableContextVariables(func);
|
|
logHIRFunction('AnalyseFunction (inner)', func);
|
|
}
|
|
|
|
function infer(
|
|
loweredFunc: LoweredFunction,
|
|
state: IdentifierState,
|
|
context: Array<Place>,
|
|
): void {
|
|
const mutations = new Map<string, Effect>();
|
|
for (const operand of loweredFunc.func.context) {
|
|
if (
|
|
isMutatedOrReassigned(operand.identifier) &&
|
|
operand.identifier.name !== null
|
|
) {
|
|
mutations.set(operand.identifier.name.value, operand.effect);
|
|
}
|
|
}
|
|
|
|
for (const dep of loweredFunc.dependencies) {
|
|
let name: IdentifierName | null = null;
|
|
|
|
if (state.properties.has(dep.identifier)) {
|
|
const receiver = state.properties.get(dep.identifier)!;
|
|
name = receiver.identifier.name;
|
|
} else {
|
|
name = dep.identifier.name;
|
|
}
|
|
|
|
if (isRefOrRefValue(dep.identifier)) {
|
|
/*
|
|
* TODO: this is a hack to ensure we treat functions which reference refs
|
|
* as having a capture and therefore being considered mutable. this ensures
|
|
* the function gets a mutable range which accounts for anywhere that it
|
|
* could be called, and allows us to help ensure it isn't called during
|
|
* render
|
|
*/
|
|
dep.effect = Effect.Capture;
|
|
} else if (name !== null) {
|
|
const effect = mutations.get(name.value);
|
|
if (effect !== undefined) {
|
|
dep.effect = effect === Effect.Unknown ? Effect.Capture : effect;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This could potentially add duplicate deps to mutatedDeps in the case of
|
|
* mutating a context ref in the child function and in this parent function.
|
|
* It might be useful to dedupe this.
|
|
*
|
|
* In practice this never really matters because the Component function has no
|
|
* context refs, so it will never have duplicate deps.
|
|
*/
|
|
for (const place of context) {
|
|
CompilerError.invariant(place.identifier.name !== null, {
|
|
reason: 'context refs should always have a name',
|
|
description: null,
|
|
loc: place.loc,
|
|
suggestions: null,
|
|
});
|
|
|
|
const effect = mutations.get(place.identifier.name.value);
|
|
if (effect !== undefined) {
|
|
place.effect = effect === Effect.Unknown ? Effect.Capture : effect;
|
|
loweredFunc.dependencies.push(place);
|
|
}
|
|
}
|
|
|
|
for (const operand of loweredFunc.func.context) {
|
|
operand.identifier.mutableRange.start = makeInstructionId(0);
|
|
operand.identifier.mutableRange.end = makeInstructionId(0);
|
|
operand.identifier.scope = null;
|
|
}
|
|
}
|
|
|
|
function isMutatedOrReassigned(id: Identifier): boolean {
|
|
/*
|
|
* This check checks for mutation and reassingnment, so the usual check for
|
|
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
|
|
* enough.
|
|
*
|
|
* We need to track re-assignments in context refs as we need to reflect the
|
|
* re-assignment back to the captured refs.
|
|
*/
|
|
return id.mutableRange.end > id.mutableRange.start;
|
|
}
|