Skip to content

Commit c50ee4d

Browse files
committed
[compiler][bugfix] Returned functions are not always frozen
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
1 parent 5dc00d6 commit c50ee4d

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;
Lines changed: 92 additions & 0 deletions
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>
Lines changed: 37 additions & 0 deletions
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+
};
Lines changed: 101 additions & 0 deletions
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)