Skip to content

Commit 942c9b4

Browse files
committed
compiler: Promote pruned scope declarations to temporaries if used in a later scope
There's a category of bug currently where pruned reactive scopes whose outputs are non-reactive can have their code end up inlining into another scope, moving the location of the instruction. Any value that had a scope assigned has to have its order of evaluation preserved, despite the fact that it got pruned, so naively we could just force every pruned scope to have its declarations promoted to named variables. However, that ends up assigning names to _tons_ of scope declarations that don't really need to be promoted. For example, a scope with just a hook call ends up with: ``` const x = useFoo(); => scope { $t0 = Call read useFoo$ (...); } $t1 = StoreLocal 'x' = read $t0; ``` Where t0 doesn't need to be promoted since it's used immediately to assign to another value which is a non-temporary. So the idea of this PR is that we can track outputs of pruned scopes which are directly referenced from inside a later scope. This fixes one of the two cases of the above pattern. We'll also likely have to consider values from pruned scopes as always reactive, i'll do that in the next PR. ghstack-source-id: 2ef7fb0 Pull Request resolved: #29789
1 parent 82631e0 commit 942c9b4

File tree

4 files changed

+94
-27
lines changed

4 files changed

+94
-27
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
getHookKind,
3737
makeIdentifierName,
3838
} from "../HIR/HIR";
39-
import { printPlace } from "../HIR/PrintHIR";
39+
import { printIdentifier, printPlace } from "../HIR/PrintHIR";
4040
import { eachPatternOperand } from "../HIR/visitors";
4141
import { Err, Ok, Result } from "../Utils/Result";
4242
import { GuardKind } from "../Utils/RuntimeDiagnosticConstants";
@@ -524,8 +524,8 @@ function codegenReactiveScope(
524524
}
525525

526526
CompilerError.invariant(identifier.name != null, {
527-
reason: `Expected identifier '@${identifier.id}' to be named`,
528-
description: null,
527+
reason: `Expected scope declaration identifier to be named`,
528+
description: `Declaration \`${printIdentifier(identifier)}\` is unnamed in scope @${scope.id}`,
529529
loc: null,
530530
suggestions: null,
531531
});

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

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@ import {
1212
IdentifierId,
1313
InstructionId,
1414
Place,
15+
PrunedReactiveScopeBlock,
1516
ReactiveFunction,
1617
ReactiveScopeBlock,
1718
ReactiveValue,
19+
ScopeId,
1820
promoteTemporary,
1921
promoteTemporaryJsxTag,
2022
} from "../HIR/HIR";
2123
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
2224

