= new Map();
- for (const [_, block] of fn.body.blocks) {
- if (
- block.terminal.kind === 'try' &&
- block.terminal.handlerBinding !== null
- ) {
- handlerParams.set(
- block.terminal.handler,
- block.terminal.handlerBinding.identifier,
- );
- } else if (block.terminal.kind === 'maybe-throw') {
- const handlerParam = handlerParams.get(block.terminal.handler);
- if (handlerParam === undefined) {
- /*
- * There's no catch clause param, nothing to alias to so
- * skip this block
- */
- continue;
- }
- /*
- * Otherwise alias all values created in this block to the
- * catch clause param
- */
- for (const instr of block.instructions) {
- aliases.union([handlerParam, instr.lvalue.identifier]);
- }
- }
- }
-}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts
index 93b99fb38526..eb645cc218dc 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts
@@ -7,8 +7,6 @@
export {default as analyseFunctions} from './AnalyseFunctions';
export {dropManualMemoization} from './DropManualMemoization';
-export {inferMutableRanges} from './InferMutableRanges';
export {inferReactivePlaces} from './InferReactivePlaces';
-export {default as inferReferenceEffects} from './InferReferenceEffects';
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
export {inferEffectDependencies} from './InferEffectDependencies';
diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
index 8484a1ca8d93..3afb00b71a81 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
@@ -24,7 +24,6 @@ import {
getHookKind,
isMutableEffect,
} from '../HIR';
-import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
import {
@@ -35,6 +34,7 @@ import {
visitReactiveFunction,
} from './visitors';
import {printPlace} from '../HIR/PrintHIR';
+import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/*
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
index 31bbf8c94d35..163d645957c0 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
@@ -12,7 +12,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
-import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
+import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/**
* Validates that local variables cannot be reassigned after render.
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
index 7a79c74780e2..f71bb40545bb 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
@@ -125,10 +125,7 @@ export function validateNoFreezingKnownMutableFunctions(
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
- } else if (
- fn.env.config.enableNewMutationAliasingModel &&
- value.loweredFunc.func.aliasingEffects != null
- ) {
+ } else if (value.loweredFunc.func.aliasingEffects != null) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts
index 85adb79ceb85..e7f8b1c0e5b1 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts
@@ -7,7 +7,7 @@
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {HIRFunction} from '../HIR';
-import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
+import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md
new file mode 100644
index 000000000000..90ffebd69583
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md
@@ -0,0 +1,39 @@
+
+## Input
+
+```javascript
+// @enableNewMutationAliasingModel:false
+function Component() {
+ const foo = () => {
+ someGlobal = true;
+ };
+ // spreading a function is weird, but it doesn't call the function so this is allowed
+ return ;
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
+function Component() {
+ const $ = _c(1);
+ const foo = _temp;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = ;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+function _temp() {
+ someGlobal = true;
+}
+
+```
+
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.js
similarity index 61%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.js
index e749f10f78fb..2ed0c70a7f96 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.js
@@ -3,5 +3,6 @@ function Component() {
const foo = () => {
someGlobal = true;
};
+ // spreading a function is weird, but it doesn't call the function so this is allowed
return ;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md
deleted file mode 100644
index 7d14f2a5dc8e..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md
+++ /dev/null
@@ -1,107 +0,0 @@
-
-## Input
-
-```javascript
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
-import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
-
-/**
- * 1. `InferMutableRanges` derives the mutable range of identifiers and their
- * aliases from `LoadLocal`, `PropertyLoad`, etc
- * - After this pass, y's mutable range only extends to `arrayPush(x, y)`
- * - We avoid assigning mutable ranges to loads after y's mutable range, as
- * these are working with an immutable value. As a result, `LoadLocal y` and
- * `PropertyLoad y` do not get mutable ranges
- * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
- * as according to the 'co-mutation' of different values
- * - Here, we infer that
- * - `arrayPush(y, x)` might alias `x` and `y` to each other
- * - `setPropertyKey(x, ...)` may mutate both `x` and `y`
- * - This pass correctly extends the mutable range of `y`
- * - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
- * PropertyLoads still don't have a mutable range
- *
- * Note that the this bug is an edge case. Compiler output is only invalid for:
- * - function expressions with
- * `enableTransitivelyFreezeFunctionExpressions:false`
- * - functions that throw and get retried without clearing the memocache
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok)
- * {"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}
- * Forget:
- * (kind: ok)
- * {"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
- */
-function useFoo({a, b}: {a: number, b: number}) {
- const x = [];
- const y = {value: a};
-
- arrayPush(x, y); // x and y co-mutate
- const y_alias = y;
- const cb = () => y_alias.value;
- setPropertyByKey(x[0], 'value', b); // might overwrite y.value
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: useFoo,
- params: [{a: 2, b: 10}],
- sequentialRenders: [
- {a: 2, b: 10},
- {a: 2, b: 11},
- ],
-};
-
-```
-
-## Code
-
-```javascript
-import { c as _c } from "react/compiler-runtime";
-import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
-
-function useFoo(t0) {
- const $ = _c(5);
- const { a, b } = t0;
- let t1;
- if ($[0] !== a || $[1] !== b) {
- const x = [];
- const y = { value: a };
-
- arrayPush(x, y);
- const y_alias = y;
- let t2;
- if ($[3] !== y_alias.value) {
- t2 = () => y_alias.value;
- $[3] = y_alias.value;
- $[4] = t2;
- } else {
- t2 = $[4];
- }
- const cb = t2;
- setPropertyByKey(x[0], "value", b);
- t1 = ;
- $[0] = a;
- $[1] = b;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- return t1;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: useFoo,
- params: [{ a: 2, b: 10 }],
- sequentialRenders: [
- { a: 2, b: 10 },
- { a: 2, b: 11 },
- ],
-};
-
-```
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js
deleted file mode 100644
index 911c06e64466..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js
+++ /dev/null
@@ -1,53 +0,0 @@
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
-import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
-
-/**
- * 1. `InferMutableRanges` derives the mutable range of identifiers and their
- * aliases from `LoadLocal`, `PropertyLoad`, etc
- * - After this pass, y's mutable range only extends to `arrayPush(x, y)`
- * - We avoid assigning mutable ranges to loads after y's mutable range, as
- * these are working with an immutable value. As a result, `LoadLocal y` and
- * `PropertyLoad y` do not get mutable ranges
- * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
- * as according to the 'co-mutation' of different values
- * - Here, we infer that
- * - `arrayPush(y, x)` might alias `x` and `y` to each other
- * - `setPropertyKey(x, ...)` may mutate both `x` and `y`
- * - This pass correctly extends the mutable range of `y`
- * - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
- * PropertyLoads still don't have a mutable range
- *
- * Note that the this bug is an edge case. Compiler output is only invalid for:
- * - function expressions with
- * `enableTransitivelyFreezeFunctionExpressions:false`
- * - functions that throw and get retried without clearing the memocache
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok)
- * {"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}
- * Forget:
- * (kind: ok)
- * {"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
- */
-function useFoo({a, b}: {a: number, b: number}) {
- const x = [];
- const y = {value: a};
-
- arrayPush(x, y); // x and y co-mutate
- const y_alias = y;
- const cb = () => y_alias.value;
- setPropertyByKey(x[0], 'value', b); // might overwrite y.value
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: useFoo,
- params: [{a: 2, b: 10}],
- sequentialRenders: [
- {a: 2, b: 10},
- {a: 2, b: 11},
- ],
-};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md
deleted file mode 100644
index 698562dad18d..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md
+++ /dev/null
@@ -1,87 +0,0 @@
-
-## Input
-
-```javascript
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
-import {setPropertyByKey, Stringify} from 'shared-runtime';
-
-/**
- * Variation of bug in `bug-aliased-capture-aliased-mutate`
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok)
- * {"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}
- * Forget:
- * (kind: ok)
- * {"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
- */
-
-function useFoo({a}: {a: number, b: number}) {
- const arr = [];
- const obj = {value: a};
-
- setPropertyByKey(obj, 'arr', arr);
- const obj_alias = obj;
- const cb = () => obj_alias.arr.length;
- for (let i = 0; i < a; i++) {
- arr.push(i);
- }
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: useFoo,
- params: [{a: 2}],
- sequentialRenders: [{a: 2}, {a: 3}],
-};
-
-```
-
-## Code
-
-```javascript
-import { c as _c } from "react/compiler-runtime";
-import { setPropertyByKey, Stringify } from "shared-runtime";
-
-function useFoo(t0) {
- const $ = _c(4);
- const { a } = t0;
- let t1;
- if ($[0] !== a) {
- const arr = [];
- const obj = { value: a };
-
- setPropertyByKey(obj, "arr", arr);
- const obj_alias = obj;
- let t2;
- if ($[2] !== obj_alias.arr.length) {
- t2 = () => obj_alias.arr.length;
- $[2] = obj_alias.arr.length;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- const cb = t2;
- for (let i = 0; i < a; i++) {
- arr.push(i);
- }
-
- t1 = ;
- $[0] = a;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: useFoo,
- params: [{ a: 2 }],
- sequentialRenders: [{ a: 2 }, { a: 3 }],
-};
-
-```
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js
deleted file mode 100644
index 1311a9dcfa69..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
-import {setPropertyByKey, Stringify} from 'shared-runtime';
-
-/**
- * Variation of bug in `bug-aliased-capture-aliased-mutate`
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok)
- * {"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}
- * Forget:
- * (kind: ok)
- * {"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
- * {"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
- */
-
-function useFoo({a}: {a: number, b: number}) {
- const arr = [];
- const obj = {value: a};
-
- setPropertyByKey(obj, 'arr', arr);
- const obj_alias = obj;
- const cb = () => obj_alias.arr.length;
- for (let i = 0; i < a; i++) {
- arr.push(i);
- }
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: useFoo,
- params: [{a: 2}],
- sequentialRenders: [{a: 2}, {a: 3}],
-};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md
index ea33e361e3ba..8d1e85222558 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md
@@ -85,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime";
* used when we analyze CallExpressions.
*/
function Component(t0) {
- const $ = _c(5);
+ const $ = _c(3);
const { foo, bar } = t0;
- let t1;
- if ($[0] !== foo) {
- t1 = { foo };
- $[0] = foo;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const x = t1;
let y;
- if ($[2] !== bar || $[3] !== x) {
+ if ($[0] !== bar || $[1] !== foo) {
+ const x = { foo };
y = { bar };
const f0 = function () {
const a = makeArray(y);
@@ -108,11 +100,11 @@ function Component(t0) {
f0();
mutate(y.x);
- $[2] = bar;
- $[3] = x;
- $[4] = y;
+ $[0] = bar;
+ $[1] = foo;
+ $[2] = y;
} else {
- y = $[4];
+ y = $[2];
}
return y;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md
deleted file mode 100644
index 9c874fa68ebc..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md
+++ /dev/null
@@ -1,92 +0,0 @@
-
-## Input
-
-```javascript
-// @enableNewMutationAliasingModel:false
-import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
-
-/**
- * Fixture showing an edge case for ReactiveScope variable propagation.
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * Forget:
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * [[ (exception in render) Error: invariant broken ]]
- *
- */
-function Component() {
- const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
- const boxedInner = [obj?.inner];
- useIdentity(null);
- mutate(obj);
- if (boxedInner[0] !== obj?.inner) {
- throw new Error('invariant broken');
- }
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Component,
- params: [{arg: 0}],
- sequentialRenders: [{arg: 0}, {arg: 1}],
-};
-
-```
-
-## Code
-
-```javascript
-import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
-import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
-
-/**
- * Fixture showing an edge case for ReactiveScope variable propagation.
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * Forget:
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * [[ (exception in render) Error: invariant broken ]]
- *
- */
-function Component() {
- const $ = _c(4);
- const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = [obj?.inner];
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- const boxedInner = t0;
- useIdentity(null);
- mutate(obj);
- if (boxedInner[0] !== obj?.inner) {
- throw new Error("invariant broken");
- }
- let t1;
- if ($[1] !== boxedInner || $[2] !== obj) {
- t1 = ;
- $[1] = boxedInner;
- $[2] = obj;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- return t1;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Component,
- params: [{ arg: 0 }],
- sequentialRenders: [{ arg: 0 }, { arg: 1 }],
-};
-
-```
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx
deleted file mode 100644
index 1a7c996a9e29..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-// @enableNewMutationAliasingModel:false
-import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
-
-/**
- * Fixture showing an edge case for ReactiveScope variable propagation.
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * Forget:
- * {"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
- * [[ (exception in render) Error: invariant broken ]]
- *
- */
-function Component() {
- const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
- const boxedInner = [obj?.inner];
- useIdentity(null);
- mutate(obj);
- if (boxedInner[0] !== obj?.inner) {
- throw new Error('invariant broken');
- }
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Component,
- params: [{arg: 0}],
- sequentialRenders: [{arg: 0}, {arg: 1}],
-};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md b/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 file mode 100644
index 93098b916d72..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md
+++ /dev/null
@@ -1,110 +0,0 @@
-
-## Input
-
-```javascript
-// @enableNewMutationAliasingModel:false
-import {identity, mutate} from 'shared-runtime';
-
-/**
- * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
- * with the mutation hoisted to a named variable instead of being directly
- * inlined into the Object key.
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * Forget:
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
- */
-function Component(props) {
- const key = {};
- const tmp = (mutate(key), key);
- const context = {
- // Here, `tmp` is frozen (as it's inferred to be a primitive/string)
- [tmp]: identity([props.value]),
- };
- mutate(key);
- return [context, key];
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Component,
- params: [{value: 42}],
- sequentialRenders: [{value: 42}, {value: 42}],
-};
-
-```
-
-## Code
-
-```javascript
-import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
-import { identity, mutate } from "shared-runtime";
-
-/**
- * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
- * with the mutation hoisted to a named variable instead of being directly
- * inlined into the Object key.
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * Forget:
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
- */
-function Component(props) {
- const $ = _c(8);
- let key;
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- key = {};
- t0 = (mutate(key), key);
- $[0] = key;
- $[1] = t0;
- } else {
- key = $[0];
- t0 = $[1];
- }
- const tmp = t0;
- let t1;
- if ($[2] !== props.value) {
- t1 = identity([props.value]);
- $[2] = props.value;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- let t2;
- if ($[4] !== t1) {
- t2 = { [tmp]: t1 };
- $[4] = t1;
- $[5] = t2;
- } else {
- t2 = $[5];
- }
- const context = t2;
-
- mutate(key);
- let t3;
- if ($[6] !== context) {
- t3 = [context, key];
- $[6] = context;
- $[7] = t3;
- } else {
- t3 = $[7];
- }
- return t3;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Component,
- params: [{ value: 42 }],
- sequentialRenders: [{ value: 42 }, { value: 42 }],
-};
-
-```
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js
deleted file mode 100644
index 620f5eeb17af..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// @enableNewMutationAliasingModel:false
-import {identity, mutate} from 'shared-runtime';
-
-/**
- * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
- * with the mutation hoisted to a named variable instead of being directly
- * inlined into the Object key.
- *
- * Found differences in evaluator results
- * Non-forget (expected):
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * Forget:
- * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
- * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
- */
-function Component(props) {
- const key = {};
- const tmp = (mutate(key), key);
- const context = {
- // Here, `tmp` is frozen (as it's inferred to be a primitive/string)
- [tmp]: identity([props.value]),
- };
- mutate(key);
- return [context, key];
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Component,
- params: [{value: 42}],
- sequentialRenders: [{value: 42}, {value: 42}],
-};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md
deleted file mode 100644
index 8476885de773..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md
+++ /dev/null
@@ -1,33 +0,0 @@
-
-## Input
-
-```javascript
-// @enableNewMutationAliasingModel:false
-function Component() {
- const foo = () => {
- someGlobal = true;
- };
- return ;
-}
-
-```
-
-
-## Error
-
-```
-Found 1 error:
-
-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)
-
-error.assign-global-in-jsx-spread-attribute.ts:4:4
- 2 | function Component() {
- 3 | const foo = () => {
-> 4 | someGlobal = true;
- | ^^^^^^^^^^ 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)
- 5 | };
- 6 | return ;
- 7 | }
-```
-
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md
deleted file mode 100644
index cc0ad9de117d..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md
+++ /dev/null
@@ -1,72 +0,0 @@
-
-## Input
-
-```javascript
-// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
-
-import {useCallback, useEffect, useRef} from 'react';
-import {useHook} from 'shared-runtime';
-
-function Component() {
- const params = useHook();
- const update = useCallback(
- partialParams => {
- const nextParams = {
- ...params,
- ...partialParams,
- };
- nextParams.param = 'value';
- console.log(nextParams);
- },
- [params]
- );
- const ref = useRef(null);
- useEffect(() => {
- if (ref.current === null) {
- update();
- }
- }, [update]);
-
- return 'ok';
-}
-
-```
-
-
-## Error
-
-```
-Found 1 error:
-
-Error: Cannot modify local variables after render completes
-
-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.
-
-error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
- 18 | );
- 19 | const ref = useRef(null);
-> 20 | useEffect(() => {
- | ^^^^^^^
-> 21 | if (ref.current === null) {
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-> 22 | update();
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-> 23 | }
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-> 24 | }, [update]);
- | ^^^^ This function may (indirectly) reassign or modify a local variable after render
- 25 |
- 26 | return 'ok';
- 27 | }
-
-error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
- 12 | ...partialParams,
- 13 | };
-> 14 | nextParams.param = 'value';
- | ^^^^^^^^^^ This modifies a local variable
- 15 | console.log(nextParams);
- 16 | },
- 17 | [params]
-```
-
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js
deleted file mode 100644
index b5d70dbd8161..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
-
-import {useCallback, useEffect, useRef} from 'react';
-import {useHook} from 'shared-runtime';
-
-function Component() {
- const params = useHook();
- const update = useCallback(
- partialParams => {
- const nextParams = {
- ...params,
- ...partialParams,
- };
- nextParams.param = 'value';
- console.log(nextParams);
- },
- [params]
- );
- const ref = useRef(null);
- useEffect(() => {
- if (ref.current === null) {
- update();
- }
- }, [update]);
-
- return 'ok';
-}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md
deleted file mode 100644
index 89bcedf956c7..000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md
+++ /dev/null
@@ -1,39 +0,0 @@
-
-## Input
-
-```javascript
-// @enableNewMutationAliasingModel:false
-function Foo() {
- const x = () => {
- window.href = 'foo';
- };
- const y = {x};
- return ;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Foo,
- params: [],
-};
-
-```
-
-
-## Error
-
-```
-Found 1 error:
-
-Error: Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
-
-error.object-capture-global-mutation.ts:4:4
- 2 | function Foo() {
- 3 | const x = () => {
-> 4 | window.href = 'foo';
- | ^^^^^^ Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
- 5 | };
- 6 | const y = {x};
- 7 | return ;
-```
-
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md
new file mode 100644
index 000000000000..9d970ef9e675
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md
@@ -0,0 +1,49 @@
+
+## Input
+
+```javascript
+function Foo() {
+ const x = () => {
+ window.href = 'foo';
+ };
+ const y = {x};
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Foo,
+ params: [],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+function Foo() {
+ const $ = _c(1);
+ const x = _temp;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ const y = { x };
+ t0 = ;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+function _temp() {
+ window.href = "foo";
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Foo,
+ params: [],
+};
+
+```
+
+### Eval output
+(kind: exception) Bar is not defined
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.js
similarity index 81%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.js
index d95a0a6265cc..b3c936a2a284 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.js
@@ -1,4 +1,3 @@
-// @enableNewMutationAliasingModel:false
function Foo() {
const x = () => {
window.href = 'foo';
From 5063b3283fcae4bb43756d0d18d32008e3910bea Mon Sep 17 00:00:00 2001
From: Joseph Savona <6425824+josephsavona@users.noreply.github.com>
Date: Fri, 15 Aug 2025 15:27:30 -0700
Subject: [PATCH 4/5] [compiler] Remove now-unused FunctionEffect type (#34029)
The new mutation/aliasing model significantly expands on the idea of
FunctionEffect. The type (and its usage in HIRFunction.effects) was only
necessary for the now-deleted old inference model so we can clean up
this code now.
---
.../src/HIR/HIR.ts | 21 ++---------
.../src/HIR/PrintHIR.ts | 14 +-------
.../src/Optimization/LowerContextAccess.ts | 2 +-
.../src/Optimization/OutlineJsx.ts | 2 +-
...ValidateNoFreezingKnownMutableFunctions.ts | 36 ++++---------------
5 files changed, 11 insertions(+), 64 deletions(-)
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
index 51715b3c1e1d..6b3ba6f94c8a 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
@@ -7,7 +7,7 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
-import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
+import {CompilerError} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
@@ -282,30 +282,13 @@ export type HIRFunction = {
returnTypeAnnotation: t.FlowType | t.TSType | null;
returns: Place;
context: Array;
- effects: Array | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array;
- aliasingEffects?: Array | null;
+ aliasingEffects: Array | null;
};
-export type FunctionEffect =
- | {
- kind: 'GlobalMutation';
- error: CompilerErrorDetailOptions;
- }
- | {
- kind: 'ReactMutation';
- error: CompilerErrorDetailOptions;
- }
- | {
- kind: 'ContextMutation';
- places: ReadonlySet;
- effect: Effect;
- loc: SourceLocation;
- };
-
/*
* Each reactive scope may have its own control-flow, so the instructions form
* a control-flow graph. The graph comprises a set of basic blocks which reference
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
index aa6a7b0c65ce..34ce7f7694b7 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
@@ -554,23 +554,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const context = instrValue.loweredFunc.func.context
.map(dep => printPlace(dep))
.join(',');
- const effects =
- instrValue.loweredFunc.func.effects
- ?.map(effect => {
- if (effect.kind === 'ContextMutation') {
- return `ContextMutation places=[${[...effect.places]
- .map(place => printPlace(place))
- .join(', ')}] effect=${effect.effect}`;
- } else {
- return `GlobalMutation`;
- }
- })
- .join(', ') ?? '';
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
- value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
+ value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts
index 2d0b073a0459..62845934c16f 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts
@@ -255,7 +255,6 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction {
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
- effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -263,6 +262,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction {
generator: false,
async: false,
directives: [],
+ aliasingEffects: [],
};
reversePostorderBlocks(fn.body);
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts
index e59d60271a0d..6232456e620c 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts
@@ -370,7 +370,6 @@ function emitOutlinedFn(
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
- effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -378,6 +377,7 @@ function emitOutlinedFn(
generator: false,
async: false,
directives: [],
+ aliasingEffects: [],
};
return fn;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
index f71bb40545bb..f3543e113a17 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
@@ -7,10 +7,8 @@
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {
- FunctionEffect,
HIRFunction,
IdentifierId,
- isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
@@ -18,8 +16,8 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
+import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
-import {Iterable_some} from '../Utils/utils';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -50,14 +48,14 @@ export function validateNoFreezingKnownMutableFunctions(
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
- Extract
+ Extract
> = new Map();
function visitOperand(operand: Place): void {
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
- const place = [...effect.places][0];
+ const place = effect.value;
const variable =
place != null &&
place.identifier.name != null &&
@@ -77,7 +75,7 @@ export function validateNoFreezingKnownMutableFunctions(
})
.withDetail({
kind: 'error',
- loc: effect.loc,
+ loc: effect.value.loc,
message: `This modifies ${variable}`,
}),
);
@@ -108,24 +106,7 @@ export function validateNoFreezingKnownMutableFunctions(
break;
}
case 'FunctionExpression': {
- const knownMutation = (value.loweredFunc.func.effects ?? []).find(
- effect => {
- return (
- effect.kind === 'ContextMutation' &&
- (effect.effect === Effect.Store ||
- effect.effect === Effect.Mutate) &&
- Iterable_some(effect.places, place => {
- return (
- isMutableEffect(place.effect, place.loc) &&
- !isRefOrRefLikeMutableType(place.identifier.type)
- );
- })
- );
- },
- );
- if (knownMutation && knownMutation.kind === 'ContextMutation') {
- contextMutationEffects.set(lvalue.identifier.id, knownMutation);
- } else if (value.loweredFunc.func.aliasingEffects != null) {
+ if (value.loweredFunc.func.aliasingEffects != null) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
@@ -146,12 +127,7 @@ export function validateNoFreezingKnownMutableFunctions(
context.has(effect.value.identifier.id) &&
!isRefOrRefLikeMutableType(effect.value.identifier.type)
) {
- contextMutationEffects.set(lvalue.identifier.id, {
- kind: 'ContextMutation',
- effect: Effect.Mutate,
- loc: effect.value.loc,
- places: new Set([effect.value]),
- });
+ contextMutationEffects.set(lvalue.identifier.id, effect);
break effects;
}
break;
From 431bb0bddb640d01d668448f1133e44bd3eb3e11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?=
Date: Fri, 15 Aug 2025 18:32:27 -0400
Subject: [PATCH 5/5] [DevTools] Mark Unknown Reasons for Suspending with a
Note (#34200)
We currently only track the reason something might suspend in
development mode through debug info but this excludes some cases. As a
result we can end up with boundary that suspends but has no cause. This
tries to detect that and show a notice for why that might be. I'm also
trying to make it work with old React versions to cover everything.
In production we don't track any of this meta data like `_debugInfo`,
`_debugThenable` etc. so after resolution there's no information to take
from. Except suspensey images / css which we can track in prod too. We
could track lazy component types already. We'd have to add something
that tracks after the fact if something used a lazy child, child as a
promise, hooks, etc. which doesn't exist today. So that's not backwards
compatible and might add some perf/memory cost. However, another
strategy is also to try to replay the components after the fact which
could be backwards compatible. That's tricky for child position since
there's so many rules for how to do that which would have to be
replicated.
If you're in development you get a different error. Given that we've
added instrumentation very recently. If you're on an older development
version of React, then you get a different error. Unfortunately I think
my feature test is not quite perfect because it's tricky to test for the
instrumentation I just added.
https://github.com/facebook/react/pull/34146 So I think for some
prereleases that has `_debugOwner` but doesn't have that you'll get a
misleading error.
Finally, if you're in a modern development environment, the only reason
we should have any gaps is because of throw-a-Promise. This will
highlight it as missing. We can detect that something threw if a
Suspense boundary commits with a RetryCache but since it's a WeakSet we
can't look into it to see anything about what it might have been. I
don't plan on doing anything to improve this since it would only apply
to new versions of React anyway and it's just inherently flawed. So just
deprecate it #34032.
Note that nothing in here can detect that we suspended Transition. So
throwing at the root or in an update won't show that anywhere.
---
.../src/backend/fiber/renderer.js | 75 +++++++++++++++++++
.../src/backend/legacy/renderer.js | 2 +
.../src/backend/types.js | 2 +
.../react-devtools-shared/src/backendAPI.js | 2 +
.../react-devtools-shared/src/constants.js | 7 ++
.../InspectedElementSharedStyles.css | 9 +++
.../Components/InspectedElementSuspendedBy.js | 47 +++++++++++-
.../src/frontend/types.js | 2 +
8 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js
index 439d522801a3..94c394cbd701 100644
--- a/packages/react-devtools-shared/src/backend/fiber/renderer.js
+++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js
@@ -15,6 +15,7 @@ import type {
ReactIOInfo,
ReactStackTrace,
ReactCallSite,
+ Wakeable,
} from 'shared/ReactTypes';
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
@@ -87,6 +88,10 @@ import {
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
+ UNKNOWN_SUSPENDERS_NONE,
+ UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
+ UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
+ UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE,
} from '../../constants';
import {inspectHooksOfFiber} from 'react-debug-tools';
import {
@@ -296,6 +301,9 @@ type SuspenseNode = {
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
// also in the parent sets. This determine whether this could contribute in the loading sequence.
hasUniqueSuspenders: boolean,
+ // Track whether anything suspended in this boundary that we can't track either because it was using throw
+ // a promise, an older version of React or because we're inspecting prod.
+ hasUnknownSuspenders: boolean,
};
function createSuspenseNode(
@@ -309,6 +317,7 @@ function createSuspenseNode(
rects: null,
suspendedBy: new Map(),
hasUniqueSuspenders: false,
+ hasUnknownSuspenders: false,
});
}
@@ -2745,6 +2754,8 @@ export function attach(
parentSuspenseNode.hasUniqueSuspenders = true;
}
}
+ // We have observed at least one known reason this might have been suspended.
+ parentSuspenseNode.hasUnknownSuspenders = false;
// Suspending right below the root is not attributed to any particular component in UI
// other than the SuspenseNode and the HostRoot's FiberInstance.
const suspendedBy = parentInstance.suspendedBy;
@@ -2783,6 +2794,7 @@ export function attach(
// It can now be marked as having unique suspenders. We can skip its children
// since they'll still be blocked by this one.
node.hasUniqueSuspenders = true;
+ node.hasUnknownSuspenders = false;
} else if (node.firstChild !== null) {
node = node.firstChild;
continue;
@@ -3458,6 +3470,25 @@ export function attach(
insertSuspendedBy(asyncInfo);
}
+ function trackThrownPromisesFromRetryCache(
+ suspenseNode: SuspenseNode,
+ retryCache: ?WeakSet,
+ ): void {
+ if (retryCache != null) {
+ // If a Suspense boundary ever committed in fallback state with a retryCache, that
+ // suggests that something unique to that boundary was suspensey since otherwise
+ // it wouldn't have thrown and so never created the retryCache.
+ // Unfortunately if we don't have any DEV time debug info or debug thenables then
+ // we have no meta data to show. However, we still mark this Suspense boundary as
+ // participating in the loading sequence since apparently it can suspend.
+ suspenseNode.hasUniqueSuspenders = true;
+ // We have not seen any reason yet for why this suspense node might have been
+ // suspended but it clearly has been at some point. If we later discover a reason
+ // we'll clear this flag again.
+ suspenseNode.hasUnknownSuspenders = true;
+ }
+ }
+
function mountVirtualChildrenRecursively(
firstChild: Fiber,
lastChild: null | Fiber, // non-inclusive
@@ -3749,6 +3780,9 @@ export function attach(
} else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) {
// Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the
// Offscreen wrapper itself specially.
+ if (newSuspenseNode !== null) {
+ trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
+ }
const isTimedOut = fiber.memoizedState !== null;
if (isTimedOut) {
// Special case: if Suspense mounts in a timed-out state,
@@ -3791,6 +3825,9 @@ export function attach(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
}
+
+ trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
+
const fallbackFiber = contentFiber.sibling;
// First update only the Offscreen boundary. I.e. the main content.
@@ -4600,6 +4637,18 @@ export function attach(
const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null;
const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null;
+ if (isLegacySuspense) {
+ if (
+ fiberInstance !== null &&
+ fiberInstance.suspenseNode !== null &&
+ (prevFiber.stateNode === null) !== (nextFiber.stateNode === null)
+ ) {
+ trackThrownPromisesFromRetryCache(
+ fiberInstance.suspenseNode,
+ nextFiber.stateNode,
+ );
+ }
+ }
// The logic below is inspired by the code paths in updateSuspenseComponent()
// inside ReactFiberBeginWork in the React source code.
if (prevDidTimeout && nextDidTimeOut) {
@@ -4726,6 +4775,13 @@ export function attach(
const prevFallbackFiber = prevContentFiber.sibling;
const nextFallbackFiber = nextContentFiber.sibling;
+ if ((prevFiber.stateNode === null) !== (nextFiber.stateNode === null)) {
+ trackThrownPromisesFromRetryCache(
+ fiberInstance.suspenseNode,
+ nextFiber.stateNode,
+ );
+ }
+
// First update only the Offscreen boundary. I.e. the main content.
updateFlags |= updateVirtualChildrenRecursively(
nextContentFiber,
@@ -6100,6 +6156,23 @@ export function attach(
getNearestSuspenseNode(fiberInstance),
);
+ let unknownSuspenders = UNKNOWN_SUSPENDERS_NONE;
+ if (
+ fiberInstance.suspenseNode !== null &&
+ fiberInstance.suspenseNode.hasUnknownSuspenders &&
+ !isTimedOutSuspense
+ ) {
+ // Something unknown threw to suspended this boundary. Let's figure out why that might be.
+ if (renderer.bundleType === 0) {
+ unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_PRODUCTION;
+ } else if (!('_debugInfo' in fiber)) {
+ // TODO: We really should detect _debugThenable and the auto-instrumentation for lazy/thenables too.
+ unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_OLD_VERSION;
+ } else {
+ unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE;
+ }
+ }
+
return {
id: fiberInstance.id,
@@ -6164,6 +6237,7 @@ export function attach(
suspendedBy: suspendedBy,
suspendedByRange: suspendedByRange,
+ unknownSuspenders: unknownSuspenders,
// List of owners
owners,
@@ -6280,6 +6354,7 @@ export function attach(
serializeAsyncInfo(info, virtualInstance, null),
),
suspendedByRange: suspendedByRange,
+ unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
// List of owners
owners,
diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js
index 592238934f5d..d1623ff24bfd 100644
--- a/packages/react-devtools-shared/src/backend/legacy/renderer.js
+++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js
@@ -34,6 +34,7 @@ import {
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_REORDER_CHILDREN,
+ UNKNOWN_SUSPENDERS_NONE,
} from '../../constants';
import {decorateMany, forceUpdate, restoreMany} from './utils';
@@ -860,6 +861,7 @@ export function attach(
// Not supported in legacy renderers.
suspendedBy: [],
suspendedByRange: null,
+ unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
// List of owners
owners,
diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js
index 978b546b8353..ffbacea01aab 100644
--- a/packages/react-devtools-shared/src/backend/types.js
+++ b/packages/react-devtools-shared/src/backend/types.js
@@ -34,6 +34,7 @@ import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {ReactFunctionLocation, ReactStackTrace} from 'shared/ReactTypes';
import type Agent from './agent';
+import type {UnknownSuspendersReason} from '../constants';
type BundleType =
| 0 // PROD
@@ -303,6 +304,7 @@ export type InspectedElement = {
// Things that suspended this Instances
suspendedBy: Object, // DehydratedData or Array
suspendedByRange: null | [number, number],
+ unknownSuspenders: UnknownSuspendersReason,
// List of owners
owners: Array | null,
diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js
index eb1b6f6df3ff..f8fa4da37254 100644
--- a/packages/react-devtools-shared/src/backendAPI.js
+++ b/packages/react-devtools-shared/src/backendAPI.js
@@ -272,6 +272,7 @@ export function convertInspectedElementBackendToFrontend(
warnings,
suspendedBy,
suspendedByRange,
+ unknownSuspenders,
nativeTag,
} = inspectedElementBackend;
@@ -317,6 +318,7 @@ export function convertInspectedElementBackendToFrontend(
? []
: hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo),
suspendedByRange,
+ unknownSuspenders,
nativeTag,
};
diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js
index ce6ed0b308a8..8071d3d4a2c6 100644
--- a/packages/react-devtools-shared/src/constants.js
+++ b/packages/react-devtools-shared/src/constants.js
@@ -32,6 +32,13 @@ export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
+export const UNKNOWN_SUSPENDERS_NONE: UnknownSuspendersReason = 0; // If we had at least one debugInfo, then that might have been the reason.
+export const UNKNOWN_SUSPENDERS_REASON_PRODUCTION: UnknownSuspendersReason = 1; // We're running in prod. That might be why we had unknown suspenders.
+export const UNKNOWN_SUSPENDERS_REASON_OLD_VERSION: UnknownSuspendersReason = 2; // We're running an old version of React that doesn't have full coverage. That might be the reason.
+export const UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE: UnknownSuspendersReason = 3; // If we're in dev, didn't detect and debug info and still suspended (other than CSS/image) the only reason is thrown promise.
+
+export opaque type UnknownSuspendersReason = 0 | 1 | 2 | 3;
+
export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab';
export const LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY =
'React::DevTools::componentFilters';
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
index 0fb5107361c1..978077d2d9a2 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
@@ -52,6 +52,15 @@
min-width: 1rem;
}
+.InfoRow {
+ border-top: 1px solid var(--color-border);
+ padding: 0.5rem 1rem;
+}
+
+.InfoRow:last-child {
+ margin-bottom: -0.25rem;
+}
+
.CollapsableRow {
border-top: 1px solid var(--color-border);
}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
index 3527c23b0669..451b53b4ac59 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
@@ -27,6 +27,13 @@ import type {
} from 'react-devtools-shared/src/frontend/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+import {
+ UNKNOWN_SUSPENDERS_NONE,
+ UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
+ UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
+ UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE,
+} from '../../../constants';
+
type RowProps = {
bridge: FrontendBridge,
element: Element,
@@ -295,7 +302,10 @@ export default function InspectedElementSuspendedBy({
const {suspendedBy, suspendedByRange} = inspectedElement;
// Skip the section if nothing suspended this component.
- if (suspendedBy == null || suspendedBy.length === 0) {
+ if (
+ (suspendedBy == null || suspendedBy.length === 0) &&
+ inspectedElement.unknownSuspenders === UNKNOWN_SUSPENDERS_NONE
+ ) {
return null;
}
@@ -327,9 +337,41 @@ export default function InspectedElementSuspendedBy({
minTime = maxTime - 25;
}
- const sortedSuspendedBy = suspendedBy.slice(0);
+ const sortedSuspendedBy = suspendedBy === null ? [] : suspendedBy.slice(0);
sortedSuspendedBy.sort(compareTime);
+ let unknownSuspenders = null;
+ switch (inspectedElement.unknownSuspenders) {
+ case UNKNOWN_SUSPENDERS_REASON_PRODUCTION:
+ unknownSuspenders = (
+
+ Something suspended but we don't know the exact reason in production
+ builds of React. Test this in development mode to see exactly what
+ might suspend.
+
+ );
+ break;
+ case UNKNOWN_SUSPENDERS_REASON_OLD_VERSION:
+ unknownSuspenders = (
+
+ Something suspended but we don't track all the necessary information
+ in older versions of React. Upgrade to the latest version of React to
+ see exactly what might suspend.
+
+ );
+ break;
+ case UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE:
+ unknownSuspenders = (
+
+ Something threw a Promise to suspend this boundary. It's likely an
+ outdated version of a library that doesn't yet fully take advantage of
+ use(). Upgrade your data fetching library to see exactly what might
+ suspend.
+
+ );
+ break;
+ }
+
return (
@@ -351,6 +393,7 @@ export default function InspectedElementSuspendedBy({
maxTime={maxTime}
/>
))}
+ {unknownSuspenders}
);
}
diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js
index d7d22b9530c9..4aa936ab6481 100644
--- a/packages/react-devtools-shared/src/frontend/types.js
+++ b/packages/react-devtools-shared/src/frontend/types.js
@@ -19,6 +19,7 @@ import type {
Unserializable,
} from 'react-devtools-shared/src/hydration';
import type {ReactFunctionLocation, ReactStackTrace} from 'shared/ReactTypes';
+import type {UnknownSuspendersReason} from '../constants';
export type BrowserTheme = 'dark' | 'light';
@@ -283,6 +284,7 @@ export type InspectedElement = {
suspendedBy: Object,
// Minimum start time to maximum end time + a potential (not actual) throttle, within the nearest boundary.
suspendedByRange: null | [number, number],
+ unknownSuspenders: UnknownSuspendersReason,
// List of owners
owners: Array
| null,