1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {
9
+ Effect,
10
+ HIRFunction,
11
+ Identifier,
12
+ isMutableEffect,
13
+ isRefOrRefLikeMutableType,
14
+ makeInstructionId,
15
+} from '../HIR/HIR';
16
+import {eachInstructionValueOperand} from '../HIR/visitors';
17
+import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
18
+import DisjointSet from '../Utils/DisjointSet';
19
+
20
+/**
21
+ * If a function captures a mutable value but never gets called, we don't infer a
22
+ * mutable range for that function. This means that we also don't alias the function
23
+ * with its mutable captures.
24
+ *
25
+ * This case is tricky, because we don't generally know for sure what is a mutation
26
+ * and what may just be a normal function call. For example:
27
+ *
28
+ * ```
29
+ * hook useFoo() {
30
+ * const x = makeObject();
31
+ * return () => {
32
+ * return readObject(x); // could be a mutation!
33
+ * }
34
+ * }
35
+ * ```
36
+ *
37
+ * If we pessimistically assume that all such cases are mutations, we'd have to group
38
+ * lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
39
+ *
40
+ * ```
41
+ * hook useFoo(createEntryForKey) {
42
+ * const cache = new WeakMap();
43
+ * return (key) => {
44
+ * let entry = cache.get(key);
45
+ * if (entry == null) {
46
+ * entry = createEntryForKey(key);
47
+ * cache.set(key, entry); // known mutation!
48
+ * }
49
+ * return entry;
50
+ * }
51
+ * }
52
+ * ```
53
+ *
54
+ * Then we have to ensure that the function and its mutable captures alias together and
55
+ * end up in the same scope. However, aliasing together isn't enough if the function
56
+ * and operands all have empty mutable ranges (end = start + 1).
57
+ *
58
+ * This pass finds function expressions and object methods that have an empty mutable range
59
+ * and known-mutable operands which also don't have a mutable range, and ensures that the
60
+ * function and those operands are aliased together *and* that their ranges are updated to
61
+ * end after the function expression. This is sufficient to ensure that a reactive scope is
62
+ * created for the alias set.
63
+ */
64
+export function inferAliasForUncalledFunctions(
65
+ fn: HIRFunction,
66
+ aliases: DisjointSet<Identifier>,
67
+): void {
68
+ for (const block of fn.body.blocks.values()) {
69
+ instrs: for (const instr of block.instructions) {
70
+ const {lvalue, value} = instr;
71
+ if (
72
+ value.kind !== 'ObjectMethod' &&
73
+ value.kind !== 'FunctionExpression'
74
+ ) {
75
+ continue;
76
+ }
77
+ /*
78
+ * If the function is known to be mutated, we will have
79
+ * already aliased any mutable operands with it
80
+ */
81
+ const range = lvalue.identifier.mutableRange;
82
+ if (range.end > range.start + 1) {
83
+ continue;
84
+ }
85
+ /*
86
+ * If the function already has operands with an active mutable range,
87
+ * then we don't need to do anything — the function will have already
88
+ * been visited and included in some mutable alias set. This case can
89
+ * also occur due to visiting the same function in an earlier iteration
90
+ * of the outer fixpoint loop.
91
+ */
92
+ for (const operand of eachInstructionValueOperand(value)) {
93
+ if (isMutable(instr, operand)) {
94
+ continue instrs;
95
+ }
96
+ }
97
+ const operands: Set<Identifier> = new Set();
98
+ for (const effect of value.loweredFunc.func.effects ?? []) {
99
+ if (effect.kind !== 'ContextMutation') {
100
+ continue;
101
+ }
102
+ /*
103
+ * We're looking for known-mutations only, so we look at the effects
104
+ * rather than function context
105
+ */
106
+ if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
107
+ for (const operand of effect.places) {
108
+ /*
109
+ * It's possible that function effect analysis thinks there was a context mutation,
110
+ * but then InferReferenceEffects figures out some operands are globals and therefore
111
+ * creates a non-mutable effect for those operands.
112
+ * We should change InferReferenceEffects to swap the ContextMutation for a global
113
+ * mutation in that case, but for now we just filter them out here
114
+ */
115
+ if (
116
+ isMutableEffect(operand.effect, operand.loc) &&
117
+ !isRefOrRefLikeMutableType(operand.identifier.type)
118
+ ) {
119
+ operands.add(operand.identifier);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (operands.size !== 0) {
125
+ operands.add(lvalue.identifier);
126
+ aliases.union([...operands]);
127
+ // Update mutable ranges, if the ranges are empty then a reactive scope isn't created
128
+ for (const operand of operands) {
129
+ operand.mutableRange.end = makeInstructionId(instr.id + 1);
130
+ }
131
+ }
132
+ }
133
+ }
134
+}