23-
type VisitorState = {
24-
tags: JsxExpressionTags;
25-
};
26-
class Visitor extends ReactiveFunctionVisitor<VisitorState> {
27-
override visitScope(block: ReactiveScopeBlock, state: VisitorState): void {
28-
this.traverseScope(block, state);
29-
for (const dep of block.scope.dependencies) {
25+
class Visitor extends ReactiveFunctionVisitor<State> {
26+
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
27+
this.traverseScope(scopeBlock, state);
28+
for (const dep of scopeBlock.scope.dependencies) {
3029
const { identifier } = dep;
3130
if (identifier.name == null) {
3231
promoteIdentifier(identifier, state);
@@ -39,14 +38,29 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
3938
* Many of our current test fixtures do not return a value, so
4039
* it is better for now to promote (and memoize) every output.
4140
*/
42-
for (const [, declaration] of block.scope.declarations) {
41+
for (const [, declaration] of scopeBlock.scope.declarations) {
4342
if (declaration.identifier.name == null) {
4443
promoteIdentifier(declaration.identifier, state);
4544
}
4645
}
4746
}
4847

49-
override visitParam(place: Place, state: VisitorState): void {
48+
override visitPrunedScope(
49+
scopeBlock: PrunedReactiveScopeBlock,
50+
state: State
51+
): void {
52+
this.traversePrunedScope(scopeBlock, state);
53+
for (const [, declaration] of scopeBlock.scope.declarations) {
54+
if (
55+
declaration.identifier.name == null &&
56+
state.pruned.get(declaration.identifier.id)?.usedOutsideScope === true
57+
) {
58+
promoteIdentifier(declaration.identifier, state);
59+
}
60+
}
61+
}
62+
63+
override visitParam(place: Place, state: State): void {
5064
if (place.identifier.name === null) {
5165
promoteIdentifier(place.identifier, state);
5266
}
@@ -55,7 +69,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
5569
override visitValue(
5670
id: InstructionId,
5771
value: ReactiveValue,
58-
state: VisitorState
72+
state: State
5973
): void {
6074
this.traverseValue(id, value, state);
6175
if (value.kind === "FunctionExpression" || value.kind === "ObjectMethod") {
@@ -67,7 +81,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
6781
_id: InstructionId,
6882
_dependencies: Array<Place>,
6983
fn: ReactiveFunction,
70-
state: VisitorState
84+
state: State
7185
): void {
7286
for (const operand of fn.params) {
7387
const place = operand.kind === "Identifier" ? operand : operand.place;
@@ -80,25 +94,65 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
8094
}
8195

8296
type JsxExpressionTags = Set<IdentifierId>;
83-
class CollectJsxTagsVisitor extends ReactiveFunctionVisitor<JsxExpressionTags> {
97+
type State = {
98+
tags: JsxExpressionTags;
99+
pruned: Map<
100+
IdentifierId,
101+
{ activeScopes: Array<ScopeId>; usedOutsideScope: boolean }
102+
>; // true if referenced within another scope, false if only accessed outside of scopes
103+
};
104+
105+
class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
106+
activeScopes: Array<ScopeId> = [];
107+
108+
override visitPlace(_id: InstructionId, place: Place, state: State): void {
109+
if (
110+
this.activeScopes.length !== 0 &&
111+
state.pruned.has(place.identifier.id)
112+
) {
113+
const prunedPlace = state.pruned.get(place.identifier.id)!;
114+
if (prunedPlace.activeScopes.indexOf(this.activeScopes.at(-1)!) === -1) {
115+
prunedPlace.usedOutsideScope = true;
116+
}
117+
}
118+
}
119+
84120
override visitValue(
85121
id: InstructionId,
86122
value: ReactiveValue,
87-
state: JsxExpressionTags
123+
state: State
88124
): void {
89125
this.traverseValue(id, value, state);
90126
if (value.kind === "JsxExpression" && value.tag.kind === "Identifier") {
91-
state.add(value.tag.identifier.id);
127+
state.tags.add(value.tag.identifier.id);
92128
}
93129
}
130+
131+
override visitPrunedScope(
132+
scopeBlock: PrunedReactiveScopeBlock,
133+
state: State
134+
): void {
135+
for (const [id] of scopeBlock.scope.declarations) {
136+
state.pruned.set(id, {
137+
activeScopes: [...this.activeScopes],
138+
usedOutsideScope: false,
139+
});
140+
}
141+
}
142+
143+
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
144+
this.activeScopes.push(scopeBlock.scope.id);
145+
this.traverseScope(scopeBlock, state);
146+
this.activeScopes.pop();
147+
}
94148
}
95149

96150
export function promoteUsedTemporaries(fn: ReactiveFunction): void {
97-
const tags: JsxExpressionTags = new Set();
98-
visitReactiveFunction(fn, new CollectJsxTagsVisitor(), tags);
99-
const state: VisitorState = {
100-
tags,
151+
const state: State = {
152+
tags: new Set(),
153+
pruned: new Map(),
101154
};
155+
visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
102156
for (const operand of fn.params) {
103157
const place = operand.kind === "Identifier" ? operand : operand.place;
104158
if (place.identifier.name === null) {
@@ -108,7 +162,7 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
108162
visitReactiveFunction(fn, new Visitor(), state);
109163
}
110164

111-
function promoteIdentifier(identifier: Identifier, state: VisitorState): void {
165+
function promoteIdentifier(identifier: Identifier, state: State): void {
112166
CompilerError.invariant(identifier.name === null, {
113167
reason:
114168
"promoteTemporary: Expected to be called only for temporary variables",

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isUseRefType,
1919
makeInstructionId,
2020
Place,
21+
PrunedReactiveScopeBlock,
2122
ReactiveFunction,
2223
ReactiveInstruction,
2324
ReactiveScope,
@@ -687,6 +688,17 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
687688
scope.scope.dependencies = scopeDependencies;
688689
}
689690

691+
override visitPrunedScope(
692+
scopeBlock: PrunedReactiveScopeBlock,
693+
context: Context
694+
): void {
695+
// NOTE: we explicitly throw away the deps, we only enter() the scope to record its
696+
// declarations
697+
const _scopeDepdencies = context.enter(scopeBlock.scope, () => {
698+
this.visitBlock(scopeBlock.instructions, context);
699+
});
700+
}
701+
690702
override visitInstruction(
691703
instruction: ReactiveInstruction,
692704
context: Context

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-reactivity-value-block.expect.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,15 @@ function Foo() {
7171
useNoAlias();
7272

7373
const shouldCaptureObj = obj != null && CONST_TRUE;
74-
let t0;
74+
const t0 = shouldCaptureObj ? identity(obj) : null;
75+
let t1;
7576
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
76-
t0 = [shouldCaptureObj ? identity(obj) : null, obj];
77-
$[0] = t0;
77+
t1 = [t0, obj];
78+
$[0] = t1;
7879
} else {
79-
t0 = $[0];
80+
t1 = $[0];
8081
}
81-
const result = t0;
82+
const result = t1;
8283

8384
useNoAlias(result, obj);
8485
if (shouldCaptureObj && result[0] !== obj) {

0 commit comments

Comments
 (0)