Skip to content

Commit 87d7495

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: c008e0b Pull Request resolved: #33449
1 parent 776e2fc commit 87d7495

File tree

3 files changed

+257
-18
lines changed

3 files changed

+257
-18
lines changed

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

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {FunctionSignature} from '../HIR/ObjectShape';
6363
import {getWriteErrorReason} from './InferFunctionEffects';
6464
import prettyFormat from 'pretty-format';
6565
import {createTemporaryPlace} from '../HIR/HIRBuilder';
66+
import {pretty} from './InferMutationAliasingRanges';
6667

6768
export function inferMutationAliasingEffects(
6869
fn: HIRFunction,
@@ -189,6 +190,7 @@ export function inferMutationAliasingEffects(
189190
}
190191

191192
class Context {
193+
internedEffects: Map<string, AliasingEffect> = new Map();
192194
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
193195
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
194196
new Map();
@@ -200,6 +202,17 @@ class Context {
200202
this.isFuctionExpression = isFunctionExpression;
201203
this.fn = fn;
202204
}
205+
206+
internEffect(effect: AliasingEffect): AliasingEffect {
207+
const hash = hashEffect(effect);
208+
let interned = this.internedEffects.get(hash);
209+
if (interned == null) {
210+
console.log(`intern: ${hash}`);
211+
this.internedEffects.set(hash, effect);
212+
interned = effect;
213+
}
214+
return interned;
215+
}
203216
}
204217

205218
function inferParam(
@@ -388,10 +401,11 @@ function applySignature(
388401
function applyEffect(
389402
context: Context,
390403
state: InferenceState,
391-
effect: AliasingEffect,
404+
_effect: AliasingEffect,
392405
aliased: Set<IdentifierId>,
393406
effects: Array<AliasingEffect>,
394407
): void {
408+
const effect = context.internEffect(_effect);
395409
if (DEBUG) {
396410
console.log(printAliasingEffect(effect));
397411
}
@@ -465,15 +479,12 @@ function applyEffect(
465479
break;
466480
}
467481
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-
}
482+
effects.push({
483+
// OK: recording information flow
484+
kind: 'CreateFrom', // prev Alias
485+
from: effect.from,
486+
into: effect.into,
487+
});
477488
}
478489
}
479490
break;
@@ -1363,11 +1374,19 @@ function computeSignatureForInstruction(
13631374
}
13641375
case 'PropertyLoad':
13651376
case 'ComputedLoad': {
1366-
effects.push({
1367-
kind: 'CreateFrom',
1368-
from: value.object,
1369-
into: lvalue,
1370-
});
1377+
if (isPrimitiveType(lvalue.identifier)) {
1378+
effects.push({
1379+
kind: 'Create',
1380+
into: lvalue,
1381+
value: ValueKind.Primitive,
1382+
});
1383+
} else {
1384+
effects.push({
1385+
kind: 'CreateFrom',
1386+
from: value.object,
1387+
into: lvalue,
1388+
});
1389+
}
13711390
break;
13721391
}
13731392
case 'PropertyStore':
@@ -1378,8 +1397,11 @@ function computeSignatureForInstruction(
13781397
from: value.value,
13791398
into: value.object,
13801399
});
1381-
// OK: lvalues are assigned to
1382-
effects.push({kind: 'Assign', from: value.value, into: lvalue});
1400+
effects.push({
1401+
kind: 'Create',
1402+
into: lvalue,
1403+
value: ValueKind.Primitive,
1404+
});
13831405
break;
13841406
}
13851407
case 'ObjectMethod':
@@ -2207,7 +2229,23 @@ export type AliasedPlace = {place: Place; kind: 'alias' | 'capture'};
22072229

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

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

23102409
export type AliasingSignature = {
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)