Skip to content

Commit 50f8538

Browse files
committed
[compiler][rewrite] Represent scope dependencies with value blocks
(needs cleanup) - Scopes no longer store a flat list of their dependencies. Instead: - Scope terminals are effectively a `goto` for scope dependency instructions (represented as value blocks that terminate with a `goto scopeBlock` for HIR and a series of ReactiveInstructions for ReactiveIR) - Scopes themselves store `dependencies: Array<Place>`, which refer to temporaries written to by scope dependency instructions Next steps: - new pass to dedupe scope dependency instructions after all dependency and scope pruning passes, effectively 'hoisting' dependencies out - more complex dependencies (unary ops like `Boolean` or `Not`, binary ops like `!==` or logical operators)
1 parent 1cb99ca commit 50f8538

27 files changed

+1088
-163
lines changed

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ type TerminalRewriteInfo =
191191
| {
192192
kind: 'StartScope';
193193
blockId: BlockId;
194+
dependencyId: BlockId;
194195
fallthroughId: BlockId;
195196
instrId: InstructionId;
196197
scope: ReactiveScope;
@@ -215,12 +216,14 @@ function pushStartScopeTerminal(
215216
scope: ReactiveScope,
216217
context: ScopeTraversalContext,
217218
): void {
219+
const dependencyId = context.env.nextBlockId;
218220
const blockId = context.env.nextBlockId;
219221
const fallthroughId = context.env.nextBlockId;
220222
context.rewrites.push({
221223
kind: 'StartScope',
222224
blockId,
223225
fallthroughId,
226+
dependencyId,
224227
instrId: scope.range.start,
225228
scope,
226229
});
@@ -262,10 +265,13 @@ type RewriteContext = {
262265
* instr1, instr2, instr3, instr4, [[ original terminal ]]
263266
* Rewritten:
264267
* bb0:
265-
* instr1, [[ scope start block=bb1]]
268+
* instr1, [[ scope start dependencies=bb1 block=bb2]]
266269
* bb1:
267-
* instr2, instr3, [[ scope end goto=bb2 ]]
270+
* [[ empty, filled in in PropagateScopeDependenciesHIR ]]
271+
* goto bb2
268272
* bb2:
273+
* instr2, instr3, [[ scope end goto=bb3 ]]
274+
* bb3:
269275
* instr4, [[ original terminal ]]
270276
*/
271277
function handleRewrite(
@@ -279,6 +285,7 @@ function handleRewrite(
279285
? {
280286
kind: 'scope',
281287
fallthrough: terminalInfo.fallthroughId,
288+
dependencies: terminalInfo.dependencyId,
282289
block: terminalInfo.blockId,
283290
scope: terminalInfo.scope,
284291
id: terminalInfo.instrId,
@@ -305,7 +312,28 @@ function handleRewrite(
305312
context.nextPreds = new Set([currBlockId]);
306313
context.nextBlockId =
307314
terminalInfo.kind === 'StartScope'
308-
? terminalInfo.blockId
315+
? terminalInfo.dependencyId
309316
: terminalInfo.fallthroughId;
310317
context.instrSliceIdx = idx;
318+
319+
if (terminalInfo.kind === 'StartScope') {
320+
const currBlockId = context.nextBlockId;
321+
context.rewrites.push({
322+
kind: context.source.kind,
323+
id: currBlockId,
324+
instructions: [],
325+
preds: context.nextPreds,
326+
// Only the first rewrite should reuse source block phis
327+
phis: new Set(),
328+
terminal: {
329+
kind: 'goto',
330+
variant: GotoVariant.Break,
331+
block: terminal.block,
332+
id: terminalInfo.instrId,
333+
loc: GeneratedSource,
334+
},
335+
});
336+
context.nextPreds = new Set([currBlockId]);
337+
context.nextBlockId = terminalInfo.blockId;
338+
}
311339
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ type PropertyPathNode =
241241
class PropertyPathRegistry {
242242
roots: Map<IdentifierId, RootNode> = new Map();
243243

244-
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
244+
getOrCreateIdentifier(
245+
identifier: Identifier,
246+
reactive: boolean,
247+
): PropertyPathNode {
245248
/**
246249
* Reads from a statically scoped variable are always safe in JS,
247250
* with the exception of TDZ (not addressed by this pass).
@@ -255,12 +258,19 @@ class PropertyPathRegistry {
255258
optionalProperties: new Map(),
256259
fullPath: {
257260
identifier,
261+
reactive,
258262
path: [],
259263
},
260264
hasOptional: false,
261265
parent: null,
262266
};
263267
this.roots.set(identifier.id, rootNode);
268+
} else {
269+
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
270+
reason:
271+
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
272+
loc: identifier.loc,
273+
});
264274
}
265275
return rootNode;
266276
}
@@ -278,6 +288,7 @@ class PropertyPathRegistry {
278288
parent: parent,
279289
fullPath: {
280290
identifier: parent.fullPath.identifier,
291+
reactive: parent.fullPath.reactive,
281292
path: parent.fullPath.path.concat(entry),
282293
},
283294
hasOptional: parent.hasOptional || entry.optional,
@@ -293,7 +304,7 @@ class PropertyPathRegistry {
293304
* so all subpaths of a PropertyLoad should already exist
294305
* (e.g. a.b is added before a.b.c),
295306
*/
296-
let currNode = this.getOrCreateIdentifier(n.identifier);
307+
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
297308
if (n.path.length === 0) {
298309
return currNode;
299310
}
@@ -315,10 +326,11 @@ function getMaybeNonNullInInstruction(
315326
instr: InstructionValue,
316327
context: CollectHoistablePropertyLoadsContext,
317328
): PropertyPathNode | null {
318-
let path = null;
329+
let path: ReactiveScopeDependency | null = null;
319330
if (instr.kind === 'PropertyLoad') {
320331
path = context.temporaries.get(instr.object.identifier.id) ?? {
321332
identifier: instr.object.identifier,
333+
reactive: instr.object.reactive,
322334
path: [],
323335
};
324336
} else if (instr.kind === 'Destructure') {
@@ -381,7 +393,7 @@ function collectNonNullsInBlocks(
381393
) {
382394
const identifier = fn.params[0].identifier;
383395
knownNonNullIdentifiers.add(
384-
context.registry.getOrCreateIdentifier(identifier),
396+
context.registry.getOrCreateIdentifier(identifier, true),
385397
);
386398
}
387399
const nodes = new Map<
@@ -616,9 +628,11 @@ function reduceMaybeOptionalChains(
616628
changed = false;
617629

618630
for (const original of optionalChainNodes) {
619-
let {identifier, path: origPath} = original.fullPath;
620-
let currNode: PropertyPathNode =
621-
registry.getOrCreateIdentifier(identifier);
631+
let {identifier, path: origPath, reactive} = original.fullPath;
632+
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
633+
identifier,
634+
reactive,
635+
);
622636
for (let i = 0; i < origPath.length; i++) {
623637
const entry = origPath[i];
624638
// If the base is known to be non-null, replace with a non-optional load

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export type OptionalChainSidemap = {
114114
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
115115
};
116116

117-
type OptionalTraversalContext = {
117+
export type OptionalTraversalContext = {
118118
currFn: HIRFunction;
119119
blocks: ReadonlyMap<BlockId, BasicBlock>;
120120

@@ -235,7 +235,7 @@ function matchOptionalTestBlock(
235235
* property loads. If any part of the optional chain is not hoistable, returns
236236
* null.
237237
*/
238-
function traverseOptionalBlock(
238+
export function traverseOptionalBlock(
239239
optional: TBasicBlock<OptionalTerminal>,
240240
context: OptionalTraversalContext,
241241
outerAlternate: BlockId | null,
@@ -290,6 +290,7 @@ function traverseOptionalBlock(
290290
);
291291
baseObject = {
292292
identifier: maybeTest.instructions[0].value.place.identifier,
293+
reactive: maybeTest.instructions[0].value.place.reactive,
293294
path,
294295
};
295296
test = maybeTest.terminal;
@@ -391,6 +392,7 @@ function traverseOptionalBlock(
391392
);
392393
const load = {
393394
identifier: baseObject.identifier,
395+
reactive: baseObject.reactive,
394396
path: [
395397
...baseObject.path,
396398
{

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ export class ReactiveScopeDependencyTreeHIR {
2525
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
2626
* evaluate (non-optional) PropertyLoads from.
2727
*/
28-
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
29-
#deps: Map<Identifier, DependencyNode> = new Map();
28+
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
29+
new Map();
30+
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
3031

3132
/**
3233
* @param hoistableObjects a set of paths from which we can safely evaluate
@@ -35,9 +36,10 @@ export class ReactiveScopeDependencyTreeHIR {
3536
* duplicates when traversing the CFG.
3637
*/
3738
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
38-
for (const {path, identifier} of hoistableObjects) {
39+
for (const {path, identifier, reactive} of hoistableObjects) {
3940
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
4041
identifier,
42+
reactive,
4143
this.#hoistableObjects,
4244
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
4345
);
@@ -70,7 +72,8 @@ export class ReactiveScopeDependencyTreeHIR {
7072

7173
static #getOrCreateRoot<T extends string>(
7274
identifier: Identifier,
73-
roots: Map<Identifier, TreeNode<T>>,
75+
reactive: boolean,
76+
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
7477
defaultAccessType: T,
7578
): TreeNode<T> {
7679
// roots can always be accessed unconditionally in JS
@@ -79,9 +82,16 @@ export class ReactiveScopeDependencyTreeHIR {
7982
if (rootNode === undefined) {
8083
rootNode = {
8184
properties: new Map(),
85+
reactive,
8286
accessType: defaultAccessType,
8387
};
8488
roots.set(identifier, rootNode);
89+
} else {
90+
CompilerError.invariant(reactive === rootNode.reactive, {
91+
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
92+
description: `Identifier ${printIdentifier(identifier)}`,
93+
loc: GeneratedSource,
94+
});
8595
}
8696
return rootNode;
8797
}
@@ -92,9 +102,10 @@ export class ReactiveScopeDependencyTreeHIR {
92102
* safe-to-evaluate subpath
93103
*/
94104
addDependency(dep: ReactiveScopeDependency): void {
95-
const {identifier, path} = dep;
105+
const {identifier, reactive, path} = dep;
96106
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
97107
identifier,
108+
reactive,
98109
this.#deps,
99110
PropertyAccessType.UnconditionalAccess,
100111
);
@@ -172,7 +183,13 @@ export class ReactiveScopeDependencyTreeHIR {
172183
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
173184
const results = new Set<ReactiveScopeDependency>();
174185
for (const [rootId, rootNode] of this.#deps.entries()) {
175-
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
186+
collectMinimalDependenciesInSubtree(
187+
rootNode,
188+
rootNode.reactive,
189+
rootId,
190+
[],
191+
results,
192+
);
176193
}
177194

178195
return results;
@@ -294,25 +311,24 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
294311
type DependencyNode = TreeNode<PropertyAccessType>;
295312

296313
/**
297-
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
298-
* longer have conditionally accessed nodes, we can simplify
299-
*
300314
* Recursively calculates minimal dependencies in a subtree.
301315
* @param node DependencyNode representing a dependency subtree.
302316
* @returns a minimal list of dependencies in this subtree.
303317
*/
304318
function collectMinimalDependenciesInSubtree(
305319
node: DependencyNode,
320+
reactive: boolean,
306321
rootIdentifier: Identifier,
307322
path: Array<DependencyPathEntry>,
308323
results: Set<ReactiveScopeDependency>,
309324
): void {
310325
if (isDependency(node.accessType)) {
311-
results.add({identifier: rootIdentifier, path});
326+
results.add({identifier: rootIdentifier, reactive, path});
312327
} else {
313328
for (const [childName, childNode] of node.properties) {
314329
collectMinimalDependenciesInSubtree(
315330
childNode,
331+
reactive,
316332
rootIdentifier,
317333
[
318334
...path,

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ export type ReactiveFunction = {
6262

6363
export type ReactiveScopeBlock = {
6464
kind: 'scope';
65+
dependencyInstructions: Array<ReactiveInstructionStatement>;
6566
scope: ReactiveScope;
6667
instructions: ReactiveBlock;
6768
};
6869

6970
export type PrunedReactiveScopeBlock = {
7071
kind: 'pruned-scope';
72+
dependencyInstructions: Array<ReactiveInstructionStatement>;
7173
scope: ReactiveScope;
7274
instructions: ReactiveBlock;
7375
};
@@ -614,6 +616,7 @@ export type MaybeThrowTerminal = {
614616
export type ReactiveScopeTerminal = {
615617
kind: 'scope';
616618
fallthrough: BlockId;
619+
dependencies: BlockId;
617620
block: BlockId;
618621
scope: ReactiveScope;
619622
id: InstructionId;
@@ -623,6 +626,7 @@ export type ReactiveScopeTerminal = {
623626
export type PrunedScopeTerminal = {
624627
kind: 'pruned-scope';
625628
fallthrough: BlockId;
629+
dependencies: BlockId;
626630
block: BlockId;
627631
scope: ReactiveScope;
628632
id: InstructionId;
@@ -1472,9 +1476,10 @@ export type ReactiveScope = {
14721476
range: MutableRange;
14731477

14741478
/**
1475-
* The inputs to this reactive scope
1479+
* Note the dependencies of a reactive scope are tracked in HIR and
1480+
* ReactiveFunction
14761481
*/
1477-
dependencies: ReactiveScopeDependencies;
1482+
dependencies: Array<Place>;
14781483

14791484
/**
14801485
* The set of values produced by this scope. This may be empty
@@ -1535,6 +1540,18 @@ export type DependencyPathEntry = {
15351540
export type DependencyPath = Array<DependencyPathEntry>;
15361541
export type ReactiveScopeDependency = {
15371542
identifier: Identifier;
1543+
/**
1544+
* Reflects whether the base identifier is reactive. Note that some reactive
1545+
* objects may have non-reactive properties, but we do not currently track
1546+
* this.
1547+
*
1548+
* ```js
1549+
* // Technically, result[0] is reactive and result[1] is not.
1550+
* // Currently, both dependencies would be marked as reactive.
1551+
* const result = useState();
1552+
* ```
1553+
*/
1554+
reactive: boolean;
15381555
path: DependencyPath;
15391556
};
15401557

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,13 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
286286
case 'scope': {
287287
value = `[${terminal.id}] Scope ${printReactiveScopeSummary(
288288
terminal.scope,
289-
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
289+
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
290290
break;
291291
}
292292
case 'pruned-scope': {
293293
value = `[${terminal.id}] <pruned> Scope ${printReactiveScopeSummary(
294294
terminal.scope,
295-
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
295+
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
296296
break;
297297
}
298298
case 'try': {

0 commit comments

Comments
 (0)