Skip to content

Commit 1e6fc45

Browse files
committed
[compiler] Move co-mutation range extension to InferMutableRanges
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't. The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root. ghstack-source-id: f918efa Pull Request resolved: #33157
1 parent 67f2808 commit 1e6fc45

File tree

10 files changed

+219
-216
lines changed

10 files changed

+219
-216
lines changed

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ import {
2525
eachInstructionLValue,
2626
eachInstructionValueOperand,
2727
} from '../HIR/visitors';
28-
import prettyFormat from 'pretty-format';
29-
import {printIdentifier} from '../HIR/PrintHIR';
3028
import {Iterable_some} from '../Utils/utils';
3129

3230
export default function analyseFunctions(func: HIRFunction): void {
@@ -69,16 +67,6 @@ function lower(func: HIRFunction): DisjointSet<Identifier> {
6967
return aliases;
7068
}
7169

72-
export function debugAliases(aliases: DisjointSet<Identifier>): void {
73-
console.log(
74-
prettyFormat(
75-
aliases
76-
.buildSets()
77-
.map(set => [...set].map(ident => printIdentifier(ident))),
78-
),
79-
);
80-
}
81-
8270
/**
8371
* The alias sets returned by InferMutableRanges() accounts only for aliases that
8472
* are known to mutate together. Notably this skips cases where a value is captured

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

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {inferAliasForPhis} from './InferAliasForPhis';
1414
import {inferAliasForStores} from './InferAliasForStores';
1515
import {inferMutableLifetimes} from './InferMutableLifetimes';
1616
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
17+
import {inferMutableRangesForComutation} from './InferMutableRangesForComutation';
1718
import {inferTryCatchAliases} from './InferTryCatchAliases';
1819

1920
export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
@@ -32,11 +33,13 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
3233
* Eagerly canonicalize so that if nothing changes we can bail out
3334
* after a single iteration
3435
*/
35-
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
36+
let prevAliases: Map<Identifier, string> = canonicalize(aliases);
3637
while (true) {
3738
// Infer mutable ranges for aliases that are not fields
3839
inferMutableRangesForAlias(ir, aliases);
3940

41+
inferMutableRangesForComutation(ir);
42+
4043
// Update aliasing information of fields
4144
inferAliasForStores(ir, aliases);
4245

@@ -45,7 +48,7 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
4548
// Update aliasing information of phis
4649
inferAliasForPhis(ir, aliases);
4750

48-
const nextAliases = aliases.canonicalize();
51+
const nextAliases = canonicalize(aliases);
4952
if (areEqualMaps(prevAliases, nextAliases)) {
5053
break;
5154
}
@@ -77,12 +80,13 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
7780
* but does not modify values that `y` "contains" such as the
7881
* object literal or `z`.
7982
*/
80-
prevAliases = aliases.canonicalize();
83+
prevAliases = canonicalize(aliases);
8184
while (true) {
8285
inferMutableRangesForAlias(ir, aliases);
86+
inferMutableRangesForComutation(ir);
8387
inferAliasForPhis(ir, aliases);
8488
inferAliasForUncalledFunctions(ir, aliases);
85-
const nextAliases = aliases.canonicalize();
89+
const nextAliases = canonicalize(aliases);
8690
if (areEqualMaps(prevAliases, nextAliases)) {
8791
break;
8892
}
@@ -92,7 +96,27 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
9296
return aliases;
9397
}
9498

95-
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
99+
/**
100+
* Canonicalizes the alias set and mutable range information calculated at the current time.
101+
* The returned value maps each identifier in the program to the root identifier of its alias
102+
* set and the the mutable range of that set.
103+
*
104+
* This ensures that we fixpoint the mutable ranges themselves and not just the alias sets.
105+
*/
106+
function canonicalize(
107+
aliases: DisjointSet<Identifier>,
108+
): Map<Identifier, string> {
109+
const entries = new Map<Identifier, string>();
110+
aliases.forEach((item, root) => {
111+
entries.set(
112+
item,
113+
`${root.id}:${root.mutableRange.start}:${root.mutableRange.end}`,
114+
);
115+
});
116+
return entries;
117+
}
118+
119+
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
96120
if (a.size !== b.size) {
97121
return false;
98122
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {
9+
HIRFunction,
10+
Identifier,
11+
isRefOrRefValue,
12+
makeInstructionId,
13+
} from '../HIR';
14+
import {eachInstructionOperand} from '../HIR/visitors';
15+
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
16+
17+
/**
18+
* Finds instructions with operands that co-mutate and extends all their mutable ranges
19+
* to end at the same point (the highest `end` value of the group). Note that the
20+
* alias sets used in InferMutableRanges are meant for values that strictly alias:
21+
* a mutation of one value in the set would directly modify the same object as some
22+
* other value in the set.
23+
*
24+
* However, co-mutation can cause an alias to one object to be stored within another object,
25+
* for example:
26+
*
27+
* ```
28+
* const a = {};
29+
* const b = {};
30+
* const f = () => b.c; //
31+
* setProperty(a, 'b', b); // equiv to a.b = b
32+
*
33+
* a.b.c = 'c'; // this mutates b!
34+
* ```
35+
*
36+
* Here, the co-mutation in `setProperty(a, 'b', b)` means that a reference to b may be stored
37+
* in a, vice-versa, or both. We need to extend the mutable range of both a and b to reflect
38+
* the fact the values may mutate together.
39+
*
40+
* Previously this was implemented in InferReactiveScopeVariables, but that is too late:
41+
* we need this to be part of the InferMutableRanges fixpoint iteration to account for functions
42+
* like `f` in the example, which capture a reference to a value that may change later. `f`
43+
* cannot be independently memoized from the `setProperty()` call due to the co-mutation.
44+
*
45+
* See aliased-capture-mutate and aliased-capture-aliased-mutate fixtures for examples.
46+
*/
47+
export function inferMutableRangesForComutation(fn: HIRFunction): void {
48+
for (const block of fn.body.blocks.values()) {
49+
for (const instr of block.instructions) {
50+
let operands: Array<Identifier> | null = null;
51+
for (const operand of eachInstructionOperand(instr)) {
52+
if (
53+
isMutable(instr, operand) &&
54+
operand.identifier.mutableRange.start > 0
55+
) {
56+
if (
57+
instr.value.kind === 'FunctionExpression' ||
58+
instr.value.kind === 'ObjectMethod'
59+
) {
60+
if (operand.identifier.type.kind === 'Primitive') {
61+
continue;
62+
}
63+
}
64+
operands ??= [];
65+
operands.push(operand.identifier);
66+
}
67+
}
68+
if (operands != null) {
69+
// Find the last instruction which mutates any of the mutable operands
70+
let lastMutatingInstructionId = makeInstructionId(0);
71+
for (const id of operands) {
72+
if (id.mutableRange.end > lastMutatingInstructionId) {
73+
lastMutatingInstructionId = id.mutableRange.end;
74+
}
75+
}
76+
77+
/**
78+
* Update all mutable operands's mutable ranges to end at the same point
79+
*/
80+
for (const id of operands) {
81+
if (
82+
id.mutableRange.end < lastMutatingInstructionId &&
83+
!isRefOrRefValue(id)
84+
) {
85+
id.mutableRange.end = lastMutatingInstructionId;
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @flow @enableTransitivelyFreezeFunctionExpressions:false
6+
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
7+
8+
function useFoo({a, b}: {a: number, b: number}) {
9+
const x = [];
10+
const y = {value: a};
11+
12+
arrayPush(x, y); // x and y co-mutate
13+
const y_alias = y;
14+
const cb = () => y_alias.value;
15+
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
16+
return <Stringify cb={cb} shouldInvokeFns={true} />;
17+
}
18+
19+
export const FIXTURE_ENTRYPOINT = {
20+
fn: useFoo,
21+
params: [{a: 2, b: 10}],
22+
sequentialRenders: [
23+
{a: 2, b: 10},
24+
{a: 2, b: 11},
25+
],
26+
};
27+
28+
```
29+
30+
## Code
31+
32+
```javascript
33+
import { c as _c } from "react/compiler-runtime";
34+
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
35+
36+
function useFoo(t0) {
37+
const $ = _c(3);
38+
const { a, b } = t0;
39+
let t1;
40+
if ($[0] !== a || $[1] !== b) {
41+
const x = [];
42+
const y = { value: a };
43+
44+
arrayPush(x, y);
45+
const y_alias = y;
46+
const cb = () => y_alias.value;
47+
setPropertyByKey(x[0], "value", b);
48+
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
49+
$[0] = a;
50+
$[1] = b;
51+
$[2] = t1;
52+
} else {
53+
t1 = $[2];
54+
}
55+
return t1;
56+
}
57+
58+
export const FIXTURE_ENTRYPOINT = {
59+
fn: useFoo,
60+
params: [{ a: 2, b: 10 }],
61+
sequentialRenders: [
62+
{ a: 2, b: 10 },
63+
{ a: 2, b: 11 },
64+
],
65+
};
66+
67+
```
68+
69+
### Eval output
70+
(kind: ok) <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
71+
<div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// @flow @enableTransitivelyFreezeFunctionExpressions:false
2+
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
3+
4+
function useFoo({a, b}: {a: number, b: number}) {
5+
const x = [];
6+
const y = {value: a};
7+
8+
arrayPush(x, y); // x and y co-mutate
9+
const y_alias = y;
10+
const cb = () => y_alias.value;
11+
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
12+
return <Stringify cb={cb} shouldInvokeFns={true} />;
13+
}
14+
15+
export const FIXTURE_ENTRYPOINT = {
16+
fn: useFoo,
17+
params: [{a: 2, b: 10}],
18+
sequentialRenders: [
19+
{a: 2, b: 10},
20+
{a: 2, b: 11},
21+
],
22+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md renamed to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-capture-mutate.expect.md

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,6 @@
55
// @flow @enableTransitivelyFreezeFunctionExpressions:false
66
import {setPropertyByKey, Stringify} from 'shared-runtime';
77

8-
/**
9-
* Variation of bug in `bug-aliased-capture-aliased-mutate`
10-
* Found differences in evaluator results
11-
* Non-forget (expected):
12-
* (kind: ok)
13-
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
14-
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
15-
* Forget:
16-
* (kind: ok)
17-
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
18-
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
19-
*/
20-
218
function useFoo({a}: {a: number, b: number}) {
229
const arr = [];
2310
const obj = {value: a};
@@ -46,7 +33,7 @@ import { c as _c } from "react/compiler-runtime";
4633
import { setPropertyByKey, Stringify } from "shared-runtime";
4734

4835
function useFoo(t0) {
49-
const $ = _c(4);
36+
const $ = _c(2);
5037
const { a } = t0;
5138
let t1;
5239
if ($[0] !== a) {
@@ -55,15 +42,7 @@ function useFoo(t0) {
5542

5643
setPropertyByKey(obj, "arr", arr);
5744
const obj_alias = obj;
58-
let t2;
59-
if ($[2] !== obj_alias.arr.length) {
60-
t2 = () => obj_alias.arr.length;
61-
$[2] = obj_alias.arr.length;
62-
$[3] = t2;
63-
} else {
64-
t2 = $[3];
65-
}
66-
const cb = t2;
45+
const cb = () => obj_alias.arr.length;
6746
for (let i = 0; i < a; i++) {
6847
arr.push(i);
6948
}
@@ -84,4 +63,7 @@ export const FIXTURE_ENTRYPOINT = {
8463
};
8564

8665
```
87-
66+
67+
### Eval output
68+
(kind: ok) <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
69+
<div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js renamed to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-capture-mutate.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
// @flow @enableTransitivelyFreezeFunctionExpressions:false
22
import {setPropertyByKey, Stringify} from 'shared-runtime';
33

4-
/**
5-
* Variation of bug in `bug-aliased-capture-aliased-mutate`
6-
* Found differences in evaluator results
7-
* Non-forget (expected):
8-
* (kind: ok)
9-
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
10-
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
11-
* Forget:
12-
* (kind: ok)
13-
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
14-
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
15-
*/
16-
174
function useFoo({a}: {a: number, b: number}) {
185
const arr = [];
196
const obj = {value: a};

0 commit comments

Comments
 (0)