Skip to content

Commit 13034bb

Browse files
committed
[compiler][wip] Track and propagate function effects of arrays and objects
ghstack-source-id: 9039083 Pull Request resolved: #30578
1 parent c708a6e commit 13034bb

File tree

8 files changed

+252
-80
lines changed

8 files changed

+252
-80
lines changed

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

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ class InferenceState {
276276
*/
277277
#variables: Map<IdentifierId, Set<InstructionValue>>;
278278

279+
#functionEffects: Map<InstructionValue, Array<FunctionEffect>> = new Map();
280+
279281
constructor(
280282
env: Environment,
281283
values: Map<InstructionValue, AbstractValue>,
@@ -302,6 +304,19 @@ class InferenceState {
302304
this.#values.set(value, kind);
303305
}
304306

307+
setFunctionEffects(value: InstructionValue, effect: Array<FunctionEffect>): void {
308+
CompilerError.invariant(value.kind !== 'LoadLocal', {
309+
reason:
310+
'Expected all top-level identifiers to be defined as variables, not values',
311+
description: null,
312+
loc: value.loc,
313+
suggestions: null,
314+
});
315+
const curEffects = this.#functionEffects.get(value) ?? [];
316+
curEffects.push(...effect);
317+
this.#functionEffects.set(value, curEffects);
318+
}
319+
305320
values(place: Place): Array<InstructionValue> {
306321
const values = this.#variables.get(place.identifier.id);
307322
CompilerError.invariant(values != null, {
@@ -400,50 +415,57 @@ class InferenceState {
400415
}
401416

402417
// Propagate effects of function expressions to the outer (ie current) effect context
418+
const dependentEffects = [];
403419
for (const value of values) {
404420
if (
405421
(value.kind === 'FunctionExpression' ||
406-
value.kind === 'ObjectMethod') &&
407-
value.loweredFunc.func.effects != null
422+
value.kind === 'ObjectMethod')
408423
) {
409-
for (const effect of value.loweredFunc.func.effects) {
410-
if (
411-
effect.kind === 'GlobalMutation' ||
412-
effect.kind === 'ReactMutation'
413-
) {
414-
// Known effects are always propagated upwards
415-
functionEffects.push(effect);
416-
} else {
417-
/**
418-
* Contextual effects need to be replayed against the current inference
419-
* state, which may know more about the value to which the effect applied.
420-
* The main cases are:
421-
* 1. The mutated context value is _still_ a context value in the current scope,
422-
* so we have to continue propagating the original context mutation.
423-
* 2. The mutated context value is a mutable value in the current scope,
424-
* so the context mutation was fine and we can skip propagating the effect.
425-
* 3. The mutated context value is an immutable value in the current scope,
426-
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
427-
* more detailed effect to the current function context.
428-
*/
429-
for (const place of effect.places) {
430-
if (this.isDefined(place)) {
431-
const replayedEffect = this.reference(
432-
{...place, loc: effect.loc},
433-
effect.effect,
434-
reason,
435-
);
436-
if (replayedEffect != null) {
437-
if (replayedEffect.kind === 'ContextMutation') {
438-
// Case 1, still a context value so propagate the original effect
439-
functionEffects.push(effect);
440-
} else {
441-
// Case 3, immutable value so propagate the more precise effect
442-
functionEffects.push(replayedEffect);
443-
}
444-
} // else case 2, local mutable value so this effect was fine
424+
dependentEffects.push(...value.loweredFunc.func.effects ?? []);
425+
}
426+
const knownEffects = this.#functionEffects.get(value);
427+
if (knownEffects != null) {
428+
dependentEffects.push(...knownEffects)
429+
}
430+
}
431+
432+
433+
for (const effect of dependentEffects) {
434+
if (
435+
effect.kind === 'GlobalMutation' ||
436+
effect.kind === 'ReactMutation'
437+
) {
438+
// Known effects are always propagated upwards
439+
functionEffects.push(effect);
440+
} else {
441+
/**
442+
* Contextual effects need to be replayed against the current inference
443+
* state, which may know more about the value to which the effect applied.
444+
* The main cases are:
445+
* 1. The mutated context value is _still_ a context value in the current scope,
446+
* so we have to continue propagating the original context mutation.
447+
* 2. The mutated context value is a mutable value in the current scope,
448+
* so the context mutation was fine and we can skip propagating the effect.
449+
* 3. The mutated context value is an immutable value in the current scope,
450+
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
451+
* more detailed effect to the current function context.
452+
*/
453+
for (const place of effect.places) {
454+
if (this.isDefined(place)) {
455+
const replayedEffect = this.reference(
456+
{...place, loc: effect.loc},
457+
effect.effect,
458+
reason,
459+
);
460+
if (replayedEffect != null) {
461+
if (replayedEffect.kind === 'ContextMutation') {
462+
// Case 1, still a context value so propagate the original effect
463+
functionEffects.push(effect);
464+
} else {
465+
// Case 3, immutable value so propagate the more precise effect
466+
functionEffects.push(replayedEffect);
445467
}
446-
}
468+
} // else case 2, local mutable value so this effect was fine
447469
}
448470
}
449471
}
@@ -995,8 +1017,22 @@ function inferBlock(
9951017
context: new Set(),
9961018
};
9971019
effect = {kind: Effect.Capture, reason: ValueReason.Other};
998-
lvalueEffect = Effect.Store;
999-
break;
1020+
1021+
const arrEffects: Array<FunctionEffect> = [];
1022+
for (const operand of eachInstructionOperand(instr)) {
1023+
state.referenceAndRecordEffects(
1024+
operand,
1025+
effect.kind,
1026+
effect.reason,
1027+
arrEffects,
1028+
);
1029+
}
1030+
1031+
state.initialize(instrValue, valueKind);
1032+
state.define(instr.lvalue, instrValue);
1033+
instr.lvalue.effect = Effect.Store;
1034+
state.setFunctionEffects(instrValue, arrEffects);
1035+
continue;
10001036
}
10011037
case 'NewExpression': {
10021038
/**
@@ -1039,6 +1075,7 @@ function inferBlock(
10391075
continue;
10401076
}
10411077
case 'ObjectExpression': {
1078+
const objEffects: Array<FunctionEffect> = [];
10421079
valueKind = hasContextRefOperand(state, instrValue)
10431080
? {
10441081
kind: ValueKind.Context,
@@ -1060,15 +1097,15 @@ function inferBlock(
10601097
property.key.name,
10611098
Effect.Freeze,
10621099
ValueReason.Other,
1063-
functionEffects,
1100+
objEffects,
10641101
);
10651102
}
10661103
// Object construction captures but does not modify the key/property values
10671104
state.referenceAndRecordEffects(
10681105
property.place,
10691106
Effect.Capture,
10701107
ValueReason.Other,
1071-
functionEffects,
1108+
objEffects,
10721109
);
10731110
break;
10741111
}
@@ -1078,7 +1115,7 @@ function inferBlock(
10781115
property.place,
10791116
Effect.Capture,
10801117
ValueReason.Other,
1081-
functionEffects,
1118+
objEffects,
10821119
);
10831120
break;
10841121
}
@@ -1092,6 +1129,7 @@ function inferBlock(
10921129
}
10931130

10941131
state.initialize(instrValue, valueKind);
1132+
state.setFunctionEffects(instrValue, objEffects);
10951133
state.define(instr.lvalue, instrValue);
10961134
instr.lvalue.effect = Effect.Store;
10971135
continue;
@@ -1547,14 +1585,16 @@ function inferBlock(
15471585
break;
15481586
}
15491587
case 'PropertyLoad': {
1588+
const loadEffects: Array<FunctionEffect> = [];
15501589
state.referenceAndRecordEffects(
15511590
instrValue.object,
15521591
Effect.Read,
15531592
ValueReason.Other,
1554-
functionEffects,
1593+
loadEffects,
15551594
);
15561595
const lvalue = instr.lvalue;
15571596
lvalue.effect = Effect.ConditionallyMutate;
1597+
state.setFunctionEffects(instrValue, loadEffects);
15581598
state.initialize(instrValue, state.kind(instrValue.object));
15591599
state.define(lvalue, instrValue);
15601600
continue;
@@ -1611,22 +1651,24 @@ function inferBlock(
16111651
continue;
16121652
}
16131653
case 'ComputedLoad': {
1654+
const loadEffects: Array<FunctionEffect> = [];
16141655
state.referenceAndRecordEffects(
16151656
instrValue.object,
16161657
Effect.Read,
16171658
ValueReason.Other,
1618-
functionEffects,
1659+
loadEffects,
16191660
);
16201661
state.referenceAndRecordEffects(
16211662
instrValue.property,
16221663
Effect.Read,
16231664
ValueReason.Other,
1624-
functionEffects,
1665+
loadEffects,
16251666
);
16261667
const lvalue = instr.lvalue;
16271668
lvalue.effect = Effect.ConditionallyMutate;
16281669
state.initialize(instrValue, state.kind(instrValue.object));
16291670
state.define(lvalue, instrValue);
1671+
state.setFunctionEffects(instrValue, loadEffects);
16301672
continue;
16311673
}
16321674
case 'Await': {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
## Input
3+
4+
```javascript
5+
let b = 1;
6+
7+
export default function useMyHook() {
8+
const fn = () => {
9+
b = 2;
10+
};
11+
const obj = { fn };
12+
obj.fn();
13+
}
14+
15+
export const FIXTURE_ENTRYPOINT = {
16+
fn: useMyHook,
17+
params: [],
18+
};
19+
20+
```
21+
22+
23+
## Error
24+
25+
```
26+
3 | export default function useMyHook() {
27+
4 | const fn = () => {
28+
> 5 | b = 2;
29+
| ^ InvalidReact: 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:5)
30+
6 | };
31+
7 | const obj = { fn };
32+
8 | obj.fn();
33+
```
34+
35+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
let b = 1;
2+
3+
export default function useMyHook() {
4+
const fn = () => {
5+
b = 2;
6+
};
7+
const obj = { fn };
8+
const arr = [obj];
9+
foo(arr);
10+
}
11+
12+
export const FIXTURE_ENTRYPOINT = {
13+
fn: useMyHook,
14+
params: [],
15+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Foo() {
6+
const x = () => {
7+
window.href = 'foo';
8+
};
9+
const y = {x};
10+
return <Bar y={y} />;
11+
}
12+
13+
export const FIXTURE_ENTRYPOINT = {
14+
fn: Foo,
15+
params: [],
16+
};
17+
18+
```
19+
20+
## Code
21+
22+
```javascript
23+
import { c as _c } from "react/compiler-runtime";
24+
function Foo() {
25+
const $ = _c(1);
26+
const x = _temp;
27+
let t0;
28+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
29+
const y = { x };
30+
t0 = <Bar y={y} />;
31+
$[0] = t0;
32+
} else {
33+
t0 = $[0];
34+
}
35+
return t0;
36+
}
37+
function _temp() {
38+
window.href = "foo";
39+
}
40+
41+
export const FIXTURE_ENTRYPOINT = {
42+
fn: Foo,
43+
params: [],
44+
};
45+
46+
```
47+
48+
### Eval output
49+
(kind: exception) Bar is not defined

0 commit comments

Comments
 (0)