mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
ec6fe57a50
Summary: Using the change detection code to debug codebases that violate the rules of react is a lot easier when we have a source location corresponding to the value that has changed inappropriately. I didn't see an easy way to track that information in the existing data structures at the point of codegen, so this PR adds locations to identifiers and reactive scopes (the location of a reactive scope is the range of the locations of its included identifiers).
I'm interested if there's a better way to do this that I missed!
ghstack-source-id: aed5f7edda
Pull Request resolved: https://github.com/facebook/react/pull/29658
298 lines
8.8 KiB
TypeScript
298 lines
8.8 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 {
|
|
BasicBlock,
|
|
BlockId,
|
|
Effect,
|
|
Environment,
|
|
FunctionExpression,
|
|
GeneratedSource,
|
|
GotoVariant,
|
|
HIRFunction,
|
|
IdentifierId,
|
|
InstructionKind,
|
|
LabelTerminal,
|
|
Place,
|
|
makeInstructionId,
|
|
makeType,
|
|
promoteTemporary,
|
|
reversePostorderBlocks,
|
|
} from "../HIR";
|
|
import { markInstructionIds, markPredecessors } from "../HIR/HIRBuilder";
|
|
import { eachInstructionValueOperand } from "../HIR/visitors";
|
|
import { retainWhere } from "../Utils/utils";
|
|
|
|
/*
|
|
* Inlines immediately invoked function expressions (IIFEs) to allow more fine-grained memoization
|
|
* of the values they produce.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```
|
|
* const x = (() => {
|
|
* const x = [];
|
|
* x.push(foo());
|
|
* return x;
|
|
* })();
|
|
*
|
|
* =>
|
|
*
|
|
* bb0:
|
|
* // placeholder for the result, all return statements will assign here
|
|
* let t0;
|
|
* // Label allows using a goto (break) to exit out of the body
|
|
* Label block=bb1 fallthrough=bb2
|
|
* bb1:
|
|
* // code within the function expression
|
|
* const x0 = [];
|
|
* x0.push(foo());
|
|
* // return is replaced by assignment to the result variable...
|
|
* t0 = x0;
|
|
* // ...and a goto to the code after the function expression invocation
|
|
* Goto bb2
|
|
* bb2:
|
|
* // code after the IIFE call
|
|
* const x = t0;
|
|
* ```
|
|
*
|
|
* The implementation relies on HIR's ability to support labeled blocks:
|
|
* - We terminate the basic block just prior to the CallExpression of the IIFE
|
|
* with a LabelTerminal whose fallback is the code following the CallExpression.
|
|
* Just prior to the terminal we also create a named temporary variable which
|
|
* will hold the result.
|
|
* - We then inline the contents of the function "in between" (conceptually) those
|
|
* two blocks.
|
|
* - All return statements in the original function expression are replaced with a
|
|
* StoreLocal to the temporary we allocated before plus a Goto to the fallthrough
|
|
* block (code following the CallExpression).
|
|
*/
|
|
export function inlineImmediatelyInvokedFunctionExpressions(
|
|
fn: HIRFunction
|
|
): void {
|
|
// Track all function expressions that are assigned to a temporary
|
|
const functions = new Map<IdentifierId, FunctionExpression>();
|
|
// Functions that are inlined
|
|
const inlinedFunctions = new Set<IdentifierId>();
|
|
|
|
/*
|
|
* Iterate the *existing* blocks from the outer component to find IIFEs
|
|
* and inline them. During iteration we will modify `fn` (by inlining the CFG
|
|
* of IIFEs) so we explicitly copy references to just the original
|
|
* function's blocks first. As blocks are split to make room for IIFE calls,
|
|
* the split portions of the blocks will be added to this queue.
|
|
*/
|
|
const queue = Array.from(fn.body.blocks.values());
|
|
queue: for (const block of queue) {
|
|
for (let ii = 0; ii < block.instructions.length; ii++) {
|
|
const instr = block.instructions[ii]!;
|
|
switch (instr.value.kind) {
|
|
case "FunctionExpression": {
|
|
if (instr.lvalue.identifier.name === null) {
|
|
functions.set(instr.lvalue.identifier.id, instr.value);
|
|
}
|
|
break;
|
|
}
|
|
case "CallExpression": {
|
|
if (instr.value.args.length !== 0) {
|
|
// We don't support inlining when there are arguments
|
|
continue;
|
|
}
|
|
const body = functions.get(instr.value.callee.identifier.id);
|
|
if (body === undefined) {
|
|
// Not invoking a local function expression, can't inline
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
body.loweredFunc.func.params.length > 0 ||
|
|
body.loweredFunc.func.async ||
|
|
body.loweredFunc.func.generator
|
|
) {
|
|
// Can't inline functions with params, or async/generator functions
|
|
continue;
|
|
}
|
|
|
|
// We know this function is used for an IIFE and can prune it later
|
|
inlinedFunctions.add(instr.value.callee.identifier.id);
|
|
|
|
// Create a new block which will contain code following the IIFE call
|
|
const continuationBlockId = fn.env.nextBlockId;
|
|
const continuationBlock: BasicBlock = {
|
|
id: continuationBlockId,
|
|
instructions: block.instructions.slice(ii + 1),
|
|
kind: block.kind,
|
|
phis: new Set(),
|
|
preds: new Set(),
|
|
terminal: block.terminal,
|
|
};
|
|
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
|
|
|
/*
|
|
* Trim the original block to contain instructions up to (but not including)
|
|
* the IIFE
|
|
*/
|
|
block.instructions.length = ii;
|
|
|
|
/*
|
|
* To account for complex control flow within the lambda, we treat the lambda
|
|
* as if it were a single labeled statement, and replace all returns with gotos
|
|
* to the label fallthrough.
|
|
*/
|
|
const newTerminal: LabelTerminal = {
|
|
block: body.loweredFunc.func.body.entry,
|
|
id: makeInstructionId(0),
|
|
kind: "label",
|
|
fallthrough: continuationBlockId,
|
|
loc: block.terminal.loc,
|
|
};
|
|
block.terminal = newTerminal;
|
|
|
|
// We store the result in the IIFE temporary
|
|
const result = instr.lvalue;
|
|
|
|
// Declare the IIFE temporary
|
|
declareTemporary(fn.env, block, result);
|
|
|
|
// Promote the temporary with a name as we require this to persist
|
|
promoteTemporary(result.identifier);
|
|
|
|
/*
|
|
* Rewrite blocks from the lambda to replace any `return` with a
|
|
* store to the result and `goto` the continuation block
|
|
*/
|
|
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
|
block.preds.clear();
|
|
rewriteBlock(fn.env, block, continuationBlockId, result);
|
|
fn.body.blocks.set(id, block);
|
|
}
|
|
|
|
/*
|
|
* Ensure we visit the continuation block, since there may have been
|
|
* sequential IIFEs that need to be visited.
|
|
*/
|
|
queue.push(continuationBlock);
|
|
continue queue;
|
|
}
|
|
default: {
|
|
for (const place of eachInstructionValueOperand(instr.value)) {
|
|
// Any other use of a function expression means it isn't an IIFE
|
|
functions.delete(place.identifier.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (inlinedFunctions.size !== 0) {
|
|
// Remove instructions that define lambdas which we inlined
|
|
for (const [, block] of fn.body.blocks) {
|
|
retainWhere(
|
|
block.instructions,
|
|
(instr) => !inlinedFunctions.has(instr.lvalue.identifier.id)
|
|
);
|
|
}
|
|
|
|
/*
|
|
* If terminals have changed then blocks may have become newly unreachable.
|
|
* Re-run minification of the graph (incl reordering instruction ids)
|
|
*/
|
|
reversePostorderBlocks(fn.body);
|
|
markInstructionIds(fn.body);
|
|
markPredecessors(fn.body);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Rewrites the block so that all `return` terminals are replaced:
|
|
* * Add a StoreLocal <returnValue> = <terminal.value>
|
|
* * Replace the terminal with a Goto to <returnTarget>
|
|
*/
|
|
function rewriteBlock(
|
|
env: Environment,
|
|
block: BasicBlock,
|
|
returnTarget: BlockId,
|
|
returnValue: Place
|
|
): void {
|
|
const { terminal } = block;
|
|
if (terminal.kind !== "return") {
|
|
return;
|
|
}
|
|
block.instructions.push({
|
|
id: makeInstructionId(0),
|
|
loc: terminal.loc,
|
|
lvalue: {
|
|
effect: Effect.Unknown,
|
|
identifier: {
|
|
id: env.nextIdentifierId,
|
|
mutableRange: {
|
|
start: makeInstructionId(0),
|
|
end: makeInstructionId(0),
|
|
},
|
|
name: null,
|
|
scope: null,
|
|
type: makeType(),
|
|
loc: terminal.loc,
|
|
},
|
|
kind: "Identifier",
|
|
reactive: false,
|
|
loc: terminal.loc,
|
|
},
|
|
value: {
|
|
kind: "StoreLocal",
|
|
lvalue: { kind: InstructionKind.Reassign, place: { ...returnValue } },
|
|
value: terminal.value,
|
|
type: null,
|
|
loc: terminal.loc,
|
|
},
|
|
});
|
|
block.terminal = {
|
|
kind: "goto",
|
|
block: returnTarget,
|
|
id: makeInstructionId(0),
|
|
variant: GotoVariant.Break,
|
|
loc: block.terminal.loc,
|
|
};
|
|
}
|
|
|
|
function declareTemporary(
|
|
env: Environment,
|
|
block: BasicBlock,
|
|
result: Place
|
|
): void {
|
|
block.instructions.push({
|
|
id: makeInstructionId(0),
|
|
loc: GeneratedSource,
|
|
lvalue: {
|
|
effect: Effect.Unknown,
|
|
identifier: {
|
|
id: env.nextIdentifierId,
|
|
mutableRange: {
|
|
start: makeInstructionId(0),
|
|
end: makeInstructionId(0),
|
|
},
|
|
name: null,
|
|
scope: null,
|
|
type: makeType(),
|
|
loc: result.loc,
|
|
},
|
|
kind: "Identifier",
|
|
reactive: false,
|
|
loc: GeneratedSource,
|
|
},
|
|
value: {
|
|
kind: "DeclareLocal",
|
|
lvalue: {
|
|
place: result,
|
|
kind: InstructionKind.Let,
|
|
},
|
|
type: null,
|
|
loc: result.loc,
|
|
},
|
|
});
|
|
}
|