[compiler] Remove redundant InferMutableContextVariables (#32097)
This removes special casing for `PropertyStore` mutability inference within FunctionExpressions. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32097). * #32287 * #32104 * #32098 * __->__ #32097
mofeiZ committed
Feb 18, 2025 at 09:37 UTC
a92acdb188990b2b64130d74ccd6437dc9db1901
5 files changed
+54
-174
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+4
-23
@@ -10,7 +10,6 @@ import {
10
Effect,
11
HIRFunction,
12
Identifier,
13
- IdentifierId,
13
LoweredFunction,
14
isRefOrRefValue,
15
makeInstructionId,
@@ -18,27 +17,9 @@ import {
17
import {deadCodeElimination} from '../Optimization';
18
import {inferReactiveScopeVariables} from '../ReactiveScopes';
19
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
21
-import {inferMutableContextVariables} from './InferMutableContextVariables';
20
import {inferMutableRanges} from './InferMutableRanges';
21
import inferReferenceEffects from './InferReferenceEffects';
22
25
-// Helper class to track indirections such as LoadLocal and PropertyLoad.
26
-export class IdentifierState {
27
- properties: Map<IdentifierId, Identifier> = new Map();
28
-
29
- resolve(identifier: Identifier): Identifier {
30
- const resolved = this.properties.get(identifier.id);
31
- if (resolved !== undefined) {
32
- return resolved;
33
- }
34
- return identifier;
35
- }
36
-
37
- alias(lvalue: Identifier, value: Identifier): void {
38
- this.properties.set(lvalue.id, this.properties.get(value.id) ?? value);
39
- }
40
-}
41
-
23
export default function analyseFunctions(func: HIRFunction): void {
24
for (const [_, block] of func.body.blocks) {
25
for (const instr of block.instructions) {
@@ -78,7 +59,6 @@ function lower(func: HIRFunction): void {
59
}
60
61
function infer(loweredFunc: LoweredFunction): void {
81
- const knownMutated = inferMutableContextVariables(loweredFunc.func);
62
for (const operand of loweredFunc.func.context) {
63
const identifier = operand.identifier;
64
CompilerError.invariant(operand.effect === Effect.Unknown, {
@@ -95,10 +75,11 @@ function infer(loweredFunc: LoweredFunction): void {
75
* render
76
*/
77
operand.effect = Effect.Capture;
98
- } else if (knownMutated.has(operand)) {
99
- operand.effect = Effect.Mutate;
78
} else if (isMutatedOrReassigned(identifier)) {
101
- // Note that this also reflects if identifier is ConditionallyMutated
79
+ /**
80
+ * Reflects direct reassignments, PropertyStores, and ConditionallyMutate
81
+ * (directly or through maybe-aliases)
82
+ */
83
operand.effect = Effect.Capture;
84
} else {
85
operand.effect = Effect.Read;
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableContextVariables.ts
deleted
-105
@@ -1,105 +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 {Effect, HIRFunction, Identifier, Place} from '../HIR';
9
-import {
10
- eachInstructionValueOperand,
11
- eachTerminalOperand,
12
-} from '../HIR/visitors';
13
-import {IdentifierState} from './AnalyseFunctions';
14
-
15
-/*
16
- * This pass infers which of the given function's context (free) variables
17
- * are definitively mutated by the function. This analysis is *partial*,
18
- * and only annotates provable mutations, and may miss mutations via indirections.
19
- * The intent of this pass is to drive validations, rejecting known-bad code
20
- * while avoiding false negatives, and the inference should *not* be used to
21
- * drive changes in output.
22
- *
23
- * Note that a complete analysis is possible but would have too many false negatives.
24
- * The approach would be to run LeaveSSA and InferReactiveScopeVariables in order to
25
- * find all possible aliases of a context variable which may be mutated. However, this
26
- * can lead to false negatives:
27
- *
28
- * ```
29
- * const [x, setX] = useState(null); // x is frozen
30
- * const fn = () => { // context=[x]
31
- * const z = {}; // z is mutable
32
- * foo(z, x); // potentially mutate z and x
33
- * z.a = true; // definitively mutate z
34
- * }
35
- * fn();
36
- * ```
37
- *
38
- * When we analyze function expressions we assume that context variables are mutable,
39
- * so we assume that `x` is mutable. We infer that `foo(z, x)` could be mutating the
40
- * two variables to alias each other, such that `z.a = true` could be mutating `x`,
41
- * and we would infer that `x` is definitively mutated. Then when we run
42
- * InferReferenceEffects on the outer code we'd reject it, since there is a definitive
43
- * mutation of a frozen value.
44
- *
45
- * Thus the actual implementation looks at only basic aliasing. The above example would
46
- * pass, since z does not directly alias `x`. However, mutations through trivial aliases
47
- * are detected:
48
- *
49
- * ```
50
- * const [x, setX] = useState(null); // x is frozen
51
- * const fn = () => { // context=[x]
52
- * const z = x;
53
- * z.a = true; // ERROR: mutates x
54
- * }
55
- * fn();
56
- * ```
57
- */
58
-export function inferMutableContextVariables(fn: HIRFunction): Set<Place> {
59
- const state = new IdentifierState();
60
- const knownMutatedIdentifiers = new Set<Identifier>();
61
- for (const [, block] of fn.body.blocks) {
62
- for (const instr of block.instructions) {
63
- switch (instr.value.kind) {
64
- case 'PropertyLoad':
65
- case 'ComputedLoad': {
66
- state.alias(instr.lvalue.identifier, instr.value.object.identifier);
67
- break;
68
- }
69
- case 'LoadLocal':
70
- case 'LoadContext': {
71
- if (instr.lvalue.identifier.name === null) {
72
- state.alias(instr.lvalue.identifier, instr.value.place.identifier);
73
- }
74
- break;
75
- }
76
- default: {
77
- for (const operand of eachInstructionValueOperand(instr.value)) {
78
- visitOperand(state, knownMutatedIdentifiers, operand);
79
- }
80
- }
81
- }
82
- }
83
- for (const operand of eachTerminalOperand(block.terminal)) {
84
- visitOperand(state, knownMutatedIdentifiers, operand);
85
- }
86
- }
87
- const results = new Set<Place>();
88
- for (const operand of fn.context) {
89
- if (knownMutatedIdentifiers.has(operand.identifier)) {
90
- results.add(operand);
91
- }
92
- }
93
- return results;
94
-}
95
-
96
-function visitOperand(
97
- state: IdentifierState,
98
- knownMutatedIdentifiers: Set<Identifier>,
99
- operand: Place,
100
-): void {
101
- const resolved = state.resolve(operand.identifier);
102
- if (operand.effect === Effect.Mutate || operand.effect === Effect.Store) {
103
- knownMutatedIdentifiers.add(resolved);
104
- }
105
-}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md
+32
-34
@@ -19,9 +19,14 @@ function Component() {
19
20
// capture into a separate variable that is not a context variable.
21
const y = x;
22
+ /**
23
+ * Note that this fixture currently produces a stale effect closure if `y = x
24
+ * = someGlobal` changes between renders. Under current compiler assumptions,
25
+ * that would be a rule of react violation.
26
+ */
27
useEffect(() => {
28
y.value = 'hello';
24
- }, []);
29
+ });
30
31
useEffect(() => {
32
setState(someGlobal.value);
@@ -46,57 +51,50 @@ import { useEffect, useState } from "react";
51
let someGlobal = { value: null };
52
53
function Component() {
49
- const $ = _c(7);
54
+ const $ = _c(5);
55
const [state, setState] = useState(someGlobal);
56
+
57
+ let x = someGlobal;
58
+ while (x == null) {
59
+ x = someGlobal;
60
+ }
61
+
62
+ const y = x;
63
let t0;
52
- let t1;
53
- let t2;
64
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
55
- let x = someGlobal;
56
- while (x == null) {
57
- x = someGlobal;
58
- }
59
-
60
- const y = x;
61
- t0 = useEffect;
62
- t1 = () => {
65
+ t0 = () => {
66
y.value = "hello";
67
};
65
- t2 = [];
68
$[0] = t0;
69
+ } else {
70
+ t0 = $[0];
71
+ }
72
+ useEffect(t0);
73
+ let t1;
74
+ let t2;
75
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
76
+ t1 = () => {
77
+ setState(someGlobal.value);
78
+ };
79
+ t2 = [someGlobal];
80
$[1] = t1;
81
$[2] = t2;
82
} else {
70
- t0 = $[0];
83
t1 = $[1];
84
t2 = $[2];
85
}
74
- t0(t1, t2);
75
- let t3;
86
+ useEffect(t1, t2);
87
+
88
+ const t3 = String(state);
89
let t4;
77
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
78
- t3 = () => {
79
- setState(someGlobal.value);
80
- };
81
- t4 = [someGlobal];
90
+ if ($[3] !== t3) {
91
+ t4 = <div>{t3}</div>;
92
$[3] = t3;
93
$[4] = t4;
94
} else {
85
- t3 = $[3];
95
t4 = $[4];
96
}
88
- useEffect(t3, t4);
89
-
90
- const t5 = String(state);
91
- let t6;
92
- if ($[5] !== t5) {
93
- t6 = <div>{t5}</div>;
94
- $[5] = t5;
95
- $[6] = t6;
96
- } else {
97
- t6 = $[6];
98
- }
99
- return t6;
97
+ return t4;
98
}
99
100
export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.js
+6
-1
@@ -15,9 +15,14 @@ function Component() {
15
16
// capture into a separate variable that is not a context variable.
17
const y = x;
18
+ /**
19
+ * Note that this fixture currently produces a stale effect closure if `y = x
20
+ * = someGlobal` changes between renders. Under current compiler assumptions,
21
+ * that would be a rule of react violation.
22
+ */
23
useEffect(() => {
24
y.value = 'hello';
20
- }, []);
25
+ });
26
27
useEffect(() => {
28
setState(someGlobal.value);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.expect.md
+12
-11
@@ -36,21 +36,22 @@ import { useSharedValue } from "react-native-reanimated";
36
* of render
37
*/
38
function SomeComponent() {
39
- const $ = _c(3);
39
+ const $ = _c(2);
40
const sharedVal = useSharedValue(0);
41
-
42
- const T0 = Button;
43
- const t0 = () => (sharedVal.value = Math.random());
44
- let t1;
45
- if ($[0] !== T0 || $[1] !== t0) {
46
- t1 = <T0 onPress={t0} title="Randomize" />;
47
- $[0] = T0;
41
+ let t0;
42
+ if ($[0] !== sharedVal) {
43
+ t0 = (
44
+ <Button
45
+ onPress={() => (sharedVal.value = Math.random())}
46
+ title="Randomize"
47
+ />
48
+ );
49
+ $[0] = sharedVal;
50
$[1] = t0;
49
- $[2] = t1;
51
} else {
51
- t1 = $[2];
52
+ t0 = $[1];
53
}
53
- return t1;
54
+ return t0;
55
}
56
57
```