5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import { CompilerError } from "..";
8
+import { CompilerError, SourceLocation } from "..";
9
import {
10
+ CallExpression,
11
Effect,
12
+ Environment,
13
+ FinishMemoize,
14
FunctionExpression,
15
HIRFunction,
16
IdentifierId,
17
Instruction,
18
+ InstructionId,
19
+ LoadGlobal,
20
+ LoadLocal,
21
+ MethodCall,
22
Place,
23
+ PropertyLoad,
24
SpreadPattern,
25
+ StartMemoize,
26
+ TInstruction,
27
getHookKindForType,
28
makeInstructionId,
29
} from "../HIR";
30
import { createTemporaryPlace, markInstructionIds } from "../HIR/HIRBuilder";
21
-import { HookKind } from "../HIR/ObjectShape";
31
import { eachInstructionValueOperand } from "../HIR/visitors";
32
33
+type ManualMemoCallee = {
34
+ kind: "useMemo" | "useCallback";
35
+ loadInstr: TInstruction<LoadGlobal> | TInstruction<PropertyLoad>;
36
+};
37
+
38
+type IdentifierSidemap = {
39
+ functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
40
+ manualMemos: Map<IdentifierId, ManualMemoCallee>;
41
+ react: Set<IdentifierId>;
42
+};
43
+
44
+function collectTemporaries(
45
+ instr: Instruction,
46
+ env: Environment,
47
+ sidemap: IdentifierSidemap
48
+): void {
49
+ const { value } = instr;
50
+ switch (value.kind) {
51
+ case "FunctionExpression": {
52
+ sidemap.functions.set(
53
+ instr.lvalue.identifier.id,
54
+ instr as TInstruction<FunctionExpression>
55
+ );
56
+ break;
57
+ }
58
+ case "LoadGlobal": {
59
+ const global = env.getGlobalDeclaration(value.name);
60
+ const hookKind = global !== null ? getHookKindForType(env, global) : null;
61
+ const lvalId = instr.lvalue.identifier.id;
62
+ if (hookKind === "useMemo" || hookKind === "useCallback") {
63
+ sidemap.manualMemos.set(lvalId, {
64
+ kind: hookKind,
65
+ loadInstr: instr as TInstruction<LoadGlobal>,
66
+ });
67
+ } else if (value.name === "React") {
68
+ sidemap.react.add(lvalId);
69
+ }
70
+ break;
71
+ }
72
+ case "PropertyLoad": {
73
+ if (sidemap.react.has(value.object.identifier.id)) {
74
+ if (value.property === "useMemo" || value.property === "useCallback") {
75
+ sidemap.manualMemos.set(instr.lvalue.identifier.id, {
76
+ kind: value.property,
77
+ loadInstr: instr as TInstruction<PropertyLoad>,
78
+ });
79
+ }
80
+ }
81
+ break;
82
+ }
83
+ }
84
+}
85
+
86
+function makeManualMemoizationMarkers(
87
+ fnExpr: Place,
88
+ env: Environment,
89
+ depsList: Array<Place>,
90
+ memoDecl: Place
91
+): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
92
+ return [
93
+ {
94
+ id: makeInstructionId(0),
95
+ lvalue: createTemporaryPlace(env),
96
+ value: {
97
+ kind: "StartMemoize",
98
+ /*
99
+ * Use deps list from source instead of inferred deps
100
+ * as dependencies
101
+ */
102
+ deps: depsList,
103
+ loc: fnExpr.loc,
104
+ },
105
+ loc: fnExpr.loc,
106
+ },
107
+ {
108
+ id: makeInstructionId(0),
109
+ lvalue: createTemporaryPlace(env),
110
+ value: {
111
+ kind: "FinishMemoize",
112
+ decl: { ...memoDecl },
113
+ loc: fnExpr.loc,
114
+ },
115
+ loc: fnExpr.loc,
116
+ },
117
+ ];
118
+}
119
+
120
+function getManualMemoizationReplacement(
121
+ fn: Place,
122
+ loc: SourceLocation,
123
+ kind: "useMemo" | "useCallback"
124
+): LoadLocal | CallExpression {
125
+ if (kind === "useMemo") {
126
+ /*
127
+ * Replace the hook callee with the fn arg.
128
+ *
129
+ * before:
130
+ * $1 = LoadGlobal useMemo // load the useMemo global
131
+ * $2 = FunctionExpression ... // memo function
132
+ * $3 = ArrayExpression [ ... ] // deps array
133
+ * $4 = Call $1 ($2, $3 ) // invoke useMemo w fn and deps
134
+ *
135
+ * after:
136
+ * $1 = LoadGlobal useMemo // load the useMemo global (dead code)
137
+ * $2 = FunctionExpression ... // memo function
138
+ * $3 = ArrayExpression [ ... ] // deps array (dead code)
139
+ * $4 = Call $2 () // invoke the memo function itself
140
+ *
141
+ * Note that a later pass (InlineImmediatelyInvokedFunctionExpressions) will
142
+ * inline the useMemo callback along with any other immediately invoked IIFEs.
143
+ */
144
+ return {
145
+ kind: "CallExpression",
146
+ callee: fn,
147
+ /*
148
+ * Drop the args, including the deps array which DCE will remove
149
+ * later.
150
+ */
151
+ args: [],
152
+ loc,
153
+ };
154
+ } else {
155
+ /*
156
+ * Instead of a Call, just alias the callback directly.
157
+ *
158
+ * before:
159
+ * $1 = LoadGlobal useCallback
160
+ * $2 = FunctionExpression ... // the callback being memoized
161
+ * $3 = ArrayExpression ... // deps array
162
+ * $4 = Call $1 ( $2, $3 ) // invoke useCallback
163
+ *
164
+ * after:
165
+ * $1 = LoadGlobal useCallback // dead code
166
+ * $2 = FunctionExpression ... // the callback being memoized
167
+ * $3 = ArrayExpression ... // deps array (dead code)
168
+ * $4 = LoadLocal $2 // reference the function
169
+ */
170
+ return {
171
+ kind: "LoadLocal",
172
+ place: {
173
+ kind: "Identifier",
174
+ identifier: fn.identifier,
175
+ effect: Effect.Unknown,
176
+ reactive: false,
177
+ loc,
178
+ },
179
+ loc,
180
+ };
181
+ }
182
+}
183
+
184
+function extractManualMemoizationArgs(
185
+ instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
186
+ kind: "useCallback" | "useMemo"
187
+): {
188
+ fnPlace: Place;
189
+} {
190
+ const [fnPlace] = instr.value.args as Array<
191
+ Place | SpreadPattern | undefined
192
+ >;
193
+ if (fnPlace == null) {
194
+ CompilerError.throwInvalidReact({
195
+ reason: `Expected ${kind} call to pass a callback function`,
196
+ loc: instr.value.loc,
197
+ suggestions: null,
198
+ });
199
+ }
200
+ if (fnPlace?.kind !== "Identifier") {
201
+ CompilerError.throwInvalidReact({
202
+ reason: `Unexpected arguments to ${kind} call`,
203
+ loc: instr.value.loc,
204
+ suggestions: null,
205
+ });
206
+ }
207
+ return {
208
+ fnPlace,
209
+ };
210
+}
211
+
212
/*
213
* Removes manual memoization using the `useMemo` and `useCallback` APIs. This pass is designed
214
* to compose with InlineImmediatelyInvokedFunctionExpressions, and needs to run prior to entering
218
* eg `React.useMemo()`.
219
*/
220
export function dropManualMemoization(func: HIRFunction): void {
33
- const functions = new Map<IdentifierId, FunctionExpression>();
34
- const hooks = new Map<IdentifierId, HookKind>();
35
- const react = new Set<IdentifierId>();
36
- let hasChanges = false;
221
+ const isValidationEnabled =
222
+ func.env.config.validatePreserveExistingMemoizationGuarantees ||
223
+ func.env.config.enablePreserveExistingMemoizationGuarantees;
224
+ const sidemap: IdentifierSidemap = {
225
+ functions: new Map(),
226
+ manualMemos: new Map(),
227
+ react: new Set(),
228
+ };
229
+
230
+ /**
231
+ * Phase 1:
232
+ * - Overwrite manual memoization from
233
+ * CallExpression callee="useMemo/Callback", args=[fnArg, depslist])
234
+ * to either
235
+ * CallExpression callee=fnArg
236
+ * LoadLocal fnArg
237
+ * - (if validation is enabled) collect manual memoization markers
238
+ */
239
+ const queuedInserts: Map<
240
+ InstructionId,
241
+ {
242
+ kind: "before" | "after";
243
+ value: TInstruction<StartMemoize> | TInstruction<FinishMemoize>;
244
+ }
245
+ > = new Map();
246
for (const [_, block] of func.body.blocks) {
38
- let nextInstructions: Array<Instruction> | null = null;
247
for (let i = 0; i < block.instructions.length; i++) {
248
const instr = block.instructions[i]!;
41
- switch (instr.value.kind) {
42
- case "FunctionExpression": {
43
- functions.set(instr.lvalue.identifier.id, instr.value);
44
- break;
45
- }
46
- case "LoadGlobal": {
47
- const global = func.env.getGlobalDeclaration(instr.value.name);
48
- const hookKind =
49
- global !== null ? getHookKindForType(func.env, global) : null;
50
- if (hookKind === "useMemo" || hookKind === "useCallback") {
51
- hooks.set(instr.lvalue.identifier.id, hookKind);
52
- } else if (instr.value.name === "React") {
53
- react.add(instr.lvalue.identifier.id);
54
- }
55
- break;
56
- }
57
- case "PropertyLoad": {
58
- if (react.has(instr.value.object.identifier.id)) {
59
- if (
60
- instr.value.property === "useMemo" ||
61
- instr.value.property === "useCallback"
62
- ) {
63
- hooks.set(instr.lvalue.identifier.id, instr.value.property);
64
- }
65
- }
66
- break;
67
- }
68
- case "MethodCall":
69
- case "CallExpression": {
70
- const id =
71
- instr.value.kind === "CallExpression"
72
- ? instr.value.callee.identifier.id
73
- : instr.value.property.identifier.id;
74
- const hookKind = hooks.get(id);
75
- if (hookKind != null) {
76
- if (hookKind === "useMemo") {
77
- const [fn] = instr.value.args as Array<
78
- Place | SpreadPattern | undefined
79
- >;
80
- if (fn == null) {
81
- CompilerError.throwInvalidReact({
82
- reason: "Expected useMemo call to pass a callback function",
83
- loc: instr.loc,
84
- suggestions: null,
85
- });
86
- }
87
- /*
88
- * Replace the hook callee with the fn arg.
89
- *
90
- * before:
91
- * $1 = LoadGlobal useMemo // load the useMemo global
92
- * $2 = FunctionExpression ... // memo function
93
- * $3 = ArrayExpression [ ... ] // deps array
94
- * $4 = Call $1 ($2, $3 ) // invoke useMemo w fn and deps
95
- *
96
- * after:
97
- * $1 = LoadGlobal useMemo // load the useMemo global (dead code)
98
- * $2 = FunctionExpression ... // memo function
99
- * $3 = ArrayExpression [ ... ] // deps array (dead code)
100
- * $4 = Call $2 () // invoke the memo function itself
101
- *
102
- * Note that a later pass (InlineImmediatelyInvokedFunctionExpressions) will
103
- * inline the useMemo callback along with any other immediately invoked IIFEs.
104
- */
105
- if (fn.kind === "Identifier") {
106
- instr.value = {
107
- kind: "CallExpression",
108
- callee: fn,
109
- /*
110
- * Drop the args, including the deps array which DCE will remove
111
- * later.
112
- */
113
- args: [],
114
- loc: instr.value.loc,
115
- };
249
+ if (
250
+ instr.value.kind === "CallExpression" ||
251
+ instr.value.kind === "MethodCall"
252
+ ) {
253
+ const id =
254
+ instr.value.kind === "CallExpression"
255
+ ? instr.value.callee.identifier.id
256
+ : instr.value.property.identifier.id;
257
117
- if (
118
- func.env.config.enablePreserveExistingMemoizationGuarantees ||
119
- func.env.config.validatePreserveExistingMemoizationGuarantees
120
- ) {
121
- /**
122
- * When this flag is enabled we also compile in a 'Memoize' instruction
123
- * to preserve the intended memoization boundary:
124
- *
125
- * Normal output:
126
- * $1 = LoadGlobal useMemo // load the useMemo global (dead code)
127
- * $2 = FunctionExpression ... // memo function
128
- * $3 = ArrayExpression [ ... ] // deps array (dead code)
129
- * $4 = Call $2 () // invoke the memo function itself
130
- *
131
- * Output w flag enabled:
132
- * $1 = LoadGlobal useMemo // load the useMemo global (dead code)
133
- * $2 = FunctionExpression ... // memo function
134
- * $3 = ArrayExpression [ ... ] // deps array (dead code)
135
- * .. = Memoize ... // memoize dependencies
136
- * $4 = Call $2 () // invoke the memo function itself
137
- * .. = Memoize $4 // preserve memo information
138
- *
139
- * Note that Memoize does not produce a result and is called for its side
140
- * effects only.
141
- */
142
- nextInstructions =
143
- nextInstructions ?? block.instructions.slice(0, i);
144
-
145
- const functionExpression = functions.get(fn.identifier.id);
146
- if (functionExpression !== undefined) {
147
- for (const operand of eachInstructionValueOperand(
148
- functionExpression
149
- )) {
150
- const temp = createTemporaryPlace(func.env);
151
- nextInstructions.push({
152
- id: makeInstructionId(0),
153
- lvalue: temp,
154
- value: {
155
- kind: "Memoize",
156
- value: { ...operand },
157
- loc: instr.loc,
158
- },
159
- loc: instr.loc,
160
- });
161
- }
162
- }
163
-
164
- nextInstructions.push(instr);
165
-
166
- const temp = createTemporaryPlace(func.env);
167
- nextInstructions.push({
168
- id: makeInstructionId(0),
169
- lvalue: temp,
170
- value: {
171
- kind: "Memoize",
172
- value: { ...instr.lvalue },
173
- loc: instr.loc,
174
- },
175
- loc: instr.loc,
176
- });
177
- continue;
178
- }
179
- }
180
- } else if (hookKind === "useCallback") {
181
- const [fn] = instr.value.args as Array<
182
- Place | SpreadPattern | undefined
183
- >;
184
- if (fn == null) {
185
- CompilerError.throwInvalidReact({
186
- reason: "Expected useMemo call to pass a callback function",
187
- loc: instr.loc,
188
- suggestions: null,
189
- });
190
- }
191
-
192
- /*
193
- * Instead of a Call, just alias the callback directly.
194
- *
195
- * before:
196
- * $1 = LoadGlobal useCallback
197
- * $2 = FunctionExpression ... // the callback being memoized
198
- * $3 = ArrayExpression ... // deps array
199
- * $4 = Call $1 ( $2, $3 ) // invoke useCallback
200
- *
201
- * after:
202
- * $1 = LoadGlobal useCallback // dead code
203
- * $2 = FunctionExpression ... // the callback being memoized
204
- * $3 = ArrayExpression ... // deps array (dead code)
205
- * $4 = LoadLocal $2 // reference the function
206
- */
207
- if (fn.kind === "Identifier") {
208
- instr.value = {
209
- kind: "LoadLocal",
210
- place: {
258
+ const manualMemo = sidemap.manualMemos.get(id);
259
+ if (manualMemo != null) {
260
+ const { fnPlace } = extractManualMemoizationArgs(
261
+ instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
262
+ manualMemo.kind
263
+ );
264
+ instr.value = getManualMemoizationReplacement(
265
+ fnPlace,
266
+ instr.value.loc,
267
+ manualMemo.kind
268
+ );
269
+ if (isValidationEnabled) {
270
+ const inlineMemoFn = sidemap.functions.get(fnPlace.identifier.id);
271
+ if (inlineMemoFn == null) {
272
+ CompilerError.throwInvalidReact({
273
+ reason:
274
+ "DepsValidation: Expected function literal as manual memoization callback",
275
+ suggestions: [],
276
+ loc: fnPlace.loc,
277
+ });
278
+ }
279
+ const memoDecl: Place =
280
+ manualMemo.kind === "useMemo"
281
+ ? instr.lvalue
282
+ : {
283
kind: "Identifier",
212
- identifier: fn.identifier,
284
+ identifier: fnPlace.identifier,
285
effect: Effect.Unknown,
286
reactive: false,
215
- loc: instr.value.loc,
216
- },
217
- loc: instr.value.loc,
218
- };
219
- if (
220
- func.env.config.enablePreserveExistingMemoizationGuarantees ||
221
- func.env.config.validatePreserveExistingMemoizationGuarantees
222
- ) {
223
- nextInstructions =
224
- nextInstructions ?? block.instructions.slice(0, i);
225
- /**
226
- * With the flag enabled the output changes to use a Memoize instruction instead
227
- * a loadlocal to load the function expression into the original temporary:
228
- *
229
- * Normal output:
230
- * $1 = LoadGlobal useCallback // dead code
231
- * $2 = FunctionExpression ... // the callback being memoized
232
- * $3 = ArrayExpression ... // deps array (dead code)
233
- * $4 = LoadLocal $2 // reference the function
234
- *
235
- * With flag enabled:
236
- * $1 = LoadGlobal useCallback // dead code
237
- * $2 = FunctionExpression ... // the callback being memoized
238
- * $3 = ArrayExpression ... // deps array (dead code)
239
- * .. = Memoize ... // memoize dependencies
240
- * $n = Memoize $2 // reference the function
241
- * $4 = LoadLocal $2 // reference the function
242
- *
243
- * Note that Memoize does not produce a result and is called for its side effects
244
- * only.
245
- */
246
- const functionExpression = functions.get(fn.identifier.id);
247
- if (functionExpression !== undefined) {
248
- for (const operand of eachInstructionValueOperand(
249
- functionExpression
250
- )) {
251
- const temp = createTemporaryPlace(func.env);
252
- nextInstructions.push({
253
- id: makeInstructionId(0),
254
- lvalue: temp,
255
- value: {
256
- kind: "Memoize",
257
- value: { ...operand },
258
- loc: instr.loc,
259
- },
260
- loc: instr.loc,
261
- });
262
- }
263
- }
264
- nextInstructions.push(instr);
287
+ loc: fnPlace.loc,
288
+ };
289
266
- const temp = createTemporaryPlace(func.env);
267
- nextInstructions.push({
268
- id: makeInstructionId(0),
269
- lvalue: { ...temp },
270
- value: {
271
- kind: "Memoize",
272
- value: {
273
- kind: "Identifier",
274
- identifier: fn.identifier,
275
- effect: Effect.Unknown,
276
- reactive: false,
277
- loc: instr.value.loc,
278
- },
279
- loc: instr.value.loc,
280
- },
281
- loc: instr.loc,
282
- });
283
- continue;
284
- }
285
- }
286
- }
290
+ const [startMarker, finishMarker] = makeManualMemoizationMarkers(
291
+ fnPlace,
292
+ func.env,
293
+ // Next PR will replace this with depslist from source
294
+ [...eachInstructionValueOperand(inlineMemoFn.value)],
295
+ memoDecl
296
+ );
297
+
298
+ /*
299
+ * This PR reorders startMarker to right before the inlineMemoFn
300
+ * since startMarker references inlineMemoFn.deps.
301
+ * Next PR will move startMarker earlier, to after the `useMemo`/
302
+ * `useCallback` load itself (as it also changes startMarker to
303
+ * not reference lowered deps anymore).
304
+ */
305
+ queuedInserts.set(inlineMemoFn.id, {
306
+ kind: "before",
307
+ value: startMarker,
308
+ });
309
+ queuedInserts.set(instr.id, { kind: "after", value: finishMarker });
310
+ continue;
311
+ }
312
+ }
313
+ } else {
314
+ collectTemporaries(instr, func.env, sidemap);
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Phase 2: Insert manual memoization markers as needed
321
+ */
322
+ if (queuedInserts.size > 0) {
323
+ let hasChanges = false;
324
+ for (const [_, block] of func.body.blocks) {
325
+ let nextInstructions: Array<Instruction> | null = null;
326
+ for (let i = 0; i < block.instructions.length; i++) {
327
+ const instr = block.instructions[i];
328
+ const insertInstr = queuedInserts.get(instr.id);
329
+ if (insertInstr != null) {
330
+ nextInstructions = nextInstructions ?? block.instructions.slice(0, i);
331
+ if (insertInstr.kind === "before") {
332
+ nextInstructions.push(insertInstr.value);
333
+ nextInstructions.push(instr);
334
+ } else {
335
+ nextInstructions.push(instr);
336
+ nextInstructions.push(insertInstr.value);
337
}
288
- break;
338
+ } else if (nextInstructions != null) {
339
+ nextInstructions.push(instr);
340
}
341
}
342
if (nextInstructions !== null) {
292
- nextInstructions.push(instr);
343
+ block.instructions = nextInstructions;
344
+ hasChanges = true;
345
}
346
}
295
- if (nextInstructions !== null) {
296
- block.instructions = nextInstructions;
297
- hasChanges = true;
347
+
348
+ if (hasChanges) {
349
+ markInstructionIds(func.body);
350
}
351
}
300
- if (hasChanges) {
301
- markInstructionIds(func.body);
302
- }
352
}