Skip to content

Commit d9b212d

Browse files
committed
compiler: More precise escape analysis
Our current escape analysis currently works by determing values which are returned, finding the scope those values are produced in, and then walking the dependencies of those scopes. Notably, when we traverse into scopes, we _force_ their values to be memoized — even in cases where we can prove it isn't necessary. The idea of this PR is to change escape analysis to be purely based on the flow of values. So rather than assume all scope deps need to be force-memoized, we look at how those values are used. Note: the main motivation for this PR is the follow-up, which allows us to infer dependencies for pruned scopes. Without this change, inferring deps for pruned scopes causes the escape analysis pass to consider values as escaping which shouldn't be. [ghstack-poisoned]
1 parent 4fb7a93 commit d9b212d

File tree

4 files changed

+76
-51
lines changed

4 files changed

+76
-51
lines changed

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Place,
1515
ReactiveFunction,
1616
ReactiveInstruction,
17+
ReactiveScope,
1718
ReactiveScopeBlock,
1819
ReactiveStatement,
1920
ReactiveTerminal,
@@ -122,18 +123,12 @@ export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
122123
}
123124
visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env), state);
124125

125-
// log(() => prettyFormat(state));
126-
127126
/*
128127
* Then walk outward from the returned values and find all captured operands.
129128
* This forms the set of identifiers which should be memoized.
130129
*/
131130
const memoized = computeMemoizedIdentifiers(state);
132131

