Skip to content

Commit 06adef6

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 0b6247f commit 06adef6

26 files changed

+1065
-159
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
@@ -184,6 +184,7 @@ type TerminalRewriteInfo =
184184
| {
185185
kind: 'StartScope';
186186
blockId: BlockId;
187+
dependencyId: BlockId;
187188
fallthroughId: BlockId;
188189
instrId: InstructionId;
189190
scope: ReactiveScope;
@@ -208,12 +209,14 @@ function pushStartScopeTerminal(
208209
scope: ReactiveScope,
209210
context: ScopeTraversalContext,
210211
): void {
212+
const dependencyId = context.env.nextBlockId;
211213
const blockId = context.env.nextBlockId;
212214
const fallthroughId = context.env.nextBlockId;
213215
context.rewrites.push({
214216
kind: 'StartScope',
215217
blockId,
216218
fallthroughId,
219+
dependencyId,
217220
instrId: scope.range.start,
218221
scope,
219222
});
@@ -255,10 +258,13 @@ type RewriteContext = {
255258
* instr1, instr2, instr3, instr4, [[ original terminal ]]
256259
* Rewritten:
257260
* bb0:
258-
* instr1, [[ scope start block=bb1]]
261+
* instr1, [[ scope start dependencies=bb1 block=bb2]]
259262
* bb1:
260-
* instr2, instr3, [[ scope end goto=bb2 ]]
263+
* [[ empty, filled in in PropagateScopeDependenciesHIR ]]
264+
* goto bb2
261265
* bb2:
266+
* instr2, instr3, [[ scope end goto=bb3 ]]
267+
* bb3:
262268
* instr4, [[ original terminal ]]
263269
*/
264270
function handleRewrite(
@@ -272,6 +278,7 @@ function handleRewrite(
272278
? {
273279
kind: 'scope',
274280
fallthrough: terminalInfo.fallthroughId,
281+
dependencies: terminalInfo.dependencyId,
275282
block: terminalInfo.blockId,
276283
scope: terminalInfo.scope,
277284
id: terminalInfo.instrId,
@@ -298,7 +305,28 @@ function handleRewrite(
298305
context.nextPreds = new Set([currBlockId]);
299306
context.nextBlockId =
300307
terminalInfo.kind === 'StartScope'
301-
? terminalInfo.blockId
308+
? terminalInfo.dependencyId
302309
: terminalInfo.fallthroughId;
303310
context.instrSliceIdx = idx;
311+
312+
if (terminalInfo.kind === 'StartScope') {
313+
const currBlockId = context.nextBlockId;
314+
context.rewrites.push({
315+
kind: context.source.kind,
316+
id: currBlockId,
317+
instructions: [],
318+
preds: context.nextPreds,
319+
// Only the first rewrite should reuse source block phis
320+
phis: new Set(),
321+
terminal: {
322+
kind: 'goto',
323+
variant: GotoVariant.Break,
324+
block: terminal.block,
325+
id: terminalInfo.instrId,
326+
loc: GeneratedSource,
327+
},
328+
});
329+
context.nextPreds = new Set([currBlockId]);
330+
context.nextBlockId = terminalInfo.blockId;
331+
}
304332
}

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

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

197-
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
197+
getOrCreateIdentifier(
198+
identifier: Identifier,
199+
reactive: boolean,
200+
): PropertyPathNode {
198201
/**
199202
* Reads from a statically scoped variable are always safe in JS,
200203
* with the exception of TDZ (not addressed by this pass).
@@ -208,12 +211,19 @@ class PropertyPathRegistry {
208211
optionalProperties: new Map(),
209212
fullPath: {
210213
identifier,
214+
reactive,
211215
path: [],
212216
},
213217
hasOptional: false,
214218
parent: null,
215219
};
216220
this.roots.set(identifier.id, rootNode);
221+
} else {
222+
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
223+
reason:
224+
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag after ',
225+
loc: GeneratedSource,
226+
});
217227
}
218228
return rootNode;
219229
}
@@ -231,6 +241,7 @@ class PropertyPathRegistry {
231241
parent: parent,
232242
fullPath: {
233243
identifier: parent.fullPath.identifier,
244+
reactive: parent.fullPath.reactive,
234245
path: parent.fullPath.path.concat(entry),
235246
},
236247
hasOptional: parent.hasOptional || entry.optional,
@@ -246,7 +257,7 @@ class PropertyPathRegistry {
246257
* so all subpaths of a PropertyLoad should already exist
247258
* (e.g. a.b is added before a.b.c),
248259
*/
249-
let currNode = this.getOrCreateIdentifier(n.identifier);
260+
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
250261
if (n.path.length === 0) {
251262
return currNode;
252263
}
@@ -268,10 +279,11 @@ function getMaybeNonNullInInstruction(
268279
instr: InstructionValue,
269280
context: CollectHoistablePropertyLoadsContext,
270281
): PropertyPathNode | null {
271-
let path = null;
282+
let path: ReactiveScopeDependency | null = null;
272283
if (instr.kind === 'PropertyLoad') {
273284
path = context.temporaries.get(instr.object.identifier.id) ?? {
274285
identifier: instr.object.identifier,
286+
reactive: instr.object.reactive,
275287
path: [],
276288
};
277289
} else if (instr.kind === 'Destructure') {
@@ -334,7 +346,7 @@ function collectNonNullsInBlocks(
334346
) {
335347
const identifier = fn.params[0].identifier;
336348
knownNonNullIdentifiers.add(
337-
context.registry.getOrCreateIdentifier(identifier),
349+
context.registry.getOrCreateIdentifier(identifier, true),
338350
);
339351
}
340352
const nodes = new Map<BlockId, BlockInfo>();
@@ -565,9 +577,11 @@ function reduceMaybeOptionalChains(
565577
changed = false;
566578

567579
for (const original of optionalChainNodes) {
568-
let {identifier, path: origPath} = original.fullPath;
569-
let currNode: PropertyPathNode =
570-
registry.getOrCreateIdentifier(identifier);
580+
let {identifier, path: origPath, reactive} = original.fullPath;
581+
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
582+
identifier,
583+
reactive,
584+
);
571585
for (let i = 0; i < origPath.length; i++) {
572586
const entry = origPath[i];
573587
// 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
@@ -106,7 +106,7 @@ export type OptionalChainSidemap = {
106106
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
107107
};
108108

109-
type OptionalTraversalContext = {
109+
export type OptionalTraversalContext = {
110110
currFn: HIRFunction;
111111
blocks: ReadonlyMap<BlockId, BasicBlock>;
112112

@@ -227,7 +227,7 @@ function matchOptionalTestBlock(
227227
* property loads. If any part of the optional chain is not hoistable, returns
228228
* null.
229229
*/
230-
function traverseOptionalBlock(
230+
export function traverseOptionalBlock(
231231
optional: TBasicBlock<OptionalTerminal>,
232232
context: OptionalTraversalContext,
233233
outerAlternate: BlockId | null,
@@ -282,6 +282,7 @@ function traverseOptionalBlock(
282282
);
283283
baseObject = {
284284
identifier: maybeTest.instructions[0].value.place.identifier,
285+
reactive: maybeTest.instructions[0].value.place.reactive,
285286
path,
286287
};
287288
test = maybeTest.terminal;
@@ -383,6 +384,7 @@ function traverseOptionalBlock(
383384
);
384385
const load = {
385386
identifier: baseObject.identifier,
387+
reactive: baseObject.reactive,
386388
path: [
387389
...baseObject.path,
388390
{

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

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

3031
/**
3132
* @param hoistableObjects a set of paths from which we can safely evaluate
@@ -34,9 +35,10 @@ export class ReactiveScopeDependencyTreeHIR {
3435
* duplicates when traversing the CFG.
3536
*/
3637
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
37-
for (const {path, identifier} of hoistableObjects) {
38+
for (const {path, identifier, reactive} of hoistableObjects) {
3839
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
3940
identifier,
41+
reactive,
4042
this.#hoistableObjects,
4143
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
4244
);
@@ -69,7 +71,8 @@ export class ReactiveScopeDependencyTreeHIR {
6971

7072
static #getOrCreateRoot<T extends string>(
7173
identifier: Identifier,
72-
roots: Map<Identifier, TreeNode<T>>,
74+
reactive: boolean,
75+
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
7376
defaultAccessType: T,
7477
): TreeNode<T> {
7578
// roots can always be accessed unconditionally in JS
@@ -78,9 +81,16 @@ export class ReactiveScopeDependencyTreeHIR {
7881
if (rootNode === undefined) {
7982
rootNode = {
8083
properties: new Map(),
84+
reactive,
8185
accessType: defaultAccessType,
8286
};
8387
roots.set(identifier, rootNode);
88+
} else {
89+
CompilerError.invariant(reactive === rootNode.reactive, {
90+
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
91+
description: `Identifier ${printIdentifier(identifier)}`,
92+
loc: GeneratedSource,
93+
});
8494
}
8595
return rootNode;
8696
}
@@ -91,9 +101,10 @@ export class ReactiveScopeDependencyTreeHIR {
91101
* safe-to-evaluate subpath
92102
*/
93103
addDependency(dep: ReactiveScopeDependency): void {
94-
const {identifier, path} = dep;
104+
const {identifier, reactive, path} = dep;
95105
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
96106
identifier,
107+
reactive,
97108
this.#deps,
98109
PropertyAccessType.UnconditionalAccess,
99110
);
@@ -171,7 +182,13 @@ export class ReactiveScopeDependencyTreeHIR {
171182
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
172183
const results = new Set<ReactiveScopeDependency>();
173184
for (const [rootId, rootNode] of this.#deps.entries()) {
174-
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
185+
collectMinimalDependenciesInSubtree(
186+
rootNode,
187+
rootNode.reactive,
188+
rootId,
189+
[],
190+
results,
191+
);
175192
}
176193

177194
return results;
@@ -293,25 +310,24 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
293310
type DependencyNode = TreeNode<PropertyAccessType>;
294311

295312
/**
296-
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
297-
* longer have conditionally accessed nodes, we can simplify
298-
*
299313
* Recursively calculates minimal dependencies in a subtree.
300314
* @param node DependencyNode representing a dependency subtree.
301315
* @returns a minimal list of dependencies in this subtree.
302316
*/
303317
function collectMinimalDependenciesInSubtree(
304318
node: DependencyNode,
319+
reactive: boolean,
305320
rootIdentifier: Identifier,
306321
path: Array<DependencyPathEntry>,
307322
results: Set<ReactiveScopeDependency>,
308323
): void {
309324
if (isDependency(node.accessType)) {
310-
results.add({identifier: rootIdentifier, path});
325+
results.add({identifier: rootIdentifier, reactive, path});
311326
} else {
312327
for (const [childName, childNode] of node.properties) {
313328
collectMinimalDependenciesInSubtree(
314329
childNode,
330+
reactive,
315331
rootIdentifier,
316332
[
317333
...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;
@@ -1453,9 +1457,10 @@ export type ReactiveScope = {
14531457
range: MutableRange;
14541458

14551459
/**
1456-
* The inputs to this reactive scope
1460+
* Note the dependencies of a reactive scope are tracked in HIR and
1461+
* ReactiveFunction
14571462
*/
1458-
dependencies: ReactiveScopeDependencies;
1463+
dependencies: Array<Place>;
14591464

14601465
/**
14611466
* The set of values produced by this scope. This may be empty
@@ -1506,6 +1511,18 @@ export type DependencyPathEntry = {property: string; optional: boolean};
15061511
export type DependencyPath = Array<DependencyPathEntry>;
15071512
export type ReactiveScopeDependency = {
15081513
identifier: Identifier;
1514+
/**
1515+
* Reflects whether the base identifier is reactive. Note that some reactive
1516+
* objects may have non-reactive properties, but we do not currently track
1517+
* this.
1518+
*
1519+
* ```js
1520+
* // Technically, result[0] is reactive and result[1] is not.
1521+
* // Currently, both dependencies would be marked as reactive.
1522+
* const result = useState();
1523+
* ```
1524+
*/
1525+
reactive: boolean;
15091526
path: DependencyPath;
15101527
};
15111528

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77

88
import generate from '@babel/generator';
9-
import {printReactiveFunction} from '..';
109
import {CompilerError} from '../CompilerError';
1110
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
1211
import DisjointSet from '../Utils/DisjointSet';
@@ -287,13 +286,13 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
287286
case 'scope': {
288287
value = `[${terminal.id}] Scope ${printReactiveScopeSummary(
289288
terminal.scope,
290-
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
289+
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
291290
break;
292291
}
293292
case 'pruned-scope': {
294293
value = `[${terminal.id}] <pruned> Scope ${printReactiveScopeSummary(
295294
terminal.scope,
296-
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
295+
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
297296
break;
298297
}
299298
case 'try': {

0 commit comments

Comments
 (0)