@samitouri / QOS-React / commits / a92acdb188

[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 ```