@samitouri / QOS-React / commits / edacbde73f

[compiler][hir-rewrite] Check mutability of base identifier when hoisting (#31032)

Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at bottom): * #31066 * __->__ #31032 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)

mofeiZ committed Oct 3, 2024 at 13:55 UTC edacbde73f50cc9dc00819d61275cd43f12665c1
8 files changed +250 -71
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+48 -61
@@ -15,7 +15,7 @@ import {
15 HIRFunction,
16 Identifier,
17 IdentifierId,
18 - InstructionId,
18 + InstructionValue,
19 ReactiveScopeDependency,
20 ScopeId,
21 } from './HIR';
@@ -209,33 +209,23 @@ class PropertyPathRegistry {
209 }
210 }
211
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;
227 }
228 + return path != null ? registry.getOrCreateProperty(path) : null;
229 }
230
231 function collectNonNullsInBlocks(
@@ -286,41 +276,38 @@ function collectNonNullsInBlocks(
276 );
277 }
278 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,
305 );
306 + if (
307 + !isMutableAtInstr ||
308 + knownImmutableIdentifiers.has(baseIdentifier.id)
309 + ) {
310 + assumedNonNullObjects.add(maybeNonNull);
311 }
312 }
313 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.expect.md new
+65
@@ -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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.ts new
+22
@@ -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 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.expect.md new
+79
@@ -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"]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.ts new
+23
@@ -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
+6 -5
@@ -32,9 +32,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
32 const { throwInput } = require("shared-runtime");
33
34 function Component(props) {
35 - const $ = _c(2);
35 + const $ = _c(3);
36 let x;
37 - if ($[0] !== props) {
37 + if ($[0] !== props.y || $[1] !== props.e) {
38 try {
39 const y = [];
40 y.push(props.y);
@@ -44,10 +44,11 @@ function Component(props) {
44 e.push(props.e);
45 x = e;
46 }
47 - $[0] = props;
48 - $[1] = x;
47 + $[0] = props.y;
48 + $[1] = props.e;
49 + $[2] = x;
50 } else {
50 - x = $[1];
51 + x = $[2];
52 }
53 return x;
54 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md
+6 -5
@@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
31 const { throwInput } = require("shared-runtime");
32
33 function Component(props) {
34 - const $ = _c(2);
34 + const $ = _c(3);
35 let t0;
36 - if ($[0] !== props) {
36 + if ($[0] !== props.y || $[1] !== props.e) {
37 t0 = Symbol.for("react.early_return_sentinel");
38 bb0: {
39 try {
@@ -47,10 +47,11 @@ function Component(props) {
47 break bb0;
48 }
49 }
50 - $[0] = props;
51 - $[1] = t0;
50 + $[0] = props.y;
51 + $[1] = props.e;
52 + $[2] = t0;
53 } else {
53 - t0 = $[1];
54 + t0 = $[2];
55 }
56 if (t0 !== Symbol.for("react.early_return_sentinel")) {
57 return t0;
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -478,6 +478,7 @@ const skipFilter = new Set([
478 'fbt/bug-fbt-plural-multiple-function-calls',
479 'fbt/bug-fbt-plural-multiple-mixed-call-tag',
480 'bug-invalid-hoisting-functionexpr',
481 + 'bug-try-catch-maybe-null-dependency',
482 'reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond',
483 'original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block',
484 'original-reactive-scopes-fork/bug-hoisted-declaration-with-scope',