Skip to content

Commit 81b76c7

Browse files
committed
[compiler][newinference] ensure fixpoint converges for loops w backedges
The fixpoint converges based on the abstract values being equal, but we synthesize new values cached based on the effects and new effects can be synthesized on the fly. So here we intern the effect objects by "value" (cache key computed from the value) to ensure that effects are stable, values cached based on them are stable, and that the fixpoint can converge. ghstack-source-id: 9ca53d7 Pull Request resolved: #33449
1 parent 4b5029c commit 81b76c7

File tree

4 files changed

+256
-21
lines changed

4 files changed

+256
-21
lines changed

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

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ export function inferMutationAliasingEffects(
189189
}
190190

191191
class Context {
192+
internedEffects: Map<string, AliasingEffect> = new Map();
192193
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
193194
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
194195
new Map();
@@ -200,6 +201,17 @@ class Context {
200201
this.isFuctionExpression = isFunctionExpression;
201202
this.fn = fn;
202203
}
204+
205+
internEffect(effect: AliasingEffect): AliasingEffect {
206+
const hash = hashEffect(effect);
207+
let interned = this.internedEffects.get(hash);
208+
if (interned == null) {
209+
console.log(`intern: ${hash}`);
210+
this.internedEffects.set(hash, effect);
211+
interned = effect;
212+
}
213+
return interned;
214+
}
203215
}
204216

205217
function inferParam(
@@ -388,10 +400,11 @@ function applySignature(
388400
function applyEffect(
389401
context: Context,
390402
state: InferenceState,
391-
effect: AliasingEffect,
403+
_effect: AliasingEffect,
392404
aliased: Set<IdentifierId>,
393405
effects: Array<AliasingEffect>,
394406
): void {
407+
const effect = context.internEffect(_effect);
395408
if (DEBUG) {
396409
console.log(printAliasingEffect(effect));
397410
}
@@ -465,15 +478,12 @@ function applyEffect(
465478
break;
466479
}
467480
default: {
468-
// Even if the source is non-primitive, the destination may be. skip primitives.
469-
if (!isPrimitiveType(effect.into.identifier)) {
470-
effects.push({
471-
// OK: recording information flow
472-
kind: 'CreateFrom', // prev Alias
473-
from: effect.from,
474-
into: effect.into,
475-
});
476-
}
481+
effects.push({
482+
// OK: recording information flow
483+
kind: 'CreateFrom', // prev Alias
484+
from: effect.from,
485+
into: effect.into,
486+
});
477487
}
478488
}
479489
break;
@@ -1363,11 +1373,19 @@ function computeSignatureForInstruction(
13631373
}
13641374
case 'PropertyLoad':
13651375
case 'ComputedLoad': {
1366-
effects.push({
1367-
kind: 'CreateFrom',
1368-
from: value.object,
1369-
into: lvalue,
1370-
});
1376+
if (isPrimitiveType(lvalue.identifier)) {
1377+
effects.push({
1378+
kind: 'Create',
1379+
into: lvalue,
1380+
value: ValueKind.Primitive,
1381+
});
1382+
} else {
1383+
effects.push({
1384+
kind: 'CreateFrom',
1385+
from: value.object,
1386+
into: lvalue,
1387+
});
1388+
}
13711389
break;
13721390
}
13731391
case 'PropertyStore':
@@ -1378,8 +1396,11 @@ function computeSignatureForInstruction(
13781396
from: value.value,
13791397
into: value.object,
13801398
});
1381-
// OK: lvalues are assigned to
1382-
effects.push({kind: 'Assign', from: value.value, into: lvalue});
1399+
effects.push({
1400+
kind: 'Create',
1401+
into: lvalue,
1402+
value: ValueKind.Primitive,
1403+
});
13831404
break;
13841405
}
13851406
case 'ObjectMethod':
@@ -2207,7 +2228,23 @@ export type AliasedPlace = {place: Place; kind: 'alias' | 'capture'};
22072228

