Skip to content

Commit 7ffe06d

Browse files
committed
[compiler] Delay mutation of function expr context variables until function is called
See comments in code. The idea is that rather than immediately processing function expression effects when declaring the function, we record Capture effects for context variables that may be captured/mutated in the function. Then, transitive mutations of the function value itself will extend the range of these values via the normal captured value comutation inference established earlier in the stack (if capture a -> b, then transitiveMutate(b) => mutate(a)). So capture contextVar -> function and transitiveMutate(function) => mutate(contextVar). ghstack-source-id: 3558955 Pull Request resolved: #33369
1 parent 261f190 commit 7ffe06d

File tree

4 files changed

+117
-43
lines changed

4 files changed

+117
-43
lines changed

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
Effect,
1111
HIRFunction,
1212
Identifier,
13+
IdentifierId,
1314
LoweredFunction,
1415
Place,
1516
isRefOrRefValue,
1617
makeInstructionId,
17-
printFunction,
1818
} from '../HIR';
1919
import {deadCodeElimination} from '../Optimization';
2020
import {inferReactiveScopeVariables} from '../ReactiveScopes';
@@ -26,7 +26,7 @@ import {
2626
eachInstructionLValue,
2727
eachInstructionValueOperand,
2828
} from '../HIR/visitors';
29-
import {Iterable_some} from '../Utils/utils';
29+
import {assertExhaustive, Iterable_some} from '../Utils/utils';
3030
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
3131
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
3232
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
@@ -73,6 +73,49 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
7373
});
7474
const effects = inferMutationAliasingFunctionEffects(fn);
7575
fn.aliasingEffects = effects;
76+
77+
const capturedOrMutated = new Set<IdentifierId>();
78+
for (const effect of effects ?? []) {
79+
switch (effect.kind) {
80+
case 'Alias':
81+
case 'Capture':
82+
case 'CreateFrom': {
83+
capturedOrMutated.add(effect.from.identifier.id);
84+
break;
85+
}
86+
case 'Apply': {
87+
capturedOrMutated.add(effect.function.place.identifier.id);
88+
break;
89+
}
90+
case 'Mutate':
91+
case 'MutateConditionally':
92+
case 'MutateTransitive':
93+
case 'MutateTransitiveConditionally': {
94+
capturedOrMutated.add(effect.value.identifier.id);
95+
break;
96+
}
97+
case 'Create':
98+
case 'Freeze':
99+
case 'ImmutableCapture': {
100+
// no-op
101+
break;
102+
}
103+
default: {
104+
assertExhaustive(
105+
effect,
106+
`Unexpected effect kind ${(effect as any).kind}`,
107+
);
108+
}
109+
}
110+
}
111+
112+
for (const operand of fn.context) {
113+
if (capturedOrMutated.has(operand.identifier.id)) {
114+
operand.effect = Effect.Capture;
115+
} else {
116+
operand.effect = Effect.Read;
117+
}
118+
}
76119
}
77120

78121
function lower(func: HIRFunction): DisjointSet<Identifier> {

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, ValueKind} from '..';
8+
import {CompilerError, Effect, ValueKind} from '..';
99
import {
1010
BasicBlock,
1111
BlockId,
@@ -1038,8 +1038,43 @@ function computeSignatureForInstruction(
10381038
into: lvalue,
10391039
value: ValueKind.Mutable,
10401040
});
1041-
if (value.loweredFunc.func.aliasingEffects != null) {
1042-
effects.push(...value.loweredFunc.func.aliasingEffects);
1041+
/**
1042+
* We've already analyzed the function expression in AnalyzeFunctions. There, we assign
1043+
* a Capture effect to any context variable that appears (locally) to be aliased and/or
1044+
* mutated. The precise effects are annotated on the function expression's aliasingEffects
1045+
* property, but we don't want to execute those effects yet. We can only use those when
1046+
* we know exactly how the function is invoked — via an Apply effect from a custom signature.
1047+
*
1048+
* But in the general case, functions can be passed around and possibly called in ways where
1049+
* we don't know how to interpret their precise effects. For example:
1050+
*
1051+
* ```
1052+
* const a = {};
1053+
* // We don't want to consider a as mutating here either, this just declares the function
1054+
* const f = () => { maybeMutate(a) };
1055+
* // We don't want to consider a as mutating here either, it can't possibly call f yet
1056+
* const x = [f];
1057+
* // Here we have to assume that f can be called (transitively), and have to consider a
1058+
* // as mutating
1059+
* callAllFunctionInArray(x);
1060+
* ```
1061+
*
1062+
* So for any context variables that were inferred as captured or mutated, we record a
1063+
* Capture effect. If the resulting function is transitively mutated, this will mean
1064+
* that those operands are also considered mutated. If the function is never called,
1065+
* they won't be!
1066+
*
1067+
* Note that if the type of the context variables are frozen, global, or primitive, the
1068+
* Capture will either get pruned or downgraded to an ImmutableCapture.
1069+
*/
1070+
for (const operand of value.loweredFunc.func.context) {
1071+
if (operand.effect === Effect.Capture) {
1072+
effects.push({
1073+
kind: 'Capture',
1074+
from: operand,
1075+
into: lvalue,
1076+
});
1077+
}
10431078
}
10441079
break;
10451080
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.expect.md

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ function Component({a, b}) {
1010
y.x = x;
1111
mutate(y);
1212
};
13-
return <div onClick={f}>{x}</div>;
13+
f();
14+
return <div>{x}</div>;
1415
}
1516

