@samitouri / QOS-React / commits / 633a0fe536

[compiler] Factor out function effects from reference effects

Summary: This PR performs a major refactor of InferReferenceEffects to separate out the work on marking places with Effects from inferring FunctionEffects. The behavior should be identical after this change (see [internal sync](https://www.internalfb.com/intern/everpaste/?handle=GN74VxscnUaztTYDAL8q0CRWBIxibsIXAAAB)) but the FunctionEffect logic should be easier to work with. These analyses are unfortunately still deeply linked--the FunctionEffect analysis needs to reason about the "current" value kind for each point in the program, while the InferReferenceEffects algorithm performs global updates on the state of the program (e.g. freezing). In the future, it might be possible to make these entirely separate passes if we store the ValueKind directly on places. For the most part, the logic of reference effects and function effects can be cleanly separated: for each instruction and terminal, we visit its places and infer their effects, and then we visit its places and infer any function effects that they cause. The biggest wrinkle here is that when a transitive function freeze operation occurs, it has to happen *after* inferring the function effects on the place, because otherwise we may convert a value from Context to Frozen, which will cause the ContextualMutation function effect to be converted to a ReactMutation effect too early. This can be observed in a case like this: ``` export default component C() { foo(() => { const p = {}; return () => { p['a'] = 1 }; }); } ``` Here when the outer function returns the inner function, it freezes the inner function which transitively freezes `p`. But before that freeze happens, we need to replay the ContextualMutation on the inner function to determine that the value is mutable in the outer context. If we froze `p` first, we would instead convert the ContextualMutation to a ReactMutation and error. To handle this, InferReferenceEffects now delays the exection of the freezeValue action until after it's called the helper functions that generate function effects. So the order of operations on a given place is now set effect --> generate function effects --> transitively freeze dependencies, if applicable ghstack-source-id: 21cb50c14054e7e7a307acb595ef30b54c2f2a52 Pull Request resolved: https://github.com/facebook/react/pull/30920

Mike Vitousek committed Sep 13, 2024 at 12:38 UTC 633a0fe536febefa02698db124b7265a3fde55e1
2 files changed +614 -427
compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts new
+335
@@ -0,0 +1,335 @@
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, ErrorSeverity, ValueKind} from '..';
9 +import {
10 + AbstractValue,
11 + BasicBlock,
12 + Effect,
13 + Environment,
14 + FunctionEffect,
15 + Instruction,
16 + InstructionValue,
17 + Place,
18 + ValueReason,
19 + getHookKind,
20 + isRefOrRefValue,
21 +} from '../HIR';
22 +import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
23 +import {assertExhaustive} from '../Utils/utils';
24 +
25 +interface State {
26 + kind(place: Place): AbstractValue;
27 + values(place: Place): Array<InstructionValue>;
28 + isDefined(place: Place): boolean;
29 +}
30 +
31 +function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
32 + const value = state.kind(place);
33 + CompilerError.invariant(value != null, {
34 + reason: 'Expected operand to have a kind',
35 + loc: null,
36 + });
37 +
38 + switch (place.effect) {
39 + case Effect.Store:
40 + case Effect.Mutate: {
41 + if (isRefOrRefValue(place.identifier)) {
42 + break;
43 + } else if (value.kind === ValueKind.Context) {
44 + return {
45 + kind: 'ContextMutation',
46 + loc: place.loc,
47 + effect: place.effect,
48 + places: value.context.size === 0 ? new Set([place]) : value.context,
49 + };
50 + } else if (
51 + value.kind !== ValueKind.Mutable &&
52 + // We ignore mutations of primitives since this is not a React-specific problem
53 + value.kind !== ValueKind.Primitive
54 + ) {
55 + let reason = getWriteErrorReason(value);
56 + return {
57 + kind:
58 + value.reason.size === 1 && value.reason.has(ValueReason.Global)
59 + ? 'GlobalMutation'
60 + : 'ReactMutation',
61 + error: {
62 + reason,
63 + description:
64 + place.identifier.name !== null &&
65 + place.identifier.name.kind === 'named'
66 + ? `Found mutation of \`${place.identifier.name.value}\``
67 + : null,
68 + loc: place.loc,
69 + suggestions: null,
70 + severity: ErrorSeverity.InvalidReact,
71 + },
72 + };
73 + }
74 + break;
75 + }
76 + }
77 + return null;
78 +}
79 +
80 +function inheritFunctionEffects(
81 + state: State,
82 + place: Place,
83 +): Array<FunctionEffect> {
84 + const effects = inferFunctionInstrEffects(state, place);
85 +
86 + return effects
87 + .flatMap(effect => {
88 + if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
89 + return [effect];
90 + } else {
91 + const effects: Array<FunctionEffect | null> = [];
92 + CompilerError.invariant(effect.kind === 'ContextMutation', {
93 + reason: 'Expected ContextMutation',
94 + loc: null,
95 + });
96 + /**
97 + * Contextual effects need to be replayed against the current inference
98 + * state, which may know more about the value to which the effect applied.
99 + * The main cases are:
100 + * 1. The mutated context value is _still_ a context value in the current scope,
101 + * so we have to continue propagating the original context mutation.
102 + * 2. The mutated context value is a mutable value in the current scope,
103 + * so the context mutation was fine and we can skip propagating the effect.
104 + * 3. The mutated context value is an immutable value in the current scope,
105 + * resulting in a non-ContextMutation FunctionEffect. We propagate that new,
106 + * more detailed effect to the current function context.
107 + */
108 + for (const place of effect.places) {
109 + if (state.isDefined(place)) {
110 + const replayedEffect = inferOperandEffect(state, {
111 + ...place,
112 + loc: effect.loc,
113 + effect: effect.effect,
114 + });
115 + if (replayedEffect != null) {
116 + if (replayedEffect.kind === 'ContextMutation') {
117 + // Case 1, still a context value so propagate the original effect
118 + effects.push(effect);
119 + } else {
120 + // Case 3, immutable value so propagate the more precise effect
121 + effects.push(replayedEffect);
122 + }
123 + } // else case 2, local mutable value so this effect was fine
124 + }
125 + }
126 + return effects;
127 + }
128 + })
129 + .filter((effect): effect is FunctionEffect => effect != null);
130 +}
131 +
132 +function inferFunctionInstrEffects(
133 + state: State,
134 + place: Place,
135 +): Array<FunctionEffect> {
136 + const effects: Array<FunctionEffect> = [];
137 + const instrs = state.values(place);
138 + CompilerError.invariant(instrs != null, {
139 + reason: 'Expected operand to have instructions',
140 + loc: null,
141 + });
142 +
143 + for (const instr of instrs) {
144 + if (
145 + (instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
146 + instr.loweredFunc.func.effects != null
147 + ) {
148 + effects.push(...instr.loweredFunc.func.effects);
149 + }
150 + }
151 +
152 + return effects;
153 +}
154 +
155 +function operandEffects(
156 + state: State,
157 + place: Place,
158 + filterRenderSafe: boolean,
159 +): Array<FunctionEffect> {
160 + const functionEffects: Array<FunctionEffect> = [];
161 + const effect = inferOperandEffect(state, place);
162 + effect && functionEffects.push(effect);
163 + functionEffects.push(...inheritFunctionEffects(state, place));
164 + if (filterRenderSafe) {
165 + return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
166 + } else {
167 + return functionEffects;
168 + }
169 +}
170 +
171 +export function inferInstructionFunctionEffects(
172 + env: Environment,
173 + state: State,
174 + instr: Instruction,
175 +): Array<FunctionEffect> {
176 + const functionEffects: Array<FunctionEffect> = [];
177 + switch (instr.value.kind) {
178 + case 'JsxExpression': {
179 + if (instr.value.tag.kind === 'Identifier') {
180 + functionEffects.push(...operandEffects(state, instr.value.tag, false));
181 + }
182 + instr.value.children?.forEach(child =>
183 + functionEffects.push(...operandEffects(state, child, false)),
184 + );
185 + for (const attr of instr.value.props) {
186 + if (attr.kind === 'JsxSpreadAttribute') {
187 + functionEffects.push(...operandEffects(state, attr.argument, false));
188 + } else {
189 + functionEffects.push(...operandEffects(state, attr.place, true));
190 + }
191 + }
192 + break;
193 + }
194 + case 'ObjectMethod':
195 + case 'FunctionExpression': {
196 + /**
197 + * If this function references other functions, propagate the referenced function's
198 + * effects to this function.
199 + *
200 + * ```
201 + * let f = () => global = true;
202 + * let g = () => f();
203 + * g();
204 + * ```
205 + *
206 + * In this example, because `g` references `f`, we propagate the GlobalMutation from
207 + * `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
208 + * function effect context and report an error. But if instead we do:
209 + *
210 + * ```
211 + * let f = () => global = true;
212 + * let g = () => f();
213 + * useEffect(() => g(), [g])
214 + * ```
215 + *
216 + * Now `g`'s effects will be discarded since they're in a useEffect.
217 + */
218 + for (const operand of eachInstructionOperand(instr)) {
219 + instr.value.loweredFunc.func.effects ??= [];
220 + instr.value.loweredFunc.func.effects.push(
221 + ...inferFunctionInstrEffects(state, operand),
222 + );
223 + }
224 + break;
225 + }
226 + case 'MethodCall':
227 + case 'CallExpression': {
228 + let callee;
229 + if (instr.value.kind === 'MethodCall') {
230 + callee = instr.value.property;
231 + functionEffects.push(
232 + ...operandEffects(state, instr.value.receiver, false),
233 + );
234 + } else {
235 + callee = instr.value.callee;
236 + }
237 + functionEffects.push(...operandEffects(state, callee, false));
238 + let isHook = getHookKind(env, callee.identifier) != null;
239 + for (const arg of instr.value.args) {
240 + const place = arg.kind === 'Identifier' ? arg : arg.place;
241 + /*
242 + * Join the effects of the argument with the effects of the enclosing function,
243 + * unless the we're detecting a global mutation inside a useEffect hook
244 + */
245 + functionEffects.push(...operandEffects(state, place, isHook));
246 + }
247 + break;
248 + }
249 + case 'StartMemoize':
250 + case 'FinishMemoize':
251 + case 'LoadLocal':
252 + case 'StoreLocal': {
253 + break;
254 + }
255 + case 'StoreGlobal': {
256 + functionEffects.push({
257 + kind: 'GlobalMutation',
258 + error: {
259 + reason:
260 + '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)',
261 + loc: instr.loc,
262 + suggestions: null,
263 + severity: ErrorSeverity.InvalidReact,
264 + },
265 + });
266 + break;
267 + }
268 + default: {
269 + for (const operand of eachInstructionOperand(instr)) {
270 + functionEffects.push(...operandEffects(state, operand, false));
271 + }
272 + }
273 + }
274 + return functionEffects;
275 +}
276 +
277 +export function inferTerminalFunctionEffects(
278 + state: State,
279 + block: BasicBlock,
280 +): Array<FunctionEffect> {
281 + const functionEffects: Array<FunctionEffect> = [];
282 + for (const operand of eachTerminalOperand(block.terminal)) {
283 + functionEffects.push(...operandEffects(state, operand, true));
284 + }
285 + return functionEffects;
286 +}
287 +
288 +export function raiseFunctionEffectErrors(
289 + functionEffects: Array<FunctionEffect>,
290 +): void {
291 + functionEffects.forEach(eff => {
292 + switch (eff.kind) {
293 + case 'ReactMutation':
294 + case 'GlobalMutation': {
295 + CompilerError.throw(eff.error);
296 + }
297 + case 'ContextMutation': {
298 + CompilerError.throw({
299 + severity: ErrorSeverity.Invariant,
300 + reason: `Unexpected ContextMutation in top-level function effects`,
301 + loc: eff.loc,
302 + });
303 + }
304 + default:
305 + assertExhaustive(
306 + eff,
307 + `Unexpected function effect kind \`${(eff as any).kind}\``,
308 + );
309 + }
310 + });
311 +}
312 +
313 +function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
314 + return effect.kind === 'GlobalMutation';
315 +}
316 +
317 +function getWriteErrorReason(abstractValue: AbstractValue): string {
318 + if (abstractValue.reason.has(ValueReason.Global)) {
319 + return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
320 + } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
321 + return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
322 + } else if (abstractValue.reason.has(ValueReason.Context)) {
323 + return `Mutating a value returned from 'useContext()', which should not be mutated`;
324 + } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
325 + return 'Mutating a value returned from a function whose return value should not be mutated';
326 + } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
327 + return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
328 + } else if (abstractValue.reason.has(ValueReason.State)) {
329 + return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
330 + } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
331 + return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
332 + } else {
333 + return 'This mutates a variable that React considers immutable';
334 + }
335 +}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+279 -427
@@ -5,7 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {CompilerError, ErrorSeverity} from '../CompilerError';
8 +import {CompilerError} from '../CompilerError';
9 import {Environment} from '../HIR';
10 import {
11 AbstractValue,
@@ -26,11 +26,9 @@ import {
26 Type,
27 ValueKind,
28 ValueReason,
29 - getHookKind,
29 isArrayType,
30 isMutableEffect,
31 isObjectType,
33 - isRefOrRefValue,
32 } from '../HIR/HIR';
33 import {FunctionSignature} from '../HIR/ObjectShape';
34 import {
@@ -48,6 +46,11 @@ import {
46 eachTerminalSuccessor,
47 } from '../HIR/visitors';
48 import {assertExhaustive} from '../Utils/utils';
49 +import {
50 + inferTerminalFunctionEffects,
51 + inferInstructionFunctionEffects,
52 + raiseFunctionEffectErrors,
53 +} from './InferFunctionEffects';
54
55 const UndefinedValue: InstructionValue = {
56 kind: 'Primitive',
@@ -228,7 +231,7 @@ export default function inferReferenceEffects(
231
232 statesByBlock.set(blockId, incomingState);
233 const state = incomingState.clone();
231 - inferBlock(fn.env, functionEffects, state, block);
234 + inferBlock(fn.env, state, block, functionEffects);
235
236 for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
237 queue(nextBlockId, state);
@@ -236,37 +239,20 @@ export default function inferReferenceEffects(
239 }
240 }
241
239 - if (!options.isFunctionExpression) {
240 - functionEffects.forEach(eff => {
241 - switch (eff.kind) {
242 - case 'ReactMutation':
243 - case 'GlobalMutation': {
244 - CompilerError.throw(eff.error);
245 - }
246 - case 'ContextMutation': {
247 - CompilerError.throw({
248 - severity: ErrorSeverity.Invariant,
249 - reason: `Unexpected ContextMutation in top-level function effects`,
250 - loc: eff.loc,
251 - });
252 - }
253 - default:
254 - assertExhaustive(
255 - eff,
256 - `Unexpected function effect kind \`${(eff as any).kind}\``,
257 - );
258 - }
259 - });
260 - } else {
242 + if (options.isFunctionExpression) {
243 fn.effects = functionEffects;
244 + } else {
245 + raiseFunctionEffectErrors(functionEffects);
246 }
247 }
248
249 +type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
250 +
251 // Maintains a mapping of top-level variables to the kind of value they hold
252 class InferenceState {
253 #env: Environment;
254
269 - // The kind of reach value, based on its allocation site
255 + // The kind of each value, based on its allocation site
256 #values: Map<InstructionValue, AbstractValue>;
257 /*
258 * The set of values pointed to by each identifier. This is a set
@@ -378,10 +364,10 @@ class InferenceState {
364 * value is already frozen or is immutable.
365 */
366 referenceAndRecordEffects(
367 + freezeActions: Array<FreezeAction>,
368 place: Place,
369 effectKind: Effect,
370 reason: ValueReason,
384 - functionEffects: Array<FunctionEffect>,
371 ): void {
372 const values = this.#variables.get(place.identifier.id);
373 if (values === undefined) {
@@ -398,59 +384,8 @@ class InferenceState {
384 return;
385 }
386
401 - // Propagate effects of function expressions to the outer (ie current) effect context
402 - for (const value of values) {
403 - if (
404 - (value.kind === 'FunctionExpression' ||
405 - value.kind === 'ObjectMethod') &&
406 - value.loweredFunc.func.effects != null
407 - ) {
408 - for (const effect of value.loweredFunc.func.effects) {
409 - if (
410 - effect.kind === 'GlobalMutation' ||
411 - effect.kind === 'ReactMutation'
412 - ) {
413 - // Known effects are always propagated upwards
414 - functionEffects.push(effect);
415 - } else {
416 - /**
417 - * Contextual effects need to be replayed against the current inference
418 - * state, which may know more about the value to which the effect applied.
419 - * The main cases are:
420 - * 1. The mutated context value is _still_ a context value in the current scope,
421 - * so we have to continue propagating the original context mutation.
422 - * 2. The mutated context value is a mutable value in the current scope,
423 - * so the context mutation was fine and we can skip propagating the effect.
424 - * 3. The mutated context value is an immutable value in the current scope,
425 - * resulting in a non-ContextMutation FunctionEffect. We propagate that new,
426 - * more detailed effect to the current function context.
427 - */
428 - for (const place of effect.places) {
429 - if (this.isDefined(place)) {
430 - const replayedEffect = this.reference(
431 - {...place, loc: effect.loc},
432 - effect.effect,
433 - reason,
434 - );
435 - if (replayedEffect != null) {
436 - if (replayedEffect.kind === 'ContextMutation') {
437 - // Case 1, still a context value so propagate the original effect
438 - functionEffects.push(effect);
439 - } else {
440 - // Case 3, immutable value so propagate the more precise effect
441 - functionEffects.push(replayedEffect);
442 - }
443 - } // else case 2, local mutable value so this effect was fine
444 - }
445 - }
446 - }
447 - }
448 - }
449 - }
450 - const functionEffect = this.reference(place, effectKind, reason);
451 - if (functionEffect !== null) {
452 - functionEffects.push(functionEffect);
453 - }
387 + const action = this.reference(place, effectKind, reason);
388 + action && freezeActions.push(action);
389 }
390
391 freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
@@ -488,7 +423,7 @@ class InferenceState {
423 place: Place,
424 effectKind: Effect,
425 reason: ValueReason,
491 - ): FunctionEffect | null {
426 + ): null | FreezeAction {
427 const values = this.#variables.get(place.identifier.id);
428 CompilerError.invariant(values !== undefined, {
429 reason: '[InferReferenceEffects] Expected value to be initialized',
@@ -498,7 +433,7 @@ class InferenceState {
433 });
434 let valueKind: AbstractValue | null = this.kind(place);
435 let effect: Effect | null = null;
501 - let functionEffect: FunctionEffect | null = null;
436 + let freeze: null | FreezeAction = null;
437 switch (effectKind) {
438 case Effect.Freeze: {
439 if (
@@ -513,7 +448,7 @@ class InferenceState {
448 reason: reasonSet,
449 context: new Set(),
450 };
516 - this.freezeValues(values, reasonSet);
451 + freeze = {values, reason: reasonSet};
452 } else {
453 effect = Effect.Read;
454 }
@@ -531,85 +466,10 @@ class InferenceState {
466 break;
467 }
468 case Effect.Mutate: {
534 - if (isRefOrRefValue(place.identifier)) {
535 - // no-op: refs are validate via ValidateNoRefAccessInRender
536 - } else if (valueKind.kind === ValueKind.Context) {
537 - functionEffect = {
538 - kind: 'ContextMutation',
539 - loc: place.loc,
540 - effect: effectKind,
541 - places:
542 - valueKind.context.size === 0
543 - ? new Set([place])
544 - : valueKind.context,
545 - };
546 - } else if (
547 - valueKind.kind !== ValueKind.Mutable &&
548 - // We ignore mutations of primitives since this is not a React-specific problem
549 - valueKind.kind !== ValueKind.Primitive
550 - ) {
551 - let reason = getWriteErrorReason(valueKind);
552 - functionEffect = {
553 - kind:
554 - valueKind.reason.size === 1 &&
555 - valueKind.reason.has(ValueReason.Global)
556 - ? 'GlobalMutation'
557 - : 'ReactMutation',
558 - error: {
559 - reason,
560 - description:
561 - place.identifier.name !== null &&
562 - place.identifier.name.kind === 'named'
563 - ? `Found mutation of \`${place.identifier.name.value}\``
564 - : null,
565 - loc: place.loc,
566 - suggestions: null,
567 - severity: ErrorSeverity.InvalidReact,
568 - },
569 - };
570 - }
469 effect = Effect.Mutate;
470 break;
471 }
472 case Effect.Store: {
575 - if (isRefOrRefValue(place.identifier)) {
576 - // no-op: refs are validate via ValidateNoRefAccessInRender
577 - } else if (valueKind.kind === ValueKind.Context) {
578 - functionEffect = {
579 - kind: 'ContextMutation',
580 - loc: place.loc,
581 - effect: effectKind,
582 - places:
583 - valueKind.context.size === 0
584 - ? new Set([place])
585 - : valueKind.context,
586 - };
587 - } else if (
588 - valueKind.kind !== ValueKind.Mutable &&
589 - // We ignore mutations of primitives since this is not a React-specific problem
590 - valueKind.kind !== ValueKind.Primitive
591 - ) {
592 - let reason = getWriteErrorReason(valueKind);
593 - functionEffect = {
594 - kind:
595 - valueKind.reason.size === 1 &&
596 - valueKind.reason.has(ValueReason.Global)
597 - ? 'GlobalMutation'
598 - : 'ReactMutation',
599 - error: {
600 - reason,
601 - description:
602 - place.identifier.name !== null &&
603 - place.identifier.name.kind === 'named'
604 - ? `Found mutation of \`${place.identifier.name.value}\``
605 - : null,
606 - loc: place.loc,
607 - suggestions: null,
608 - severity: ErrorSeverity.InvalidReact,
609 - },
610 - };
611 - }
612 -
473 /*
474 * TODO(gsn): This should be bailout once we add bailout infra.
475 *
@@ -661,7 +521,7 @@ class InferenceState {
521 suggestions: null,
522 });
523 place.effect = effect;
664 - return functionEffect;
524 + return freeze;
525 }
526
527 /*
@@ -952,15 +812,24 @@ function mergeAbstractValues(
812 return {kind, reason, context};
813 }
814
815 +type Continuation =
816 + | {
817 + kind: 'initialize';
818 + valueKind: AbstractValue;
819 + effect: {kind: Effect; reason: ValueReason} | null;
820 + lvalueEffect?: Effect;
821 + }
822 + | {kind: 'funeffects'};
823 +
824 /*
825 * Iterates over the given @param block, defining variables and
826 * recording references on the @param state according to JS semantics.
827 */
828 function inferBlock(
829 env: Environment,
961 - functionEffects: Array<FunctionEffect>,
830 state: InferenceState,
831 block: BasicBlock,
832 + functionEffects: Array<FunctionEffect>,
833 ): void {
834 for (const phi of block.phis) {
835 state.inferPhi(phi);
@@ -968,24 +837,27 @@ function inferBlock(
837
838 for (const instr of block.instructions) {
839 const instrValue = instr.value;
971 - let effect: {kind: Effect; reason: ValueReason} | null = null;
972 - let lvalueEffect = Effect.ConditionallyMutate;
973 - let valueKind: AbstractValue;
840 + const defaultLvalueEffect = Effect.ConditionallyMutate;
841 + let continuation: Continuation;
842 + const freezeActions: Array<FreezeAction> = [];
843 switch (instrValue.kind) {
844 case 'BinaryExpression': {
976 - valueKind = {
977 - kind: ValueKind.Primitive,
978 - reason: new Set([ValueReason.Other]),
979 - context: new Set(),
980 - };
981 - effect = {
982 - kind: Effect.Read,
983 - reason: ValueReason.Other,
845 + continuation = {
846 + kind: 'initialize',
847 + valueKind: {
848 + kind: ValueKind.Primitive,
849 + reason: new Set([ValueReason.Other]),
850 + context: new Set(),
851 + },
852 + effect: {
853 + kind: Effect.Read,
854 + reason: ValueReason.Other,
855 + },
856 };
857 break;
858 }
859 case 'ArrayExpression': {
988 - valueKind = hasContextRefOperand(state, instrValue)
860 + const valueKind: AbstractValue = hasContextRefOperand(state, instrValue)
861 ? {
862 kind: ValueKind.Context,
863 reason: new Set([ValueReason.Other]),
@@ -996,8 +868,12 @@ function inferBlock(
868 reason: new Set([ValueReason.Other]),
869 context: new Set(),
870 };
999 - effect = {kind: Effect.Capture, reason: ValueReason.Other};
1000 - lvalueEffect = Effect.Store;
871 + continuation = {
872 + kind: 'initialize',
873 + valueKind,
874 + effect: {kind: Effect.Capture, reason: ValueReason.Other},
875 + lvalueEffect: Effect.Store,
876 + };
877 break;
878 }
879 case 'NewExpression': {
@@ -1014,34 +890,35 @@ function inferBlock(
890 * Classes / functions created during render could technically capture and
891 * mutate their enclosing scope, which we currently do not detect.
892 */
1017 - valueKind = {
893 + const valueKind: AbstractValue = {
894 kind: ValueKind.Mutable,
895 reason: new Set([ValueReason.Other]),
896 context: new Set(),
897 };
898 state.referenceAndRecordEffects(
899 + freezeActions,
900 instrValue.callee,
901 Effect.Read,
902 ValueReason.Other,
1026 - functionEffects,
903 );
904
905 for (const operand of eachCallArgument(instrValue.args)) {
906 state.referenceAndRecordEffects(
907 + freezeActions,
908 operand,
909 Effect.ConditionallyMutate,
910 ValueReason.Other,
1034 - functionEffects,
911 );
912 }
913
914 state.initialize(instrValue, valueKind);
915 state.define(instr.lvalue, instrValue);
1040 - instr.lvalue.effect = lvalueEffect;
1041 - continue;
916 + instr.lvalue.effect = Effect.ConditionallyMutate;
917 + continuation = {kind: 'funeffects'};
918 + break;
919 }
920 case 'ObjectExpression': {
1044 - valueKind = hasContextRefOperand(state, instrValue)
921 + const valueKind: AbstractValue = hasContextRefOperand(state, instrValue)
922 ? {
923 kind: ValueKind.Context,
924 reason: new Set([ValueReason.Other]),
@@ -1059,28 +936,28 @@ function inferBlock(
936 if (property.key.kind === 'computed') {
937 // Object keys must be primitives, so we know they're frozen at this point
938 state.referenceAndRecordEffects(
939 + freezeActions,
940 property.key.name,
941 Effect.Freeze,
942 ValueReason.Other,
1065 - functionEffects,
943 );
944 }
945 // Object construction captures but does not modify the key/property values
946 state.referenceAndRecordEffects(
947 + freezeActions,
948 property.place,
949 Effect.Capture,
950 ValueReason.Other,
1073 - functionEffects,
951 );
952 break;
953 }
954 case 'Spread': {
955 // Object construction captures but does not modify the key/property values
956 state.referenceAndRecordEffects(
957 + freezeActions,
958 property.place,
959 Effect.Capture,
960 ValueReason.Other,
1083 - functionEffects,
961 );
962 break;
963 }
@@ -1096,65 +973,67 @@ function inferBlock(
973 state.initialize(instrValue, valueKind);
974 state.define(instr.lvalue, instrValue);
975 instr.lvalue.effect = Effect.Store;
1099 - continue;
976 + continuation = {kind: 'funeffects'};
977 + break;
978 }
979 case 'UnaryExpression': {
1102 - valueKind = {
1103 - kind: ValueKind.Primitive,
1104 - reason: new Set([ValueReason.Other]),
1105 - context: new Set(),
980 + continuation = {
981 + kind: 'initialize',
982 + valueKind: {
983 + kind: ValueKind.Primitive,
984 + reason: new Set([ValueReason.Other]),
985 + context: new Set(),
986 + },
987 + effect: {kind: Effect.Read, reason: ValueReason.Other},
988 };
1107 - effect = {kind: Effect.Read, reason: ValueReason.Other};
989 break;
990 }
991 case 'UnsupportedNode': {
992 // TODO: handle other statement kinds
1112 - valueKind = {
1113 - kind: ValueKind.Mutable,
1114 - reason: new Set([ValueReason.Other]),
1115 - context: new Set(),
993 + continuation = {
994 + kind: 'initialize',
995 + valueKind: {
996 + kind: ValueKind.Mutable,
997 + reason: new Set([ValueReason.Other]),
998 + context: new Set(),
999 + },
1000 + effect: null,
1001 };
1002 break;
1003 }
1004 case 'JsxExpression': {
1005 if (instrValue.tag.kind === 'Identifier') {
1006 state.referenceAndRecordEffects(
1007 + freezeActions,
1008 instrValue.tag,
1009 Effect.Freeze,
1010 ValueReason.JsxCaptured,
1125 - functionEffects,
1011 );
1012 }
1013 if (instrValue.children !== null) {
1014 for (const child of instrValue.children) {
1015 state.referenceAndRecordEffects(
1016 + freezeActions,
1017 child,
1018 Effect.Freeze,
1019 ValueReason.JsxCaptured,
1134 - functionEffects,
1020 );
1021 }
1022 }
1023 for (const attr of instrValue.props) {
1024 if (attr.kind === 'JsxSpreadAttribute') {
1025 state.referenceAndRecordEffects(
1026 + freezeActions,
1027 attr.argument,
1028 Effect.Freeze,
1029 ValueReason.JsxCaptured,
1144 - functionEffects,
1030 );
1031 } else {
1147 - const propEffects: Array<FunctionEffect> = [];
1032 state.referenceAndRecordEffects(
1033 + freezeActions,
1034 attr.place,
1035 Effect.Freeze,
1036 ValueReason.JsxCaptured,
1152 - propEffects,
1153 - );
1154 - functionEffects.push(
1155 - ...propEffects.filter(
1156 - effect => !isEffectSafeOutsideRender(effect),
1157 - ),
1037 );
1038 }
1039 }
@@ -1166,17 +1045,21 @@ function inferBlock(
1045 });
1046 state.define(instr.lvalue, instrValue);
1047 instr.lvalue.effect = Effect.ConditionallyMutate;
1169 - continue;
1048 + continuation = {kind: 'funeffects'};
1049 + break;
1050 }
1051 case 'JsxFragment': {
1172 - valueKind = {
1173 - kind: ValueKind.Frozen,
1174 - reason: new Set([ValueReason.Other]),
1175 - context: new Set(),
1176 - };
1177 - effect = {
1178 - kind: Effect.Freeze,
1179 - reason: ValueReason.Other,
1052 + continuation = {
1053 + kind: 'initialize',
1054 + valueKind: {
1055 + kind: ValueKind.Frozen,
1056 + reason: new Set([ValueReason.Other]),
1057 + context: new Set(),
1058 + },
1059 + effect: {
1060 + kind: Effect.Freeze,
1061 + reason: ValueReason.Other,
1062 + },
1063 };
1064 break;
1065 }
@@ -1185,53 +1068,71 @@ function inferBlock(
1068 * template literal (with no tag function) always produces
1069 * an immutable string
1070 */
1188 - valueKind = {
1189 - kind: ValueKind.Primitive,
1190 - reason: new Set([ValueReason.Other]),
1191 - context: new Set(),
1071 + continuation = {
1072 + kind: 'initialize',
1073 + valueKind: {
1074 + kind: ValueKind.Primitive,
1075 + reason: new Set([ValueReason.Other]),
1076 + context: new Set(),
1077 + },
1078 + effect: {kind: Effect.Read, reason: ValueReason.Other},
1079 };
1193 - effect = {kind: Effect.Read, reason: ValueReason.Other};
1080 break;
1081 }
1082 case 'RegExpLiteral': {
1083 // RegExp instances are mutable objects
1198 - valueKind = {
1199 - kind: ValueKind.Mutable,
1200 - reason: new Set([ValueReason.Other]),
1201 - context: new Set(),
1202 - };
1203 - effect = {
1204 - kind: Effect.ConditionallyMutate,
1205 - reason: ValueReason.Other,
1084 + continuation = {
1085 + kind: 'initialize',
1086 + valueKind: {
1087 + kind: ValueKind.Mutable,
1088 + reason: new Set([ValueReason.Other]),
1089 + context: new Set(),
1090 + },
1091 + effect: {
1092 + kind: Effect.ConditionallyMutate,
1093 + reason: ValueReason.Other,
1094 + },
1095 };
1096 break;
1097 }
1098 case 'MetaProperty': {
1099 if (instrValue.meta !== 'import' || instrValue.property !== 'meta') {
1211 - continue;
1100 + continuation = {kind: 'funeffects'};
1101 + break;
1102 }
1213 -
1214 - valueKind = {
1215 - kind: ValueKind.Global,
1216 - reason: new Set([ValueReason.Global]),
1217 - context: new Set(),
1103 + continuation = {
1104 + kind: 'initialize',
1105 + valueKind: {
1106 + kind: ValueKind.Global,
1107 + reason: new Set([ValueReason.Global]),
1108 + context: new Set(),
1109 + },
1110 + effect: null,
1111 };
1112 break;
1113 }
1114 case 'LoadGlobal':
1222 - valueKind = {
1223 - kind: ValueKind.Global,
1224 - reason: new Set([ValueReason.Global]),
1225 - context: new Set(),
1115 + continuation = {
1116 + kind: 'initialize',
1117 + valueKind: {
1118 + kind: ValueKind.Global,
1119 + reason: new Set([ValueReason.Global]),
1120 + context: new Set(),
1121 + },
1122 + effect: null,
1123 };
1124 break;
1125 case 'Debugger':
1126 case 'JSXText':
1127 case 'Primitive': {
1231 - valueKind = {
1232 - kind: ValueKind.Primitive,
1233 - reason: new Set([ValueReason.Other]),
1234 - context: new Set(),
1128 + continuation = {
1129 + kind: 'initialize',
1130 + valueKind: {
1131 + kind: ValueKind.Primitive,
1132 + reason: new Set([ValueReason.Other]),
1133 + context: new Set(),
1134 + },
1135 + effect: null,
1136 };
1137 break;
1138 }
@@ -1241,51 +1142,15 @@ function inferBlock(
1142 const mutableOperands: Array<Place> = [];
1143 for (const operand of eachInstructionOperand(instr)) {
1144 state.referenceAndRecordEffects(
1145 + freezeActions,
1146 operand,
1147 operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
1148 ValueReason.Other,
1247 - [],
1149 );
1150 if (isMutableEffect(operand.effect, operand.loc)) {
1151 mutableOperands.push(operand);
1152 }
1153 hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
1253 -
1254 - /**
1255 - * If this function references other functions, propagate the referenced function's
1256 - * effects to this function.
1257 - *
1258 - * ```
1259 - * let f = () => global = true;
1260 - * let g = () => f();
1261 - * g();
1262 - * ```
1263 - *
1264 - * In this example, because `g` references `f`, we propagate the GlobalMutation from
1265 - * `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
1266 - * function effect context and report an error. But if instead we do:
1267 - *
1268 - * ```
1269 - * let f = () => global = true;
1270 - * let g = () => f();
1271 - * useEffect(() => g(), [g])
1272 - * ```
1273 - *
1274 - * Now `g`'s effects will be discarded since they're in a useEffect.
1275 - */
1276 - const values = state.values(operand);
1277 - for (const value of values) {
1278 - if (
1279 - (value.kind === 'ObjectMethod' ||
1280 - value.kind === 'FunctionExpression') &&
1281 - value.loweredFunc.func.effects !== null
1282 - ) {
1283 - instrValue.loweredFunc.func.effects ??= [];
1284 - instrValue.loweredFunc.func.effects.push(
1285 - ...value.loweredFunc.func.effects,
1286 - );
1287 - }
1288 - }
1154 }
1155 /*
1156 * If a closure did not capture any mutable values, then we can consider it to be
@@ -1298,7 +1163,8 @@ function inferBlock(
1163 });
1164 state.define(instr.lvalue, instrValue);
1165 instr.lvalue.effect = Effect.Store;
1301 - continue;
1166 + continuation = {kind: 'funeffects'};
1167 + break;
1168 }
1169 case 'TaggedTemplateExpression': {
1170 const operands = [...eachInstructionValueOperand(instrValue)];
@@ -1331,15 +1197,16 @@ function inferBlock(
1197 context: new Set(),
1198 };
1199 state.referenceAndRecordEffects(
1200 + freezeActions,
1201 instrValue.tag,
1202 calleeEffect,
1203 ValueReason.Other,
1337 - functionEffects,
1204 );
1205 state.initialize(instrValue, returnValueKind);
1206 state.define(instr.lvalue, instrValue);
1207 instr.lvalue.effect = Effect.ConditionallyMutate;
1342 - continue;
1208 + continuation = {kind: 'funeffects'};
1209 + break;
1210 }
1211 case 'CallExpression': {
1212 const signature = getFunctionCallSignature(
@@ -1365,50 +1232,39 @@ function inferBlock(
1232 context: new Set(),
1233 };
1234 let hasCaptureArgument = false;
1368 - let isHook = getHookKind(env, instrValue.callee.identifier) != null;
1235 for (let i = 0; i < instrValue.args.length; i++) {
1370 - const argumentEffects: Array<FunctionEffect> = [];
1236 const arg = instrValue.args[i];
1237 const place = arg.kind === 'Identifier' ? arg : arg.place;
1238 if (effects !== null) {
1239 state.referenceAndRecordEffects(
1240 + freezeActions,
1241 place,
1242 effects[i],
1243 ValueReason.Other,
1378 - argumentEffects,
1244 );
1245 } else {
1246 state.referenceAndRecordEffects(
1247 + freezeActions,
1248 place,
1249 Effect.ConditionallyMutate,
1250 ValueReason.Other,
1385 - argumentEffects,
1251 );
1252 }
1388 - /*
1389 - * Join the effects of the argument with the effects of the enclosing function,
1390 - * unless the we're detecting a global mutation inside a useEffect hook
1391 - */
1392 - functionEffects.push(
1393 - ...argumentEffects.filter(
1394 - argEffect => !isHook || !isEffectSafeOutsideRender(argEffect),
1395 - ),
1396 - );
1253 hasCaptureArgument ||= place.effect === Effect.Capture;
1254 }
1255 if (signature !== null) {
1256 state.referenceAndRecordEffects(
1257 + freezeActions,
1258 instrValue.callee,
1259 signature.calleeEffect,
1260 ValueReason.Other,
1404 - functionEffects,
1261 );
1262 } else {
1263 state.referenceAndRecordEffects(
1264 + freezeActions,
1265 instrValue.callee,
1266 Effect.ConditionallyMutate,
1267 ValueReason.Other,
1411 - functionEffects,
1268 );
1269 }
1270 hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
@@ -1418,7 +1274,8 @@ function inferBlock(
1274 instr.lvalue.effect = hasCaptureArgument
1275 ? Effect.Store
1276 : Effect.ConditionallyMutate;
1421 - continue;
1277 + continuation = {kind: 'funeffects'};
1278 + break;
1279 }
1280 case 'MethodCall': {
1281 CompilerError.invariant(state.isDefined(instrValue.receiver), {
@@ -1429,10 +1286,10 @@ function inferBlock(
1286 suggestions: null,
1287 });
1288 state.referenceAndRecordEffects(
1289 + freezeActions,
1290 instrValue.property,
1291 Effect.Read,
1292 ValueReason.Other,
1435 - functionEffects,
1293 );
1294
1295 const signature = getFunctionCallSignature(
@@ -1465,17 +1322,17 @@ function inferBlock(
1322 for (const arg of instrValue.args) {
1323 const place = arg.kind === 'Identifier' ? arg : arg.place;
1324 state.referenceAndRecordEffects(
1325 + freezeActions,
1326 place,
1327 Effect.Read,
1328 ValueReason.Other,
1471 - functionEffects,
1329 );
1330 }
1331 state.referenceAndRecordEffects(
1332 + freezeActions,
1333 instrValue.receiver,
1334 Effect.Capture,
1335 ValueReason.Other,
1478 - functionEffects,
1336 );
1337 state.initialize(instrValue, returnValueKind);
1338 state.define(instr.lvalue, instrValue);
@@ -1483,15 +1340,14 @@ function inferBlock(
1340 instrValue.receiver.effect === Effect.Capture
1341 ? Effect.Store
1342 : Effect.ConditionallyMutate;
1486 - continue;
1343 + continuation = {kind: 'funeffects'};
1344 + break;
1345 }
1346
1347 const effects =
1348 signature !== null ? getFunctionEffects(instrValue, signature) : null;
1349 let hasCaptureArgument = false;
1492 - let isHook = getHookKind(env, instrValue.property.identifier) != null;
1350 for (let i = 0; i < instrValue.args.length; i++) {
1494 - const argumentEffects: Array<FunctionEffect> = [];
1351 const arg = instrValue.args[i];
1352 const place = arg.kind === 'Identifier' ? arg : arg.place;
1353 if (effects !== null) {
@@ -1500,43 +1356,34 @@ function inferBlock(
1356 * mutating effects
1357 */
1358 state.referenceAndRecordEffects(
1359 + freezeActions,
1360 place,
1361 effects[i],
1362 ValueReason.Other,
1506 - argumentEffects,
1363 );
1364 } else {
1365 state.referenceAndRecordEffects(
1366 + freezeActions,
1367 place,
1368 Effect.ConditionallyMutate,
1369 ValueReason.Other,
1513 - argumentEffects,
1370 );
1371 }
1516 - /*
1517 - * Join the effects of the argument with the effects of the enclosing function,
1518 - * unless the we're detecting a global mutation inside a useEffect hook
1519 - */
1520 - functionEffects.push(
1521 - ...argumentEffects.filter(
1522 - argEffect => !isHook || !isEffectSafeOutsideRender(argEffect),
1523 - ),
1524 - );
1372 hasCaptureArgument ||= place.effect === Effect.Capture;
1373 }
1374 if (signature !== null) {
1375 state.referenceAndRecordEffects(
1376 + freezeActions,
1377 instrValue.receiver,
1378 signature.calleeEffect,
1379 ValueReason.Other,
1532 - functionEffects,
1380 );
1381 } else {
1382 state.referenceAndRecordEffects(
1383 + freezeActions,
1384 instrValue.receiver,
1385 Effect.ConditionallyMutate,
1386 ValueReason.Other,
1539 - functionEffects,
1387 );
1388 }
1389 hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture;
@@ -1546,7 +1393,8 @@ function inferBlock(
1393 instr.lvalue.effect = hasCaptureArgument
1394 ? Effect.Store
1395 : Effect.ConditionallyMutate;
1549 - continue;
1396 + continuation = {kind: 'funeffects'};
1397 + break;
1398 }
1399 case 'PropertyStore': {
1400 const effect =
@@ -1554,45 +1402,50 @@ function inferBlock(
1402 ? Effect.ConditionallyMutate
1403 : Effect.Capture;
1404 state.referenceAndRecordEffects(
1405 + freezeActions,
1406 instrValue.value,
1407 effect,
1408 ValueReason.Other,
1560 - functionEffects,
1409 );
1410 state.referenceAndRecordEffects(
1411 + freezeActions,
1412 instrValue.object,
1413 Effect.Store,
1414 ValueReason.Other,
1566 - functionEffects,
1415 );
1416
1417 const lvalue = instr.lvalue;
1418 state.alias(lvalue, instrValue.value);
1419 lvalue.effect = Effect.Store;
1572 - continue;
1420 + continuation = {kind: 'funeffects'};
1421 + break;
1422 }
1423 case 'PropertyDelete': {
1424 // `delete` returns a boolean (immutable) and modifies the object
1576 - valueKind = {
1577 - kind: ValueKind.Primitive,
1578 - reason: new Set([ValueReason.Other]),
1579 - context: new Set(),
1425 + continuation = {
1426 + kind: 'initialize',
1427 + valueKind: {
1428 + kind: ValueKind.Primitive,
1429 + reason: new Set([ValueReason.Other]),
1430 + context: new Set(),
1431 + },
1432 + effect: {kind: Effect.Mutate, reason: ValueReason.Other},
1433 };
1581 - effect = {kind: Effect.Mutate, reason: ValueReason.Other};
1434 break;
1435 }
1436 case 'PropertyLoad': {
1437 state.referenceAndRecordEffects(
1438 + freezeActions,
1439 instrValue.object,
1440 Effect.Read,
1441 ValueReason.Other,
1589 - functionEffects,
1442 );
1443 const lvalue = instr.lvalue;
1444 lvalue.effect = Effect.ConditionallyMutate;
1445 state.initialize(instrValue, state.kind(instrValue.object));
1446 state.define(lvalue, instrValue);
1595 - continue;
1447 + continuation = {kind: 'funeffects'};
1448 + break;
1449 }
1450 case 'ComputedStore': {
1451 const effect =
@@ -1600,41 +1453,42 @@ function inferBlock(
1453 ? Effect.ConditionallyMutate
1454 : Effect.Capture;
1455 state.referenceAndRecordEffects(
1456 + freezeActions,
1457 instrValue.value,
1458 effect,
1459 ValueReason.Other,
1606 - functionEffects,
1460 );
1461 state.referenceAndRecordEffects(
1462 + freezeActions,
1463 instrValue.property,
1464 Effect.Capture,
1465 ValueReason.Other,
1612 - functionEffects,
1466 );
1467 state.referenceAndRecordEffects(
1468 + freezeActions,
1469 instrValue.object,
1470 Effect.Store,
1471 ValueReason.Other,
1618 - functionEffects,
1472 );
1473
1474 const lvalue = instr.lvalue;
1475 state.alias(lvalue, instrValue.value);
1476 lvalue.effect = Effect.Store;
1624 - continue;
1477 + continuation = {kind: 'funeffects'};
1478 + break;
1479 }
1480 case 'ComputedDelete': {
1481 state.referenceAndRecordEffects(
1482 + freezeActions,
1483 instrValue.object,
1484 Effect.Mutate,
1485 ValueReason.Other,
1631 - functionEffects,
1486 );
1487 state.referenceAndRecordEffects(
1488 + freezeActions,
1489 instrValue.property,
1490 Effect.Read,
1491 ValueReason.Other,
1637 - functionEffects,
1492 );
1493 state.initialize(instrValue, {
1494 kind: ValueKind.Primitive,
@@ -1643,26 +1497,28 @@ function inferBlock(
1497 });
1498 state.define(instr.lvalue, instrValue);
1499 instr.lvalue.effect = Effect.Mutate;
1646 - continue;
1500 + continuation = {kind: 'funeffects'};
1501 + break;
1502 }
1503 case 'ComputedLoad': {
1504 state.referenceAndRecordEffects(
1505 + freezeActions,
1506 instrValue.object,
1507 Effect.Read,
1508 ValueReason.Other,
1653 - functionEffects,
1509 );
1510 state.referenceAndRecordEffects(
1511 + freezeActions,
1512 instrValue.property,
1513 Effect.Read,
1514 ValueReason.Other,
1659 - functionEffects,
1515 );
1516 const lvalue = instr.lvalue;
1517 lvalue.effect = Effect.ConditionallyMutate;
1518 state.initialize(instrValue, state.kind(instrValue.object));
1519 state.define(lvalue, instrValue);
1665 - continue;
1520 + continuation = {kind: 'funeffects'};
1521 + break;
1522 }
1523 case 'Await': {
1524 state.initialize(instrValue, state.kind(instrValue.value));
@@ -1672,15 +1528,16 @@ function inferBlock(
1528 * will occur.
1529 */
1530 state.referenceAndRecordEffects(
1531 + freezeActions,
1532 instrValue.value,
1533 Effect.ConditionallyMutate,
1534 ValueReason.Other,
1678 - functionEffects,
1535 );
1536 const lvalue = instr.lvalue;
1537 lvalue.effect = Effect.ConditionallyMutate;
1538 state.alias(lvalue, instrValue.value);
1683 - continue;
1539 + continuation = {kind: 'funeffects'};
1540 + break;
1541 }
1542 case 'TypeCastExpression': {
1543 /*
@@ -1693,32 +1550,33 @@ function inferBlock(
1550 */
1551 state.initialize(instrValue, state.kind(instrValue.value));
1552 state.referenceAndRecordEffects(
1553 + freezeActions,
1554 instrValue.value,
1555 Effect.Read,
1556 ValueReason.Other,
1699 - functionEffects,
1557 );
1558 const lvalue = instr.lvalue;
1559 lvalue.effect = Effect.ConditionallyMutate;
1560 state.alias(lvalue, instrValue.value);
1704 - continue;
1561 + continuation = {kind: 'funeffects'};
1562 + break;
1563 }
1564 case 'StartMemoize':
1565 case 'FinishMemoize': {
1566 for (const val of eachInstructionValueOperand(instrValue)) {
1567 if (env.config.enablePreserveExistingMemoizationGuarantees) {
1568 state.referenceAndRecordEffects(
1569 + freezeActions,
1570 val,
1571 Effect.Freeze,
1572 ValueReason.Other,
1714 - [],
1573 );
1574 } else {
1575 state.referenceAndRecordEffects(
1576 + freezeActions,
1577 val,
1578 Effect.Read,
1579 ValueReason.Other,
1721 - [],
1580 );
1581 }
1582 }
@@ -1730,7 +1588,8 @@ function inferBlock(
1588 context: new Set(),
1589 });
1590 state.define(lvalue, instrValue);
1733 - continue;
1591 + continuation = {kind: 'funeffects'};
1592 + break;
1593 }
1594 case 'LoadLocal': {
1595 const lvalue = instr.lvalue;
@@ -1740,29 +1599,31 @@ function inferBlock(
1599 ? Effect.ConditionallyMutate
1600 : Effect.Capture;
1601 state.referenceAndRecordEffects(
1602 + freezeActions,
1603 instrValue.place,
1604 effect,
1605 ValueReason.Other,
1746 - [],
1606 );
1607 lvalue.effect = Effect.ConditionallyMutate;
1608 // direct aliasing: `a = b`;
1609 state.alias(lvalue, instrValue.place);
1751 - continue;
1610 + continuation = {kind: 'funeffects'};
1611 + break;
1612 }
1613 case 'LoadContext': {
1614 state.referenceAndRecordEffects(
1615 + freezeActions,
1616 instrValue.place,
1617 Effect.Capture,
1618 ValueReason.Other,
1758 - functionEffects,
1619 );
1620 const lvalue = instr.lvalue;
1621 lvalue.effect = Effect.ConditionallyMutate;
1622 const valueKind = state.kind(instrValue.place);
1623 state.initialize(instrValue, valueKind);
1624 state.define(lvalue, instrValue);
1765 - continue;
1625 + continuation = {kind: 'funeffects'};
1626 + break;
1627 }
1628 case 'DeclareLocal': {
1629 const value = UndefinedValue;
@@ -1782,7 +1643,8 @@ function inferBlock(
1643 },
1644 );
1645 state.define(instrValue.lvalue.place, value);
1785 - continue;
1646 + continuation = {kind: 'funeffects'};
1647 + break;
1648 }
1649 case 'DeclareContext': {
1650 state.initialize(instrValue, {
@@ -1791,7 +1653,8 @@ function inferBlock(
1653 context: new Set(),
1654 });
1655 state.define(instrValue.lvalue.place, instrValue);
1794 - continue;
1656 + continuation = {kind: 'funeffects'};
1657 + break;
1658 }
1659 case 'PostfixUpdate':
1660 case 'PrefixUpdate': {
@@ -1801,10 +1664,10 @@ function inferBlock(
1664 ? Effect.ConditionallyMutate
1665 : Effect.Capture;
1666 state.referenceAndRecordEffects(
1667 + freezeActions,
1668 instrValue.value,
1669 effect,
1670 ValueReason.Other,
1807 - functionEffects,
1671 );
1672
1673 const lvalue = instr.lvalue;
@@ -1818,7 +1681,8 @@ function inferBlock(
1681 * replacing it
1682 */
1683 instrValue.lvalue.effect = Effect.Store;
1821 - continue;
1684 + continuation = {kind: 'funeffects'};
1685 + break;
1686 }
1687 case 'StoreLocal': {
1688 const effect =
@@ -1827,10 +1691,10 @@ function inferBlock(
1691 ? Effect.ConditionallyMutate
1692 : Effect.Capture;
1693 state.referenceAndRecordEffects(
1694 + freezeActions,
1695 instrValue.value,
1696 effect,
1697 ValueReason.Other,
1833 - [],
1698 );
1699
1700 const lvalue = instr.lvalue;
@@ -1844,48 +1708,40 @@ function inferBlock(
1708 * replacing it
1709 */
1710 instrValue.lvalue.place.effect = Effect.Store;
1847 - continue;
1711 + continuation = {kind: 'funeffects'};
1712 + break;
1713 }
1714 case 'StoreContext': {
1715 state.referenceAndRecordEffects(
1716 + freezeActions,
1717 instrValue.value,
1718 Effect.ConditionallyMutate,
1719 ValueReason.Other,
1854 - functionEffects,
1720 );
1721 state.referenceAndRecordEffects(
1722 + freezeActions,
1723 instrValue.lvalue.place,
1724 Effect.Mutate,
1725 ValueReason.Other,
1860 - functionEffects,
1726 );
1727
1728 const lvalue = instr.lvalue;
1729 state.alias(lvalue, instrValue.value);
1730 lvalue.effect = Effect.Store;
1866 - continue;
1731 + continuation = {kind: 'funeffects'};
1732 + break;
1733 }
1734 case 'StoreGlobal': {
1735 state.referenceAndRecordEffects(
1736 + freezeActions,
1737 instrValue.value,
1738 Effect.Capture,
1739 ValueReason.Other,
1873 - functionEffects,
1740 );
1741 const lvalue = instr.lvalue;
1742 lvalue.effect = Effect.Store;
1877 -
1878 - functionEffects.push({
1879 - kind: 'GlobalMutation',
1880 - error: {
1881 - reason:
1882 - '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)',
1883 - loc: instr.loc,
1884 - suggestions: null,
1885 - severity: ErrorSeverity.InvalidReact,
1886 - },
1887 - });
1888 - continue;
1743 + continuation = {kind: 'funeffects'};
1744 + break;
1745 }
1746 case 'Destructure': {
1747 let effect: Effect = Effect.Capture;
@@ -1899,10 +1755,10 @@ function inferBlock(
1755 }
1756 }
1757 state.referenceAndRecordEffects(
1758 + freezeActions,
1759 instrValue.value,
1760 effect,
1761 ValueReason.Other,
1905 - functionEffects,
1762 );
1763
1764 const lvalue = instr.lvalue;
@@ -1918,7 +1774,8 @@ function inferBlock(
1774 */
1775 place.effect = Effect.Store;
1776 }
1921 - continue;
1777 + continuation = {kind: 'funeffects'};
1778 + break;
1779 }
1780 case 'GetIterator': {
1781 /**
@@ -1938,6 +1795,8 @@ function inferBlock(
1795 const kind = state.kind(instrValue.collection).kind;
1796 const isMutable =
1797 kind === ValueKind.Mutable || kind === ValueKind.Context;
1798 + let effect;
1799 + let valueKind: AbstractValue;
1800 if (!isMutable || isArrayType(instrValue.collection.identifier)) {
1801 // Case 1, assume iterator is a separate mutable object
1802 effect = {
@@ -1957,7 +1816,12 @@ function inferBlock(
1816 };
1817 valueKind = state.kind(instrValue.collection);
1818 }
1960 - lvalueEffect = Effect.Store;
1819 + continuation = {
1820 + kind: 'initialize',
1821 + effect,
1822 + valueKind,
1823 + lvalueEffect: Effect.Store,
1824 + };
1825 break;
1826 }
1827 case 'IteratorNext': {
@@ -1972,10 +1836,10 @@ function inferBlock(
1836 * ConditionallyMutate reflects this "mutate if mutable" semantic.
1837 */
1838 state.referenceAndRecordEffects(
1839 + freezeActions,
1840 instrValue.iterator,
1841 Effect.ConditionallyMutate,
1842 ValueReason.Other,
1978 - functionEffects,
1843 );
1844 /**
1845 * Regardless of the effect on the iterator, the *result* of advancing the iterator
@@ -1984,23 +1848,27 @@ function inferBlock(
1848 * ensure that the item is mutable or frozen if the collection is mutable/frozen.
1849 */
1850 state.referenceAndRecordEffects(
1851 + freezeActions,
1852 instrValue.collection,
1853 Effect.Capture,
1854 ValueReason.Other,
1990 - functionEffects,
1855 );
1856 state.initialize(instrValue, state.kind(instrValue.collection));
1857 state.define(instr.lvalue, instrValue);
1858 instr.lvalue.effect = Effect.Store;
1995 - continue;
1859 + continuation = {kind: 'funeffects'};
1860 + break;
1861 }
1862 case 'NextPropertyOf': {
1998 - effect = {kind: Effect.Read, reason: ValueReason.Other};
1999 - lvalueEffect = Effect.Store;
2000 - valueKind = {
2001 - kind: ValueKind.Primitive,
2002 - reason: new Set([ValueReason.Other]),
2003 - context: new Set(),
1863 + continuation = {
1864 + kind: 'initialize',
1865 + effect: {kind: Effect.Read, reason: ValueReason.Other},
1866 + lvalueEffect: Effect.Store,
1867 + valueKind: {
1868 + kind: ValueKind.Primitive,
1869 + reason: new Set([ValueReason.Other]),
1870 + context: new Set(),
1871 + },
1872 };
1873 break;
1874 }
@@ -2009,26 +1877,34 @@ function inferBlock(
1877 }
1878 }
1879
2012 - for (const operand of eachInstructionOperand(instr)) {
2013 - CompilerError.invariant(effect != null, {
2014 - reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
2015 - description: null,
2016 - loc: instrValue.loc,
2017 - suggestions: null,
2018 - });
2019 - state.referenceAndRecordEffects(
2020 - operand,
2021 - effect.kind,
2022 - effect.reason,
2023 - functionEffects,
2024 - );
1880 + if (continuation.kind === 'initialize') {
1881 + for (const operand of eachInstructionOperand(instr)) {
1882 + CompilerError.invariant(continuation.effect != null, {
1883 + reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
1884 + description: null,
1885 + loc: instrValue.loc,
1886 + suggestions: null,
1887 + });
1888 + state.referenceAndRecordEffects(
1889 + freezeActions,
1890 + operand,
1891 + continuation.effect.kind,
1892 + continuation.effect.reason,
1893 + );
1894 + }
1895 +
1896 + state.initialize(instrValue, continuation.valueKind);
1897 + state.define(instr.lvalue, instrValue);
1898 + instr.lvalue.effect = continuation.lvalueEffect ?? defaultLvalueEffect;
1899 }
1900
2027 - state.initialize(instrValue, valueKind);
2028 - state.define(instr.lvalue, instrValue);
2029 - instr.lvalue.effect = lvalueEffect;
1901 + functionEffects.push(...inferInstructionFunctionEffects(env, state, instr));
1902 + freezeActions.forEach(({values, reason}) =>
1903 + state.freezeValues(values, reason),
1904 + );
1905 }
1906
1907 + const terminalFreezeActions: Array<FreezeAction> = [];
1908 for (const operand of eachTerminalOperand(block.terminal)) {
1909 let effect;
1910 if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
@@ -2043,17 +1919,17 @@ function inferBlock(
1919 } else {
1920 effect = Effect.Read;
1921 }
2046 - const propEffects: Array<FunctionEffect> = [];
1922 state.referenceAndRecordEffects(
1923 + terminalFreezeActions,
1924 operand,
1925 effect,
1926 ValueReason.Other,
2051 - propEffects,
2052 - );
2053 - functionEffects.push(
2054 - ...propEffects.filter(effect => !isEffectSafeOutsideRender(effect)),
1927 );
1928 }
1929 + functionEffects.push(...inferTerminalFunctionEffects(state, block));
1930 + terminalFreezeActions.forEach(({values, reason}) =>
1931 + state.freezeValues(values, reason),
1932 + );
1933 }
1934
1935 function hasContextRefOperand(
@@ -2089,7 +1965,7 @@ export function getFunctionCallSignature(
1965 * @param sig
1966 * @returns Inferred effects of function arguments, or null if inference fails.
1967 */
2092 -function getFunctionEffects(
1968 +export function getFunctionEffects(
1969 fn: MethodCall | CallExpression,
1970 sig: FunctionSignature,
1971 ): Array<Effect> | null {
@@ -2164,27 +2040,3 @@ function areArgumentsImmutableAndNonMutating(
2040 }
2041 return true;
2042 }
2167 -
2168 -function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
2169 - return effect.kind === 'GlobalMutation';
2170 -}
2171 -
2172 -function getWriteErrorReason(abstractValue: AbstractValue): string {
2173 - if (abstractValue.reason.has(ValueReason.Global)) {
2174 - return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
2175 - } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
2176 - return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
2177 - } else if (abstractValue.reason.has(ValueReason.Context)) {
2178 - return `Mutating a value returned from 'useContext()', which should not be mutated`;
2179 - } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
2180 - return 'Mutating a value returned from a function whose return value should not be mutated';
2181 - } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
2182 - return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
2183 - } else if (abstractValue.reason.has(ValueReason.State)) {
2184 - return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
2185 - } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
2186 - return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
2187 - } else {
2188 - return 'This mutates a variable that React considers immutable';
2189 - }
2190 -}