133-
// log(() => prettyFormat(memoized));
134-
135-
// log(() => printReactiveFunction(fn));
136-
137132
// Prune scopes that do not declare/reassign any escaping values
138133
visitReactiveFunction(fn, new PruneScopesTransform(), memoized);
139134
}
@@ -200,7 +195,9 @@ type IdentifierNode = {
200195

201196
// A scope node describing its dependencies
202197
type ScopeNode = {
203-
dependencies: Array<IdentifierId>;
198+
declarations: Array<IdentifierId>;
199+
inputs: Set<IdentifierId>;
200+
innerScopes: Set<ScopeId>;
204201
seen: boolean;
205202
};
206203

@@ -216,6 +213,7 @@ class State {
216213
identifiers: Map<IdentifierId, IdentifierNode> = new Map();
217214
scopes: Map<ScopeId, ScopeNode> = new Map();
218215
escapingValues: Set<IdentifierId> = new Set();
216+
currentScope: ReactiveScope | null = null;
219217

220218
constructor(env: Environment) {
221219
this.env = env;
@@ -247,11 +245,18 @@ class State {
247245
let node = this.scopes.get(scope.id);
248246
if (node === undefined) {
249247
node = {
250-
dependencies: [...scope.dependencies].map((dep) => dep.identifier.id),
248+
declarations: [...scope.declarations]
249+
.map(([id]) => id)
250+
.concat(
251+
[...scope.reassignments].map((identifier) => identifier.id)
252+
),
253+
inputs: new Set(),
254+
innerScopes: new Set(),
251255
seen: false,
252256
};
253257
this.scopes.set(scope.id, node);
254258
}
259+
node.inputs.add(identifier);
255260
const identifierNode = this.identifiers.get(identifier);
256261
CompilerError.invariant(identifierNode !== undefined, {
257262
reason: "Expected identifier to be initialized",
@@ -262,6 +267,22 @@ class State {
262267
identifierNode.scopes.add(scope.id);
263268
}
264269
}
270+
271+
enterScope(scope: ReactiveScope, fn: () => void): void {
272+
const parentScope = this.currentScope;
273+
this.currentScope = scope;
274+
fn();
275+
this.currentScope = parentScope;
276+
277+
if (
278+
parentScope !== null &&
279+
scope.declarations.size === 0 &&
280+
scope.reassignments.size === 0
281+
) {
282+
const parentNode = this.scopes.get(parentScope.id)!;
283+
parentNode.innerScopes.add(scope.id);
284+
}
285+
}
265286
}
266287

267288
/*
@@ -273,7 +294,7 @@ function computeMemoizedIdentifiers(state: State): Set<IdentifierId> {
273294
const memoized = new Set<IdentifierId>();
274295

275296
// Visit an identifier, optionally forcing it to be memoized
276-
function visit(id: IdentifierId, forceMemoize: boolean = false): boolean {
297+
function visit(id: IdentifierId): boolean {
277298
const node = state.identifiers.get(id);
278299
CompilerError.invariant(node !== undefined, {
279300
reason: `Expected a node for all identifiers, none found for \`${id}\``,
@@ -301,21 +322,19 @@ function computeMemoizedIdentifiers(state: State): Set<IdentifierId> {
301322

302323
if (
303324
node.level === MemoizationLevel.Memoized ||
304-
(node.level === MemoizationLevel.Conditional &&
305-
(hasMemoizedDependency || forceMemoize)) ||
306-
(node.level === MemoizationLevel.Unmemoized && forceMemoize)
325+
(node.level === MemoizationLevel.Conditional && hasMemoizedDependency)
307326
) {
308327
node.memoized = true;
309328
memoized.add(id);
310329
for (const scope of node.scopes) {
311-
forceMemoizeScopeDependencies(scope);
330+
memoizeScopeDependencies(scope);
312331
}
313332
}
314333
return node.memoized;
315334
}
316335

317336
// Force all the scope's optionally-memoizeable dependencies (not "Never") to be memoized
318-
function forceMemoizeScopeDependencies(id: ScopeId): void {
337+
function memoizeScopeDependencies(id: ScopeId): void {
319338
const node = state.scopes.get(id);
320339
CompilerError.invariant(node !== undefined, {
321340
reason: "Expected a node for all scopes",
@@ -328,8 +347,14 @@ function computeMemoizedIdentifiers(state: State): Set<IdentifierId> {
328347
}
329348
node.seen = true;
330349

331-
for (const dep of node.dependencies) {
332-
visit(dep, true);
350+
for (const dep of node.declarations) {
351+
visit(dep);
352+
}
353+
for (const dep of node.inputs) {
354+
visit(dep);
355+
}
356+
for (const scope of node.innerScopes) {
357+
memoizeScopeDependencies(scope);
333358
}
334359
return;
335360
}
@@ -814,6 +839,12 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
814839
};
815840
}
816841

842+
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
843+
state.enterScope(scopeBlock.scope, () => {
844+
this.traverseScope(scopeBlock, state);
845+
});
846+
}
847+
817848
override visitInstruction(
818849
instruction: ReactiveInstruction,
819850
state: State

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate.expect.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ export const FIXTURE_ENTRYPOINT = {
2626
```javascript
2727
import { c as _c } from "react/compiler-runtime";
2828
function component(a, b) {
29-
const $ = _c(3);
29+
const $ = _c(5);
3030
let z;
3131
if ($[0] !== a || $[1] !== b) {
3232
z = { a };
33-
const y = { b };
33+
let t0;
34+
if ($[3] !== b) {
35+
t0 = { b };
36+
$[3] = b;
37+
$[4] = t0;
38+
} else {
39+
t0 = $[4];
40+
}
41+
const y = t0;
3442
const x = function () {
3543
z.a = 2;
3644
console.log(y.b);

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/method-call-computed.expect.md

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function foo(a, b, c) {
2323
```javascript
2424
import { c as _c } from "react/compiler-runtime";
2525
function foo(a, b, c) {
26-
const $ = _c(8);
26+
const $ = _c(6);
2727
let t0;
2828
if ($[0] !== a) {
2929
t0 = makeObject(a);
@@ -33,26 +33,18 @@ function foo(a, b, c) {
3333
t0 = $[1];
3434
}
3535
const x = t0;
36+
const y = makeObject(a);
3637
let t1;
37-
if ($[2] !== a) {
38-
t1 = makeObject(a);
39-
$[2] = a;
40-
$[3] = t1;
41-
} else {
42-
t1 = $[3];
43-
}
44-
const y = t1;
45-
let t2;
46-
if ($[4] !== x || $[5] !== y.method || $[6] !== b) {
47-
t2 = x[y.method](b);
48-
$[4] = x;
49-
$[5] = y.method;
50-
$[6] = b;
51-
$[7] = t2;
38+
if ($[2] !== x || $[3] !== y.method || $[4] !== b) {
39+
t1 = x[y.method](b);
40+
$[2] = x;
41+
$[3] = y.method;
42+
$[4] = b;
43+
$[5] = t1;
5244
} else {
53-
t2 = $[7];
45+
t1 = $[5];
5446
}
55-
const z = t2;
47+
const z = t1;
5648
return z;
5749
}
5850

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutate-outer-scope-within-value-block.expect.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,20 @@ import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
6868
* should be merged.
6969
*/
7070
function useFoo(t0) {
71-
const $ = _c(3);
71+
const $ = _c(2);
7272
const { input } = t0;
7373
const arr = shallowCopy(input);
74+
75+
const cond = identity(false);
7476
let t1;
75-
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
76-
t1 = identity(false);
77-
$[0] = t1;
78-
} else {
79-
t1 = $[0];
80-
}
81-
const cond = t1;
82-
let t2;
83-
if ($[1] !== arr) {
84-
t2 = cond ? { val: CONST_TRUE } : mutate(arr);
85-
$[1] = arr;
86-
$[2] = t2;
77+
if ($[0] !== arr) {
78+
t1 = cond ? { val: CONST_TRUE } : mutate(arr);
79+
$[0] = arr;
80+
$[1] = t1;
8781
} else {
88-
t2 = $[2];
82+
t1 = $[1];
8983
}
90-
return t2;
84+
return t1;
9185
}
9286

9387
export const FIXTURE_ENTRYPOINT = {

0 commit comments

Comments
 (0)