@samitouri / QOS-React / commits / eaf6adb127

[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';