22082229
export type AliasingEffect =
22092230
/**
2210-
* Marks the given value, its aliases, and indirect captures, as frozen.
2231+
* Marks the given value and its direct aliases as frozen.
2232+
*
2233+
* Captured values are *not* considered frozen, because we cannot be sure that a previously
2234+
* captured value will still be captured at the point of the freeze.
2235+
*
2236+
* For example:
2237+
* const x = {};
2238+
* const y = [x];
2239+
* y.pop(); // y dosn't contain x anymore!
2240+
* freeze(y);
2241+
* mutate(x); // safe to mutate!
2242+
*
2243+
* The exception to this is FunctionExpressions - since it is impossible to change which
2244+
* value a function closes over[1] we can transitively freeze functions and their captures.
2245+
*
2246+
* [1] Except for `let` values that are reassigned and closed over by a function, but we
2247+
* handle this explicitly with StoreContext/LoadContext.
22112248
*/
22122249
| {kind: 'Freeze'; value: Place; reason: ValueReason}
22132250
/**
@@ -2305,6 +2342,67 @@ export type AliasingEffect =
23052342
error: CompilerErrorDetailOptions;
23062343
};
23072344

2345+
function hashEffect(effect: AliasingEffect): string {
2346+
switch (effect.kind) {
2347+
case 'Apply': {
2348+
return [
2349+
effect.kind,
2350+
effect.receiver.identifier.id,
2351+
effect.function.identifier.id,
2352+
effect.mutatesFunction,
2353+
effect.args
2354+
.map(a => {
2355+
if (a.kind === 'Hole') {
2356+
return '';
2357+
} else if (a.kind === 'Identifier') {
2358+
return a.identifier.id;
2359+
} else {
2360+
return `...${a.place.identifier.id}`;
2361+
}
2362+
})
2363+
.join(','),
2364+
effect.into.identifier.id,
2365+
].join(':');
2366+
}
2367+
case 'CreateFrom':
2368+
case 'ImmutableCapture':
2369+
case 'Assign':
2370+
case 'Alias':
2371+
case 'Capture': {
2372+
return [
2373+
effect.kind,
2374+
effect.from.identifier.id,
2375+
effect.into.identifier.id,
2376+
].join(':');
2377+
}
2378+
case 'Create': {
2379+
return [effect.kind, effect.into.identifier.id].join(':');
2380+
}
2381+
case 'Freeze': {
2382+
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
2383+
}
2384+
case 'MutateFrozen':
2385+
case 'MutateGlobal': {
2386+
return [effect.kind, effect.place.identifier.id].join(':');
2387+
}
2388+
case 'Mutate':
2389+
case 'MutateConditionally':
2390+
case 'MutateTransitive':
2391+
case 'MutateTransitiveConditionally': {
2392+
return [effect.kind, effect.value.identifier.id].join(':');
2393+
}
2394+
case 'CreateFunction': {
2395+
return [
2396+
effect.kind,
2397+
effect.into.identifier.id,
2398+
// return places are a unique way to identify functions themselves
2399+
effect.function.loweredFunc.func.returns.identifier.id,
2400+
effect.captures.map(p => p.identifier.id).join(','),
2401+
].join(':');
2402+
}
2403+
}
2404+
}
2405+
23082406
export type AliasingSignatureEffect = AliasingEffect;
23092407

23102408
export type AliasingSignature = {

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,6 @@ export function inferMutationAliasingFunctionEffects(
160160
});
161161
}
162162

163-
// console.log(pretty(tracked));
164-
// console.log(pretty(dataFlow));
165-
// console.log('FunctionEffects', effects.map(printAliasingEffect).join('\n'));
166163
return effects;
167164
}
168165

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableNewMutationAliasingModel
6+
import {arrayPush, Stringify} from 'shared-runtime';
7+
8+
function Component({prop1, prop2}) {
9+
'use memo';
10+
11+
// we'll ultimately extract the item from this array as z, and mutate later
12+
let x = [{value: prop1}];
13+
let z;
14+
while (x.length < 2) {
15+
// there's a phi here for x (value before the loop and the reassignment later)
16+
17+
// this mutation occurs before the reassigned value
18+
arrayPush(x, {value: prop2});
19+
20+
// this condition will never be true, so x doesn't get reassigned
21+
if (x[0].value === null) {
22+
x = [{value: prop2}];
23+
const y = x;
24+
z = y[0];
25+
}
26+
}
27+
// the code is set up so that z will always be the value from the original x
28+
z.other = true;
29+
return <Stringify z={z} />;
30+
}
31+
32+
export const FIXTURE_ENTRYPOINT = {
33+
fn: Component,
34+
params: [{prop1: 0, prop2: 0}],
35+
sequentialRenders: [
36+
{prop1: 0, prop2: 0},
37+
{prop1: 1, prop2: 0},
38+
{prop1: 1, prop2: 1},
39+
{prop1: 0, prop2: 1},
40+
{prop1: 0, prop2: 0},
41+
],
42+
};
43+
44+
```
45+
46+
## Code
47+
48+
```javascript
49+
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
50+
import { arrayPush, Stringify } from "shared-runtime";
51+
52+
function Component(t0) {
53+
"use memo";
54+
const $ = _c(5);
55+
const { prop1, prop2 } = t0;
56+
let z;
57+
if ($[0] !== prop1 || $[1] !== prop2) {
58+
let x = [{ value: prop1 }];
59+
while (x.length < 2) {
60+
arrayPush(x, { value: prop2 });
61+
if (x[0].value === null) {
62+
x = [{ value: prop2 }];
63+
const y = x;
64+
z = y[0];
65+
}
66+
}
67+
68+
z.other = true;
69+
$[0] = prop1;
70+
$[1] = prop2;
71+
$[2] = z;
72+
} else {
73+
z = $[2];
74+
}
75+
let t1;
76+
if ($[3] !== z) {
77+
t1 = <Stringify z={z} />;
78+
$[3] = z;
79+
$[4] = t1;
80+
} else {
81+
t1 = $[4];
82+
}
83+
return t1;
84+
}
85+
86+
export const FIXTURE_ENTRYPOINT = {
87+
fn: Component,
88+
params: [{ prop1: 0, prop2: 0 }],
89+
sequentialRenders: [
90+
{ prop1: 0, prop2: 0 },
91+
{ prop1: 1, prop2: 0 },
92+
{ prop1: 1, prop2: 1 },
93+
{ prop1: 0, prop2: 1 },
94+
{ prop1: 0, prop2: 0 },
95+
],
96+
};
97+
98+
```
99+
100+
### Eval output
101+
(kind: ok) [[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
102+
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
103+
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
104+
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
105+
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// @enableNewMutationAliasingModel
2+
import {arrayPush, Stringify} from 'shared-runtime';
3+
4+
function Component({prop1, prop2}) {
5+
'use memo';
6+
7+
let x = [{value: prop1}];
8+
let z;
9+
while (x.length < 2) {
10+
// there's a phi here for x (value before the loop and the reassignment later)
11+
12+
// this mutation occurs before the reassigned value
13+
arrayPush(x, {value: prop2});
14+
15+
if (x[0].value === prop1) {
16+
x = [{value: prop2}];
17+
const y = x;
18+
z = y[0];
19+
}
20+
}
21+
z.other = true;
22+
return <Stringify z={z} />;
23+
}
24+
25+
// export const FIXTURE_ENTRYPOINT = {
26+
// fn: Component,
27+
// params: [{prop1: 0, prop2: 0}],
28+
// sequentialRenders: [
29+
// {prop1: 0, prop2: 0},
30+
// {prop1: 1, prop2: 0},
31+
// {prop1: 1, prop2: 1},
32+
// {prop1: 0, prop2: 1},
33+
// {prop1: 0, prop2: 0},
34+
// ],
35+
// };

0 commit comments

Comments
 (0)