Skip to content

Commit a78bbf9

Browse files
authored
[compiler] Context variables as dependencies (facebook#31582)
We previously didn't track context variables in the hoistable values sidemap of `propagateScopeDependencies`. This was overly conservative as we *do* track the mutable range of context variables, and it is safe to hoist accesses to context variables after their last direct / aliased maybe-assignment. ```js function Component({value}) { // start of mutable range for `x` let x = DEFAULT; const setX = () => x = value; const aliasedSet = maybeAlias(setX); maybeCall(aliasedSet); // end of mutable range for `x` // here, we should be able to take x (and property reads // off of x) as dependencies return <Jsx value={x} /> } ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31582). * facebook#31583 * __->__ facebook#31582
1 parent c869063 commit a78bbf9

20 files changed

+447
-194
lines changed

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,11 @@ export type LoadLocal = {
840840
place: Place;
841841
loc: SourceLocation;
842842
};
843+
export type LoadContext = {
844+
kind: 'LoadContext';
845+
place: Place;
846+
loc: SourceLocation;
847+
};
843848

844849
/*
845850
* The value of a given instruction. Note that values are not recursive: complex
@@ -852,11 +857,7 @@ export type LoadLocal = {
852857

853858
export type InstructionValue =
854859
| LoadLocal
855-
| {
856-
kind: 'LoadContext';
857-
place: Place;
858-
loc: SourceLocation;
859-
}
860+
| LoadContext
860861
| {
861862
kind: 'DeclareLocal';
862863
lvalue: LValue;

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

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import {
1717
areEqualPaths,
1818
IdentifierId,
1919
Terminal,
20+
InstructionValue,
21+
LoadContext,
22+
TInstruction,
23+
FunctionExpression,
24+
ObjectMethod,
2025
} from './HIR';
2126
import {
2227
collectHoistablePropertyLoads,
@@ -223,11 +228,25 @@ export function collectTemporariesSidemap(
223228
fn,
224229
usedOutsideDeclaringScope,
225230
temporaries,
226-
false,
231+
null,
227232
);
228233
return temporaries;
229234
}
230235

236+
function isLoadContextMutable(
237+
instrValue: InstructionValue,
238+
id: InstructionId,
239+
): instrValue is LoadContext {
240+
if (instrValue.kind === 'LoadContext') {
241+
CompilerError.invariant(instrValue.place.identifier.scope != null, {
242+
reason:
243+
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
244+
loc: instrValue.loc,
245+
});
246+
return id >= instrValue.place.identifier.scope.range.end;
247+
}
248+
return false;
249+
}
231250
/**
232251
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
233252
* function and all nested functions.
@@ -239,17 +258,21 @@ function collectTemporariesSidemapImpl(
239258
fn: HIRFunction,
240259
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
241260
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
242-
isInnerFn: boolean,
261+
innerFnContext: {instrId: InstructionId} | null,
243262
): void {
244263
for (const [_, block] of fn.body.blocks) {
245-
for (const instr of block.instructions) {
246-
const {value, lvalue} = instr;
264+
for (const {value, lvalue, id: origInstrId} of block.instructions) {
265+
const instrId =
266+
innerFnContext != null ? innerFnContext.instrId : origInstrId;
247267
const usedOutside = usedOutsideDeclaringScope.has(
248268
lvalue.identifier.declarationId,
249269
);
250270

251271
if (value.kind === 'PropertyLoad' && !usedOutside) {
252-
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
272+
if (
273+
innerFnContext == null ||
274+
temporaries.has(value.object.identifier.id)
275+
) {
253276
/**
254277
* All dependencies of a inner / nested function must have a base
255278
* identifier from the outermost component / hook. This is because the
@@ -265,13 +288,13 @@ function collectTemporariesSidemapImpl(
265288
temporaries.set(lvalue.identifier.id, property);
266289
}
267290
} else if (
268-
value.kind === 'LoadLocal' &&
291+
(value.kind === 'LoadLocal' || isLoadContextMutable(value, instrId)) &&
269292
lvalue.identifier.name == null &&
270293
value.place.identifier.name !== null &&
271294
!usedOutside
272295
) {
273296
if (
274-
!isInnerFn ||
297+
innerFnContext == null ||
275298
fn.context.some(
276299
context => context.identifier.id === value.place.identifier.id,
277300
)
@@ -289,7 +312,7 @@ function collectTemporariesSidemapImpl(
289312
value.loweredFunc.func,
290313
usedOutsideDeclaringScope,
291314
temporaries,
292-
true,
315+
innerFnContext ?? {instrId},
293316
);
294317
}
295318
}
@@ -364,7 +387,7 @@ class Context {
364387
* Tracks the traversal state. See Context.declare for explanation of why this
365388
* is needed.
366389
*/
367-
inInnerFn: boolean = false;
390+
#innerFnContext: {outerInstrId: InstructionId} | null = null;
368391

369392
constructor(
370393
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
@@ -434,7 +457,7 @@ class Context {
434457
* by root identifier mutable ranges).
435458
*/
436459
declare(identifier: Identifier, decl: Decl): void {
437-
if (this.inInnerFn) return;
460+
if (this.#innerFnContext != null) return;
438461
if (!this.#declarations.has(identifier.declarationId)) {
439462
this.#declarations.set(identifier.declarationId, decl);
440463
}
@@ -577,11 +600,14 @@ class Context {
577600
currentScope.reassignments.add(place.identifier);
578601
}
579602
}
580-
enterInnerFn<T>(cb: () => T): T {
581-
const wasInInnerFn = this.inInnerFn;
582-
this.inInnerFn = true;
603+
enterInnerFn<T>(
604+
innerFn: TInstruction<FunctionExpression> | TInstruction<ObjectMethod>,
605+
cb: () => T,
606+
): T {
607+
const prevContext = this.#innerFnContext;
608+
this.#innerFnContext = this.#innerFnContext ?? {outerInstrId: innerFn.id};
583609
const result = cb();
584-
this.inInnerFn = wasInInnerFn;
610+
this.#innerFnContext = prevContext;
585611
return result;
586612
}
587613

@@ -724,9 +750,14 @@ function collectDependencies(
724750
* Recursively visit the inner function to extract dependencies there
725751
*/
726752
const innerFn = instr.value.loweredFunc.func;
727-
context.enterInnerFn(() => {
728-
handleFunction(innerFn);
729-
});
753+
context.enterInnerFn(
754+
instr as
755+
| TInstruction<FunctionExpression>
756+
| TInstruction<ObjectMethod>,
757+
() => {
758+
handleFunction(innerFn);
759+
},
760+
);
730761
} else {
731762
handleInstruction(instr, context);
732763
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-functiondecl-hoisting.expect.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,16 @@ function Foo(t0) {
5858
bar = $[1];
5959
result = $[2];
6060
}
61-
62-
const t1 = bar;
63-
let t2;
64-
if ($[3] !== result || $[4] !== t1) {
65-
t2 = <Stringify result={result} fn={t1} shouldInvokeFns={true} />;
66-
$[3] = result;
67-
$[4] = t1;
68-
$[5] = t2;
61+
let t1;
62+
if ($[3] !== bar || $[4] !== result) {
63+
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
64+
$[3] = bar;
65+
$[4] = result;
66+
$[5] = t1;
6967
} else {
70-
t2 = $[5];
68+
t1 = $[5];
7169
}
72-
return t2;
70+
return t1;
7371
}
7472

7573
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-assignment-to-context-var.expect.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,15 @@ function Component(props) {
4343
} else {
4444
x = $[1];
4545
}
46-
const t0 = x;
47-
let t1;
48-
if ($[2] !== t0) {
49-
t1 = { x: t0 };
50-
$[2] = t0;
51-
$[3] = t1;
46+
let t0;
47+
if ($[2] !== x) {
48+
t0 = { x };
49+
$[2] = x;
50+
$[3] = t0;
5251
} else {
53-
t1 = $[3];
52+
t0 = $[3];
5453
}
55-
return t1;
54+
return t0;
5655
}
5756

5857
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@ function Component(props) {
4242
} else {
4343
x = $[1];
4444
}
45-
const t0 = x;
46-
let t1;
47-
if ($[2] !== t0) {
48-
t1 = <div>{t0}</div>;
49-
$[2] = t0;
50-
$[3] = t1;
45+
let t0;
46+
if ($[2] !== x) {
47+
t0 = <div>{x}</div>;
48+
$[2] = x;
49+
$[3] = t0;
5150
} else {
52-
t1 = $[3];
51+
t0 = $[3];
5352
}
54-
return t1;
53+
return t0;
5554
}
5655

5756
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-object-assignment-to-context-var.expect.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,15 @@ function Component(props) {
4343
} else {
4444
x = $[1];
4545
}
46-
const t0 = x;
47-
let t1;
48-
if ($[2] !== t0) {
49-
t1 = { x: t0 };
50-
$[2] = t0;
51-
$[3] = t1;
46+
let t0;
47+
if ($[2] !== x) {
48+
t0 = { x };
49+
$[2] = x;
50+
$[3] = t0;
5251
} else {
53-
t1 = $[3];
52+
t0 = $[3];
5453
}
55-
return t1;
54+
return t0;
5655
}
5756

5857
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-object-declaration-to-context-var.expect.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@ function Component(props) {
4242
} else {
4343
x = $[1];
4444
}
45-
const t0 = x;
46-
let t1;
47-
if ($[2] !== t0) {
48-
t1 = { x: t0 };
49-
$[2] = t0;
50-
$[3] = t1;
45+
let t0;
46+
if ($[2] !== x) {
47+
t0 = { x };
48+
$[2] = x;
49+
$[3] = t0;
5150
} else {
52-
t1 = $[3];
51+
t0 = $[3];
5352
}
54-
return t1;
53+
return t0;
5554
}
5655

5756
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,15 @@ function f(a) {
3333
} else {
3434
x = $[1];
3535
}
36-
37-
const t0 = x;
38-
let t1;
39-
if ($[2] !== t0) {
40-
t1 = <div x={t0} />;
41-
$[2] = t0;
42-
$[3] = t1;
36+
let t0;
37+
if ($[2] !== x) {
38+
t0 = <div x={x} />;
39+
$[2] = x;
40+
$[3] = t0;
4341
} else {
44-
t1 = $[3];
42+
t0 = $[3];
4543
}
46-
return t1;
44+
return t0;
4745
}
4846

4947
export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.expect.md

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)