1
+import {CompilerError, SourceLocation} from '..';
2
+import {
3
+ ArrayExpression,
4
+ Effect,
5
+ Environment,
6
+ FunctionExpression,
7
+ GeneratedSource,
8
+ HIRFunction,
9
+ IdentifierId,
10
+ Instruction,
11
+ isUseEffectHookType,
12
+ makeInstructionId,
13
+ TInstruction,
14
+ InstructionId,
15
+ ScopeId,
16
+ ReactiveScopeDependency,
17
+ Place,
18
+ ReactiveScopeDependencies,
19
+} from '../HIR';
20
+import {
21
+ createTemporaryPlace,
22
+ fixScopeAndIdentifierRanges,
23
+ markInstructionIds,
24
+} from '../HIR/HIRBuilder';
25
+import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
26
+
27
+/**
28
+ * Infers reactive dependencies captured by useEffect lambdas and adds them as
29
+ * a second argument to the useEffect call if no dependency array is provided.
30
+ */
31
+export function inferEffectDependencies(
32
+ env: Environment,
33
+ fn: HIRFunction,
34
+): void {
35
+ let hasRewrite = false;
36
+ const fnExpressions = new Map<
37
+ IdentifierId,
38
+ TInstruction<FunctionExpression>
39
+ >();
40
+ const scopeInfos = new Map<
41
+ ScopeId,
42
+ {pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
43
+ >();
44
+
45
+ /**
46
+ * When inserting LoadLocals, we need to retain the reactivity of the base
47
+ * identifier, as later passes e.g. PruneNonReactiveDeps take the reactivity of
48
+ * a base identifier as the "maximal" reactivity of all its references.
49
+ * Concretely,
50
+ * reactive(Identifier i) = Union_{reference of i}(reactive(reference))
51
+ */
52
+ const reactiveIds = inferReactiveIdentifiers(fn);
53
+
54
+ for (const [, block] of fn.body.blocks) {
55
+ if (
56
+ block.terminal.kind === 'scope' ||
57
+ block.terminal.kind === 'pruned-scope'
58
+ ) {
59
+ const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
60
+ scopeInfos.set(block.terminal.scope.id, {
61
+ pruned: block.terminal.kind === 'pruned-scope',
62
+ deps: block.terminal.scope.dependencies,
63
+ hasSingleInstr:
64
+ scopeBlock.instructions.length === 1 &&
65
+ scopeBlock.terminal.kind === 'goto' &&
66
+ scopeBlock.terminal.block === block.terminal.fallthrough,
67
+ });
68
+ }
69
+ const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
70
+ for (const instr of block.instructions) {
71
+ const {value, lvalue} = instr;
72
+ if (value.kind === 'FunctionExpression') {
73
+ fnExpressions.set(
74
+ lvalue.identifier.id,
75
+ instr as TInstruction<FunctionExpression>,
76
+ );
77
+ } else if (
78
+ /*
79
+ * This check is not final. Right now we only look for useEffects without a dependency array.
80
+ * This is likely not how we will ship this feature, but it is good enough for us to make progress
81
+ * on the implementation and test it.
82
+ */
83
+ value.kind === 'CallExpression' &&
84
+ isUseEffectHookType(value.callee.identifier) &&
85
+ value.args.length === 1 &&
86
+ value.args[0].kind === 'Identifier'
87
+ ) {
88
+ const fnExpr = fnExpressions.get(value.args[0].identifier.id);
89
+ if (fnExpr != null) {
90
+ const scopeInfo =
91
+ fnExpr.lvalue.identifier.scope != null
92
+ ? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
93
+ : null;
94
+ CompilerError.invariant(scopeInfo != null, {
95
+ reason: 'Expected function expression scope to exist',
96
+ loc: value.loc,
97
+ });
98
+ if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
99
+ /**
100
+ * TODO: retry pipeline that ensures effect function expressions
101
+ * are placed into their own scope
102
+ */
103
+ CompilerError.throwTodo({
104
+ reason:
105
+ '[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
106
+ loc: fnExpr.loc,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Step 1: write new instructions to insert a dependency array
112
+ *
113
+ * Note that it's invalid to prune non-reactive deps in this pass, see
114
+ * the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
115
+ * explanation.
116
+ */
117
+ const effectDeps: Array<Place> = [];
118
+ const newInstructions: Array<Instruction> = [];
119
+ for (const dep of scopeInfo.deps) {
120
+ const {place, instructions} = writeDependencyToInstructions(
121
+ dep,
122
+ reactiveIds.has(dep.identifier.id),
123
+ fn.env,
124
+ fnExpr.loc,
125
+ );
126
+ newInstructions.push(...instructions);
127
+ effectDeps.push(place);
128
+ }
129
+ const deps: ArrayExpression = {
130
+ kind: 'ArrayExpression',
131
+ elements: effectDeps,
132
+ loc: GeneratedSource,
133
+ };
134
+
135
+ const depsPlace = createTemporaryPlace(env, GeneratedSource);
136
+ depsPlace.effect = Effect.Read;
137
+
138
+ newInstructions.push({
139
+ id: makeInstructionId(0),
140
+ loc: GeneratedSource,
141
+ lvalue: {...depsPlace, effect: Effect.Mutate},
142
+ value: deps,
143
+ });
144
+
145
+ // Step 2: insert the deps array as an argument of the useEffect
146
+ value.args[1] = {...depsPlace, effect: Effect.Freeze};
147
+ rewriteInstrs.set(instr.id, newInstructions);
148
+ }
149
+ }
150
+ }
151
+ if (rewriteInstrs.size > 0) {
152
+ hasRewrite = true;
153
+ const newInstrs = [];
154
+ for (const instr of block.instructions) {
155
+ const newInstr = rewriteInstrs.get(instr.id);
156
+ if (newInstr != null) {
157
+ newInstrs.push(...newInstr, instr);
158
+ } else {
159
+ newInstrs.push(instr);
160
+ }
161
+ }
162
+ block.instructions = newInstrs;
163
+ }
164
+ }
165
+ if (hasRewrite) {
166
+ // Renumber instructions and fix scope ranges
167
+ markInstructionIds(fn.body);
168
+ fixScopeAndIdentifierRanges(fn.body);
169
+ }
170
+}
171
+
172
+function writeDependencyToInstructions(
173
+ dep: ReactiveScopeDependency,
174
+ reactive: boolean,
175
+ env: Environment,
176
+ loc: SourceLocation,
177
+): {place: Place; instructions: Array<Instruction>} {
178
+ const instructions: Array<Instruction> = [];
179
+ let currValue = createTemporaryPlace(env, GeneratedSource);
180
+ currValue.reactive = reactive;
181
+ instructions.push({
182
+ id: makeInstructionId(0),
183
+ loc: GeneratedSource,
184
+ lvalue: {...currValue, effect: Effect.Mutate},
185
+ value: {
186
+ kind: 'LoadLocal',
187
+ place: {
188
+ kind: 'Identifier',
189
+ identifier: dep.identifier,
190
+ effect: Effect.Capture,
191
+ reactive,
192
+ loc: loc,
193
+ },
194
+ loc: loc,
195
+ },
196
+ });
197
+ for (const path of dep.path) {
198
+ if (path.optional) {
199
+ /**
200
+ * TODO: instead of truncating optional paths, reuse
201
+ * instructions from hoisted dependencies block(s)
202
+ */
203
+ break;
204
+ }
205
+ const nextValue = createTemporaryPlace(env, GeneratedSource);
206
+ nextValue.reactive = reactive;
207
+ instructions.push({
208
+ id: makeInstructionId(0),
209
+ loc: GeneratedSource,
210
+ lvalue: {...nextValue, effect: Effect.Mutate},
211
+ value: {
212
+ kind: 'PropertyLoad',
213
+ object: {...currValue, effect: Effect.Capture},
214
+ property: path.property,
215
+ loc: loc,
216
+ },
217
+ });
218
+ currValue = nextValue;
219
+ }
220
+ currValue.effect = Effect.Freeze;
221
+ return {place: currValue, instructions};
222
+}
223
+
224
+function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
225
+ const reactiveIds: Set<IdentifierId> = new Set();
226
+ for (const [, block] of fn.body.blocks) {
227
+ for (const instr of block.instructions) {
228
+ /**
229
+ * No need to traverse into nested functions as
230
+ * 1. their effects are recorded in `LoweredFunction.dependencies`
231
+ * 2. we don't mark `reactive` in these anyways
232
+ */
233
+ for (const place of eachInstructionOperand(instr)) {
234
+ if (place.reactive) {
235
+ reactiveIds.add(place.identifier.id);
236
+ }
237
+ }
238
+ }
239
+
240
+ for (const place of eachTerminalOperand(block.terminal)) {
241
+ if (place.reactive) {
242
+ reactiveIds.add(place.identifier.id);
243
+ }
244
+ }
245
+ }
246
+ return reactiveIds;
247
+}