Skip to content

Commit 88341cf

Browse files
committed
[compiler][hir-rewrite] Check mutability of base identifier when hoisting
Prior to this PR, we check whether the property load source (e.g. the evaluation of `<base>` in `<base>.property`) is mutable + scoped to determine whether the property load itself is eligible for hoisting. This changes to check the base identifier of the load. - This is needed for the next PR #31066. We want to evaluate whether the base identifier is mutable within the context of the *outermost function*. This is because all LoadLocals and PropertyLoads within a nested function declaration have mutable-ranges within the context of the function, but the base identifier is a context variable. - A side effect is that we no longer infer loads from props / other function arguments as mutable in edge cases (e.g. props escaping out of try-blocks or being assigned to context variables) ghstack-source-id: 83d20e1 Pull Request resolved: facebook/react#31032
1 parent 0751fac commit 88341cf

8 files changed

+250
-71
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts

Lines changed: 48 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
HIRFunction,
1616
Identifier,
1717
IdentifierId,
18-
InstructionId,
18+
InstructionValue,
1919
ReactiveScopeDependency,
2020
ScopeId,
2121
} from './HIR';
@@ -209,33 +209,23 @@ class PropertyPathRegistry {
209209
}
210210
}
211211

212-
function addNonNullPropertyPath(
213-
source: Identifier,
214-
sourceNode: PropertyPathNode,
215-
instrId: InstructionId,
216-
knownImmutableIdentifiers: Set<IdentifierId>,
217-
result: Set<PropertyPathNode>,
218-
): void {
219-
/**
220-
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
221-
* are not valid with respect to current instruction id numbering.
222-
* We use attached reactive scope ranges as a proxy for mutable range, but this
223-
* is an overestimate as (1) scope ranges merge and align to form valid program
224-
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
225-
* non-mutable identifiers.
226-
*
227-
* See comment at top of function for why we track known immutable identifiers.
228-
*/
229-
const isMutableAtInstr =
230-
source.mutableRange.end > source.mutableRange.start + 1 &&
231-
source.scope != null &&
232-
inRange({id: instrId}, source.scope.range);
233-
if (
234-
!isMutableAtInstr ||
235-
knownImmutableIdentifiers.has(sourceNode.fullPath.identifier.id)
236-
) {
237-
result.add(sourceNode);
212+
function getMaybeNonNullInInstruction(
213+
instr: InstructionValue,
214+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
215+
registry: PropertyPathRegistry,
216+
): PropertyPathNode | null {
217+
let path = null;
218+
if (instr.kind === 'PropertyLoad') {
219+
path = temporaries.get(instr.object.identifier.id) ?? {
220+
identifier: instr.object.identifier,
221+
path: [],
222+
};
223+
} else if (instr.kind === 'Destructure') {
224+
path = temporaries.get(instr.value.identifier.id) ?? null;
225+
} else if (instr.kind === 'ComputedLoad') {
226+
path = temporaries.get(instr.object.identifier.id) ?? null;
238227
}
228+
return path != null ? registry.getOrCreateProperty(path) : null;
239229
}
240230

241231
function collectNonNullsInBlocks(
@@ -286,41 +276,38 @@ function collectNonNullsInBlocks(
286276
);
287277
}
288278
for (const instr of block.instructions) {
289-
if (instr.value.kind === 'PropertyLoad') {
290-
const source = temporaries.get(instr.value.object.identifier.id) ?? {
291-
identifier: instr.value.object.identifier,
292-
path: [],
293-
};
294-
addNonNullPropertyPath(
295-
instr.value.object.identifier,
296-
registry.getOrCreateProperty(source),
297-
instr.id,
298-
knownImmutableIdentifiers,
299-
assumedNonNullObjects,
300-
);
301-
} else if (instr.value.kind === 'Destructure') {
302-
const source = instr.value.value.identifier.id;
303-
const sourceNode = temporaries.get(source);
304-
if (sourceNode != null) {
305-
addNonNullPropertyPath(
306-
instr.value.value.identifier,
307-
registry.getOrCreateProperty(sourceNode),
308-
instr.id,
309-
knownImmutableIdentifiers,
310-
assumedNonNullObjects,
311-
);
312-
}
313-
} else if (instr.value.kind === 'ComputedLoad') {
314-
const source = instr.value.object.identifier.id;
315-
const sourceNode = temporaries.get(source);
316-
if (sourceNode != null) {
317-
addNonNullPropertyPath(
318-
instr.value.object.identifier,
319-
registry.getOrCreateProperty(sourceNode),
320-
instr.id,
321-
knownImmutableIdentifiers,
322-
assumedNonNullObjects,
279+
const maybeNonNull = getMaybeNonNullInInstruction(
280+
instr.value,
281+
temporaries,
282+
registry,
283+
);
284+
if (maybeNonNull != null) {
285+
const baseIdentifier = maybeNonNull.fullPath.identifier;
286+
/**
287+
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
288+
* are not valid with respect to current instruction id numbering.
289+
* We use attached reactive scope ranges as a proxy for mutable range, but this
290+
* is an overestimate as (1) scope ranges merge and align to form valid program
291+
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
292+
* non-mutable identifiers.
293+
*
294+
* See comment at top of function for why we track known immutable identifiers.
295+
*/
296+
const isMutableAtInstr =
297+
baseIdentifier.mutableRange.end >
298+
baseIdentifier.mutableRange.start + 1 &&
299+
baseIdentifier.scope != null &&
300+
inRange(
301+
{
302+
id: instr.id,
303+
},
304+
baseIdentifier.scope.range,
323305
);
306+
if (
307+
!isMutableAtInstr ||
308+
knownImmutableIdentifiers.has(baseIdentifier.id)
309+
) {
310+
assumedNonNullObjects.add(maybeNonNull);
324311
}
325312
}
326313
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {identity} from 'shared-runtime';
6+
7+
/**
8+
* Not safe to hoist read of maybeNullObject.value.inner outside of the
9+
* try-catch block, as that might throw
10+
*/
11+
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
12+
const y = [];
13+
try {
14+
y.push(identity(maybeNullObject.value.inner));
15+
} catch {
16+
y.push('null');
17+
}
18+
19+
return y;
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: useFoo,
24+
params: [null],
25+
sequentialRenders: [null, {value: 2}, {value: 3}, null],
26+
};
27+
28+
```
29+
30+
## Code
31+
32+
```javascript
33+
import { c as _c } from "react/compiler-runtime";
34+
import { identity } from "shared-runtime";
35+
36+
/**
37+
* Not safe to hoist read of maybeNullObject.value.inner outside of the
38+
* try-catch block, as that might throw
39+
*/
40+
function useFoo(maybeNullObject) {
41+
const $ = _c(2);
42+
let y;
43+
if ($[0] !== maybeNullObject.value.inner) {
44+
y = [];
45+
try {
46+
y.push(identity(maybeNullObject.value.inner));
47+
} catch {
48+
y.push("null");
49+
}
50+
$[0] = maybeNullObject.value.inner;
51+
$[1] = y;
52+
} else {
53+
y = $[1];
54+
}
55+
return y;
56+
}
57+
58+
export const FIXTURE_ENTRYPOINT = {
59+
fn: useFoo,
60+
params: [null],
61+
sequentialRenders: [null, { value: 2 }, { value: 3 }, null],
62+
};
63+
64+
```
65+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {identity} from 'shared-runtime';
2+
3+
/**
4+
* Not safe to hoist read of maybeNullObject.value.inner outside of the
5+
* try-catch block, as that might throw
6+
*/
7+
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
8+
const y = [];
9+
try {
10+
y.push(identity(maybeNullObject.value.inner));
11+
} catch {
12+
y.push('null');
13+
}
14+
15+
return y;
16+
}
17+
18+
export const FIXTURE_ENTRYPOINT = {
19+
fn: useFoo,
20+
params: [null],
21+
sequentialRenders: [null, {value: 2}, {value: 3}, null],
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePropagateDepsInHIR
6+
import {identity} from 'shared-runtime';
7+
8+
/**
9+
* Not safe to hoist read of maybeNullObject.value.inner outside of the
10+
* try-catch block, as that might throw
11+
*/
12+
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
13+
const y = [];
14+
try {
15+
y.push(identity(maybeNullObject.value.inner));
16+
} catch {
17+
y.push('null');
18+
}
19+
20+
return y;
21+
}
22+
23+
export const FIXTURE_ENTRYPOINT = {
24+
fn: useFoo,
25+
params: [null],
26+
sequentialRenders: [null, {value: 2}, {value: 3}, null],
27+
};
28+
29+
```
30+
31+
## Code
32+
33+
```javascript
34+
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
35+
import { identity } from "shared-runtime";
36+
37+
/**
38+
* Not safe to hoist read of maybeNullObject.value.inner outside of the
39+
* try-catch block, as that might throw
40+
*/
41+
function useFoo(maybeNullObject) {
42+
const $ = _c(4);
43+
let y;
44+
if ($[0] !== maybeNullObject) {
45+
y = [];
46+
try {
47+
let t0;
48+
if ($[2] !== maybeNullObject.value.inner) {
49+
t0 = identity(maybeNullObject.value.inner);
50+
$[2] = maybeNullObject.value.inner;
51+
$[3] = t0;
52+
} else {
53+
t0 = $[3];
54+
}
55+
y.push(t0);
56+
} catch {
57+
y.push("null");
58+
}
59+
$[0] = maybeNullObject;
60+
$[1] = y;
61+
} else {
62+
y = $[1];
63+
}
64+
return y;
65+
}
66+
67+
export const FIXTURE_ENTRYPOINT = {
68+
fn: useFoo,
69+
params: [null],
70+
sequentialRenders: [null, { value: 2 }, { value: 3 }, null],
71+
};
72+
73+
```
74+
75+
### Eval output
76+
(kind: ok) ["null"]
77+
[null]
78+
[null]
79+
["null"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// @enablePropagateDepsInHIR
2+
import {identity} from 'shared-runtime';
3+
4+
/**
5+
* Not safe to hoist read of maybeNullObject.value.inner outside of the
6+
* try-catch block, as that might throw
7+
*/
8+
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
9+
const y = [];
10+
try {
11+
y.push(identity(maybeNullObject.value.inner));
12+
} catch {
13+
y.push('null');
14+
}
15+
16+
return y;
17+
}
18+
19+
export const FIXTURE_ENTRYPOINT = {
20+
fn: useFoo,
21+
params: [null],
22+
sequentialRenders: [null, {value: 2}, {value: 3}, null],
23+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
3232
const { throwInput } = require("shared-runtime");
3333

3434
function Component(props) {
35-
const $ = _c(2);
35+
const $ = _c(3);
3636
let x;
37-
if ($[0] !== props) {
37+
if ($[0] !== props.y || $[1] !== props.e) {
3838
try {
3939
const y = [];
4040
y.push(props.y);
@@ -44,10 +44,11 @@ function Component(props) {
4444
e.push(props.e);
4545
x = e;
4646
}
47-
$[0] = props;
48-
$[1] = x;
47+
$[0] = props.y;
48+
$[1] = props.e;
49+
$[2] = x;
4950
} else {
50-
x = $[1];
51+
x = $[2];
5152
}
5253
return x;
5354
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
3131
const { throwInput } = require("shared-runtime");
3232

3333
function Component(props) {
34-
const $ = _c(2);
34+
const $ = _c(3);
3535
let t0;
36-
if ($[0] !== props) {
36+
if ($[0] !== props.y || $[1] !== props.e) {
3737
t0 = Symbol.for("react.early_return_sentinel");
3838
bb0: {
3939
try {
@@ -47,10 +47,11 @@ function Component(props) {
4747
break bb0;
4848
}
4949
}
50-
$[0] = props;
51-
$[1] = t0;
50+
$[0] = props.y;
51+
$[1] = props.e;
52+
$[2] = t0;
5253
} else {
53-
t0 = $[1];
54+
t0 = $[2];
5455
}
5556
if (t0 !== Symbol.for("react.early_return_sentinel")) {
5657
return t0;

compiler/packages/snap/src/SproutTodoFilter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ const skipFilter = new Set([
478478
'fbt/bug-fbt-plural-multiple-function-calls',
479479
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
480480
'bug-invalid-hoisting-functionexpr',
481+
'bug-try-catch-maybe-null-dependency',
481482
'reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond',
482483
'original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block',
483484
'original-reactive-scopes-fork/bug-hoisted-declaration-with-scope',

0 commit comments

Comments
 (0)