[compiler][wip] Remove old mutation/aliasing implementation (#34028)
--- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34028). * #34029 * __->__ #34028
Joseph Savona committed
Aug 15, 2025 at 15:21 UTC
eaf6adb1277e4cb4f91d1b7f687f773657a5751b
36 files changed
+311
-4065
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+14
-41
@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
33
import {
34
analyseFunctions,
35
dropManualMemoization,
36
- inferMutableRanges,
36
inferReactivePlaces,
38
- inferReferenceEffects,
37
inlineImmediatelyInvokedFunctionExpressions,
38
inferEffectDependencies,
39
} from '../Inference';
@@ -100,7 +98,6 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
98
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
99
import {transformFire} from '../Transform';
100
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
103
-import {CompilerError} from '..';
101
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
102
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
103
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -229,28 +226,14 @@ function runWithEnvironment(
226
analyseFunctions(hir);
227
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
228
232
- if (!env.config.enableNewMutationAliasingModel) {
233
- const fnEffectErrors = inferReferenceEffects(hir);
234
- if (env.isInferredMemoEnabled) {
235
- if (fnEffectErrors.length > 0) {
236
- CompilerError.throw(fnEffectErrors[0]);
237
- }
238
- }
239
- log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
240
- } else {
241
- const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
242
- log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
243
- if (env.isInferredMemoEnabled) {
244
- if (mutabilityAliasingErrors.isErr()) {
245
- throw mutabilityAliasingErrors.unwrapErr();
246
- }
229
+ const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
230
+ log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
231
+ if (env.isInferredMemoEnabled) {
232
+ if (mutabilityAliasingErrors.isErr()) {
233
+ throw mutabilityAliasingErrors.unwrapErr();
234
}
235
}
236
250
- if (!env.config.enableNewMutationAliasingModel) {
251
- validateLocalsNotReassignedAfterRender(hir);
252
- }
253
-
237
// Note: Has to come after infer reference effects because "dead" code may still affect inference
238
deadCodeElimination(hir);
239
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -263,20 +246,15 @@ function runWithEnvironment(
246
pruneMaybeThrows(hir);
247
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
248
266
- if (!env.config.enableNewMutationAliasingModel) {
267
- inferMutableRanges(hir);
268
- log({kind: 'hir', name: 'InferMutableRanges', value: hir});
269
- } else {
270
- const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
271
- isFunctionExpression: false,
272
- });
273
- log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
274
- if (env.isInferredMemoEnabled) {
275
- if (mutabilityAliasingErrors.isErr()) {
276
- throw mutabilityAliasingErrors.unwrapErr();
277
- }
278
- validateLocalsNotReassignedAfterRender(hir);
249
+ const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
250
+ isFunctionExpression: false,
251
+ });
252
+ log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
253
+ if (env.isInferredMemoEnabled) {
254
+ if (mutabilityAliasingRangeErrors.isErr()) {
255
+ throw mutabilityAliasingRangeErrors.unwrapErr();
256
}
257
+ validateLocalsNotReassignedAfterRender(hir);
258
}
259
260
if (env.isInferredMemoEnabled) {
@@ -308,12 +286,7 @@ function runWithEnvironment(
286
validateNoImpureFunctionsInRender(hir).unwrap();
287
}
288
311
- if (
312
- env.config.validateNoFreezingKnownMutableFunctions ||
313
- env.config.enableNewMutationAliasingModel
314
- ) {
315
- validateNoFreezingKnownMutableFunctions(hir).unwrap();
316
- }
289
+ validateNoFreezingKnownMutableFunctions(hir).unwrap();
290
}
291
292
inferReactivePlaces(hir);
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
-5
@@ -250,11 +250,6 @@ export const EnvironmentConfigSchema = z.object({
250
*/
251
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
252
253
- /**
254
- * Enable a new model for mutability and aliasing inference
255
- */
256
- enableNewMutationAliasingModel: z.boolean().default(true),
257
-
253
/**
254
* Enables inference of optional dependency chains. Without this flag
255
* a property chain such as `props?.items?.foo` will infer as a dep on
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+2
-72
@@ -6,20 +6,10 @@
6
*/
7
8
import {CompilerError} from '../CompilerError';
9
-import {
10
- Effect,
11
- HIRFunction,
12
- Identifier,
13
- IdentifierId,
14
- LoweredFunction,
15
- isRefOrRefValue,
16
- makeInstructionId,
17
-} from '../HIR';
9
+import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
10
import {deadCodeElimination} from '../Optimization';
11
import {inferReactiveScopeVariables} from '../ReactiveScopes';
12
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
21
-import {inferMutableRanges} from './InferMutableRanges';
22
-import inferReferenceEffects from './InferReferenceEffects';
13
import {assertExhaustive} from '../Utils/utils';
14
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
15
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
@@ -30,12 +20,7 @@ export default function analyseFunctions(func: HIRFunction): void {
20
switch (instr.value.kind) {
21
case 'ObjectMethod':
22
case 'FunctionExpression': {
33
- if (!func.env.config.enableNewMutationAliasingModel) {
34
- lower(instr.value.loweredFunc.func);
35
- infer(instr.value.loweredFunc);
36
- } else {
37
- lowerWithMutationAliasing(instr.value.loweredFunc.func);
38
- }
23
+ lowerWithMutationAliasing(instr.value.loweredFunc.func);
24
25
/**
26
* Reset mutable range for outer inferReferenceEffects
@@ -140,58 +125,3 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
125
value: fn,
126
});
127
}
143
-
144
-function lower(func: HIRFunction): void {
145
- analyseFunctions(func);
146
- inferReferenceEffects(func, {isFunctionExpression: true});
147
- deadCodeElimination(func);
148
- inferMutableRanges(func);
149
- rewriteInstructionKindsBasedOnReassignment(func);
150
- inferReactiveScopeVariables(func);
151
- func.env.logger?.debugLogIRs?.({
152
- kind: 'hir',
153
- name: 'AnalyseFunction (inner)',
154
- value: func,
155
- });
156
-}
157
-
158
-function infer(loweredFunc: LoweredFunction): void {
159
- for (const operand of loweredFunc.func.context) {
160
- const identifier = operand.identifier;
161
- CompilerError.invariant(operand.effect === Effect.Unknown, {
162
- reason:
163
- '[AnalyseFunctions] Expected Function context effects to not have been set',
164
- loc: operand.loc,
165
- });
166
- if (isRefOrRefValue(identifier)) {
167
- /*
168
- * TODO: this is a hack to ensure we treat functions which reference refs
169
- * as having a capture and therefore being considered mutable. this ensures
170
- * the function gets a mutable range which accounts for anywhere that it
171
- * could be called, and allows us to help ensure it isn't called during
172
- * render
173
- */
174
- operand.effect = Effect.Capture;
175
- } else if (isMutatedOrReassigned(identifier)) {
176
- /**
177
- * Reflects direct reassignments, PropertyStores, and ConditionallyMutate
178
- * (directly or through maybe-aliases)
179
- */
180
- operand.effect = Effect.Capture;
181
- } else {
182
- operand.effect = Effect.Read;
183
- }
184
- }
185
-}
186
-
187
-function isMutatedOrReassigned(id: Identifier): boolean {
188
- /*
189
- * This check checks for mutation and reassingnment, so the usual check for
190
- * mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
191
- * enough.
192
- *
193
- * We need to track re-assignments in context refs as we need to reflect the
194
- * re-assignment back to the captured refs.
195
- */
196
- return id.mutableRange.end > id.mutableRange.start;
197
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts
deleted
-134
@@ -1,134 +0,0 @@
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
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts
deleted
-68
@@ -1,68 +0,0 @@
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
- HIRFunction,
10
- Identifier,
11
- Instruction,
12
- isPrimitiveType,
13
- Place,
14
-} from '../HIR/HIR';
15
-import DisjointSet from '../Utils/DisjointSet';
16
-
17
-export type AliasSet = Set<Identifier>;
18
-
19
-export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
20
- const aliases = new DisjointSet<Identifier>();
21
- for (const [_, block] of func.body.blocks) {
22
- for (const instr of block.instructions) {
23
- inferInstr(instr, aliases);
24
- }
25
- }
26
-
27
- return aliases;
28
-}
29
-
30
-function inferInstr(
31
- instr: Instruction,
32
- aliases: DisjointSet<Identifier>,
33
-): void {
34
- const {lvalue, value: instrValue} = instr;
35
- let alias: Place | null = null;
36
- switch (instrValue.kind) {
37
- case 'LoadLocal':
38
- case 'LoadContext': {
39
- if (isPrimitiveType(instrValue.place.identifier)) {
40
- return;
41
- }
42
- alias = instrValue.place;
43
- break;
44
- }
45
- case 'StoreLocal':
46
- case 'StoreContext': {
47
- alias = instrValue.value;
48
- break;
49
- }
50
- case 'Destructure': {
51
- alias = instrValue.value;
52
- break;
53
- }
54
- case 'ComputedLoad':
55
- case 'PropertyLoad': {
56
- alias = instrValue.object;
57
- break;
58
- }
59
- case 'TypeCastExpression': {
60
- alias = instrValue.value;
61
- break;
62
- }
63
- default:
64
- return;
65
- }
66
-
67
- aliases.union([lvalue.identifier, alias.identifier]);
68
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForPhis.ts
deleted
-27
@@ -1,27 +0,0 @@
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 {HIRFunction, Identifier} from '../HIR/HIR';
9
-import DisjointSet from '../Utils/DisjointSet';
10
-
11
-export function inferAliasForPhis(
12
- func: HIRFunction,
13
- aliases: DisjointSet<Identifier>,
14
-): void {
15
- for (const [_, block] of func.body.blocks) {
16
- for (const phi of block.phis) {
17
- const isPhiMutatedAfterCreation: boolean =
18
- phi.place.identifier.mutableRange.end >
19
- (block.instructions.at(0)?.id ?? block.terminal.id);
20
- if (isPhiMutatedAfterCreation) {
21
- for (const [, operand] of phi.operands) {
22
- aliases.union([phi.place.identifier, operand.identifier]);
23
- }
24
- }
25
- }
26
- }
27
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForStores.ts
deleted
-68
@@ -1,68 +0,0 @@
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
- InstructionId,
13
- Place,
14
-} from '../HIR/HIR';
15
-import {
16
- eachInstructionLValue,
17
- eachInstructionValueOperand,
18
-} from '../HIR/visitors';
19
-import DisjointSet from '../Utils/DisjointSet';
20
-
21
-export function inferAliasForStores(
22
- func: HIRFunction,
23
- aliases: DisjointSet<Identifier>,
24
-): void {
25
- for (const [_, block] of func.body.blocks) {
26
- for (const instr of block.instructions) {
27
- const {value, lvalue} = instr;
28
- const isStore =
29
- lvalue.effect === Effect.Store ||
30
- /*
31
- * Some typed functions annotate callees or arguments
32
- * as Effect.Store.
33
- */
34
- ![...eachInstructionValueOperand(value)].every(
35
- operand => operand.effect !== Effect.Store,
36
- );
37
-
38
- if (!isStore) {
39
- continue;
40
- }
41
- for (const operand of eachInstructionLValue(instr)) {
42
- maybeAlias(aliases, lvalue, operand, instr.id);
43
- }
44
- for (const operand of eachInstructionValueOperand(value)) {
45
- if (
46
- operand.effect === Effect.Capture ||
47
- operand.effect === Effect.Store
48
- ) {
49
- maybeAlias(aliases, lvalue, operand, instr.id);
50
- }
51
- }
52
- }
53
- }
54
-}
55
-
56
-function maybeAlias(
57
- aliases: DisjointSet<Identifier>,
58
- lvalue: Place,
59
- rvalue: Place,
60
- id: InstructionId,
61
-): void {
62
- if (
63
- lvalue.identifier.mutableRange.end > id + 1 ||
64
- rvalue.identifier.mutableRange.end > id
65
- ) {
66
- aliases.union([lvalue.identifier, rvalue.identifier]);
67
- }
68
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts
deleted
-351
@@ -1,351 +0,0 @@
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
- CompilerError,
10
- CompilerErrorDetailOptions,
11
- ErrorSeverity,
12
- ValueKind,
13
-} from '..';
14
-import {
15
- AbstractValue,
16
- BasicBlock,
17
- Effect,
18
- Environment,
19
- FunctionEffect,
20
- Instruction,
21
- InstructionValue,
22
- Place,
23
- ValueReason,
24
- getHookKind,
25
- isRefOrRefValue,
26
-} from '../HIR';
27
-import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
28
-import {assertExhaustive} from '../Utils/utils';
29
-
30
-interface State {
31
- kind(place: Place): AbstractValue;
32
- values(place: Place): Array<InstructionValue>;
33
- isDefined(place: Place): boolean;
34
-}
35
-
36
-function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
37
- const value = state.kind(place);
38
- CompilerError.invariant(value != null, {
39
- reason: 'Expected operand to have a kind',
40
- loc: null,
41
- });
42
-
43
- switch (place.effect) {
44
- case Effect.Store:
45
- case Effect.Mutate: {
46
- if (isRefOrRefValue(place.identifier)) {
47
- break;
48
- } else if (value.kind === ValueKind.Context) {
49
- CompilerError.invariant(value.context.size > 0, {
50
- reason:
51
- "[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
52
- loc: place.loc,
53
- });
54
- return {
55
- kind: 'ContextMutation',
56
- loc: place.loc,
57
- effect: place.effect,
58
- places: value.context,
59
- };
60
- } else if (
61
- value.kind !== ValueKind.Mutable &&
62
- // We ignore mutations of primitives since this is not a React-specific problem
63
- value.kind !== ValueKind.Primitive
64
- ) {
65
- let reason = getWriteErrorReason(value);
66
- return {
67
- kind:
68
- value.reason.size === 1 && value.reason.has(ValueReason.Global)
69
- ? 'GlobalMutation'
70
- : 'ReactMutation',
71
- error: {
72
- reason,
73
- description:
74
- place.identifier.name !== null &&
75
- place.identifier.name.kind === 'named'
76
- ? `Found mutation of \`${place.identifier.name.value}\``
77
- : null,
78
- loc: place.loc,
79
- suggestions: null,
80
- severity: ErrorSeverity.InvalidReact,
81
- },
82
- };
83
- }
84
- break;
85
- }
86
- }
87
- return null;
88
-}
89
-
90
-function inheritFunctionEffects(
91
- state: State,
92
- place: Place,
93
-): Array<FunctionEffect> {
94
- const effects = inferFunctionInstrEffects(state, place);
95
-
96
- return effects
97
- .flatMap(effect => {
98
- if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
99
- return [effect];
100
- } else {
101
- const effects: Array<FunctionEffect | null> = [];
102
- CompilerError.invariant(effect.kind === 'ContextMutation', {
103
- reason: 'Expected ContextMutation',
104
- loc: null,
105
- });
106
- /**
107
- * Contextual effects need to be replayed against the current inference
108
- * state, which may know more about the value to which the effect applied.
109
- * The main cases are:
110
- * 1. The mutated context value is _still_ a context value in the current scope,
111
- * so we have to continue propagating the original context mutation.
112
- * 2. The mutated context value is a mutable value in the current scope,
113
- * so the context mutation was fine and we can skip propagating the effect.
114
- * 3. The mutated context value is an immutable value in the current scope,
115
- * resulting in a non-ContextMutation FunctionEffect. We propagate that new,
116
- * more detailed effect to the current function context.
117
- */
118
- for (const place of effect.places) {
119
- if (state.isDefined(place)) {
120
- const replayedEffect = inferOperandEffect(state, {
121
- ...place,
122
- loc: effect.loc,
123
- effect: effect.effect,
124
- });
125
- if (replayedEffect != null) {
126
- if (replayedEffect.kind === 'ContextMutation') {
127
- // Case 1, still a context value so propagate the original effect
128
- effects.push(effect);
129
- } else {
130
- // Case 3, immutable value so propagate the more precise effect
131
- effects.push(replayedEffect);
132
- }
133
- } // else case 2, local mutable value so this effect was fine
134
- }
135
- }
136
- return effects;
137
- }
138
- })
139
- .filter((effect): effect is FunctionEffect => effect != null);
140
-}
141
-
142
-function inferFunctionInstrEffects(
143
- state: State,
144
- place: Place,
145
-): Array<FunctionEffect> {
146
- const effects: Array<FunctionEffect> = [];
147
- const instrs = state.values(place);
148
- CompilerError.invariant(instrs != null, {
149
- reason: 'Expected operand to have instructions',
150
- loc: null,
151
- });
152
-
153
- for (const instr of instrs) {
154
- if (
155
- (instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
156
- instr.loweredFunc.func.effects != null
157
- ) {
158
- effects.push(...instr.loweredFunc.func.effects);
159
- }
160
- }
161
-
162
- return effects;
163
-}
164
-
165
-function operandEffects(
166
- state: State,
167
- place: Place,
168
- filterRenderSafe: boolean,
169
-): Array<FunctionEffect> {
170
- const functionEffects: Array<FunctionEffect> = [];
171
- const effect = inferOperandEffect(state, place);
172
- effect && functionEffects.push(effect);
173
- functionEffects.push(...inheritFunctionEffects(state, place));
174
- if (filterRenderSafe) {
175
- return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
176
- } else {
177
- return functionEffects;
178
- }
179
-}
180
-
181
-export function inferInstructionFunctionEffects(
182
- env: Environment,
183
- state: State,
184
- instr: Instruction,
185
-): Array<FunctionEffect> {
186
- const functionEffects: Array<FunctionEffect> = [];
187
- switch (instr.value.kind) {
188
- case 'JsxExpression': {
189
- if (instr.value.tag.kind === 'Identifier') {
190
- functionEffects.push(...operandEffects(state, instr.value.tag, false));
191
- }
192
- instr.value.children?.forEach(child =>
193
- functionEffects.push(...operandEffects(state, child, false)),
194
- );
195
- for (const attr of instr.value.props) {
196
- if (attr.kind === 'JsxSpreadAttribute') {
197
- functionEffects.push(...operandEffects(state, attr.argument, false));
198
- } else {
199
- functionEffects.push(...operandEffects(state, attr.place, true));
200
- }
201
- }
202
- break;
203
- }
204
- case 'ObjectMethod':
205
- case 'FunctionExpression': {
206
- /**
207
- * If this function references other functions, propagate the referenced function's
208
- * effects to this function.
209
- *
210
- * ```
211
- * let f = () => global = true;
212
- * let g = () => f();
213
- * g();
214
- * ```
215
- *
216
- * In this example, because `g` references `f`, we propagate the GlobalMutation from
217
- * `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
218
- * function effect context and report an error. But if instead we do:
219
- *
220
- * ```
221
- * let f = () => global = true;
222
- * let g = () => f();
223
- * useEffect(() => g(), [g])
224
- * ```
225
- *
226
- * Now `g`'s effects will be discarded since they're in a useEffect.
227
- */
228
- for (const operand of eachInstructionOperand(instr)) {
229
- instr.value.loweredFunc.func.effects ??= [];
230
- instr.value.loweredFunc.func.effects.push(
231
- ...inferFunctionInstrEffects(state, operand),
232
- );
233
- }
234
- break;
235
- }
236
- case 'MethodCall':
237
- case 'CallExpression': {
238
- let callee;
239
- if (instr.value.kind === 'MethodCall') {
240
- callee = instr.value.property;
241
- functionEffects.push(
242
- ...operandEffects(state, instr.value.receiver, false),
243
- );
244
- } else {
245
- callee = instr.value.callee;
246
- }
247
- functionEffects.push(...operandEffects(state, callee, false));
248
- let isHook = getHookKind(env, callee.identifier) != null;
249
- for (const arg of instr.value.args) {
250
- const place = arg.kind === 'Identifier' ? arg : arg.place;
251
- /*
252
- * Join the effects of the argument with the effects of the enclosing function,
253
- * unless the we're detecting a global mutation inside a useEffect hook
254
- */
255
- functionEffects.push(...operandEffects(state, place, isHook));
256
- }
257
- break;
258
- }
259
- case 'StartMemoize':
260
- case 'FinishMemoize':
261
- case 'LoadLocal':
262
- case 'StoreLocal': {
263
- break;
264
- }
265
- case 'StoreGlobal': {
266
- functionEffects.push({
267
- kind: 'GlobalMutation',
268
- error: {
269
- reason:
270
- 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
271
- loc: instr.loc,
272
- suggestions: null,
273
- severity: ErrorSeverity.InvalidReact,
274
- },
275
- });
276
- break;
277
- }
278
- default: {
279
- for (const operand of eachInstructionOperand(instr)) {
280
- functionEffects.push(...operandEffects(state, operand, false));
281
- }
282
- }
283
- }
284
- return functionEffects;
285
-}
286
-
287
-export function inferTerminalFunctionEffects(
288
- state: State,
289
- block: BasicBlock,
290
-): Array<FunctionEffect> {
291
- const functionEffects: Array<FunctionEffect> = [];
292
- for (const operand of eachTerminalOperand(block.terminal)) {
293
- functionEffects.push(...operandEffects(state, operand, true));
294
- }
295
- return functionEffects;
296
-}
297
-
298
-export function transformFunctionEffectErrors(
299
- functionEffects: Array<FunctionEffect>,
300
-): Array<CompilerErrorDetailOptions> {
301
- return functionEffects.map(eff => {
302
- switch (eff.kind) {
303
- case 'ReactMutation':
304
- case 'GlobalMutation': {
305
- return eff.error;
306
- }
307
- case 'ContextMutation': {
308
- return {
309
- severity: ErrorSeverity.Invariant,
310
- reason: `Unexpected ContextMutation in top-level function effects`,
311
- loc: eff.loc,
312
- };
313
- }
314
- default:
315
- assertExhaustive(
316
- eff,
317
- `Unexpected function effect kind \`${(eff as any).kind}\``,
318
- );
319
- }
320
- });
321
-}
322
-
323
-function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
324
- return effect.kind === 'GlobalMutation';
325
-}
326
-
327
-export function getWriteErrorReason(abstractValue: AbstractValue): string {
328
- if (abstractValue.reason.has(ValueReason.Global)) {
329
- return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
330
- } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
331
- return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
332
- } else if (abstractValue.reason.has(ValueReason.Context)) {
333
- return `Modifying a value returned from 'useContext()' is not allowed.`;
334
- } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
335
- return 'Modifying a value returned from a function whose return value should not be mutated';
336
- } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
337
- return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
338
- } else if (abstractValue.reason.has(ValueReason.State)) {
339
- return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
340
- } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
341
- return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
342
- } else if (abstractValue.reason.has(ValueReason.Effect)) {
343
- return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
344
- } else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
345
- return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
346
- } else if (abstractValue.reason.has(ValueReason.HookReturn)) {
347
- return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
348
- } else {
349
- return 'This modifies a variable that React considers immutable';
350
- }
351
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts
deleted
-218
@@ -1,218 +0,0 @@
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
- InstructionId,
13
- InstructionKind,
14
- isArrayType,
15
- isMapType,
16
- isRefOrRefValue,
17
- isSetType,
18
- makeInstructionId,
19
- Place,
20
-} from '../HIR/HIR';
21
-import {printPlace} from '../HIR/PrintHIR';
22
-import {
23
- eachInstructionLValue,
24
- eachInstructionOperand,
25
- eachTerminalOperand,
26
-} from '../HIR/visitors';
27
-import {assertExhaustive} from '../Utils/utils';
28
-
29
-/*
30
- * For each usage of a value in the given function, determines if the usage
31
- * may be succeeded by a mutable usage of that same value and if so updates
32
- * the usage to be mutable.
33
- *
34
- * Stated differently, this inference ensures that inferred capabilities of
35
- * each reference are as follows:
36
- * - freeze: the value is frozen at this point
37
- * - readonly: the value is not modified at this point *or any subsequent
38
- * point*
39
- * - mutable: the value is modified at this point *or some subsequent point*.
40
- *
41
- * Note that this refines the capabilities inferered by InferReferenceCapability,
42
- * which looks at individual references and not the lifetime of a value's mutability.
43
- *
44
- * == Algorithm
45
- *
46
- * TODO:
47
- * 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
48
- * which only tracks aliasing of top-level variables (`y = x`), this analysis needs
49
- * to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
50
- * all possible locations which may have aliased a value. The concrete result is
51
- * a mapping of each Place to the set of possibly-mutable values it may alias.
52
- *
53
- * ```
54
- * const x = []; // {x: v0; v0: mutable []}
55
- * const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
56
- * y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
57
- * read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
58
- * mutate(y); // can infer that y mutates v0 and v1
59
- * ```
60
- *
61
- * DONE:
62
- * 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
63
- * the CFG and track which values are mutated in a successor.
64
- *
65
- * ```
66
- * mutate(y); // mutable y => v0, v1 mutated
67
- * read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
68
- * ...
69
- * ```
70
- */
71
-
72
-function infer(place: Place, instrId: InstructionId): void {
73
- if (!isRefOrRefValue(place.identifier)) {
74
- place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
75
- }
76
-}
77
-
78
-function inferPlace(
79
- place: Place,
80
- instrId: InstructionId,
81
- inferMutableRangeForStores: boolean,
82
-): void {
83
- switch (place.effect) {
84
- case Effect.Unknown: {
85
- throw new Error(`Found an unknown place ${printPlace(place)}}!`);
86
- }
87
- case Effect.Capture:
88
- case Effect.Read:
89
- case Effect.Freeze:
90
- return;
91
- case Effect.Store:
92
- if (inferMutableRangeForStores) {
93
- infer(place, instrId);
94
- }
95
- return;
96
- case Effect.ConditionallyMutateIterator: {
97
- const identifier = place.identifier;
98
- if (
99
- !isArrayType(identifier) &&
100
- !isSetType(identifier) &&
101
- !isMapType(identifier)
102
- ) {
103
- infer(place, instrId);
104
- }
105
- return;
106
- }
107
- case Effect.ConditionallyMutate:
108
- case Effect.Mutate: {
109
- infer(place, instrId);
110
- return;
111
- }
112
- default:
113
- assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
114
- }
115
-}
116
-
117
-export function inferMutableLifetimes(
118
- func: HIRFunction,
119
- inferMutableRangeForStores: boolean,
120
-): void {
121
- /*
122
- * Context variables only appear to mutate where they are assigned, but we need
123
- * to force their range to start at their declaration. Track the declaring instruction
124
- * id so that the ranges can be extended if/when they are reassigned
125
- */
126
- const contextVariableDeclarationInstructions = new Map<
127
- Identifier,
128
- InstructionId
129
- >();
130
- for (const [_, block] of func.body.blocks) {
131
- for (const phi of block.phis) {
132
- const isPhiMutatedAfterCreation: boolean =
133
- phi.place.identifier.mutableRange.end >
134
- (block.instructions.at(0)?.id ?? block.terminal.id);
135
- if (
136
- inferMutableRangeForStores &&
137
- isPhiMutatedAfterCreation &&
138
- phi.place.identifier.mutableRange.start === 0
139
- ) {
140
- for (const [, operand] of phi.operands) {
141
- if (phi.place.identifier.mutableRange.start === 0) {
142
- phi.place.identifier.mutableRange.start =
143
- operand.identifier.mutableRange.start;
144
- } else {
145
- phi.place.identifier.mutableRange.start = makeInstructionId(
146
- Math.min(
147
- phi.place.identifier.mutableRange.start,
148
- operand.identifier.mutableRange.start,
149
- ),
150
- );
151
- }
152
- }
153
- }
154
- }
155
-
156
- for (const instr of block.instructions) {
157
- for (const operand of eachInstructionLValue(instr)) {
158
- const lvalueId = operand.identifier;
159
-
160
- /*
161
- * lvalue start being mutable when they're initially assigned a
162
- * value.
163
- */
164
- lvalueId.mutableRange.start = instr.id;
165
-
166
- /*
167
- * Let's be optimistic and assume this lvalue is not mutable by
168
- * default.
169
- */
170
- lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
171
- }
172
- for (const operand of eachInstructionOperand(instr)) {
173
- inferPlace(operand, instr.id, inferMutableRangeForStores);
174
- }
175
-
176
- if (
177
- instr.value.kind === 'DeclareContext' ||
178
- (instr.value.kind === 'StoreContext' &&
179
- instr.value.lvalue.kind !== InstructionKind.Reassign &&
180
- !contextVariableDeclarationInstructions.has(
181
- instr.value.lvalue.place.identifier,
182
- ))
183
- ) {
184
- /**
185
- * Save declarations of context variables if they hasn't already been
186
- * declared (due to hoisted declarations).
187
- */
188
- contextVariableDeclarationInstructions.set(
189
- instr.value.lvalue.place.identifier,
190
- instr.id,
191
- );
192
- } else if (instr.value.kind === 'StoreContext') {
193
- /*
194
- * Else this is a reassignment, extend the range from the declaration (if present).
195
- * Note that declarations may not be present for context variables that are reassigned
196
- * within a function expression before (or without) a read of the same variable
197
- */
198
- const declaration = contextVariableDeclarationInstructions.get(
199
- instr.value.lvalue.place.identifier,
200
- );
201
- if (
202
- declaration != null &&
203
- !isRefOrRefValue(instr.value.lvalue.place.identifier)
204
- ) {
205
- const range = instr.value.lvalue.place.identifier.mutableRange;
206
- if (range.start === 0) {
207
- range.start = declaration;
208
- } else {
209
- range.start = makeInstructionId(Math.min(range.start, declaration));
210
- }
211
- }
212
- }
213
- }
214
- for (const operand of eachTerminalOperand(block.terminal)) {
215
- inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
216
- }
217
- }
218
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts
deleted
-102
@@ -1,102 +0,0 @@
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 {HIRFunction, Identifier} from '../HIR/HIR';
9
-import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
10
-import {inferAliases} from './InferAlias';
11
-import {inferAliasForPhis} from './InferAliasForPhis';
12
-import {inferAliasForStores} from './InferAliasForStores';
13
-import {inferMutableLifetimes} from './InferMutableLifetimes';
14
-import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
15
-import {inferTryCatchAliases} from './InferTryCatchAliases';
16
-
17
-export function inferMutableRanges(ir: HIRFunction): void {
18
- // Infer mutable ranges for non fields
19
- inferMutableLifetimes(ir, false);
20
-
21
- // Calculate aliases
22
- const aliases = inferAliases(ir);
23
- /*
24
- * Calculate aliases for try/catch, where any value created
25
- * in the try block could be aliased to the catch param
26
- */
27
- inferTryCatchAliases(ir, aliases);
28
-
29
- /*
30
- * Eagerly canonicalize so that if nothing changes we can bail out
31
- * after a single iteration
32
- */
33
- let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
34
- while (true) {
35
- // Infer mutable ranges for aliases that are not fields
36
- inferMutableRangesForAlias(ir, aliases);
37
-
38
- // Update aliasing information of fields
39
- inferAliasForStores(ir, aliases);
40
-
41
- // Update aliasing information of phis
42
- inferAliasForPhis(ir, aliases);
43
-
44
- const nextAliases = aliases.canonicalize();
45
- if (areEqualMaps(prevAliases, nextAliases)) {
46
- break;
47
- }
48
- prevAliases = nextAliases;
49
- }
50
-
51
- // Re-infer mutable ranges for all values
52
- inferMutableLifetimes(ir, true);
53
-
54
- /**
55
- * The second inferMutableLifetimes() call updates mutable ranges
56
- * of values to account for Store effects. Now we need to update
57
- * all aliases of such values to extend their ranges as well. Note
58
- * that the store only mutates the the directly aliased value and
59
- * not any of its inner captured references. For example:
60
- *
61
- * ```
62
- * let y;
63
- * if (cond) {
64
- * y = [];
65
- * } else {
66
- * y = [{}];
67
- * }
68
- * y.push(z);
69
- * ```
70
- *
71
- * The Store effect from the `y.push` modifies the values that `y`
72
- * directly aliases - the two arrays from the if/else branches -
73
- * but does not modify values that `y` "contains" such as the
74
- * object literal or `z`.
75
- */
76
- prevAliases = aliases.canonicalize();
77
- while (true) {
78
- inferMutableRangesForAlias(ir, aliases);
79
- inferAliasForPhis(ir, aliases);
80
- inferAliasForUncalledFunctions(ir, aliases);
81
- const nextAliases = aliases.canonicalize();
82
- if (areEqualMaps(prevAliases, nextAliases)) {
83
- break;
84
- }
85
- prevAliases = nextAliases;
86
- }
87
-}
88
-
89
-function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
90
- if (a.size !== b.size) {
91
- return false;
92
- }
93
- for (const [key, value] of a) {
94
- if (!b.has(key)) {
95
- return false;
96
- }
97
- if (b.get(key) !== value) {
98
- return false;
99
- }
100
- }
101
- return true;
102
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts
deleted
-54
@@ -1,54 +0,0 @@
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
- HIRFunction,
10
- Identifier,
11
- InstructionId,
12
- isRefOrRefValue,
13
-} from '../HIR/HIR';
14
-import DisjointSet from '../Utils/DisjointSet';
15
-
16
-export function inferMutableRangesForAlias(
17
- _fn: HIRFunction,
18
- aliases: DisjointSet<Identifier>,
19
-): void {
20
- const aliasSets = aliases.buildSets();
21
- for (const aliasSet of aliasSets) {
22
- /*
23
- * Update mutableRange.end only if the identifiers have actually been
24
- * mutated.
25
- */
26
- const mutatingIdentifiers = [...aliasSet].filter(
27
- id =>
28
- id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
29
- );
30
-
31
- if (mutatingIdentifiers.length > 0) {
32
- // Find final instruction which mutates this alias set.
33
- let lastMutatingInstructionId = 0;
34
- for (const id of mutatingIdentifiers) {
35
- if (id.mutableRange.end > lastMutatingInstructionId) {
36
- lastMutatingInstructionId = id.mutableRange.end;
37
- }
38
- }
39
-
40
- /*
41
- * Update mutableRange.end for all aliases in this set ending before the
42
- * last mutation.
43
- */
44
- for (const alias of aliasSet) {
45
- if (
46
- alias.mutableRange.end < lastMutatingInstructionId &&
47
- !isRefOrRefValue(alias)
48
- ) {
49
- alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
50
- }
51
- }
52
- }
53
- }
54
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
+195
-9
@@ -19,6 +19,7 @@ import {
19
DeclarationId,
20
Environment,
21
FunctionExpression,
22
+ GeneratedSource,
23
HIRFunction,
24
Hole,
25
IdentifierId,
@@ -34,6 +35,7 @@ import {
35
Phi,
36
Place,
37
SpreadPattern,
38
+ Type,
39
ValueReason,
40
} from '../HIR';
41
import {
@@ -43,12 +45,6 @@ import {
45
eachTerminalSuccessor,
46
} from '../HIR/visitors';
47
import {Ok, Result} from '../Utils/Result';
46
-import {
47
- getArgumentEffect,
48
- getFunctionCallSignature,
49
- isKnownMutableEffect,
50
- mergeValueKinds,
51
-} from './InferReferenceEffects';
48
import {
49
assertExhaustive,
50
getOrInsertDefault,
@@ -65,7 +61,6 @@ import {
61
printSourceLocation,
62
} from '../HIR/PrintHIR';
63
import {FunctionSignature} from '../HIR/ObjectShape';
68
-import {getWriteErrorReason} from './InferFunctionEffects';
64
import prettyFormat from 'pretty-format';
65
import {createTemporaryPlace} from '../HIR/HIRBuilder';
66
import {
@@ -450,7 +445,6 @@ function applySignature(
445
const reason = getWriteErrorReason({
446
kind: value.kind,
447
reason: value.reason,
453
- context: new Set(),
448
});
449
const variable =
450
effect.value.identifier.name !== null &&
@@ -1074,7 +1068,6 @@ function applyEffect(
1068
const reason = getWriteErrorReason({
1069
kind: value.kind,
1070
reason: value.reason,
1077
- context: new Set(),
1071
});
1072
const variable =
1073
effect.value.identifier.name !== null &&
@@ -2567,3 +2560,196 @@ export type AbstractValue = {
2560
kind: ValueKind;
2561
reason: ReadonlySet<ValueReason>;
2562
};
2563
+
2564
+export function getWriteErrorReason(abstractValue: AbstractValue): string {
2565
+ if (abstractValue.reason.has(ValueReason.Global)) {
2566
+ return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
2567
+ } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
2568
+ return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
2569
+ } else if (abstractValue.reason.has(ValueReason.Context)) {
2570
+ return `Modifying a value returned from 'useContext()' is not allowed.`;
2571
+ } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
2572
+ return 'Modifying a value returned from a function whose return value should not be mutated';
2573
+ } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
2574
+ return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
2575
+ } else if (abstractValue.reason.has(ValueReason.State)) {
2576
+ return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
2577
+ } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
2578
+ return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
2579
+ } else if (abstractValue.reason.has(ValueReason.Effect)) {
2580
+ return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
2581
+ } else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
2582
+ return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
2583
+ } else if (abstractValue.reason.has(ValueReason.HookReturn)) {
2584
+ return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
2585
+ } else {
2586
+ return 'This modifies a variable that React considers immutable';
2587
+ }
2588
+}
2589
+
2590
+function getArgumentEffect(
2591
+ signatureEffect: Effect | null,
2592
+ arg: Place | SpreadPattern,
2593
+): Effect {
2594
+ if (signatureEffect != null) {
2595
+ if (arg.kind === 'Identifier') {
2596
+ return signatureEffect;
2597
+ } else if (
2598
+ signatureEffect === Effect.Mutate ||
2599
+ signatureEffect === Effect.ConditionallyMutate
2600
+ ) {
2601
+ return signatureEffect;
2602
+ } else {
2603
+ // see call-spread-argument-mutable-iterator test fixture
2604
+ if (signatureEffect === Effect.Freeze) {
2605
+ CompilerError.throwTodo({
2606
+ reason: 'Support spread syntax for hook arguments',
2607
+ loc: arg.place.loc,
2608
+ });
2609
+ }
2610
+ // effects[i] is Effect.Capture | Effect.Read | Effect.Store
2611
+ return Effect.ConditionallyMutateIterator;
2612
+ }
2613
+ } else {
2614
+ return Effect.ConditionallyMutate;
2615
+ }
2616
+}
2617
+
2618
+export function getFunctionCallSignature(
2619
+ env: Environment,
2620
+ type: Type,
2621
+): FunctionSignature | null {
2622
+ if (type.kind !== 'Function') {
2623
+ return null;
2624
+ }
2625
+ return env.getFunctionSignature(type);
2626
+}
2627
+
2628
+export function isKnownMutableEffect(effect: Effect): boolean {
2629
+ switch (effect) {
2630
+ case Effect.Store:
2631
+ case Effect.ConditionallyMutate:
2632
+ case Effect.ConditionallyMutateIterator:
2633
+ case Effect.Mutate: {
2634
+ return true;
2635
+ }
2636
+
2637
+ case Effect.Unknown: {
2638
+ CompilerError.invariant(false, {
2639
+ reason: 'Unexpected unknown effect',
2640
+ description: null,
2641
+ loc: GeneratedSource,
2642
+ suggestions: null,
2643
+ });
2644
+ }
2645
+ case Effect.Read:
2646
+ case Effect.Capture:
2647
+ case Effect.Freeze: {
2648
+ return false;
2649
+ }
2650
+ default: {
2651
+ assertExhaustive(effect, `Unexpected effect \`${effect}\``);
2652
+ }
2653
+ }
2654
+}
2655
+
2656
+/**
2657
+ * Joins two values using the following rules:
2658
+ * == Effect Transitions ==
2659
+ *
2660
+ * Freezing an immutable value has not effect:
2661
+ * ┌───────────────┐
2662
+ * │ │
2663
+ * ▼ │ Freeze
2664
+ * ┌──────────────────────────┐ │
2665
+ * │ Immutable │──┘
2666
+ * └──────────────────────────┘
2667
+ *
2668
+ * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
2669
+ * value has no effect:
2670
+ * ┌───────────────┐
2671
+ * ┌─────────────────────────┐ Freeze │ │
2672
+ * │ MaybeFrozen │────┐ ▼ │ Freeze
2673
+ * └─────────────────────────┘ │ ┌──────────────────────────┐ │
2674
+ * ├────▶│ Frozen │──┘
2675
+ * │ └──────────────────────────┘
2676
+ * ┌─────────────────────────┐ │
2677
+ * │ Mutable │────┘
2678
+ * └─────────────────────────┘
2679
+ *
2680
+ * == Join Lattice ==
2681
+ * - immutable | mutable => mutable
2682
+ * The justification is that immutable and mutable values are different types,
2683
+ * and functions can introspect them to tell the difference (if the argument
2684
+ * is null return early, else if its an object mutate it).
2685
+ * - frozen | mutable => maybe-frozen
2686
+ * Frozen values are indistinguishable from mutable values at runtime, so callers
2687
+ * cannot dynamically avoid mutation of "frozen" values. If a value could be
2688
+ * frozen we have to distinguish it from a mutable value. But it also isn't known
2689
+ * frozen yet, so we distinguish as maybe-frozen.
2690
+ * - immutable | frozen => frozen
2691
+ * This is subtle and falls out of the above rules. If a value could be any of
2692
+ * immutable, mutable, or frozen, then at runtime it could either be a primitive
2693
+ * or a reference type, and callers can't distinguish frozen or not for reference
2694
+ * types. To ensure that any sequence of joins btw those three states yields the
2695
+ * correct maybe-frozen, these two have to produce a frozen value.
2696
+ * - <any> | maybe-frozen => maybe-frozen
2697
+ * - immutable | context => context
2698
+ * - mutable | context => context
2699
+ * - frozen | context => maybe-frozen
2700
+ *
2701
+ * ┌──────────────────────────┐
2702
+ * │ Immutable │───┐
2703
+ * └──────────────────────────┘ │
2704
+ * │ ┌─────────────────────────┐
2705
+ * ├───▶│ Frozen │──┐
2706
+ * ┌──────────────────────────┐ │ └─────────────────────────┘ │
2707
+ * │ Frozen │───┤ │ ┌─────────────────────────┐
2708
+ * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
2709
+ * │ ┌─────────────────────────┐ │ └─────────────────────────┘
2710
+ * ├───▶│ MaybeFrozen │──┘
2711
+ * ┌──────────────────────────┐ │ └─────────────────────────┘
2712
+ * │ Mutable │───┘
2713
+ * └──────────────────────────┘
2714
+ */
2715
+function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
2716
+ if (a === b) {
2717
+ return a;
2718
+ } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
2719
+ return ValueKind.MaybeFrozen;
2720
+ // after this a and b differ and neither are MaybeFrozen
2721
+ } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
2722
+ if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
2723
+ // frozen | mutable
2724
+ return ValueKind.MaybeFrozen;
2725
+ } else if (a === ValueKind.Context || b === ValueKind.Context) {
2726
+ // context | mutable
2727
+ return ValueKind.Context;
2728
+ } else {
2729
+ // mutable | immutable
2730
+ return ValueKind.Mutable;
2731
+ }
2732
+ } else if (a === ValueKind.Context || b === ValueKind.Context) {
2733
+ if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
2734
+ // frozen | context
2735
+ return ValueKind.MaybeFrozen;
2736
+ } else {
2737
+ // context | immutable
2738
+ return ValueKind.Context;
2739
+ }
2740
+ } else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
2741
+ return ValueKind.Frozen;
2742
+ } else if (a === ValueKind.Global || b === ValueKind.Global) {
2743
+ return ValueKind.Global;
2744
+ } else {
2745
+ CompilerError.invariant(
2746
+ a === ValueKind.Primitive && b == ValueKind.Primitive,
2747
+ {
2748
+ reason: `Unexpected value kind in mergeValues()`,
2749
+ description: `Found kinds ${a} and ${b}`,
2750
+ loc: GeneratedSource,
2751
+ },
2752
+ );
2753
+ return ValueKind.Primitive;
2754
+ }
2755
+}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
deleted
-2125
@@ -1,2125 +0,0 @@
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 {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
9
-import {Environment} from '../HIR';
10
-import {
11
- AbstractValue,
12
- BasicBlock,
13
- BlockId,
14
- CallExpression,
15
- NewExpression,
16
- Effect,
17
- FunctionEffect,
18
- GeneratedSource,
19
- HIRFunction,
20
- IdentifierId,
21
- InstructionKind,
22
- InstructionValue,
23
- MethodCall,
24
- Phi,
25
- Place,
26
- SpreadPattern,
27
- TInstruction,
28
- Type,
29
- ValueKind,
30
- ValueReason,
31
- isArrayType,
32
- isMapType,
33
- isMutableEffect,
34
- isObjectType,
35
- isSetType,
36
-} from '../HIR/HIR';
37
-import {FunctionSignature} from '../HIR/ObjectShape';
38
-import {
39
- printIdentifier,
40
- printMixedHIR,
41
- printPlace,
42
- printSourceLocation,
43
-} from '../HIR/PrintHIR';
44
-import {
45
- eachInstructionOperand,
46
- eachInstructionValueOperand,
47
- eachPatternOperand,
48
- eachTerminalOperand,
49
- eachTerminalSuccessor,
50
-} from '../HIR/visitors';
51
-import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
52
-import {
53
- inferTerminalFunctionEffects,
54
- inferInstructionFunctionEffects,
55
- transformFunctionEffectErrors,
56
-} from './InferFunctionEffects';
57
-
58
-const UndefinedValue: InstructionValue = {
59
- kind: 'Primitive',
60
- loc: GeneratedSource,
61
- value: undefined,
62
-};
63
-
64
-/*
65
- * For every usage of a value in the given function, infers the effect or action
66
- * taken at that reference. Each reference is inferred as exactly one of:
67
- * - freeze: this usage freezes the value, ie converts it to frozen. This is only inferred
68
- * when the value *may* not already be frozen.
69
- * - frozen: the value is known to already be "owned" by React and is therefore already
70
- * frozen (permanently and transitively immutable).
71
- * - immutable: the value is not owned by React, but is known to be an immutable value
72
- * that therefore cannot ever change.
73
- * - readonly: the value is not frozen or immutable, but this usage of the value does
74
- * not modify it. the value may be mutated by a subsequent reference. Examples include
75
- * referencing the operands of a binary expression, or referencing the items/properties
76
- * of an array or object literal.
77
- * - mutable: the value is not frozen or immutable, and this usage *may* modify it.
78
- * Examples include passing a value to as a function argument or assigning into an object.
79
- *
80
- * Note that the inference follows variable assignment, so assigning a frozen value
81
- * to a different value will infer usages of the other variable as frozen as well.
82
- *
83
- * The inference assumes that the code follows the rules of React:
84
- * - React function arguments are frozen (component props, hook arguments).
85
- * - Hook arguments are frozen at the point the hook is invoked.
86
- * - React function return values are frozen at the point of being returned,
87
- * thus the return value of a hook call is frozen.
88
- * - JSX represents invocation of a React function (the component) and
89
- * therefore all values passed to JSX become frozen at the point the JSX
90
- * is created.
91
- *
92
- * Internally, the inference tracks the approximate type of value held by each variable,
93
- * and iterates over the control flow graph. The inferred effect of reach reference is
94
- * a combination of the operation performed (ie, assignment into an object mutably uses the
95
- * object; an if condition reads the condition) and the type of the value. The types of values
96
- * are:
97
- * - frozen: can be any type so long as the value is known to be owned by React, permanently
98
- * and transitively immutable
99
- * - maybe-frozen: the value may or may not be frozen, conditionally depending on control flow.
100
- * - immutable: a type with value semantics: primitives, records/tuples when standardized.
101
- * - mutable: a type with reference semantics eg array, object, class instance, etc.
102
- *
103
- * When control flow paths converge the types of values are merged together, with the value
104
- * types forming a lattice to ensure convergence.
105
- */
106
-export default function inferReferenceEffects(
107
- fn: HIRFunction,
108
- options: {isFunctionExpression: boolean} = {isFunctionExpression: false},
109
-): Array<CompilerErrorDetailOptions> {
110
- /*
111
- * Initial state contains function params
112
- * TODO: include module declarations here as well
113
- */
114
- const initialState = InferenceState.empty(
115
- fn.env,
116
- options.isFunctionExpression,
117
- );
118
- const value: InstructionValue = {
119
- kind: 'Primitive',
120
- loc: fn.loc,
121
- value: undefined,
122
- };
123
- initialState.initialize(value, {
124
- kind: ValueKind.Frozen,
125
- reason: new Set([ValueReason.Other]),
126
- context: new Set(),
127
- });
128
-
129
- for (const ref of fn.context) {
130
- // TODO(gsn): This is a hack.
131
- const value: InstructionValue = {
132
- kind: 'ObjectExpression',
133
- properties: [],
134
- loc: ref.loc,
135
- };
136
- initialState.initialize(value, {
137
- kind: ValueKind.Context,
138
- reason: new Set([ValueReason.Other]),
139
- context: new Set([ref]),
140
- });
141
- initialState.define(ref, value);
142
- }
143
-
144
- const paramKind: AbstractValue = options.isFunctionExpression
145
- ? {
146
- kind: ValueKind.Mutable,
147
- reason: new Set([ValueReason.Other]),
148
- context: new Set(),
149
- }
150
- : {
151
- kind: ValueKind.Frozen,
152
- reason: new Set([ValueReason.ReactiveFunctionArgument]),
153
- context: new Set(),
154
- };
155
-
156
- if (fn.fnType === 'Component') {
157
- CompilerError.invariant(fn.params.length <= 2, {
158
- reason:
159
- 'Expected React component to have not more than two parameters: one for props and for ref',
160
- description: null,
161
- loc: fn.loc,
162
- suggestions: null,
163
- });
164
- const [props, ref] = fn.params;
165
- let value: InstructionValue;
166
- let place: Place;
167
- if (props) {
168
- inferParam(props, initialState, paramKind);
169
- }
170
- if (ref) {
171
- if (ref.kind === 'Identifier') {
172
- place = ref;
173
- value = {
174
- kind: 'ObjectExpression',
175
- properties: [],
176
- loc: ref.loc,
177
- };
178
- } else {
179
- place = ref.place;
180
- value = {
181
- kind: 'ObjectExpression',
182
- properties: [],
183
- loc: ref.place.loc,
184
- };
185
- }
186
- initialState.initialize(value, {
187
- kind: ValueKind.Mutable,
188
- reason: new Set([ValueReason.Other]),
189
- context: new Set(),
190
- });
191
- initialState.define(place, value);
192
- }
193
- } else {
194
- for (const param of fn.params) {
195
- inferParam(param, initialState, paramKind);
196
- }
197
- }
198
-
199
- // Map of blocks to the last (merged) incoming state that was processed
200
- const statesByBlock: Map<BlockId, InferenceState> = new Map();
201
-
202
- /*
203
- * Multiple predecessors may be visited prior to reaching a given successor,
204
- * so track the list of incoming state for each successor block.
205
- * These are merged when reaching that block again.
206
- */
207
- const queuedStates: Map<BlockId, InferenceState> = new Map();
208
- function queue(blockId: BlockId, state: InferenceState): void {
209
- let queuedState = queuedStates.get(blockId);
210
- if (queuedState != null) {
211
- // merge the queued states for this block
212
- state = queuedState.merge(state) ?? queuedState;
213
- queuedStates.set(blockId, state);
214
- } else {
215
- /*
216
- * this is the first queued state for this block, see whether
217
- * there are changed relative to the last time it was processed.
218
- */
219
- const prevState = statesByBlock.get(blockId);
220
- const nextState = prevState != null ? prevState.merge(state) : state;
221
- if (nextState != null) {
222
- queuedStates.set(blockId, nextState);
223
- }
224
- }
225
- }
226
- queue(fn.body.entry, initialState);
227
-
228
- const functionEffects: Array<FunctionEffect> = fn.effects ?? [];
229
-
230
- while (queuedStates.size !== 0) {
231
- for (const [blockId, block] of fn.body.blocks) {
232
- const incomingState = queuedStates.get(blockId);
233
- queuedStates.delete(blockId);
234
- if (incomingState == null) {
235
- continue;
236
- }
237
-
238
- statesByBlock.set(blockId, incomingState);
239
- const state = incomingState.clone();
240
- inferBlock(fn.env, state, block, functionEffects);
241
-
242
- for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
243
- queue(nextBlockId, state);
244
- }
245
- }
246
- }
247
-
248
- if (options.isFunctionExpression) {
249
- fn.effects = functionEffects;
250
- return [];
251
- } else {
252
- return transformFunctionEffectErrors(functionEffects);
253
- }
254
-}
255
-
256
-type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
257
-
258
-// Maintains a mapping of top-level variables to the kind of value they hold
259
-class InferenceState {
260
- env: Environment;
261
- #isFunctionExpression: boolean;
262
-
263
- // The kind of each value, based on its allocation site
264
- #values: Map<InstructionValue, AbstractValue>;
265
- /*
266
- * The set of values pointed to by each identifier. This is a set
267
- * to accomodate phi points (where a variable may have different
268
- * values from different control flow paths).
269
- */
270
- #variables: Map<IdentifierId, Set<InstructionValue>>;
271
-
272
- constructor(
273
- env: Environment,
274
- isFunctionExpression: boolean,
275
- values: Map<InstructionValue, AbstractValue>,
276
- variables: Map<IdentifierId, Set<InstructionValue>>,
277
- ) {
278
- this.env = env;
279
- this.#isFunctionExpression = isFunctionExpression;
280
- this.#values = values;
281
- this.#variables = variables;
282
- }
283
-
284
- static empty(
285
- env: Environment,
286
- isFunctionExpression: boolean,
287
- ): InferenceState {
288
- return new InferenceState(env, isFunctionExpression, new Map(), new Map());
289
- }
290
-
291
- get isFunctionExpression(): boolean {
292
- return this.#isFunctionExpression;
293
- }
294
-
295
- // (Re)initializes a @param value with its default @param kind.
296
- initialize(value: InstructionValue, kind: AbstractValue): void {
297
- CompilerError.invariant(value.kind !== 'LoadLocal', {
298
- reason:
299
- 'Expected all top-level identifiers to be defined as variables, not values',
300
- description: null,
301
- loc: value.loc,
302
- suggestions: null,
303
- });
304
- this.#values.set(value, kind);
305
- }
306
-
307
- values(place: Place): Array<InstructionValue> {
308
- const values = this.#variables.get(place.identifier.id);
309
- CompilerError.invariant(values != null, {
310
- reason: `[hoisting] Expected value kind to be initialized`,
311
- description: `${printPlace(place)}`,
312
- loc: place.loc,
313
- suggestions: null,
314
- });
315
- return Array.from(values);
316
- }
317
-
318
- // Lookup the kind of the given @param value.
319
- kind(place: Place): AbstractValue {
320
- const values = this.#variables.get(place.identifier.id);
321
- CompilerError.invariant(values != null, {
322
- reason: `[hoisting] Expected value kind to be initialized`,
323
- description: `${printPlace(place)}`,
324
- loc: place.loc,
325
- suggestions: null,
326
- });
327
- let mergedKind: AbstractValue | null = null;
328
- for (const value of values) {
329
- const kind = this.#values.get(value)!;
330
- mergedKind =
331
- mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind;
332
- }
333
- CompilerError.invariant(mergedKind !== null, {
334
- reason: `InferReferenceEffects::kind: Expected at least one value`,
335
- description: `No value found at \`${printPlace(place)}\``,
336
- loc: place.loc,
337
- suggestions: null,
338
- });
339
- return mergedKind;
340
- }
341
-
342
- // Updates the value at @param place to point to the same value as @param value.
343
- alias(place: Place, value: Place): void {
344
- const values = this.#variables.get(value.identifier.id);
345
- CompilerError.invariant(values != null, {
346
- reason: `[hoisting] Expected value for identifier to be initialized`,
347
- description: `${printIdentifier(value.identifier)}`,
348
- loc: value.loc,
349
- suggestions: null,
350
- });
351
- this.#variables.set(place.identifier.id, new Set(values));
352
- }
353
-
354
- // Defines (initializing or updating) a variable with a specific kind of value.
355
- define(place: Place, value: InstructionValue): void {
356
- CompilerError.invariant(this.#values.has(value), {
357
- reason: `Expected value to be initialized at '${printSourceLocation(
358
- value.loc,
359
- )}'`,
360
- description: null,
361
- loc: value.loc,
362
- suggestions: null,
363
- });
364
- this.#variables.set(place.identifier.id, new Set([value]));
365
- }
366
-
367
- isDefined(place: Place): boolean {
368
- return this.#variables.has(place.identifier.id);
369
- }
370
-
371
- /*
372
- * Records that a given Place was accessed with the given kind and:
373
- * - Updates the effect of @param place based on the kind of value
374
- * and the kind of reference (@param effectKind).
375
- * - Updates the value kind to reflect the effect of the reference.
376
- *
377
- * Notably, a mutable reference is downgraded to readonly if the
378
- * value unless the value is known to be mutable.
379
- *
380
- * Similarly, a freeze reference is converted to readonly if the
381
- * value is already frozen or is immutable.
382
- */
383
- referenceAndRecordEffects(
384
- freezeActions: Array<FreezeAction>,
385
- place: Place,
386
- effectKind: Effect,
387
- reason: ValueReason,
388
- ): void {
389
- const values = this.#variables.get(place.identifier.id);
390
- if (values === undefined) {
391
- CompilerError.invariant(effectKind !== Effect.Store, {
392
- reason: '[InferReferenceEffects] Unhandled store reference effect',
393
- description: null,
394
- loc: place.loc,
395
- suggestions: null,
396
- });
397
- place.effect =
398
- effectKind === Effect.ConditionallyMutate
399
- ? Effect.ConditionallyMutate
400
- : Effect.Read;
401
- return;
402
- }
403
-
404
- const action = this.reference(place, effectKind, reason);
405
- action && freezeActions.push(action);
406
- }
407
-
408
- freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
409
- for (const value of values) {
410
- if (
411
- value.kind === 'DeclareContext' ||
412
- (value.kind === 'StoreContext' &&
413
- (value.lvalue.kind === InstructionKind.Let ||
414
- value.lvalue.kind === InstructionKind.Const))
415
- ) {
416
- /**
417
- * Avoid freezing context variable declarations, hoisted or otherwise
418
- * function Component() {
419
- * const cb = useBar(() => foo(2)); // produces a hoisted context declaration
420
- * const foo = useFoo(); // reassigns to the context variable
421
- * return <Foo cb={cb} />;
422
- * }
423
- */
424
- continue;
425
- }
426
- this.#values.set(value, {
427
- kind: ValueKind.Frozen,
428
- reason,
429
- context: new Set(),
430
- });
431
- if (
432
- value.kind === 'FunctionExpression' &&
433
- (this.env.config.enablePreserveExistingMemoizationGuarantees ||
434
- this.env.config.enableTransitivelyFreezeFunctionExpressions)
435
- ) {
436
- for (const operand of value.loweredFunc.func.context) {
437
- const operandValues = this.#variables.get(operand.identifier.id);
438
- if (operandValues !== undefined) {
439
- this.freezeValues(operandValues, reason);
440
- }
441
- }
442
- }
443
- }
444
- }
445
-
446
- reference(
447
- place: Place,
448
- effectKind: Effect,
449
- reason: ValueReason,
450
- ): null | FreezeAction {
451
- const values = this.#variables.get(place.identifier.id);
452
- CompilerError.invariant(values !== undefined, {
453
- reason: '[InferReferenceEffects] Expected value to be initialized',
454
- description: null,
455
- loc: place.loc,
456
- suggestions: null,
457
- });
458
- let valueKind: AbstractValue | null = this.kind(place);
459
- let effect: Effect | null = null;
460
- let freeze: null | FreezeAction = null;
461
- switch (effectKind) {
462
- case Effect.Freeze: {
463
- if (
464
- valueKind.kind === ValueKind.Mutable ||
465
- valueKind.kind === ValueKind.Context ||
466
- valueKind.kind === ValueKind.MaybeFrozen
467
- ) {
468
- const reasonSet = new Set([reason]);
469
- effect = Effect.Freeze;
470
- valueKind = {
471
- kind: ValueKind.Frozen,
472
- reason: reasonSet,
473
- context: new Set(),
474
- };
475
- freeze = {values, reason: reasonSet};
476
- } else {
477
- effect = Effect.Read;
478
- }
479
- break;
480
- }
481
- case Effect.ConditionallyMutate: {
482
- if (
483
- valueKind.kind === ValueKind.Mutable ||
484
- valueKind.kind === ValueKind.Context
485
- ) {
486
- effect = Effect.ConditionallyMutate;
487
- } else {
488
- effect = Effect.Read;
489
- }
490
- break;
491
- }
492
- case Effect.ConditionallyMutateIterator: {
493
- if (
494
- valueKind.kind === ValueKind.Mutable ||
495
- valueKind.kind === ValueKind.Context
496
- ) {
497
- if (
498
- isArrayType(place.identifier) ||
499
- isSetType(place.identifier) ||
500
- isMapType(place.identifier)
501
- ) {
502
- effect = Effect.Capture;
503
- } else {
504
- effect = Effect.ConditionallyMutate;
505
- }
506
- } else {
507
- effect = Effect.Read;
508
- }
509
- break;
510
- }
511
- case Effect.Mutate: {
512
- effect = Effect.Mutate;
513
- break;
514
- }
515
- case Effect.Store: {
516
- /*
517
- * TODO(gsn): This should be bailout once we add bailout infra.
518
- *
519
- * invariant(
520
- * valueKind.kind === ValueKindKind.Mutable,
521
- * `expected valueKind to be 'Mutable' but found to be \`${valueKind}\``
522
- * );
523
- */
524
- effect = isObjectType(place.identifier) ? Effect.Store : Effect.Mutate;
525
- break;
526
- }
527
- case Effect.Capture: {
528
- if (
529
- valueKind.kind === ValueKind.Primitive ||
530
- valueKind.kind === ValueKind.Global ||
531
- valueKind.kind === ValueKind.Frozen ||
532
- valueKind.kind === ValueKind.MaybeFrozen
533
- ) {
534
- effect = Effect.Read;
535
- } else {
536
- effect = Effect.Capture;
537
- }
538
- break;
539
- }
540
- case Effect.Read: {
541
- effect = Effect.Read;
542
- break;
543
- }
544
- case Effect.Unknown: {
545
- CompilerError.invariant(false, {
546
- reason:
547
- 'Unexpected unknown effect, expected to infer a precise effect kind',
548
- description: null,
549
- loc: place.loc,
550
- suggestions: null,
551
- });
552
- }
553
- default: {
554
- assertExhaustive(
555
- effectKind,
556
- `Unexpected reference kind \`${effectKind as any as string}\``,
557
- );
558
- }
559
- }
560
- CompilerError.invariant(effect !== null, {
561
- reason: 'Expected effect to be set',
562
- description: null,
563
- loc: place.loc,
564
- suggestions: null,
565
- });
566
- place.effect = effect;
567
- return freeze;
568
- }
569
-
570
- /*
571
- * Combine the contents of @param this and @param other, returning a new
572
- * instance with the combined changes _if_ there are any changes, or
573
- * returning null if no changes would occur. Changes include:
574
- * - new entries in @param other that did not exist in @param this
575
- * - entries whose values differ in @param this and @param other,
576
- * and where joining the values produces a different value than
577
- * what was in @param this.
578
- *
579
- * Note that values are joined using a lattice operation to ensure
580
- * termination.
581
- */
582
- merge(other: InferenceState): InferenceState | null {
583
- let nextValues: Map<InstructionValue, AbstractValue> | null = null;
584
- let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
585
-
586
- for (const [id, thisValue] of this.#values) {
587
- const otherValue = other.#values.get(id);
588
- if (otherValue !== undefined) {
589
- const mergedValue = mergeAbstractValues(thisValue, otherValue);
590
- if (mergedValue !== thisValue) {
591
- nextValues = nextValues ?? new Map(this.#values);
592
- nextValues.set(id, mergedValue);
593
- }
594
- }
595
- }
596
- for (const [id, otherValue] of other.#values) {
597
- if (this.#values.has(id)) {
598
- // merged above
599
- continue;
600
- }
601
- nextValues = nextValues ?? new Map(this.#values);
602
- nextValues.set(id, otherValue);
603
- }
604
-
605
- for (const [id, thisValues] of this.#variables) {
606
- const otherValues = other.#variables.get(id);
607
- if (otherValues !== undefined) {
608
- let mergedValues: Set<InstructionValue> | null = null;
609
- for (const otherValue of otherValues) {
610
- if (!thisValues.has(otherValue)) {
611
- mergedValues = mergedValues ?? new Set(thisValues);
612
- mergedValues.add(otherValue);
613
- }
614
- }
615
- if (mergedValues !== null) {
616
- nextVariables = nextVariables ?? new Map(this.#variables);
617
- nextVariables.set(id, mergedValues);
618
- }
619
- }
620
- }
621
- for (const [id, otherValues] of other.#variables) {
622
- if (this.#variables.has(id)) {
623
- continue;
624
- }
625
- nextVariables = nextVariables ?? new Map(this.#variables);
626
- nextVariables.set(id, new Set(otherValues));
627
- }
628
-
629
- if (nextVariables === null && nextValues === null) {
630
- return null;
631
- } else {
632
- return new InferenceState(
633
- this.env,
634
- this.#isFunctionExpression,
635
- nextValues ?? new Map(this.#values),
636
- nextVariables ?? new Map(this.#variables),
637
- );
638
- }
639
- }
640
-
641
- /*
642
- * Returns a copy of this state.
643
- * TODO: consider using persistent data structures to make
644
- * clone cheaper.
645
- */
646
- clone(): InferenceState {
647
- return new InferenceState(
648
- this.env,
649
- this.#isFunctionExpression,
650
- new Map(this.#values),
651
- new Map(this.#variables),
652
- );
653
- }
654
-
655
- /*
656
- * For debugging purposes, dumps the state to a plain
657
- * object so that it can printed as JSON.
658
- */
659
- debug(): any {
660
- const result: any = {values: {}, variables: {}};
661
- const objects: Map<InstructionValue, number> = new Map();
662
- function identify(value: InstructionValue): number {
663
- let id = objects.get(value);
664
- if (id == null) {
665
- id = objects.size;
666
- objects.set(value, id);
667
- }
668
- return id;
669
- }
670
- for (const [value, kind] of this.#values) {
671
- const id = identify(value);
672
- result.values[id] = {kind, value: printMixedHIR(value)};
673
- }
674
- for (const [variable, values] of this.#variables) {
675
- result.variables[`$${variable}`] = [...values].map(identify);
676
- }
677
- return result;
678
- }
679
-
680
- inferPhi(phi: Phi): void {
681
- const values: Set<InstructionValue> = new Set();
682
- for (const [_, operand] of phi.operands) {
683
- const operandValues = this.#variables.get(operand.identifier.id);
684
- // This is a backedge that will be handled later by State.merge
685
- if (operandValues === undefined) continue;
686
- for (const v of operandValues) {
687
- values.add(v);
688
- }
689
- }
690
-
691
- if (values.size > 0) {
692
- this.#variables.set(phi.place.identifier.id, values);
693
- }
694
- }
695
-}
696
-
697
-function inferParam(
698
- param: Place | SpreadPattern,
699
- initialState: InferenceState,
700
- paramKind: AbstractValue,
701
-): void {
702
- let value: InstructionValue;
703
- let place: Place;
704
- if (param.kind === 'Identifier') {
705
- place = param;
706
- value = {
707
- kind: 'Primitive',
708
- loc: param.loc,
709
- value: undefined,
710
- };
711
- } else {
712
- place = param.place;
713
- value = {
714
- kind: 'Primitive',
715
- loc: param.place.loc,
716
- value: undefined,
717
- };
718
- }
719
- initialState.initialize(value, paramKind);
720
- initialState.define(place, value);
721
-}
722
-
723
-/*
724
- * Joins two values using the following rules:
725
- * == Effect Transitions ==
726
- *
727
- * Freezing an immutable value has not effect:
728
- * ┌───────────────┐
729
- * │ │
730
- * ▼ │ Freeze
731
- * ┌──────────────────────────┐ │
732
- * │ Immutable │──┘
733
- * └──────────────────────────┘
734
- *
735
- * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
736
- * value has no effect:
737
- * ┌───────────────┐
738
- * ┌─────────────────────────┐ Freeze │ │
739
- * │ MaybeFrozen │────┐ ▼ │ Freeze
740
- * └─────────────────────────┘ │ ┌──────────────────────────┐ │
741
- * ├────▶│ Frozen │──┘
742
- * │ └──────────────────────────┘
743
- * ┌─────────────────────────┐ │
744
- * │ Mutable │────┘
745
- * └─────────────────────────┘
746
- *
747
- * == Join Lattice ==
748
- * - immutable | mutable => mutable
749
- * The justification is that immutable and mutable values are different types,
750
- * and functions can introspect them to tell the difference (if the argument
751
- * is null return early, else if its an object mutate it).
752
- * - frozen | mutable => maybe-frozen
753
- * Frozen values are indistinguishable from mutable values at runtime, so callers
754
- * cannot dynamically avoid mutation of "frozen" values. If a value could be
755
- * frozen we have to distinguish it from a mutable value. But it also isn't known
756
- * frozen yet, so we distinguish as maybe-frozen.
757
- * - immutable | frozen => frozen
758
- * This is subtle and falls out of the above rules. If a value could be any of
759
- * immutable, mutable, or frozen, then at runtime it could either be a primitive
760
- * or a reference type, and callers can't distinguish frozen or not for reference
761
- * types. To ensure that any sequence of joins btw those three states yields the
762
- * correct maybe-frozen, these two have to produce a frozen value.
763
- * - <any> | maybe-frozen => maybe-frozen
764
- * - immutable | context => context
765
- * - mutable | context => context
766
- * - frozen | context => maybe-frozen
767
- *
768
- * ┌──────────────────────────┐
769
- * │ Immutable │───┐
770
- * └──────────────────────────┘ │
771
- * │ ┌─────────────────────────┐
772
- * ├───▶│ Frozen │──┐
773
- * ┌──────────────────────────┐ │ └─────────────────────────┘ │
774
- * │ Frozen │───┤ │ ┌─────────────────────────┐
775
- * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
776
- * │ ┌─────────────────────────┐ │ └─────────────────────────┘
777
- * ├───▶│ MaybeFrozen │──┘
778
- * ┌──────────────────────────┐ │ └─────────────────────────┘
779
- * │ Mutable │───┘
780
- * └──────────────────────────┘
781
- */
782
-export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
783
- if (a === b) {
784
- return a;
785
- } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
786
- return ValueKind.MaybeFrozen;
787
- // after this a and b differ and neither are MaybeFrozen
788
- } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
789
- if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
790
- // frozen | mutable
791
- return ValueKind.MaybeFrozen;
792
- } else if (a === ValueKind.Context || b === ValueKind.Context) {
793
- // context | mutable
794
- return ValueKind.Context;
795
- } else {
796
- // mutable | immutable
797
- return ValueKind.Mutable;
798
- }
799
- } else if (a === ValueKind.Context || b === ValueKind.Context) {
800
- if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
801
- // frozen | context
802
- return ValueKind.MaybeFrozen;
803
- } else {
804
- // context | immutable
805
- return ValueKind.Context;
806
- }
807
- } else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
808
- return ValueKind.Frozen;
809
- } else if (a === ValueKind.Global || b === ValueKind.Global) {
810
- return ValueKind.Global;
811
- } else {
812
- CompilerError.invariant(
813
- a === ValueKind.Primitive && b == ValueKind.Primitive,
814
- {
815
- reason: `Unexpected value kind in mergeValues()`,
816
- description: `Found kinds ${a} and ${b}`,
817
- loc: GeneratedSource,
818
- },
819
- );
820
- return ValueKind.Primitive;
821
- }
822
-}
823
-
824
-function mergeAbstractValues(
825
- a: AbstractValue,
826
- b: AbstractValue,
827
-): AbstractValue {
828
- const kind = mergeValueKinds(a.kind, b.kind);
829
- if (
830
- kind === a.kind &&
831
- kind === b.kind &&
832
- Set_isSuperset(a.reason, b.reason) &&
833
- Set_isSuperset(a.context, b.context)
834
- ) {
835
- return a;
836
- }
837
- const reason = new Set(a.reason);
838
- for (const r of b.reason) {
839
- reason.add(r);
840
- }
841
- const context = new Set(a.context);
842
- for (const c of b.context) {
843
- context.add(c);
844
- }
845
- return {kind, reason, context};
846
-}
847
-
848
-type Continuation =
849
- | {
850
- kind: 'initialize';
851
- valueKind: AbstractValue;
852
- effect: {kind: Effect; reason: ValueReason} | null;
853
- lvalueEffect?: Effect;
854
- }
855
- | {kind: 'funeffects'};
856
-
857
-/*
858
- * Iterates over the given @param block, defining variables and
859
- * recording references on the @param state according to JS semantics.
860
- */
861
-function inferBlock(
862
- env: Environment,
863
- state: InferenceState,
864
- block: BasicBlock,
865
- functionEffects: Array<FunctionEffect>,
866
-): void {
867
- for (const phi of block.phis) {
868
- state.inferPhi(phi);
869
- }
870
-
871
- for (const instr of block.instructions) {
872
- const instrValue = instr.value;
873
- const defaultLvalueEffect = Effect.ConditionallyMutate;
874
- let continuation: Continuation;
875
- const freezeActions: Array<FreezeAction> = [];
876
- switch (instrValue.kind) {
877
- case 'BinaryExpression': {
878
- continuation = {
879
- kind: 'initialize',
880
- valueKind: {
881
- kind: ValueKind.Primitive,
882
- reason: new Set([ValueReason.Other]),
883
- context: new Set(),
884
- },
885
- effect: {
886
- kind: Effect.Read,
887
- reason: ValueReason.Other,
888
- },
889
- };
890
- break;
891
- }
892
- case 'ArrayExpression': {
893
- const contextRefOperands = getContextRefOperand(state, instrValue);
894
- const valueKind: AbstractValue =
895
- contextRefOperands.length > 0
896
- ? {
897
- kind: ValueKind.Context,
898
- reason: new Set([ValueReason.Other]),
899
- context: new Set(contextRefOperands),
900
- }
901
- : {
902
- kind: ValueKind.Mutable,
903
- reason: new Set([ValueReason.Other]),
904
- context: new Set(),
905
- };
906
-
907
- for (const element of instrValue.elements) {
908
- if (element.kind === 'Spread') {
909
- state.referenceAndRecordEffects(
910
- freezeActions,
911
- element.place,
912
- Effect.ConditionallyMutateIterator,
913
- ValueReason.Other,
914
- );
915
- } else if (element.kind === 'Identifier') {
916
- state.referenceAndRecordEffects(
917
- freezeActions,
918
- element,
919
- Effect.Capture,
920
- ValueReason.Other,
921
- );
922
- } else {
923
- let _: 'Hole' = element.kind;
924
- }
925
- }
926
- state.initialize(instrValue, valueKind);
927
- state.define(instr.lvalue, instrValue);
928
- instr.lvalue.effect = Effect.Store;
929
- continuation = {
930
- kind: 'funeffects',
931
- };
932
- break;
933
- }
934
- case 'NewExpression': {
935
- inferCallEffects(
936
- state,
937
- instr as TInstruction<NewExpression>,
938
- freezeActions,
939
- getFunctionCallSignature(env, instrValue.callee.identifier.type),
940
- );
941
- continuation = {kind: 'funeffects'};
942
- break;
943
- }
944
- case 'ObjectExpression': {
945
- const contextRefOperands = getContextRefOperand(state, instrValue);
946
- const valueKind: AbstractValue =
947
- contextRefOperands.length > 0
948
- ? {
949
- kind: ValueKind.Context,
950
- reason: new Set([ValueReason.Other]),
951
- context: new Set(contextRefOperands),
952
- }
953
- : {
954
- kind: ValueKind.Mutable,
955
- reason: new Set([ValueReason.Other]),
956
- context: new Set(),
957
- };
958
-
959
- for (const property of instrValue.properties) {
960
- switch (property.kind) {
961
- case 'ObjectProperty': {
962
- if (property.key.kind === 'computed') {
963
- // Object keys must be primitives, so we know they're frozen at this point
964
- state.referenceAndRecordEffects(
965
- freezeActions,
966
- property.key.name,
967
- Effect.Freeze,
968
- ValueReason.Other,
969
- );
970
- }
971
- // Object construction captures but does not modify the key/property values
972
- state.referenceAndRecordEffects(
973
- freezeActions,
974
- property.place,
975
- Effect.Capture,
976
- ValueReason.Other,
977
- );
978
- break;
979
- }
980
- case 'Spread': {
981
- // Object construction captures but does not modify the key/property values
982
- state.referenceAndRecordEffects(
983
- freezeActions,
984
- property.place,
985
- Effect.Capture,
986
- ValueReason.Other,
987
- );
988
- break;
989
- }
990
- default: {
991
- assertExhaustive(
992
- property,
993
- `Unexpected property kind \`${(property as any).kind}\``,
994
- );
995
- }
996
- }
997
- }
998
-
999
- state.initialize(instrValue, valueKind);
1000
- state.define(instr.lvalue, instrValue);
1001
- instr.lvalue.effect = Effect.Store;
1002
- continuation = {kind: 'funeffects'};
1003
- break;
1004
- }
1005
- case 'UnaryExpression': {
1006
- continuation = {
1007
- kind: 'initialize',
1008
- valueKind: {
1009
- kind: ValueKind.Primitive,
1010
- reason: new Set([ValueReason.Other]),
1011
- context: new Set(),
1012
- },
1013
- effect: {kind: Effect.Read, reason: ValueReason.Other},
1014
- };
1015
- break;
1016
- }
1017
- case 'UnsupportedNode': {
1018
- // TODO: handle other statement kinds
1019
- continuation = {
1020
- kind: 'initialize',
1021
- valueKind: {
1022
- kind: ValueKind.Mutable,
1023
- reason: new Set([ValueReason.Other]),
1024
- context: new Set(),
1025
- },
1026
- effect: null,
1027
- };
1028
- break;
1029
- }
1030
- case 'JsxExpression': {
1031
- if (instrValue.tag.kind === 'Identifier') {
1032
- state.referenceAndRecordEffects(
1033
- freezeActions,
1034
- instrValue.tag,
1035
- Effect.Freeze,
1036
- ValueReason.JsxCaptured,
1037
- );
1038
- }
1039
- if (instrValue.children !== null) {
1040
- for (const child of instrValue.children) {
1041
- state.referenceAndRecordEffects(
1042
- freezeActions,
1043
- child,
1044
- Effect.Freeze,
1045
- ValueReason.JsxCaptured,
1046
- );
1047
- }
1048
- }
1049
- for (const attr of instrValue.props) {
1050
- if (attr.kind === 'JsxSpreadAttribute') {
1051
- state.referenceAndRecordEffects(
1052
- freezeActions,
1053
- attr.argument,
1054
- Effect.Freeze,
1055
- ValueReason.JsxCaptured,
1056
- );
1057
- } else {
1058
- state.referenceAndRecordEffects(
1059
- freezeActions,
1060
- attr.place,
1061
- Effect.Freeze,
1062
- ValueReason.JsxCaptured,
1063
- );
1064
- }
1065
- }
1066
-
1067
- state.initialize(instrValue, {
1068
- kind: ValueKind.Frozen,
1069
- reason: new Set([ValueReason.Other]),
1070
- context: new Set(),
1071
- });
1072
- state.define(instr.lvalue, instrValue);
1073
- instr.lvalue.effect = Effect.ConditionallyMutate;
1074
- continuation = {kind: 'funeffects'};
1075
- break;
1076
- }
1077
- case 'JsxFragment': {
1078
- continuation = {
1079
- kind: 'initialize',
1080
- valueKind: {
1081
- kind: ValueKind.Frozen,
1082
- reason: new Set([ValueReason.Other]),
1083
- context: new Set(),
1084
- },
1085
- effect: {
1086
- kind: Effect.Freeze,
1087
- reason: ValueReason.Other,
1088
- },
1089
- };
1090
- break;
1091
- }
1092
- case 'TemplateLiteral': {
1093
- /*
1094
- * template literal (with no tag function) always produces
1095
- * an immutable string
1096
- */
1097
- continuation = {
1098
- kind: 'initialize',
1099
- valueKind: {
1100
- kind: ValueKind.Primitive,
1101
- reason: new Set([ValueReason.Other]),
1102
- context: new Set(),
1103
- },
1104
- effect: {kind: Effect.Read, reason: ValueReason.Other},
1105
- };
1106
- break;
1107
- }
1108
- case 'RegExpLiteral': {
1109
- // RegExp instances are mutable objects
1110
- continuation = {
1111
- kind: 'initialize',
1112
- valueKind: {
1113
- kind: ValueKind.Mutable,
1114
- reason: new Set([ValueReason.Other]),
1115
- context: new Set(),
1116
- },
1117
- effect: {
1118
- kind: Effect.ConditionallyMutate,
1119
- reason: ValueReason.Other,
1120
- },
1121
- };
1122
- break;
1123
- }
1124
- case 'MetaProperty': {
1125
- if (instrValue.meta !== 'import' || instrValue.property !== 'meta') {
1126
- continuation = {kind: 'funeffects'};
1127
- break;
1128
- }
1129
- continuation = {
1130
- kind: 'initialize',
1131
- valueKind: {
1132
- kind: ValueKind.Global,
1133
- reason: new Set([ValueReason.Global]),
1134
- context: new Set(),
1135
- },
1136
- effect: null,
1137
- };
1138
- break;
1139
- }
1140
- case 'LoadGlobal':
1141
- continuation = {
1142
- kind: 'initialize',
1143
- valueKind: {
1144
- kind: ValueKind.Global,
1145
- reason: new Set([ValueReason.Global]),
1146
- context: new Set(),
1147
- },
1148
- effect: null,
1149
- };
1150
- break;
1151
- case 'Debugger':
1152
- case 'JSXText':
1153
- case 'Primitive': {
1154
- continuation = {
1155
- kind: 'initialize',
1156
- valueKind: {
1157
- kind: ValueKind.Primitive,
1158
- reason: new Set([ValueReason.Other]),
1159
- context: new Set(),
1160
- },
1161
- effect: null,
1162
- };
1163
- break;
1164
- }
1165
- case 'ObjectMethod':
1166
- case 'FunctionExpression': {
1167
- let hasMutableOperand = false;
1168
- for (const operand of eachInstructionOperand(instr)) {
1169
- CompilerError.invariant(operand.effect !== Effect.Unknown, {
1170
- reason: 'Expected fn effects to be populated',
1171
- loc: operand.loc,
1172
- });
1173
- state.referenceAndRecordEffects(
1174
- freezeActions,
1175
- operand,
1176
- operand.effect,
1177
- ValueReason.Other,
1178
- );
1179
- hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
1180
- }
1181
- /*
1182
- * If a closure did not capture any mutable values, then we can consider it to be
1183
- * frozen, which allows it to be independently memoized.
1184
- */
1185
- state.initialize(instrValue, {
1186
- kind: hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen,
1187
- reason: new Set([ValueReason.Other]),
1188
- context: new Set(),
1189
- });
1190
- state.define(instr.lvalue, instrValue);
1191
- instr.lvalue.effect = Effect.Store;
1192
- continuation = {kind: 'funeffects'};
1193
- break;
1194
- }
1195
- case 'TaggedTemplateExpression': {
1196
- const operands = [...eachInstructionValueOperand(instrValue)];
1197
- if (operands.length !== 1) {
1198
- // future-proofing to make sure we update this case when we support interpolation
1199
- CompilerError.throwTodo({
1200
- reason: 'Support tagged template expressions with interpolations',
1201
- loc: instrValue.loc,
1202
- });
1203
- }
1204
- const signature = getFunctionCallSignature(
1205
- env,
1206
- instrValue.tag.identifier.type,
1207
- );
1208
- let calleeEffect =
1209
- signature?.calleeEffect ?? Effect.ConditionallyMutate;
1210
- const returnValueKind: AbstractValue =
1211
- signature !== null
1212
- ? {
1213
- kind: signature.returnValueKind,
1214
- reason: new Set([
1215
- signature.returnValueReason ??
1216
- ValueReason.KnownReturnSignature,
1217
- ]),
1218
- context: new Set(),
1219
- }
1220
- : {
1221
- kind: ValueKind.Mutable,
1222
- reason: new Set([ValueReason.Other]),
1223
- context: new Set(),
1224
- };
1225
- state.referenceAndRecordEffects(
1226
- freezeActions,
1227
- instrValue.tag,
1228
- calleeEffect,
1229
- ValueReason.Other,
1230
- );
1231
- state.initialize(instrValue, returnValueKind);
1232
- state.define(instr.lvalue, instrValue);
1233
- instr.lvalue.effect = Effect.ConditionallyMutate;
1234
- continuation = {kind: 'funeffects'};
1235
- break;
1236
- }
1237
- case 'CallExpression': {
1238
- inferCallEffects(
1239
- state,
1240
- instr as TInstruction<CallExpression>,
1241
- freezeActions,
1242
- getFunctionCallSignature(env, instrValue.callee.identifier.type),
1243
- );
1244
- continuation = {kind: 'funeffects'};
1245
- break;
1246
- }
1247
- case 'MethodCall': {
1248
- CompilerError.invariant(state.isDefined(instrValue.receiver), {
1249
- reason:
1250
- '[InferReferenceEffects] Internal error: receiver of PropertyCall should have been defined by corresponding PropertyLoad',
1251
- description: null,
1252
- loc: instrValue.loc,
1253
- suggestions: null,
1254
- });
1255
- state.referenceAndRecordEffects(
1256
- freezeActions,
1257
- instrValue.property,
1258
- Effect.Read,
1259
- ValueReason.Other,
1260
- );
1261
- inferCallEffects(
1262
- state,
1263
- instr as TInstruction<MethodCall>,
1264
- freezeActions,
1265
- getFunctionCallSignature(env, instrValue.property.identifier.type),
1266
- );
1267
- continuation = {kind: 'funeffects'};
1268
- break;
1269
- }
1270
- case 'PropertyStore': {
1271
- const effect =
1272
- state.kind(instrValue.object).kind === ValueKind.Context
1273
- ? Effect.ConditionallyMutate
1274
- : Effect.Capture;
1275
- state.referenceAndRecordEffects(
1276
- freezeActions,
1277
- instrValue.value,
1278
- effect,
1279
- ValueReason.Other,
1280
- );
1281
- state.referenceAndRecordEffects(
1282
- freezeActions,
1283
- instrValue.object,
1284
- Effect.Store,
1285
- ValueReason.Other,
1286
- );
1287
-
1288
- const lvalue = instr.lvalue;
1289
- state.alias(lvalue, instrValue.value);
1290
- lvalue.effect = Effect.Store;
1291
- continuation = {kind: 'funeffects'};
1292
- break;
1293
- }
1294
- case 'PropertyDelete': {
1295
- // `delete` returns a boolean (immutable) and modifies the object
1296
- continuation = {
1297
- kind: 'initialize',
1298
- valueKind: {
1299
- kind: ValueKind.Primitive,
1300
- reason: new Set([ValueReason.Other]),
1301
- context: new Set(),
1302
- },
1303
- effect: {kind: Effect.Mutate, reason: ValueReason.Other},
1304
- };
1305
- break;
1306
- }
1307
- case 'PropertyLoad': {
1308
- state.referenceAndRecordEffects(
1309
- freezeActions,
1310
- instrValue.object,
1311
- Effect.Read,
1312
- ValueReason.Other,
1313
- );
1314
- const lvalue = instr.lvalue;
1315
- lvalue.effect = Effect.ConditionallyMutate;
1316
- state.initialize(instrValue, state.kind(instrValue.object));
1317
- state.define(lvalue, instrValue);
1318
- continuation = {kind: 'funeffects'};
1319
- break;
1320
- }
1321
- case 'ComputedStore': {
1322
- const effect =
1323
- state.kind(instrValue.object).kind === ValueKind.Context
1324
- ? Effect.ConditionallyMutate
1325
- : Effect.Capture;
1326
- state.referenceAndRecordEffects(
1327
- freezeActions,
1328
- instrValue.value,
1329
- effect,
1330
- ValueReason.Other,
1331
- );
1332
- state.referenceAndRecordEffects(
1333
- freezeActions,
1334
- instrValue.property,
1335
- Effect.Capture,
1336
- ValueReason.Other,
1337
- );
1338
- state.referenceAndRecordEffects(
1339
- freezeActions,
1340
- instrValue.object,
1341
- Effect.Store,
1342
- ValueReason.Other,
1343
- );
1344
-
1345
- const lvalue = instr.lvalue;
1346
- state.alias(lvalue, instrValue.value);
1347
- lvalue.effect = Effect.Store;
1348
- continuation = {kind: 'funeffects'};
1349
- break;
1350
- }
1351
- case 'ComputedDelete': {
1352
- state.referenceAndRecordEffects(
1353
- freezeActions,
1354
- instrValue.object,
1355
- Effect.Mutate,
1356
- ValueReason.Other,
1357
- );
1358
- state.referenceAndRecordEffects(
1359
- freezeActions,
1360
- instrValue.property,
1361
- Effect.Read,
1362
- ValueReason.Other,
1363
- );
1364
- state.initialize(instrValue, {
1365
- kind: ValueKind.Primitive,
1366
- reason: new Set([ValueReason.Other]),
1367
- context: new Set(),
1368
- });
1369
- state.define(instr.lvalue, instrValue);
1370
- instr.lvalue.effect = Effect.Mutate;
1371
- continuation = {kind: 'funeffects'};
1372
- break;
1373
- }
1374
- case 'ComputedLoad': {
1375
- state.referenceAndRecordEffects(
1376
- freezeActions,
1377
- instrValue.object,
1378
- Effect.Read,
1379
- ValueReason.Other,
1380
- );
1381
- state.referenceAndRecordEffects(
1382
- freezeActions,
1383
- instrValue.property,
1384
- Effect.Read,
1385
- ValueReason.Other,
1386
- );
1387
- const lvalue = instr.lvalue;
1388
- lvalue.effect = Effect.ConditionallyMutate;
1389
- state.initialize(instrValue, state.kind(instrValue.object));
1390
- state.define(lvalue, instrValue);
1391
- continuation = {kind: 'funeffects'};
1392
- break;
1393
- }
1394
- case 'Await': {
1395
- state.initialize(instrValue, state.kind(instrValue.value));
1396
- /*
1397
- * Awaiting a value causes it to change state (go from unresolved to resolved or error)
1398
- * It also means that any side-effects which would occur as part of the promise evaluation
1399
- * will occur.
1400
- */
1401
- state.referenceAndRecordEffects(
1402
- freezeActions,
1403
- instrValue.value,
1404
- Effect.ConditionallyMutate,
1405
- ValueReason.Other,
1406
- );
1407
- const lvalue = instr.lvalue;
1408
- lvalue.effect = Effect.ConditionallyMutate;
1409
- state.alias(lvalue, instrValue.value);
1410
- continuation = {kind: 'funeffects'};
1411
- break;
1412
- }
1413
- case 'TypeCastExpression': {
1414
- /*
1415
- * A type cast expression has no effect at runtime, so it's equivalent to a raw
1416
- * identifier:
1417
- * ```
1418
- * x = (y: type) // is equivalent to...
1419
- * x = y
1420
- * ```
1421
- */
1422
- state.initialize(instrValue, state.kind(instrValue.value));
1423
- state.referenceAndRecordEffects(
1424
- freezeActions,
1425
- instrValue.value,
1426
- Effect.Read,
1427
- ValueReason.Other,
1428
- );
1429
- const lvalue = instr.lvalue;
1430
- lvalue.effect = Effect.ConditionallyMutate;
1431
- state.alias(lvalue, instrValue.value);
1432
- continuation = {kind: 'funeffects'};
1433
- break;
1434
- }
1435
- case 'StartMemoize':
1436
- case 'FinishMemoize': {
1437
- for (const val of eachInstructionValueOperand(instrValue)) {
1438
- if (env.config.enablePreserveExistingMemoizationGuarantees) {
1439
- state.referenceAndRecordEffects(
1440
- freezeActions,
1441
- val,
1442
- Effect.Freeze,
1443
- ValueReason.Other,
1444
- );
1445
- } else {
1446
- state.referenceAndRecordEffects(
1447
- freezeActions,
1448
- val,
1449
- Effect.Read,
1450
- ValueReason.Other,
1451
- );
1452
- }
1453
- }
1454
- const lvalue = instr.lvalue;
1455
- lvalue.effect = Effect.ConditionallyMutate;
1456
- state.initialize(instrValue, {
1457
- kind: ValueKind.Frozen,
1458
- reason: new Set([ValueReason.Other]),
1459
- context: new Set(),
1460
- });
1461
- state.define(lvalue, instrValue);
1462
- continuation = {kind: 'funeffects'};
1463
- break;
1464
- }
1465
- case 'LoadLocal': {
1466
- /**
1467
- * Due to backedges in the CFG, we may revisit LoadLocal lvalues
1468
- * multiple times. Unlike StoreLocal which may reassign to existing
1469
- * identifiers, LoadLocal always evaluates to store a new temporary.
1470
- * This means that we should always model LoadLocal as a Capture effect
1471
- * on the rvalue.
1472
- */
1473
- const lvalue = instr.lvalue;
1474
- state.referenceAndRecordEffects(
1475
- freezeActions,
1476
- instrValue.place,
1477
- Effect.Capture,
1478
- ValueReason.Other,
1479
- );
1480
- lvalue.effect = Effect.ConditionallyMutate;
1481
- // direct aliasing: `a = b`;
1482
- state.alias(lvalue, instrValue.place);
1483
- continuation = {kind: 'funeffects'};
1484
- break;
1485
- }
1486
- case 'LoadContext': {
1487
- state.referenceAndRecordEffects(
1488
- freezeActions,
1489
- instrValue.place,
1490
- Effect.Capture,
1491
- ValueReason.Other,
1492
- );
1493
- const lvalue = instr.lvalue;
1494
- lvalue.effect = Effect.ConditionallyMutate;
1495
- const valueKind = state.kind(instrValue.place);
1496
- state.initialize(instrValue, valueKind);
1497
- state.define(lvalue, instrValue);
1498
- continuation = {kind: 'funeffects'};
1499
- break;
1500
- }
1501
- case 'DeclareLocal': {
1502
- const value = UndefinedValue;
1503
- state.initialize(
1504
- value,
1505
- // Catch params may be aliased to mutable values
1506
- instrValue.lvalue.kind === InstructionKind.Catch
1507
- ? {
1508
- kind: ValueKind.Mutable,
1509
- reason: new Set([ValueReason.Other]),
1510
- context: new Set(),
1511
- }
1512
- : {
1513
- kind: ValueKind.Primitive,
1514
- reason: new Set([ValueReason.Other]),
1515
- context: new Set(),
1516
- },
1517
- );
1518
- state.define(instrValue.lvalue.place, value);
1519
- continuation = {kind: 'funeffects'};
1520
- break;
1521
- }
1522
- case 'DeclareContext': {
1523
- state.initialize(instrValue, {
1524
- kind: ValueKind.Mutable,
1525
- reason: new Set([ValueReason.Other]),
1526
- context: new Set(),
1527
- });
1528
- state.define(instrValue.lvalue.place, instrValue);
1529
- continuation = {kind: 'funeffects'};
1530
- break;
1531
- }
1532
- case 'PostfixUpdate':
1533
- case 'PrefixUpdate': {
1534
- const effect =
1535
- state.isDefined(instrValue.lvalue) &&
1536
- state.kind(instrValue.lvalue).kind === ValueKind.Context
1537
- ? Effect.ConditionallyMutate
1538
- : Effect.Capture;
1539
- state.referenceAndRecordEffects(
1540
- freezeActions,
1541
- instrValue.value,
1542
- effect,
1543
- ValueReason.Other,
1544
- );
1545
-
1546
- const lvalue = instr.lvalue;
1547
- state.alias(lvalue, instrValue.value);
1548
- lvalue.effect = Effect.Store;
1549
- state.alias(instrValue.lvalue, instrValue.value);
1550
- /*
1551
- * NOTE: *not* using state.reference since this is an assignment.
1552
- * reference() checks if the effect is valid given the value kind,
1553
- * but here the previous value kind doesn't matter since we are
1554
- * replacing it
1555
- */
1556
- instrValue.lvalue.effect = Effect.Store;
1557
- continuation = {kind: 'funeffects'};
1558
- break;
1559
- }
1560
- case 'StoreLocal': {
1561
- const effect =
1562
- state.isDefined(instrValue.lvalue.place) &&
1563
- state.kind(instrValue.lvalue.place).kind === ValueKind.Context
1564
- ? Effect.ConditionallyMutate
1565
- : Effect.Capture;
1566
- state.referenceAndRecordEffects(
1567
- freezeActions,
1568
- instrValue.value,
1569
- effect,
1570
- ValueReason.Other,
1571
- );
1572
-
1573
- const lvalue = instr.lvalue;
1574
- state.alias(lvalue, instrValue.value);
1575
- lvalue.effect = Effect.Store;
1576
- state.alias(instrValue.lvalue.place, instrValue.value);
1577
- /*
1578
- * NOTE: *not* using state.reference since this is an assignment.
1579
- * reference() checks if the effect is valid given the value kind,
1580
- * but here the previous value kind doesn't matter since we are
1581
- * replacing it
1582
- */
1583
- instrValue.lvalue.place.effect = Effect.Store;
1584
- continuation = {kind: 'funeffects'};
1585
- break;
1586
- }
1587
- case 'StoreContext': {
1588
- state.referenceAndRecordEffects(
1589
- freezeActions,
1590
- instrValue.value,
1591
- Effect.ConditionallyMutate,
1592
- ValueReason.Other,
1593
- );
1594
- state.referenceAndRecordEffects(
1595
- freezeActions,
1596
- instrValue.lvalue.place,
1597
- Effect.Mutate,
1598
- ValueReason.Other,
1599
- );
1600
-
1601
- const lvalue = instr.lvalue;
1602
- if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
1603
- state.initialize(instrValue, {
1604
- kind: ValueKind.Mutable,
1605
- reason: new Set([ValueReason.Other]),
1606
- context: new Set(),
1607
- });
1608
- state.define(instrValue.lvalue.place, instrValue);
1609
- }
1610
- state.alias(lvalue, instrValue.value);
1611
- lvalue.effect = Effect.Store;
1612
- continuation = {kind: 'funeffects'};
1613
- break;
1614
- }
1615
- case 'StoreGlobal': {
1616
- state.referenceAndRecordEffects(
1617
- freezeActions,
1618
- instrValue.value,
1619
- Effect.Capture,
1620
- ValueReason.Other,
1621
- );
1622
- const lvalue = instr.lvalue;
1623
- lvalue.effect = Effect.Store;
1624
- continuation = {kind: 'funeffects'};
1625
- break;
1626
- }
1627
- case 'Destructure': {
1628
- let effect: Effect = Effect.Capture;
1629
- for (const place of eachPatternOperand(instrValue.lvalue.pattern)) {
1630
- if (
1631
- state.isDefined(place) &&
1632
- state.kind(place).kind === ValueKind.Context
1633
- ) {
1634
- effect = Effect.ConditionallyMutate;
1635
- break;
1636
- }
1637
- }
1638
- state.referenceAndRecordEffects(
1639
- freezeActions,
1640
- instrValue.value,
1641
- effect,
1642
- ValueReason.Other,
1643
- );
1644
-
1645
- const lvalue = instr.lvalue;
1646
- state.alias(lvalue, instrValue.value);
1647
- lvalue.effect = Effect.Store;
1648
- for (const place of eachPatternOperand(instrValue.lvalue.pattern)) {
1649
- state.alias(place, instrValue.value);
1650
- /*
1651
- * NOTE: *not* using state.reference since this is an assignment.
1652
- * reference() checks if the effect is valid given the value kind,
1653
- * but here the previous value kind doesn't matter since we are
1654
- * replacing it
1655
- */
1656
- place.effect = Effect.Store;
1657
- }
1658
- continuation = {kind: 'funeffects'};
1659
- break;
1660
- }
1661
- case 'GetIterator': {
1662
- /**
1663
- * This instruction represents the step of retrieving an iterator from the collection
1664
- * in `for (... of <collection>)` syntax. We model two cases:
1665
- *
1666
- * 1. The collection is immutable or a known collection type (e.g. Array). In this case
1667
- * we infer that the iterator produced won't be the same as the collection itself.
1668
- * If the collection is an Array, this is because it will produce a native Array
1669
- * iterator. If the collection is already frozen, we assume it must be of some
1670
- * type that returns a separate iterator. In theory you could pass an Iterator
1671
- * as props to a component and then for..of over that in the component body, but
1672
- * this already violates React's rules so we assume you're not doing this.
1673
- * 2. The collection could be an Iterator itself, such that advancing the iterator
1674
- * (modeled with IteratorNext) mutates the collection itself.
1675
- */
1676
- const kind = state.kind(instrValue.collection).kind;
1677
- const isMutable =
1678
- kind === ValueKind.Mutable || kind === ValueKind.Context;
1679
- let effect;
1680
- let valueKind: AbstractValue;
1681
- const iterator = instrValue.collection.identifier;
1682
- if (
1683
- !isMutable ||
1684
- isArrayType(iterator) ||
1685
- isMapType(iterator) ||
1686
- isSetType(iterator)
1687
- ) {
1688
- // Case 1, assume iterator is a separate mutable object
1689
- effect = {
1690
- kind: Effect.Read,
1691
- reason: ValueReason.Other,
1692
- };
1693
- valueKind = {
1694
- kind: ValueKind.Mutable,
1695
- reason: new Set([ValueReason.Other]),
1696
- context: new Set(),
1697
- };
1698
- } else {
1699
- // Case 2, assume that the iterator could be the (mutable) collection itself
1700
- effect = {
1701
- kind: Effect.Capture,
1702
- reason: ValueReason.Other,
1703
- };
1704
- valueKind = state.kind(instrValue.collection);
1705
- }
1706
- continuation = {
1707
- kind: 'initialize',
1708
- effect,
1709
- valueKind,
1710
- lvalueEffect: Effect.Store,
1711
- };
1712
- break;
1713
- }
1714
- case 'IteratorNext': {
1715
- /**
1716
- * This instruction represents advancing an iterator with .next(). We use a
1717
- * conditional mutate to model the two cases for GetIterator:
1718
- * - If the collection is a mutable iterator, we want to model the fact that
1719
- * advancing the iterator will mutate it
1720
- * - If the iterator may be different from the collection and the collection
1721
- * is frozen, we don't want to report a false positive "cannot mutate" error.
1722
- *
1723
- * ConditionallyMutate reflects this "mutate if mutable" semantic.
1724
- */
1725
- state.referenceAndRecordEffects(
1726
- freezeActions,
1727
- instrValue.iterator,
1728
- Effect.ConditionallyMutateIterator,
1729
- ValueReason.Other,
1730
- );
1731
- /**
1732
- * Regardless of the effect on the iterator, the *result* of advancing the iterator
1733
- * is to extract a value from the collection. We use a Capture effect to reflect this
1734
- * aliasing, and then initialize() the lvalue to the same kind as the colleciton to
1735
- * ensure that the item is mutable or frozen if the collection is mutable/frozen.
1736
- */
1737
- state.referenceAndRecordEffects(
1738
- freezeActions,
1739
- instrValue.collection,
1740
- Effect.Capture,
1741
- ValueReason.Other,
1742
- );
1743
- state.initialize(instrValue, state.kind(instrValue.collection));
1744
- state.define(instr.lvalue, instrValue);
1745
- instr.lvalue.effect = Effect.Store;
1746
- continuation = {kind: 'funeffects'};
1747
- break;
1748
- }
1749
- case 'NextPropertyOf': {
1750
- continuation = {
1751
- kind: 'initialize',
1752
- effect: {kind: Effect.Read, reason: ValueReason.Other},
1753
- lvalueEffect: Effect.Store,
1754
- valueKind: {
1755
- kind: ValueKind.Primitive,
1756
- reason: new Set([ValueReason.Other]),
1757
- context: new Set(),
1758
- },
1759
- };
1760
- break;
1761
- }
1762
- default: {
1763
- assertExhaustive(instrValue, 'Unexpected instruction kind');
1764
- }
1765
- }
1766
-
1767
- if (continuation.kind === 'initialize') {
1768
- for (const operand of eachInstructionOperand(instr)) {
1769
- CompilerError.invariant(continuation.effect != null, {
1770
- reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
1771
- description: null,
1772
- loc: instrValue.loc,
1773
- suggestions: null,
1774
- });
1775
- state.referenceAndRecordEffects(
1776
- freezeActions,
1777
- operand,
1778
- continuation.effect.kind,
1779
- continuation.effect.reason,
1780
- );
1781
- }
1782
-
1783
- state.initialize(instrValue, continuation.valueKind);
1784
- state.define(instr.lvalue, instrValue);
1785
- instr.lvalue.effect = continuation.lvalueEffect ?? defaultLvalueEffect;
1786
- }
1787
-
1788
- functionEffects.push(...inferInstructionFunctionEffects(env, state, instr));
1789
- freezeActions.forEach(({values, reason}) =>
1790
- state.freezeValues(values, reason),
1791
- );
1792
- }
1793
-
1794
- const terminalFreezeActions: Array<FreezeAction> = [];
1795
- for (const operand of eachTerminalOperand(block.terminal)) {
1796
- let effect;
1797
- if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
1798
- if (
1799
- state.isDefined(operand) &&
1800
- ((operand.identifier.type.kind === 'Function' &&
1801
- state.isFunctionExpression) ||
1802
- state.kind(operand).kind === ValueKind.Context)
1803
- ) {
1804
- /**
1805
- * Returned values should only be typed as 'frozen' if they are both (1)
1806
- * local and (2) not a function expression which may capture and mutate
1807
- * this function's outer context.
1808
- */
1809
- effect = Effect.ConditionallyMutate;
1810
- } else {
1811
- effect = Effect.Freeze;
1812
- }
1813
- } else {
1814
- effect = Effect.Read;
1815
- }
1816
- state.referenceAndRecordEffects(
1817
- terminalFreezeActions,
1818
- operand,
1819
- effect,
1820
- ValueReason.Other,
1821
- );
1822
- }
1823
- functionEffects.push(...inferTerminalFunctionEffects(state, block));
1824
- terminalFreezeActions.forEach(({values, reason}) =>
1825
- state.freezeValues(values, reason),
1826
- );
1827
-}
1828
-
1829
-function getContextRefOperand(
1830
- state: InferenceState,
1831
- instrValue: InstructionValue,
1832
-): Array<Place> {
1833
- const result = [];
1834
- for (const place of eachInstructionValueOperand(instrValue)) {
1835
- if (
1836
- state.isDefined(place) &&
1837
- state.kind(place).kind === ValueKind.Context
1838
- ) {
1839
- result.push(place);
1840
- }
1841
- }
1842
- return result;
1843
-}
1844
-
1845
-export function getFunctionCallSignature(
1846
- env: Environment,
1847
- type: Type,
1848
-): FunctionSignature | null {
1849
- if (type.kind !== 'Function') {
1850
- return null;
1851
- }
1852
- return env.getFunctionSignature(type);
1853
-}
1854
-
1855
-/*
1856
- * Make a best attempt at matching arguments of a {@link MethodCall} to parameter effects.
1857
- * defined in its {@link FunctionSignature}.
1858
- *
1859
- * @param fn
1860
- * @param sig
1861
- * @returns Inferred effects of function arguments, or null if inference fails.
1862
- */
1863
-export function getFunctionEffects(
1864
- fn: MethodCall | CallExpression | NewExpression,
1865
- sig: FunctionSignature,
1866
-): Array<Effect> | null {
1867
- const results = [];
1868
- for (let i = 0; i < fn.args.length; i++) {
1869
- const arg = fn.args[i];
1870
- if (i < sig.positionalParams.length) {
1871
- /*
1872
- * Only infer effects when there is a direct mapping positional arg --> positional param
1873
- * Otherwise, return null to indicate inference failed
1874
- */
1875
- if (arg.kind === 'Identifier') {
1876
- results.push(sig.positionalParams[i]);
1877
- } else {
1878
- return null;
1879
- }
1880
- } else if (sig.restParam !== null) {
1881
- results.push(sig.restParam);
1882
- } else {
1883
- /*
1884
- * If there are more arguments than positional arguments and a rest parameter is not
1885
- * defined, we'll also assume that inference failed
1886
- */
1887
- return null;
1888
- }
1889
- }
1890
- return results;
1891
-}
1892
-
1893
-export function isKnownMutableEffect(effect: Effect): boolean {
1894
- switch (effect) {
1895
- case Effect.Store:
1896
- case Effect.ConditionallyMutate:
1897
- case Effect.ConditionallyMutateIterator:
1898
- case Effect.Mutate: {
1899
- return true;
1900
- }
1901
-
1902
- case Effect.Unknown: {
1903
- CompilerError.invariant(false, {
1904
- reason: 'Unexpected unknown effect',
1905
- description: null,
1906
- loc: GeneratedSource,
1907
- suggestions: null,
1908
- });
1909
- }
1910
- case Effect.Read:
1911
- case Effect.Capture:
1912
- case Effect.Freeze: {
1913
- return false;
1914
- }
1915
- default: {
1916
- assertExhaustive(effect, `Unexpected effect \`${effect}\``);
1917
- }
1918
- }
1919
-}
1920
-/**
1921
- * Returns true if all of the arguments are both non-mutable (immutable or frozen)
1922
- * _and_ are not functions which might mutate their arguments. Note that function
1923
- * expressions count as frozen so long as they do not mutate free variables: this
1924
- * function checks that such functions also don't mutate their inputs.
1925
- */
1926
-function areArgumentsImmutableAndNonMutating(
1927
- state: InferenceState,
1928
- args: MethodCall['args'],
1929
-): boolean {
1930
- for (const arg of args) {
1931
- if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
1932
- const fnShape = state.env.getFunctionSignature(arg.identifier.type);
1933
- if (fnShape != null) {
1934
- return (
1935
- !fnShape.positionalParams.some(isKnownMutableEffect) &&
1936
- (fnShape.restParam == null ||
1937
- !isKnownMutableEffect(fnShape.restParam))
1938
- );
1939
- }
1940
- }
1941
- const place = arg.kind === 'Identifier' ? arg : arg.place;
1942
-
1943
- const kind = state.kind(place).kind;
1944
- switch (kind) {
1945
- case ValueKind.Primitive:
1946
- case ValueKind.Frozen: {
1947
- /*
1948
- * Only immutable values, or frozen lambdas are allowed.
1949
- * A lambda may appear frozen even if it may mutate its inputs,
1950
- * so we have a second check even for frozen value types
1951
- */
1952
- break;
1953
- }
1954
- default: {
1955
- /**
1956
- * Globals, module locals, and other locally defined functions may
1957
- * mutate their arguments.
1958
- */
1959
- return false;
1960
- }
1961
- }
1962
- const values = state.values(place);
1963
- for (const value of values) {
1964
- if (
1965
- value.kind === 'FunctionExpression' &&
1966
- value.loweredFunc.func.params.some(param => {
1967
- const place = param.kind === 'Identifier' ? param : param.place;
1968
- const range = place.identifier.mutableRange;
1969
- return range.end > range.start + 1;
1970
- })
1971
- ) {
1972
- // This is a function which may mutate its inputs
1973
- return false;
1974
- }
1975
- }
1976
- }
1977
- return true;
1978
-}
1979
-
1980
-export function getArgumentEffect(
1981
- signatureEffect: Effect | null,
1982
- arg: Place | SpreadPattern,
1983
-): Effect {
1984
- if (signatureEffect != null) {
1985
- if (arg.kind === 'Identifier') {
1986
- return signatureEffect;
1987
- } else if (
1988
- signatureEffect === Effect.Mutate ||
1989
- signatureEffect === Effect.ConditionallyMutate
1990
- ) {
1991
- return signatureEffect;
1992
- } else {
1993
- // see call-spread-argument-mutable-iterator test fixture
1994
- if (signatureEffect === Effect.Freeze) {
1995
- CompilerError.throwTodo({
1996
- reason: 'Support spread syntax for hook arguments',
1997
- loc: arg.place.loc,
1998
- });
1999
- }
2000
- // effects[i] is Effect.Capture | Effect.Read | Effect.Store
2001
- return Effect.ConditionallyMutateIterator;
2002
- }
2003
- } else {
2004
- return Effect.ConditionallyMutate;
2005
- }
2006
-}
2007
-
2008
-function inferCallEffects(
2009
- state: InferenceState,
2010
- instr:
2011
- | TInstruction<CallExpression>
2012
- | TInstruction<MethodCall>
2013
- | TInstruction<NewExpression>,
2014
- freezeActions: Array<FreezeAction>,
2015
- signature: FunctionSignature | null,
2016
-): void {
2017
- const instrValue = instr.value;
2018
- const returnValueKind: AbstractValue =
2019
- signature !== null
2020
- ? {
2021
- kind: signature.returnValueKind,
2022
- reason: new Set([
2023
- signature.returnValueReason ?? ValueReason.KnownReturnSignature,
2024
- ]),
2025
- context: new Set(),
2026
- }
2027
- : {
2028
- kind: ValueKind.Mutable,
2029
- reason: new Set([ValueReason.Other]),
2030
- context: new Set(),
2031
- };
2032
-
2033
- if (
2034
- instrValue.kind === 'MethodCall' &&
2035
- signature !== null &&
2036
- signature.mutableOnlyIfOperandsAreMutable &&
2037
- areArgumentsImmutableAndNonMutating(state, instrValue.args)
2038
- ) {
2039
- /*
2040
- * None of the args are mutable or mutate their params, we can downgrade to
2041
- * treating as all reads (except that the receiver may be captured)
2042
- */
2043
- for (const arg of instrValue.args) {
2044
- const place = arg.kind === 'Identifier' ? arg : arg.place;
2045
- state.referenceAndRecordEffects(
2046
- freezeActions,
2047
- place,
2048
- Effect.Read,
2049
- ValueReason.Other,
2050
- );
2051
- }
2052
- state.referenceAndRecordEffects(
2053
- freezeActions,
2054
- instrValue.receiver,
2055
- Effect.Capture,
2056
- ValueReason.Other,
2057
- );
2058
- state.initialize(instrValue, returnValueKind);
2059
- state.define(instr.lvalue, instrValue);
2060
- instr.lvalue.effect =
2061
- instrValue.receiver.effect === Effect.Capture
2062
- ? Effect.Store
2063
- : Effect.ConditionallyMutate;
2064
- return;
2065
- }
2066
-
2067
- const effects =
2068
- signature !== null ? getFunctionEffects(instrValue, signature) : null;
2069
- let hasCaptureArgument = false;
2070
- for (let i = 0; i < instrValue.args.length; i++) {
2071
- const arg = instrValue.args[i];
2072
- const place = arg.kind === 'Identifier' ? arg : arg.place;
2073
- /*
2074
- * If effects are inferred for an argument, we should fail invalid
2075
- * mutating effects
2076
- */
2077
- state.referenceAndRecordEffects(
2078
- freezeActions,
2079
- place,
2080
- getArgumentEffect(effects != null ? effects[i] : null, arg),
2081
- ValueReason.Other,
2082
- );
2083
- hasCaptureArgument ||= place.effect === Effect.Capture;
2084
- }
2085
- const callee =
2086
- instrValue.kind === 'MethodCall' ? instrValue.receiver : instrValue.callee;
2087
- if (signature !== null) {
2088
- state.referenceAndRecordEffects(
2089
- freezeActions,
2090
- callee,
2091
- signature.calleeEffect,
2092
- ValueReason.Other,
2093
- );
2094
- } else {
2095
- /**
2096
- * For new expressions, we infer a `read` effect on the Class / Function type
2097
- * to avoid extending mutable ranges of locally created classes, e.g.
2098
- * ```js
2099
- * const MyClass = getClass();
2100
- * const value = new MyClass(val1, val2)
2101
- * ^ (read) ^ (conditionally mutate)
2102
- * ```
2103
- *
2104
- * Risks:
2105
- * Classes / functions created during render could technically capture and
2106
- * mutate their enclosing scope, which we currently do not detect.
2107
- */
2108
-
2109
- state.referenceAndRecordEffects(
2110
- freezeActions,
2111
- callee,
2112
- instrValue.kind === 'NewExpression'
2113
- ? Effect.Read
2114
- : Effect.ConditionallyMutate,
2115
- ValueReason.Other,
2116
- );
2117
- }
2118
- hasCaptureArgument ||= callee.effect === Effect.Capture;
2119
-
2120
- state.initialize(instrValue, returnValueKind);
2121
- state.define(instr.lvalue, instrValue);
2122
- instr.lvalue.effect = hasCaptureArgument
2123
- ? Effect.Store
2124
- : Effect.ConditionallyMutate;
2125
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferTryCatchAliases.ts
deleted
-49
@@ -1,49 +0,0 @@
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 {BlockId, HIRFunction, Identifier} from '../HIR';
9
-import DisjointSet from '../Utils/DisjointSet';
10
-
11
-/*
12
- * Any values created within a try/catch block could be aliased to the try handler.
13
- * Our lowering ensures that every instruction within a try block will be lowered into a
14
- * basic block ending in a maybe-throw terminal that points to its catch block, so we can
15
- * iterate such blocks and alias their instruction lvalues to the handler's param (if present).
16
- */
17
-export function inferTryCatchAliases(
18
- fn: HIRFunction,
19
- aliases: DisjointSet<Identifier>,
20
-): void {
21
- const handlerParams: Map<BlockId, Identifier> = new Map();
22
- for (const [_, block] of fn.body.blocks) {
23
- if (
24
- block.terminal.kind === 'try' &&
25
- block.terminal.handlerBinding !== null
26
- ) {
27
- handlerParams.set(
28
- block.terminal.handler,
29
- block.terminal.handlerBinding.identifier,
30
- );
31
- } else if (block.terminal.kind === 'maybe-throw') {
32
- const handlerParam = handlerParams.get(block.terminal.handler);
33
- if (handlerParam === undefined) {
34
- /*
35
- * There's no catch clause param, nothing to alias to so
36
- * skip this block
37
- */
38
- continue;
39
- }
40
- /*
41
- * Otherwise alias all values created in this block to the
42
- * catch clause param
43
- */
44
- for (const instr of block.instructions) {
45
- aliases.union([handlerParam, instr.lvalue.identifier]);
46
- }
47
- }
48
- }
49
-}
compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts
-2
@@ -7,8 +7,6 @@
7
8
export {default as analyseFunctions} from './AnalyseFunctions';
9
export {dropManualMemoization} from './DropManualMemoization';
10
-export {inferMutableRanges} from './InferMutableRanges';
10
export {inferReactivePlaces} from './InferReactivePlaces';
12
-export {default as inferReferenceEffects} from './InferReferenceEffects';
11
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
12
export {inferEffectDependencies} from './InferEffectDependencies';
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
+1
-1
@@ -24,7 +24,6 @@ import {
24
getHookKind,
25
isMutableEffect,
26
} from '../HIR';
27
-import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
27
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
28
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
29
import {
@@ -35,6 +34,7 @@ import {
34
visitReactiveFunction,
35
} from './visitors';
36
import {printPlace} from '../HIR/PrintHIR';
37
+import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
38
39
/*
40
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
+1
-1
@@ -12,7 +12,7 @@ import {
12
eachInstructionValueOperand,
13
eachTerminalOperand,
14
} from '../HIR/visitors';
15
-import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
15
+import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
16
17
/**
18
* Validates that local variables cannot be reassigned after render.
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
+1
-4
@@ -125,10 +125,7 @@ export function validateNoFreezingKnownMutableFunctions(
125
);
126
if (knownMutation && knownMutation.kind === 'ContextMutation') {
127
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
128
- } else if (
129
- fn.env.config.enableNewMutationAliasingModel &&
130
- value.loweredFunc.func.aliasingEffects != null
131
- ) {
128
+ } else if (value.loweredFunc.func.aliasingEffects != null) {
129
const context = new Set(
130
value.loweredFunc.func.context.map(p => p.identifier.id),
131
);
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts
+1
-1
@@ -7,7 +7,7 @@
7
8
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
9
import {HIRFunction} from '../HIR';
10
-import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
10
+import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
11
import {Result} from '../Utils/Result';
12
13
/**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md
new
+39
@@ -0,0 +1,39 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableNewMutationAliasingModel:false
6
+function Component() {
7
+ const foo = () => {
8
+ someGlobal = true;
9
+ };
10
+ // spreading a function is weird, but it doesn't call the function so this is allowed
11
+ return <div {...foo} />;
12
+}
13
+
14
+```
15
+
16
+## Code
17
+
18
+```javascript
19
+import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
20
+function Component() {
21
+ const $ = _c(1);
22
+ const foo = _temp;
23
+ let t0;
24
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25
+ t0 = <div {...foo} />;
26
+ $[0] = t0;
27
+ } else {
28
+ t0 = $[0];
29
+ }
30
+ return t0;
31
+}
32
+function _temp() {
33
+ someGlobal = true;
34
+}
35
+
36
+```
37
+
38
+### Eval output
39
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.js
renamed
+1
@@ -3,5 +3,6 @@ function Component() {
3
const foo = () => {
4
someGlobal = true;
5
};
6
+ // spreading a function is weird, but it doesn't call the function so this is allowed
7
return <div {...foo} />;
8
}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md
deleted
-107
@@ -1,107 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
6
-import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
7
-
8
-/**
9
- * 1. `InferMutableRanges` derives the mutable range of identifiers and their
10
- * aliases from `LoadLocal`, `PropertyLoad`, etc
11
- * - After this pass, y's mutable range only extends to `arrayPush(x, y)`
12
- * - We avoid assigning mutable ranges to loads after y's mutable range, as
13
- * these are working with an immutable value. As a result, `LoadLocal y` and
14
- * `PropertyLoad y` do not get mutable ranges
15
- * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
16
- * as according to the 'co-mutation' of different values
17
- * - Here, we infer that
18
- * - `arrayPush(y, x)` might alias `x` and `y` to each other
19
- * - `setPropertyKey(x, ...)` may mutate both `x` and `y`
20
- * - This pass correctly extends the mutable range of `y`
21
- * - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
22
- * PropertyLoads still don't have a mutable range
23
- *
24
- * Note that the this bug is an edge case. Compiler output is only invalid for:
25
- * - function expressions with
26
- * `enableTransitivelyFreezeFunctionExpressions:false`
27
- * - functions that throw and get retried without clearing the memocache
28
- *
29
- * Found differences in evaluator results
30
- * Non-forget (expected):
31
- * (kind: ok)
32
- * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
33
- * <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
34
- * Forget:
35
- * (kind: ok)
36
- * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
37
- * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
38
- */
39
-function useFoo({a, b}: {a: number, b: number}) {
40
- const x = [];
41
- const y = {value: a};
42
-
43
- arrayPush(x, y); // x and y co-mutate
44
- const y_alias = y;
45
- const cb = () => y_alias.value;
46
- setPropertyByKey(x[0], 'value', b); // might overwrite y.value
47
- return <Stringify cb={cb} shouldInvokeFns={true} />;
48
-}
49
-
50
-export const FIXTURE_ENTRYPOINT = {
51
- fn: useFoo,
52
- params: [{a: 2, b: 10}],
53
- sequentialRenders: [
54
- {a: 2, b: 10},
55
- {a: 2, b: 11},
56
- ],
57
-};
58
-
59
-```
60
-
61
-## Code
62
-
63
-```javascript
64
-import { c as _c } from "react/compiler-runtime";
65
-import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
66
-
67
-function useFoo(t0) {
68
- const $ = _c(5);
69
- const { a, b } = t0;
70
- let t1;
71
- if ($[0] !== a || $[1] !== b) {
72
- const x = [];
73
- const y = { value: a };
74
-
75
- arrayPush(x, y);
76
- const y_alias = y;
77
- let t2;
78
- if ($[3] !== y_alias.value) {
79
- t2 = () => y_alias.value;
80
- $[3] = y_alias.value;
81
- $[4] = t2;
82
- } else {
83
- t2 = $[4];
84
- }
85
- const cb = t2;
86
- setPropertyByKey(x[0], "value", b);
87
- t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
88
- $[0] = a;
89
- $[1] = b;
90
- $[2] = t1;
91
- } else {
92
- t1 = $[2];
93
- }
94
- return t1;
95
-}
96
-
97
-export const FIXTURE_ENTRYPOINT = {
98
- fn: useFoo,
99
- params: [{ a: 2, b: 10 }],
100
- sequentialRenders: [
101
- { a: 2, b: 10 },
102
- { a: 2, b: 11 },
103
- ],
104
-};
105
-
106
-```
107
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js
deleted
-53
@@ -1,53 +0,0 @@
1
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
2
-import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
3
-
4
-/**
5
- * 1. `InferMutableRanges` derives the mutable range of identifiers and their
6
- * aliases from `LoadLocal`, `PropertyLoad`, etc
7
- * - After this pass, y's mutable range only extends to `arrayPush(x, y)`
8
- * - We avoid assigning mutable ranges to loads after y's mutable range, as
9
- * these are working with an immutable value. As a result, `LoadLocal y` and
10
- * `PropertyLoad y` do not get mutable ranges
11
- * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
12
- * as according to the 'co-mutation' of different values
13
- * - Here, we infer that
14
- * - `arrayPush(y, x)` might alias `x` and `y` to each other
15
- * - `setPropertyKey(x, ...)` may mutate both `x` and `y`
16
- * - This pass correctly extends the mutable range of `y`
17
- * - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
18
- * PropertyLoads still don't have a mutable range
19
- *
20
- * Note that the this bug is an edge case. Compiler output is only invalid for:
21
- * - function expressions with
22
- * `enableTransitivelyFreezeFunctionExpressions:false`
23
- * - functions that throw and get retried without clearing the memocache
24
- *
25
- * Found differences in evaluator results
26
- * Non-forget (expected):
27
- * (kind: ok)
28
- * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
29
- * <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
30
- * Forget:
31
- * (kind: ok)
32
- * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
33
- * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
34
- */
35
-function useFoo({a, b}: {a: number, b: number}) {
36
- const x = [];
37
- const y = {value: a};
38
-
39
- arrayPush(x, y); // x and y co-mutate
40
- const y_alias = y;
41
- const cb = () => y_alias.value;
42
- setPropertyByKey(x[0], 'value', b); // might overwrite y.value
43
- return <Stringify cb={cb} shouldInvokeFns={true} />;
44
-}
45
-
46
-export const FIXTURE_ENTRYPOINT = {
47
- fn: useFoo,
48
- params: [{a: 2, b: 10}],
49
- sequentialRenders: [
50
- {a: 2, b: 10},
51
- {a: 2, b: 11},
52
- ],
53
-};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md
deleted
-87
@@ -1,87 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
6
-import {setPropertyByKey, Stringify} from 'shared-runtime';
7
-
8
-/**
9
- * Variation of bug in `bug-aliased-capture-aliased-mutate`
10
- * Found differences in evaluator results
11
- * Non-forget (expected):
12
- * (kind: ok)
13
- * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
14
- * <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
15
- * Forget:
16
- * (kind: ok)
17
- * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
18
- * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
19
- */
20
-
21
-function useFoo({a}: {a: number, b: number}) {
22
- const arr = [];
23
- const obj = {value: a};
24
-
25
- setPropertyByKey(obj, 'arr', arr);
26
- const obj_alias = obj;
27
- const cb = () => obj_alias.arr.length;
28
- for (let i = 0; i < a; i++) {
29
- arr.push(i);
30
- }
31
- return <Stringify cb={cb} shouldInvokeFns={true} />;
32
-}
33
-
34
-export const FIXTURE_ENTRYPOINT = {
35
- fn: useFoo,
36
- params: [{a: 2}],
37
- sequentialRenders: [{a: 2}, {a: 3}],
38
-};
39
-
40
-```
41
-
42
-## Code
43
-
44
-```javascript
45
-import { c as _c } from "react/compiler-runtime";
46
-import { setPropertyByKey, Stringify } from "shared-runtime";
47
-
48
-function useFoo(t0) {
49
- const $ = _c(4);
50
- const { a } = t0;
51
- let t1;
52
- if ($[0] !== a) {
53
- const arr = [];
54
- const obj = { value: a };
55
-
56
- setPropertyByKey(obj, "arr", arr);
57
- const obj_alias = obj;
58
- let t2;
59
- if ($[2] !== obj_alias.arr.length) {
60
- t2 = () => obj_alias.arr.length;
61
- $[2] = obj_alias.arr.length;
62
- $[3] = t2;
63
- } else {
64
- t2 = $[3];
65
- }
66
- const cb = t2;
67
- for (let i = 0; i < a; i++) {
68
- arr.push(i);
69
- }
70
-
71
- t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
72
- $[0] = a;
73
- $[1] = t1;
74
- } else {
75
- t1 = $[1];
76
- }
77
- return t1;
78
-}
79
-
80
-export const FIXTURE_ENTRYPOINT = {
81
- fn: useFoo,
82
- params: [{ a: 2 }],
83
- sequentialRenders: [{ a: 2 }, { a: 3 }],
84
-};
85
-
86
-```
87
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js
deleted
-34
@@ -1,34 +0,0 @@
1
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
2
-import {setPropertyByKey, Stringify} from 'shared-runtime';
3
-
4
-/**
5
- * Variation of bug in `bug-aliased-capture-aliased-mutate`
6
- * Found differences in evaluator results
7
- * Non-forget (expected):
8
- * (kind: ok)
9
- * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
10
- * <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
11
- * Forget:
12
- * (kind: ok)
13
- * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
14
- * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
15
- */
16
-
17
-function useFoo({a}: {a: number, b: number}) {
18
- const arr = [];
19
- const obj = {value: a};
20
-
21
- setPropertyByKey(obj, 'arr', arr);
22
- const obj_alias = obj;
23
- const cb = () => obj_alias.arr.length;
24
- for (let i = 0; i < a; i++) {
25
- arr.push(i);
26
- }
27
- return <Stringify cb={cb} shouldInvokeFns={true} />;
28
-}
29
-
30
-export const FIXTURE_ENTRYPOINT = {
31
- fn: useFoo,
32
- params: [{a: 2}],
33
- sequentialRenders: [{a: 2}, {a: 3}],
34
-};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md
+7
-15
@@ -85,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime";
85
* used when we analyze CallExpressions.
86
*/
87
function Component(t0) {
88
- const $ = _c(5);
88
+ const $ = _c(3);
89
const { foo, bar } = t0;
90
- let t1;
91
- if ($[0] !== foo) {
92
- t1 = { foo };
93
- $[0] = foo;
94
- $[1] = t1;
95
- } else {
96
- t1 = $[1];
97
- }
98
- const x = t1;
90
let y;
100
- if ($[2] !== bar || $[3] !== x) {
91
+ if ($[0] !== bar || $[1] !== foo) {
92
+ const x = { foo };
93
y = { bar };
94
const f0 = function () {
95
const a = makeArray(y);
@@ -108,11 +100,11 @@ function Component(t0) {
100
101
f0();
102
mutate(y.x);
111
- $[2] = bar;
112
- $[3] = x;
113
- $[4] = y;
103
+ $[0] = bar;
104
+ $[1] = foo;
105
+ $[2] = y;
106
} else {
115
- y = $[4];
107
+ y = $[2];
108
}
109
return y;
110
}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md
deleted
-92
@@ -1,92 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @enableNewMutationAliasingModel:false
6
-import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
7
-
8
-/**
9
- * Fixture showing an edge case for ReactiveScope variable propagation.
10
- *
11
- * Found differences in evaluator results
12
- * Non-forget (expected):
13
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
14
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
15
- * Forget:
16
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
17
- * [[ (exception in render) Error: invariant broken ]]
18
- *
19
- */
20
-function Component() {
21
- const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
22
- const boxedInner = [obj?.inner];
23
- useIdentity(null);
24
- mutate(obj);
25
- if (boxedInner[0] !== obj?.inner) {
26
- throw new Error('invariant broken');
27
- }
28
- return <Stringify obj={obj} inner={boxedInner} />;
29
-}
30
-
31
-export const FIXTURE_ENTRYPOINT = {
32
- fn: Component,
33
- params: [{arg: 0}],
34
- sequentialRenders: [{arg: 0}, {arg: 1}],
35
-};
36
-
37
-```
38
-
39
-## Code
40
-
41
-```javascript
42
-import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
43
-import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
44
-
45
-/**
46
- * Fixture showing an edge case for ReactiveScope variable propagation.
47
- *
48
- * Found differences in evaluator results
49
- * Non-forget (expected):
50
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
51
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
52
- * Forget:
53
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
54
- * [[ (exception in render) Error: invariant broken ]]
55
- *
56
- */
57
-function Component() {
58
- const $ = _c(4);
59
- const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
60
- let t0;
61
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
62
- t0 = [obj?.inner];
63
- $[0] = t0;
64
- } else {
65
- t0 = $[0];
66
- }
67
- const boxedInner = t0;
68
- useIdentity(null);
69
- mutate(obj);
70
- if (boxedInner[0] !== obj?.inner) {
71
- throw new Error("invariant broken");
72
- }
73
- let t1;
74
- if ($[1] !== boxedInner || $[2] !== obj) {
75
- t1 = <Stringify obj={obj} inner={boxedInner} />;
76
- $[1] = boxedInner;
77
- $[2] = obj;
78
- $[3] = t1;
79
- } else {
80
- t1 = $[3];
81
- }
82
- return t1;
83
-}
84
-
85
-export const FIXTURE_ENTRYPOINT = {
86
- fn: Component,
87
- params: [{ arg: 0 }],
88
- sequentialRenders: [{ arg: 0 }, { arg: 1 }],
89
-};
90
-
91
-```
92
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx
deleted
-31
@@ -1,31 +0,0 @@
1
-// @enableNewMutationAliasingModel:false
2
-import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
3
-
4
-/**
5
- * Fixture showing an edge case for ReactiveScope variable propagation.
6
- *
7
- * Found differences in evaluator results
8
- * Non-forget (expected):
9
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
10
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
11
- * Forget:
12
- * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
13
- * [[ (exception in render) Error: invariant broken ]]
14
- *
15
- */
16
-function Component() {
17
- const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
18
- const boxedInner = [obj?.inner];
19
- useIdentity(null);
20
- mutate(obj);
21
- if (boxedInner[0] !== obj?.inner) {
22
- throw new Error('invariant broken');
23
- }
24
- return <Stringify obj={obj} inner={boxedInner} />;
25
-}
26
-
27
-export const FIXTURE_ENTRYPOINT = {
28
- fn: Component,
29
- params: [{arg: 0}],
30
- sequentialRenders: [{arg: 0}, {arg: 1}],
31
-};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md
deleted
-110
@@ -1,110 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @enableNewMutationAliasingModel:false
6
-import {identity, mutate} from 'shared-runtime';
7
-
8
-/**
9
- * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
10
- * with the mutation hoisted to a named variable instead of being directly
11
- * inlined into the Object key.
12
- *
13
- * Found differences in evaluator results
14
- * Non-forget (expected):
15
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
16
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
17
- * Forget:
18
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
19
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
20
- */
21
-function Component(props) {
22
- const key = {};
23
- const tmp = (mutate(key), key);
24
- const context = {
25
- // Here, `tmp` is frozen (as it's inferred to be a primitive/string)
26
- [tmp]: identity([props.value]),
27
- };
28
- mutate(key);
29
- return [context, key];
30
-}
31
-
32
-export const FIXTURE_ENTRYPOINT = {
33
- fn: Component,
34
- params: [{value: 42}],
35
- sequentialRenders: [{value: 42}, {value: 42}],
36
-};
37
-
38
-```
39
-
40
-## Code
41
-
42
-```javascript
43
-import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
44
-import { identity, mutate } from "shared-runtime";
45
-
46
-/**
47
- * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
48
- * with the mutation hoisted to a named variable instead of being directly
49
- * inlined into the Object key.
50
- *
51
- * Found differences in evaluator results
52
- * Non-forget (expected):
53
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
54
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
55
- * Forget:
56
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
57
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
58
- */
59
-function Component(props) {
60
- const $ = _c(8);
61
- let key;
62
- let t0;
63
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
64
- key = {};
65
- t0 = (mutate(key), key);
66
- $[0] = key;
67
- $[1] = t0;
68
- } else {
69
- key = $[0];
70
- t0 = $[1];
71
- }
72
- const tmp = t0;
73
- let t1;
74
- if ($[2] !== props.value) {
75
- t1 = identity([props.value]);
76
- $[2] = props.value;
77
- $[3] = t1;
78
- } else {
79
- t1 = $[3];
80
- }
81
- let t2;
82
- if ($[4] !== t1) {
83
- t2 = { [tmp]: t1 };
84
- $[4] = t1;
85
- $[5] = t2;
86
- } else {
87
- t2 = $[5];
88
- }
89
- const context = t2;
90
-
91
- mutate(key);
92
- let t3;
93
- if ($[6] !== context) {
94
- t3 = [context, key];
95
- $[6] = context;
96
- $[7] = t3;
97
- } else {
98
- t3 = $[7];
99
- }
100
- return t3;
101
-}
102
-
103
-export const FIXTURE_ENTRYPOINT = {
104
- fn: Component,
105
- params: [{ value: 42 }],
106
- sequentialRenders: [{ value: 42 }, { value: 42 }],
107
-};
108
-
109
-```
110
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js
deleted
-32
@@ -1,32 +0,0 @@
1
-// @enableNewMutationAliasingModel:false
2
-import {identity, mutate} from 'shared-runtime';
3
-
4
-/**
5
- * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
6
- * with the mutation hoisted to a named variable instead of being directly
7
- * inlined into the Object key.
8
- *
9
- * Found differences in evaluator results
10
- * Non-forget (expected):
11
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
12
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
13
- * Forget:
14
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
15
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
16
- */
17
-function Component(props) {
18
- const key = {};
19
- const tmp = (mutate(key), key);
20
- const context = {
21
- // Here, `tmp` is frozen (as it's inferred to be a primitive/string)
22
- [tmp]: identity([props.value]),
23
- };
24
- mutate(key);
25
- return [context, key];
26
-}
27
-
28
-export const FIXTURE_ENTRYPOINT = {
29
- fn: Component,
30
- params: [{value: 42}],
31
- sequentialRenders: [{value: 42}, {value: 42}],
32
-};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md
deleted
-33
@@ -1,33 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @enableNewMutationAliasingModel:false
6
-function Component() {
7
- const foo = () => {
8
- someGlobal = true;
9
- };
10
- return <div {...foo} />;
11
-}
12
-
13
-```
14
-
15
-
16
-## Error
17
-
18
-```
19
-Found 1 error:
20
-
21
-Error: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
22
-
23
-error.assign-global-in-jsx-spread-attribute.ts:4:4
24
- 2 | function Component() {
25
- 3 | const foo = () => {
26
-> 4 | someGlobal = true;
27
- | ^^^^^^^^^^ Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
28
- 5 | };
29
- 6 | return <div {...foo} />;
30
- 7 | }
31
-```
32
-
33
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md
deleted
-72
@@ -1,72 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
6
-
7
-import {useCallback, useEffect, useRef} from 'react';
8
-import {useHook} from 'shared-runtime';
9
-
10
-function Component() {
11
- const params = useHook();
12
- const update = useCallback(
13
- partialParams => {
14
- const nextParams = {
15
- ...params,
16
- ...partialParams,
17
- };
18
- nextParams.param = 'value';
19
- console.log(nextParams);
20
- },
21
- [params]
22
- );
23
- const ref = useRef(null);
24
- useEffect(() => {
25
- if (ref.current === null) {
26
- update();
27
- }
28
- }, [update]);
29
-
30
- return 'ok';
31
-}
32
-
33
-```
34
-
35
-
36
-## Error
37
-
38
-```
39
-Found 1 error:
40
-
41
-Error: Cannot modify local variables after render completes
42
-
43
-This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
44
-
45
-error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
46
- 18 | );
47
- 19 | const ref = useRef(null);
48
-> 20 | useEffect(() => {
49
- | ^^^^^^^
50
-> 21 | if (ref.current === null) {
51
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52
-> 22 | update();
53
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
54
-> 23 | }
55
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56
-> 24 | }, [update]);
57
- | ^^^^ This function may (indirectly) reassign or modify a local variable after render
58
- 25 |
59
- 26 | return 'ok';
60
- 27 | }
61
-
62
-error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
63
- 12 | ...partialParams,
64
- 13 | };
65
-> 14 | nextParams.param = 'value';
66
- | ^^^^^^^^^^ This modifies a local variable
67
- 15 | console.log(nextParams);
68
- 16 | },
69
- 17 | [params]
70
-```
71
-
72
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js
deleted
-27
@@ -1,27 +0,0 @@
1
-// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
2
-
3
-import {useCallback, useEffect, useRef} from 'react';
4
-import {useHook} from 'shared-runtime';
5
-
6
-function Component() {
7
- const params = useHook();
8
- const update = useCallback(
9
- partialParams => {
10
- const nextParams = {
11
- ...params,
12
- ...partialParams,
13
- };
14
- nextParams.param = 'value';
15
- console.log(nextParams);
16
- },
17
- [params]
18
- );
19
- const ref = useRef(null);
20
- useEffect(() => {
21
- if (ref.current === null) {
22
- update();
23
- }
24
- }, [update]);
25
-
26
- return 'ok';
27
-}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md
deleted
-39
@@ -1,39 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @enableNewMutationAliasingModel:false
6
-function Foo() {
7
- const x = () => {
8
- window.href = 'foo';
9
- };
10
- const y = {x};
11
- return <Bar y={y} />;
12
-}
13
-
14
-export const FIXTURE_ENTRYPOINT = {
15
- fn: Foo,
16
- params: [],
17
-};
18
-
19
-```
20
-
21
-
22
-## Error
23
-
24
-```
25
-Found 1 error:
26
-
27
-Error: Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
28
-
29
-error.object-capture-global-mutation.ts:4:4
30
- 2 | function Foo() {
31
- 3 | const x = () => {
32
-> 4 | window.href = 'foo';
33
- | ^^^^^^ Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
34
- 5 | };
35
- 6 | const y = {x};
36
- 7 | return <Bar y={y} />;
37
-```
38
-
39
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md
new
+49
@@ -0,0 +1,49 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Foo() {
6
+ const x = () => {
7
+ window.href = 'foo';
8
+ };
9
+ const y = {x};
10
+ return <Bar y={y} />;
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: Foo,
15
+ params: [],
16
+};
17
+
18
+```
19
+
20
+## Code
21
+
22
+```javascript
23
+import { c as _c } from "react/compiler-runtime";
24
+function Foo() {
25
+ const $ = _c(1);
26
+ const x = _temp;
27
+ let t0;
28
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
29
+ const y = { x };
30
+ t0 = <Bar y={y} />;
31
+ $[0] = t0;
32
+ } else {
33
+ t0 = $[0];
34
+ }
35
+ return t0;
36
+}
37
+function _temp() {
38
+ window.href = "foo";
39
+}
40
+
41
+export const FIXTURE_ENTRYPOINT = {
42
+ fn: Foo,
43
+ params: [],
44
+};
45
+
46
+```
47
+
48
+### Eval output
49
+(kind: exception) Bar is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.js
renamed
-1
@@ -1,4 +1,3 @@
1
-// @enableNewMutationAliasingModel:false
1
function Foo() {
2
const x = () => {
3
window.href = 'foo';