5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import { CompilerError } from "../CompilerError";
8
import {
9
BasicBlock,
10
BlockId,
19
InstructionKind,
20
LabelTerminal,
21
Place,
23
- getHookKind,
22
makeInstructionId,
23
makeType,
24
reversePostorderBlocks,
25
} from "../HIR";
26
import { markInstructionIds, markPredecessors } from "../HIR/HIRBuilder";
27
+import { eachInstructionValueOperand } from "../HIR/visitors";
28
import { retainWhere } from "../Utils/utils";
29
30
/**
32
- * Rewrites `useMemo()` calls, rewriting so that the lambda body becomes part of the
33
- * outer block's instructions.
31
+ * Inlines immediately invoked function expressions (IIFEs) to allow more fine-grained memoization
32
+ * of the values they produce.
33
*
34
* Example:
35
*
37
- * ```javascript
38
- * // Before
39
- * const x = useMemo(() => foo(y, z), [y, z])
40
- *
41
- * // After
42
- * const x = foo(y, z);
36
* ```
37
+ * const x = (() => {
38
+ * const x = [];
39
+ * x.push(foo());
40
+ * return x;
41
+ * })();
42
+ *
43
+ * =>
44
*
45
- * The main challenge is dealing with the possibility of complex control flow within
46
- * the lambda body. The approach is roughly:
47
- * - split the block with the useMemo call in two:
48
- * - the first block is everything up to the memo call plus the lambda body
49
- * - the second block is everything after the memo call
50
- * - use the temporary from the useMemo call result value as the place to store
51
- * the useMemo result
52
- * - for every return terminal in the lambda body:
53
- * - add a StoreLocal to the temporary, assigning the return value
54
- * - replace the terminal w a goto to the second block
45
+ * bb0:
46
+ * // placeholder for the result, all return statements will assign here
47
+ * let t0;
48
+ * // Label allows using a goto (break) to exit out of the body
49
+ * Label block=bb1 fallthrough=bb2
50
+ * bb1:
51
+ * // code within the function expression
52
+ * const x0 = [];
53
+ * x0.push(foo());
54
+ * // return is replaced by assignment to the result variable...
55
+ * t0 = x0;
56
+ * // ...and a goto to the code after the function expression invocation
57
+ * Goto bb2
58
+ * bb2:
59
+ * // code after the IIFE call
60
+ * const x = t0;
61
+ * ```
62
*
56
- * NOTE: *this pass must be run prior to EnterSSA*. Prior to entering SSA form identifiers
57
- * in the top-level function and any function expressions will have consistent
58
- * correlation between `Identifier` instances and IdentifierIds. After entering SSA
59
- * form we drop this correspondence. It's much easier to write this inlining pass
60
- * without having to worry about SSA form.
63
+ * The implementation relies on HIR's ability to support labeled blocks:
64
+ * - We terminate the basic block just prior to the CallExpression of the IIFE
65
+ * with a LabelTerminal whose fallback is the code following the CallExpression.
66
+ * Just prior to the terminal we also create a named temporary variable which
67
+ * will hold the result.
68
+ * - We then inline the contents of the function "in between" (conceptually) those
69
+ * two blocks.
70
+ * - All return statements in the original function expression are replaced with a
71
+ * StoreLocal to the temporary we allocated before plus a Goto to the fallthrough
72
+ * block (code following the CallExpression).
73
*/
62
-export function inlineUseMemo(fn: HIRFunction): void {
63
- // Track all function expressions in case they appear as the argument to a useMemo
74
+export function inlineImmediatelyInvokedFunctionExpressions(
75
+ fn: HIRFunction
76
+): void {
77
+ // Track all function expressions that are assigned to a temporary
78
const functions = new Map<IdentifierId, FunctionExpression>();
65
- // Identifiers (lvalues) for known useMemo functions, so that we can prune them
66
- // at the end of the pass
67
- const useMemoFunctions = new Set<IdentifierId>();
79
+ // Functions that are inlined
80
+ const inlinedFunctions = new Set<IdentifierId>();
81
69
- // Iterate the *existing* blocks from the outer component to find useMemo calls
82
+ // Iterate the *existing* blocks from the outer component to find IIFEs
83
// and inline them. During iteration we will modify `fn` (by inlining the CFG
71
- // of useMemo callbacks) so we explicitly copy references to just the original
72
- // function's blocks first. As blocks are split to make room for useMemo calls,
84
+ // of IIFEs) so we explicitly copy references to just the original
85
+ // function's blocks first. As blocks are split to make room for IIFE calls,
86
// the split portions of the blocks will be added to this queue.
87
const queue = Array.from(fn.body.blocks.values());
88
queue: for (const block of queue) {
90
const instr = block.instructions[ii]!;
91
switch (instr.value.kind) {
92
case "FunctionExpression": {
80
- functions.set(instr.lvalue.identifier.id, instr.value);
93
+ if (instr.lvalue.identifier.name === null) {
94
+ functions.set(instr.lvalue.identifier.id, instr.value);
95
+ }
96
break;
97
}
83
- case "MethodCall":
98
case "CallExpression": {
85
- const hookKind =
86
- instr.value.kind === "CallExpression"
87
- ? getHookKind(fn.env, instr.value.callee.identifier)
88
- : getHookKind(fn.env, instr.value.property.identifier);
89
- if (hookKind !== "useMemo") {
90
- continue;
91
- }
92
- const [lambda] = instr.value.args;
93
- if (lambda.kind === "Spread") {
99
+ if (instr.value.args.length !== 0) {
100
+ // We don't support inlining when there are arguments
101
continue;
102
}
96
- const body = functions.get(lambda.identifier.id);
103
+ const body = functions.get(instr.value.callee.identifier.id);
104
if (body === undefined) {
98
- // Allow passing a named function to useMemo, eg `useMemo(someImportedFunction, [])`
105
+ // Not invoking a local function expression, can't inline
106
continue;
107
}
108
102
- if (body.loweredFunc.func.params.length > 0) {
103
- CompilerError.invalidReact({
104
- reason: "useMemo callbacks may not accept any arguments",
105
- description: null,
106
- loc: body.loc,
107
- suggestions: null,
108
- });
109
- }
110
-
111
- if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
112
- CompilerError.invalidReact({
113
- reason:
114
- "useMemo callbacks may not be async or generator functions",
115
- description: null,
116
- loc: body.loc,
117
- suggestions: null,
118
- });
109
+ if (
110
+ body.loweredFunc.func.params.length > 0 ||
111
+ body.loweredFunc.func.async ||
112
+ body.loweredFunc.func.generator
113
+ ) {
114
+ // Can't inline functions with params, or async/generator functions
115
+ continue;
116
}
117
121
- // We know this function is used for useMemo and can prune it later
122
- useMemoFunctions.add(lambda.identifier.id);
118
+ // We know this function is used for an IIFE and can prune it later
119
+ inlinedFunctions.add(instr.value.callee.identifier.id);
120
124
- // Create a new block which will contain code following the useMemo call
121
+ // Create a new block which will contain code following the IIFE call
122
const continuationBlockId = fn.env.nextBlockId;
123
const continuationBlock: BasicBlock = {
124
id: continuationBlockId,
131
fn.body.blocks.set(continuationBlockId, continuationBlock);
132
133
// Trim the original block to contain instructions up to (but not including)
137
- // the useMemo
134
+ // the IIFE
135
block.instructions.length = ii;
136
137
// To account for complex control flow within the lambda, we treat the lambda
146
};
147
block.terminal = newTerminal;
148
152
- // We store the result in the useMemo temporary
149
+ // We store the result in the IIFE temporary
150
const result = instr.lvalue;
151
155
- // Declare the useMemo temporary
152
+ // Declare the IIFE temporary
153
declareTemporary(fn.env, block, result);
154
155
// Promote the temporary with a name as we require this to persist
164
}
165
166
// Ensure we visit the continuation block, since there may have been
170
- // sequential useMemos that need to be visited.
167
+ // sequential IIFEs that need to be visited.
168
queue.push(continuationBlock);
169
continue queue;
170
}
171
+ default: {
172
+ for (const place of eachInstructionValueOperand(instr.value)) {
173
+ // Any other use of a function expression means it isn't an IIFE
174
+ functions.delete(place.identifier.id);
175
+ }
176
+ }
177
}
178
}
179
}
180
178
- if (useMemoFunctions.size !== 0) {
181
+ if (inlinedFunctions.size !== 0) {
182
// Remove instructions that define lambdas which we inlined
183
for (const [, block] of fn.body.blocks) {
184
retainWhere(
185
block.instructions,
183
- (instr) => !useMemoFunctions.has(instr.lvalue.identifier.id)
186
+ (instr) => !inlinedFunctions.has(instr.lvalue.identifier.id)
187
);
188
}
189