Skip to content

Commit 12f4cb8

Browse files
authored
[compiler][bugfix] Returned functions are not always frozen (facebook#33047)
Fixes an edge case in React Compiler's effects inference model. Returned values should only be typed as 'frozen' if they are (1) local and (2) not a function expression which may capture and mutate this function's outer context. See test fixtures for details --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33047). * facebook#32765 * facebook#32747 * __->__ facebook#33047
1 parent 90a124a commit 12f4cb8

File tree

5 files changed

+293
-4
lines changed

5 files changed

+293
-4
lines changed

compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ export default function inferReferenceEffects(
111111
* Initial state contains function params
112112
* TODO: include module declarations here as well
113113
*/
114-
const initialState = InferenceState.empty(fn.env);
114+
const initialState = InferenceState.empty(
115+
fn.env,
116+
options.isFunctionExpression,
117+
);
115118
const value: InstructionValue = {
116119
kind: 'Primitive',
117120
loc: fn.loc,
@@ -255,6 +258,7 @@ type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
255258
// Maintains a mapping of top-level variables to the kind of value they hold
256259
class InferenceState {
257260
env: Environment;
261+
#isFunctionExpression: boolean;
258262

259263
// The kind of each value, based on its allocation site
260264
#values: Map<InstructionValue, AbstractValue>;
@@ -267,16 +271,25 @@ class InferenceState {
267271

268272
constructor(
269273
env: Environment,
274+
isFunctionExpression: boolean,
270275
values: Map<InstructionValue, AbstractValue>,
271276
variables: Map<IdentifierId, Set<InstructionValue>>,
272277
) {
273278
this.env = env;
279+
this.#isFunctionExpression = isFunctionExpression;
274280
this.#values = values;
275281
this.#variables = variables;
276282
}
277283

278-
static empty(env: Environment): InferenceState {
279-
return new InferenceState(env, new Map(), new Map());
284+
static empty(
285+
env: Environment,
286+
isFunctionExpression: boolean,
287+
): InferenceState {
288+
return new InferenceState(env, isFunctionExpression, new Map(), new Map());
289+
}
290+
291+
get isFunctionExpression(): boolean {
292+
return this.#isFunctionExpression;
280293
}
281294

282295
// (Re)initializes a @param value with its default @param kind.
@@ -613,6 +626,7 @@ class InferenceState {
613626
} else {
614627
return new InferenceState(
615628
this.env,
629+
this.#isFunctionExpression,
616630
nextValues ?? new Map(this.#values),
617631
nextVariables ?? new Map(this.#variables),
618632
);
@@ -627,6 +641,7 @@ class InferenceState {
627641
clone(): InferenceState {
628642
return new InferenceState(
629643
this.env,
644+
this.#isFunctionExpression,
630645
new Map(this.#values),
631646
new Map(this.#variables),
632647
);
@@ -1781,8 +1796,15 @@ function inferBlock(
17811796
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
17821797
if (
17831798
state.isDefined(operand) &&
1784-
state.kind(operand).kind === ValueKind.Context
1799+
((operand.identifier.type.kind === 'Function' &&
1800+
state.isFunctionExpression) ||
1801+
state.kind(operand).kind === ValueKind.Context)
17851802
) {
1803+
/**
1804+
* Returned values should only be typed as 'frozen' if they are both (1)
1805+
* local and (2) not a function expression which may capture and mutate
1806+
* this function's outer context.
1807+
*/
17861808
effect = Effect.ConditionallyMutate;
17871809
} else {
17881810
effect = Effect.Freeze;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {Stringify} from 'shared-runtime';
6+
7+
/**
8+
* Example showing that returned inner function expressions should not be
9+
* typed with `freeze` effects.
10+
*/
11+
function Foo({a, b}) {
12+
'use memo';
13+
const obj = {};
14+
const updaterFactory = () => {
15+
/**
16+
* This returned function expression *is* a local value. But it might (1)
17+
* capture and mutate its context environment and (2) be called during
18+
* render.
19+
* Typing it with `freeze` effects would be incorrect as it would mean
20+
* inferring that calls to updaterFactory()() do not mutate its captured
21+
* context.
22+
*/
23+
return newValue => {
24+
obj.value = newValue;
25+
obj.a = a;
26+
};
27+
};
28+
29+
const updater = updaterFactory();
30+
updater(b);
31+
return <Stringify cb={obj} shouldInvokeFns={true} />;
32+
}
33+
34+
export const FIXTURE_ENTRYPOINT = {
35+
fn: Foo,
36+
params: [{a: 1, b: 2}],
37+
sequentialRenders: [
38+
{a: 1, b: 2},
39+
{a: 1, b: 3},
40+
],
41+
};
42+
43+
```
44+
45+
## Code
46+
47+
```javascript
48+
import { c as _c } from "react/compiler-runtime";
49+
import { Stringify } from "shared-runtime";
50+
51+
/**
52+
* Example showing that returned inner function expressions should not be
53+
* typed with `freeze` effects.
54+
*/
55+
function Foo(t0) {
56+
"use memo";
57+
const $ = _c(3);
58+
const { a, b } = t0;
59+
let t1;
60+
if ($[0] !== a || $[1] !== b) {
61+
const obj = {};
62+
const updaterFactory = () => (newValue) => {
63+
obj.value = newValue;
64+
obj.a = a;
65+
};
66+
67+
const updater = updaterFactory();
68+
updater(b);
69+
t1 = <Stringify cb={obj} shouldInvokeFns={true} />;
70+
$[0] = a;
71+
$[1] = b;
72+
$[2] = t1;
73+
} else {
74+
t1 = $[2];
75+
}
76+
return t1;
77+
}
78+
79+
export const FIXTURE_ENTRYPOINT = {
80+
fn: Foo,
81+
params: [{ a: 1, b: 2 }],
82+
sequentialRenders: [
83+
{ a: 1, b: 2 },
84+
{ a: 1, b: 3 },
85+
],
86+
};
87+
88+
```
89+
90+
### Eval output
91+
(kind: ok) <div>{"cb":{"value":2,"a":1},"shouldInvokeFns":true}</div>
92+
<div>{"cb":{"value":3,"a":1},"shouldInvokeFns":true}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {Stringify} from 'shared-runtime';
2+
3+
/**
4+
* Example showing that returned inner function expressions should not be
5+
* typed with `freeze` effects.
6+
*/
7+
function Foo({a, b}) {
8+
'use memo';
9+
const obj = {};
10+
const updaterFactory = () => {
11+
/**
12+
* This returned function expression *is* a local value. But it might (1)
13+
* capture and mutate its context environment and (2) be called during
14+
* render.
15+
* Typing it with `freeze` effects would be incorrect as it would mean
16+
* inferring that calls to updaterFactory()() do not mutate its captured
17+
* context.
18+
*/
19+
return newValue => {
20+
obj.value = newValue;
21+
obj.a = a;
22+
};
23+
};
24+
25+
const updater = updaterFactory();
26+
updater(b);
27+
return <Stringify cb={obj} shouldInvokeFns={true} />;
28+
}
29+
30+
export const FIXTURE_ENTRYPOINT = {
31+
fn: Foo,
32+
params: [{a: 1, b: 2}],
33+
sequentialRenders: [
34+
{a: 1, b: 2},
35+
{a: 1, b: 3},
36+
],
37+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {makeArray, Stringify, useIdentity} from 'shared-runtime';
6+
7+
/**
8+
* Example showing that returned inner function expressions should not be
9+
* typed with `freeze` effects.
10+
* Also see repro-returned-inner-fn-mutates-context
11+
*/
12+
function Foo({b}) {
13+
'use memo';
14+
15+
const fnFactory = () => {
16+
/**
17+
* This returned function expression *is* a local value. But it might (1)
18+
* capture and mutate its context environment and (2) be called during
19+
* render.
20+
* Typing it with `freeze` effects would be incorrect as it would mean
21+
* inferring that calls to updaterFactory()() do not mutate its captured
22+
* context.
23+
*/
24+
return () => {
25+
myVar = () => console.log('a');
26+
};
27+
};
28+
let myVar = () => console.log('b');
29+
useIdentity();
30+
31+
const fn = fnFactory();
32+
const arr = makeArray(b);
33+
fn(arr);
34+
return <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
35+
}
36+
37+
export const FIXTURE_ENTRYPOINT = {
38+
fn: Foo,
39+
params: [{b: 1}],
40+
sequentialRenders: [{b: 1}, {b: 2}],
41+
};
42+
43+
```
44+
45+
## Code
46+
47+
```javascript
48+
import { c as _c } from "react/compiler-runtime";
49+
import { makeArray, Stringify, useIdentity } from "shared-runtime";
50+
51+
/**
52+
* Example showing that returned inner function expressions should not be
53+
* typed with `freeze` effects.
54+
* Also see repro-returned-inner-fn-mutates-context
55+
*/
56+
function Foo(t0) {
57+
"use memo";
58+
const $ = _c(3);
59+
const { b } = t0;
60+
61+
const fnFactory = () => () => {
62+
myVar = _temp;
63+
};
64+
65+
let myVar;
66+
myVar = _temp2;
67+
useIdentity();
68+
69+
const fn = fnFactory();
70+
const arr = makeArray(b);
71+
fn(arr);
72+
let t1;
73+
if ($[0] !== arr || $[1] !== myVar) {
74+
t1 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
75+
$[0] = arr;
76+
$[1] = myVar;
77+
$[2] = t1;
78+
} else {
79+
t1 = $[2];
80+
}
81+
return t1;
82+
}
83+
function _temp2() {
84+
return console.log("b");
85+
}
86+
function _temp() {
87+
return console.log("a");
88+
}
89+
90+
export const FIXTURE_ENTRYPOINT = {
91+
fn: Foo,
92+
params: [{ b: 1 }],
93+
sequentialRenders: [{ b: 1 }, { b: 2 }],
94+
};
95+
96+
```
97+
98+
### Eval output
99+
(kind: ok) <div>{"cb":{"kind":"Function"},"value":[1],"shouldInvokeFns":true}</div>
100+
<div>{"cb":{"kind":"Function"},"value":[2],"shouldInvokeFns":true}</div>
101+
logs: ['a','a']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {makeArray, Stringify, useIdentity} from 'shared-runtime';
2+
3+
/**
4+
* Example showing that returned inner function expressions should not be
5+
* typed with `freeze` effects.
6+
* Also see repro-returned-inner-fn-mutates-context
7+
*/
8+
function Foo({b}) {
9+
'use memo';
10+
11+
const fnFactory = () => {
12+
/**
13+
* This returned function expression *is* a local value. But it might (1)
14+
* capture and mutate its context environment and (2) be called during
15+
* render.
16+
* Typing it with `freeze` effects would be incorrect as it would mean
17+
* inferring that calls to updaterFactory()() do not mutate its captured
18+
* context.
19+
*/
20+
return () => {
21+
myVar = () => console.log('a');
22+
};
23+
};
24+
let myVar = () => console.log('b');
25+
useIdentity();
26+
27+
const fn = fnFactory();
28+
const arr = makeArray(b);
29+
fn(arr);
30+
return <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
31+
}
32+
33+
export const FIXTURE_ENTRYPOINT = {
34+
fn: Foo,
35+
params: [{b: 1}],
36+
sequentialRenders: [{b: 1}, {b: 2}],
37+
};

0 commit comments

Comments
 (0)