1617
```
@@ -20,36 +21,26 @@ function Component({a, b}) {
2021
```javascript
2122
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
2223
function Component(t0) {
23-
const $ = _c(7);
24+
const $ = _c(3);
2425
const { a, b } = t0;
2526
let t1;
26-
let x;
2727
if ($[0] !== a || $[1] !== b) {
28-
x = { a };
28+
const x = { a };
2929
const y = [b];
30-
t1 = () => {
30+
const f = () => {
3131
y.x = x;
3232
mutate(y);
3333
};
34+
35+
f();
36+
t1 = <div>{x}</div>;
3437
$[0] = a;
3538
$[1] = b;
3639
$[2] = t1;
37-
$[3] = x;
3840
} else {
3941
t1 = $[2];
40-
x = $[3];
41-
}
42-
const f = t1;
43-
let t2;
44-
if ($[4] !== f || $[5] !== x) {
45-
t2 = <div onClick={f}>{x}</div>;
46-
$[4] = f;
47-
$[5] = x;
48-
$[6] = t2;
49-
} else {
50-
t2 = $[6];
5142
}
52-
return t2;
43+
return t1;
5344
}
5445

5546
```

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,42 @@ function Component({a, b, c}) {
2020
```javascript
2121
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
2222
function Component(t0) {
23-
const $ = _c(8);
23+
const $ = _c(9);
2424
const { a, b, c } = t0;
2525
let t1;
26-
let x;
27-
if ($[0] !== a || $[1] !== b || $[2] !== c) {
28-
x = [a, b];
29-
t1 = () => {
26+
if ($[0] !== a || $[1] !== b) {
27+
t1 = [a, b];
28+
$[0] = a;
29+
$[1] = b;
30+
$[2] = t1;
31+
} else {
32+
t1 = $[2];
33+
}
34+
const x = t1;
35+
let t2;
36+
if ($[3] !== c || $[4] !== x) {
37+
t2 = () => {
3038
maybeMutate(x);
3139

3240
console.log(c);
3341
};
34-
$[0] = a;
35-
$[1] = b;
36-
$[2] = c;
37-
$[3] = t1;
42+
$[3] = c;
3843
$[4] = x;
44+
$[5] = t2;
3945
} else {
40-
t1 = $[3];
41-
x = $[4];
46+
t2 = $[5];
4247
}
43-
const f = t1;
44-
let t2;
45-
if ($[5] !== f || $[6] !== x) {
46-
t2 = <Foo onClick={f} value={x} />;
47-
$[5] = f;
48-
$[6] = x;
49-
$[7] = t2;
48+
const f = t2;
49+
let t3;
50+
if ($[6] !== f || $[7] !== x) {
51+
t3 = <Foo onClick={f} value={x} />;
52+
$[6] = f;
53+
$[7] = x;
54+
$[8] = t3;
5055
} else {
51-
t2 = $[7];
56+
t3 = $[8];
5257
}
53-
return t2;
58+
return t3;
5459
}
5560

5661
```

0 commit comments

Comments
 (0)