5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import { CompilerError, ErrorSeverity } from "..";
8
+import { CompilerError, Effect, ErrorSeverity } from "..";
9
import {
10
+ GeneratedSource,
11
Identifier,
12
+ IdentifierId,
13
Instruction,
14
+ InstructionValue,
15
+ ManualMemoDependency,
16
+ Place,
17
ReactiveFunction,
18
ReactiveInstruction,
19
ReactiveScopeBlock,
20
+ ReactiveScopeDependency,
21
+ ReactiveValue,
22
ScopeId,
23
} from "../HIR";
24
+import { printManualMemoDependency } from "../HIR/PrintHIR";
25
import { eachInstructionValueOperand } from "../HIR/visitors";
26
+import { collectMaybeMemoDependencies } from "../Inference/DropManualMemoization";
27
import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables";
28
import {
29
ReactiveFunctionVisitor,
38
* was pruned.
39
*/
40
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
32
- const errors = new CompilerError();
33
- visitReactiveFunction(fn, new Visitor(), errors);
34
- if (errors.hasErrors()) {
35
- throw errors;
41
+ const state = {
42
+ errors: new CompilerError(),
43
+ manualMemoState: null,
44
+ };
45
+ visitReactiveFunction(fn, new Visitor(), state);
46
+ if (state.errors.hasErrors()) {
47
+ throw state.errors;
48
}
49
}
50
39
-class Visitor extends ReactiveFunctionVisitor<CompilerError> {
51
+type ManualMemoBlockState = {
52
+ /**
53
+ * Values produced within manual memoization blocks.
54
+ * We track these to ensure our inferred dependencies are
55
+ * produced before the manual memo block starts
56
+ *
57
+ * As an example:
58
+ * ```js
59
+ * // source
60
+ * const result = useMemo(() => {
61
+ * return [makeObject(input1), input2],
62
+ * }, [input1, input2]);
63
+ * ```
64
+ * Here, we record inferred dependencies as [input1, input2]
65
+ * but not t0
66
+ * ```js
67
+ * // StartMemoize
68
+ * let t0;
69
+ * if ($[0] != input1) {
70
+ * t0 = makeObject(input1);
71
+ * // ...
72
+ * } else { ... }
73
+ *
74
+ * let result;
75
+ * if ($[1] != t0 || $[2] != input2) {
76
+ * result = [t0, input2];
77
+ * } else { ... }
78
+ * ```
79
+ */
80
+ decls: Set<IdentifierId>;
81
+
82
+ /*
83
+ * normalized depslist from useMemo/useCallback
84
+ * callsite in source
85
+ */
86
+ depsFromSource: Array<ManualMemoDependency> | null;
87
+ manualMemoId: number;
88
+};
89
+
90
+type VisitorState = {
91
+ errors: CompilerError;
92
+ manualMemoState: ManualMemoBlockState | null;
93
+};
94
+
95
+function prettyPrintScopeDependency(val: ReactiveScopeDependency): string {
96
+ let rootStr;
97
+ if (val.identifier.name?.kind === "named") {
98
+ rootStr = val.identifier.name.value;
99
+ } else {
100
+ rootStr = "[unnamed]";
101
+ }
102
+ return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`;
103
+}
104
+function depsEqual(
105
+ dep1: ManualMemoDependency,
106
+ dep2: ManualMemoDependency
107
+): boolean {
108
+ const rootsEqual =
109
+ (dep1.root.kind === "Global" &&
110
+ dep2.root.kind === "Global" &&
111
+ dep1.root.identifierName === dep2.root.identifierName) ||
112
+ (dep1.root.kind === "NamedLocal" &&
113
+ dep2.root.kind === "NamedLocal" &&
114
+ dep1.root.value.identifier.id === dep2.root.value.identifier.id);
115
+ return (
116
+ rootsEqual &&
117
+ dep1.path.length === dep2.path.length &&
118
+ dep1.path.every((val, idx) => val === dep2.path[idx])
119
+ );
120
+}
121
+
122
+function validateInferredDep(
123
+ dep: ReactiveScopeDependency,
124
+ temporaries: Map<IdentifierId, ManualMemoDependency>,
125
+ declsWithinMemoBlock: Set<IdentifierId>,
126
+ validDepsInMemoBlock: Array<ManualMemoDependency>,
127
+ errorState: CompilerError
128
+): void {
129
+ let normalizedDep: ManualMemoDependency;
130
+ const maybeNormalizedRoot = temporaries.get(dep.identifier.id);
131
+ if (maybeNormalizedRoot != null) {
132
+ normalizedDep = {
133
+ root: maybeNormalizedRoot.root,
134
+ path: [...maybeNormalizedRoot.path, ...dep.path],
135
+ };
136
+ } else {
137
+ CompilerError.invariant(dep.identifier.name?.kind === "named", {
138
+ reason:
139
+ "ValidatePreservedManualMemoization: expected scope dependency to be named",
140
+ loc: GeneratedSource,
141
+ suggestions: null,
142
+ });
143
+ normalizedDep = {
144
+ root: {
145
+ kind: "NamedLocal",
146
+ value: {
147
+ kind: "Identifier",
148
+ identifier: dep.identifier,
149
+ loc: GeneratedSource,
150
+ effect: Effect.Read,
151
+ reactive: false,
152
+ },
153
+ },
154
+ path: [...dep.path],
155
+ };
156
+ }
157
+ for (const originalDep of validDepsInMemoBlock) {
158
+ if (depsEqual(originalDep, normalizedDep)) {
159
+ return;
160
+ }
161
+ }
162
+ for (const decl of declsWithinMemoBlock) {
163
+ const normalizedDecl = temporaries.get(decl);
164
+ if (normalizedDecl != null && depsEqual(normalizedDecl, normalizedDep)) {
165
+ return;
166
+ } else if (
167
+ normalizedDep.root.kind === "NamedLocal" &&
168
+ decl === normalizedDep.root.value.identifier.id
169
+ ) {
170
+ return;
171
+ }
172
+ }
173
+ errorState.push({
174
+ severity: ErrorSeverity.Todo,
175
+ reason:
176
+ "Could not preserve manual memoization because an inferred dependency does not match the dependency list in source",
177
+ description: `The inferred dependency was \`${prettyPrintScopeDependency(
178
+ dep
179
+ )}\`, but the source dependencies were [${validDepsInMemoBlock
180
+ .map((dep) => printManualMemoDependency(dep, true))
181
+ .join(", ")}]`,
182
+ loc: GeneratedSource,
183
+ suggestions: null,
184
+ });
185
+}
186
+
187
+class Visitor extends ReactiveFunctionVisitor<VisitorState> {
188
scopes: Set<ScopeId> = new Set();
189
+ scopeMapping = new Map();
190
+ temporaries: Map<IdentifierId, ManualMemoDependency> = new Map();
191
+
192
+ collectMaybeMemoDependencies(
193
+ value: ReactiveValue,
194
+ state: VisitorState
195
+ ): ManualMemoDependency | null {
196
+ switch (value.kind) {
197
+ case "SequenceExpression": {
198
+ for (const instr of value.instructions) {
199
+ this.visitInstruction(instr, state);
200
+ }
201
+ const result = this.collectMaybeMemoDependencies(value.value, state);
202
+
203
+ return result;
204
+ }
205
+ case "OptionalExpression": {
206
+ return this.collectMaybeMemoDependencies(value.value, state);
207
+ }
208
+ case "ReactiveFunctionValue":
209
+ case "ConditionalExpression":
210
+ case "LogicalExpression": {
211
+ return null;
212
+ }
213
+ default: {
214
+ const dep = collectMaybeMemoDependencies(value, this.temporaries);
215
+ if (value.kind === "StoreLocal" || value.kind === "StoreContext") {
216
+ const storeTarget = value.lvalue.place;
217
+ state.manualMemoState?.decls.add(storeTarget.identifier.id);
218
+ if (storeTarget.identifier.name?.kind === "named" && dep == null) {
219
+ const dep: ManualMemoDependency = {
220
+ root: {
221
+ kind: "NamedLocal",
222
+ value: storeTarget,
223
+ },
224
+ path: [],
225
+ };
226
+ this.temporaries.set(storeTarget.identifier.id, dep);
227
+ return dep;
228
+ }
229
+ }
230
+ return dep;
231
+ }
232
+ }
233
+ }
234
+
235
+ recordTemporaries(instr: ReactiveInstruction, state: VisitorState): void {
236
+ const temporaries = this.temporaries;
237
+ const { value } = instr;
238
+ const lvalId = instr.lvalue?.identifier.id;
239
+ if (lvalId != null && temporaries.has(lvalId)) {
240
+ return;
241
+ }
242
+ const isNamedLocal =
243
+ lvalId != null && instr.lvalue?.identifier.name?.kind === "named";
244
+ if (isNamedLocal && state.manualMemoState != null) {
245
+ state.manualMemoState.decls.add(lvalId);
246
+ }
247
+
248
+ const maybeDep = this.collectMaybeMemoDependencies(value, state);
249
+ if (lvalId != null) {
250
+ if (maybeDep != null) {
251
+ temporaries.set(lvalId, maybeDep);
252
+ } else if (isNamedLocal) {
253
+ temporaries.set(lvalId, {
254
+ root: {
255
+ kind: "NamedLocal",
256
+ value: { ...(instr.lvalue as Place) },
257
+ },
258
+ path: [],
259
+ });
260
+ }
261
+ }
262
+ }
263
264
override visitScope(
265
scopeBlock: ReactiveScopeBlock,
44
- state: CompilerError
266
+ state: VisitorState
267
): void {
268
this.traverseScope(scopeBlock, state);
269
270
+ if (
271
+ state.manualMemoState != null &&
272
+ state.manualMemoState.depsFromSource != null
273
+ ) {
274
+ for (const dep of scopeBlock.scope.dependencies) {
275
+ validateInferredDep(
276
+ dep,
277
+ this.temporaries,
278
+ state.manualMemoState.decls,
279
+ state.manualMemoState.depsFromSource,
280
+ state.errors
281
+ );
282
+ }
283
+ }
284
+
285
/*
286
* Record scopes that exist in the AST so we can later check to see if
287
* effect dependencies which should be memoized (have a scope assigned)
306
307
override visitInstruction(
308
instruction: ReactiveInstruction,
72
- state: CompilerError
309
+ state: VisitorState
310
): void {
311
this.traverseInstruction(instruction, state);
75
- if (
76
- instruction.value.kind === "StartMemoize" ||
77
- instruction.value.kind === "FinishMemoize"
78
- ) {
79
- for (const value of eachInstructionValueOperand(instruction.value)) {
312
+ this.recordTemporaries(instruction, state);
313
+ if (instruction.value.kind === "StartMemoize") {
314
+ let depsFromSource: Array<ManualMemoDependency> | null = null;
315
+ if (instruction.value.deps != null) {
316
+ depsFromSource = instruction.value.deps;
317
+ }
318
+ CompilerError.invariant(state.manualMemoState == null, {
319
+ reason: "Unexpected nested StartMemoize instructions",
320
+ description: `Bad manual memoization ids: ${state.manualMemoState?.manualMemoId}, ${instruction.value.manualMemoId}`,
321
+ loc: instruction.value.loc,
322
+ suggestions: null,
323
+ });
324
+
325
+ state.manualMemoState = {
326
+ decls: new Set(),
327
+ depsFromSource,
328
+ manualMemoId: instruction.value.manualMemoId,
329
+ };
330
+ }
331
+ if (instruction.value.kind === "FinishMemoize") {
332
+ CompilerError.invariant(
333
+ state.manualMemoState != null &&
334
+ state.manualMemoState.manualMemoId === instruction.value.manualMemoId,
335
+ {
336
+ reason: "Unexpected mismatch between StartMemoize and FinishMemoize",
337
+ description: `Encountered StartMemoize id=${state.manualMemoState?.manualMemoId} followed by FinishMemoize id=${instruction.value.manualMemoId}`,
338
+ loc: instruction.value.loc,
339
+ suggestions: null,
340
+ }
341
+ );
342
+ state.manualMemoState = null;
343
+ }
344
+
345
+ const isDep = instruction.value.kind === "StartMemoize";
346
+ const isDecl =
347
+ instruction.value.kind === "FinishMemoize" && !instruction.value.pruned;
348
+ if (isDep || isDecl) {
349
+ for (const value of eachInstructionValueOperand(
350
+ instruction.value as InstructionValue
351
+ )) {
352
if (
353
isMutable(instruction as Instruction, value) ||
82
- isUnmemoized(value.identifier, this.scopes)
354
+ (isDecl && isUnmemoized(value.identifier, this.scopes))
355
) {
84
- state.push({
356
+ state.errors.push({
357
reason:
358
"This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized",
359
description: null,