Skip to content

Commit f28f13b

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 a7c7e48 commit f28f13b

30 files changed

+1133
-184
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ function run(
123123
fnType,
124124
config,
125125
contextIdentifiers,
126+
func,
126127
logger,
127128
filename,
128129
code,

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,14 @@ import {BuiltInArrayId} from './ObjectShape';
6868
export function lower(
6969
func: NodePath<t.Function>,
7070
env: Environment,
71+
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
7172
bindings: Bindings | null = null,
7273
capturedRefs: Array<t.Identifier> = [],
73-
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
74-
parent: NodePath<t.Function> | null = null,
7574
): Result<HIRFunction, CompilerError> {
76-
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
75+
const builder = new HIRBuilder(env, {
76+
bindings,
77+
context: capturedRefs,
78+
});
7779
const context: HIRFunction['context'] = [];
7880

7981
for (const ref of capturedRefs ?? []) {
@@ -213,7 +215,7 @@ export function lower(
213215
return Ok({
214216
id,
215217
params,
216-
fnType: parent == null ? env.fnType : 'Other',
218+
fnType: bindings == null ? env.fnType : 'Other',
217219
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
218220
returnType: makeType(),
219221
body: builder.build(),
@@ -3375,7 +3377,7 @@ function lowerFunction(
33753377
| t.ObjectMethod
33763378
>,
33773379
): LoweredFunction | null {
3378-
const componentScope: Scope = builder.parentFunction.scope;
3380+
const componentScope: Scope = builder.environment.parentFunction.scope;
33793381
const capturedContext = gatherCapturedContext(expr, componentScope);
33803382

33813383
/*
@@ -3386,13 +3388,10 @@ function lowerFunction(
33863388
* This isn't a problem in practice because use Babel's scope analysis to
33873389
* identify the correct references.
33883390
*/
3389-
const lowering = lower(
3390-
expr,
3391-
builder.environment,
3392-
builder.bindings,
3393-
[...builder.context, ...capturedContext],
3394-
builder.parentFunction,
3395-
);
3391+
const lowering = lower(expr, builder.environment, builder.bindings, [
3392+
...builder.context,
3393+
...capturedContext,
3394+
]);
33963395
let loweredFunc: HIRFunction;
33973396
if (lowering.isErr()) {
33983397
lowering
@@ -3414,7 +3413,7 @@ function lowerExpressionToTemporary(
34143413
return lowerValueToTemporary(builder, value);
34153414
}
34163415

3417-
function lowerValueToTemporary(
3416+
export function lowerValueToTemporary(
34183417
builder: HIRBuilder,
34193418
value: InstructionValue,
34203419
): Place {

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: 20 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,18 @@ 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: 'Inconsistent reactive flag',
224+
loc: GeneratedSource,
225+
});
217226
}
218227
return rootNode;
219228
}
@@ -231,6 +240,7 @@ class PropertyPathRegistry {
231240
parent: parent,
232241
fullPath: {
233242
identifier: parent.fullPath.identifier,
243+
reactive: parent.fullPath.reactive,
234244
path: parent.fullPath.path.concat(entry),
235245
},
236246
hasOptional: parent.hasOptional || entry.optional,
@@ -246,7 +256,7 @@ class PropertyPathRegistry {
246256
* so all subpaths of a PropertyLoad should already exist
247257
* (e.g. a.b is added before a.b.c),
248258
*/
249-
let currNode = this.getOrCreateIdentifier(n.identifier);
259+
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
250260
if (n.path.length === 0) {
251261
return currNode;
252262
}
@@ -268,10 +278,11 @@ function getMaybeNonNullInInstruction(
268278
instr: InstructionValue,
269279
context: CollectHoistablePropertyLoadsContext,
270280
): PropertyPathNode | null {
271-
let path = null;
281+
let path: ReactiveScopeDependency | null = null;
272282
if (instr.kind === 'PropertyLoad') {
273283
path = context.temporaries.get(instr.object.identifier.id) ?? {
274284
identifier: instr.object.identifier,
285+
reactive: instr.object.reactive,
275286
path: [],
276287
};
277288
} else if (instr.kind === 'Destructure') {
@@ -334,7 +345,7 @@ function collectNonNullsInBlocks(
334345
) {
335346
const identifier = fn.params[0].identifier;
336347
knownNonNullIdentifiers.add(
337-
context.registry.getOrCreateIdentifier(identifier),
348+
context.registry.getOrCreateIdentifier(identifier, true),
338349
);
339350
}
340351
const nodes = new Map<BlockId, BlockInfo>();
@@ -565,9 +576,11 @@ function reduceMaybeOptionalChains(
565576
changed = false;
566577

567578
for (const original of optionalChainNodes) {
568-
let {identifier, path: origPath} = original.fullPath;
569-
let currNode: PropertyPathNode =
570-
registry.getOrCreateIdentifier(identifier);
579+
let {identifier, path: origPath, reactive} = original.fullPath;
580+
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
581+
identifier,
582+
reactive,
583+
);
571584
for (let i = 0; i < origPath.length; i++) {
572585
const entry = origPath[i];
573586
// 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,

0 commit comments

Comments
 (0)