Skip to content

Commit e0acdd8

Browse files
committed
[wip][will rewrite] Working draft of PropagateScopeDeps hir rewrite
[ghstack-poisoned]
1 parent 2c9fdc4 commit e0acdd8

File tree

47 files changed

+2327
-250
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2327
-250
lines changed

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
validatePreservedManualMemoization,
9797
validateUseMemo,
9898
} from "../Validation";
99+
import { propagateScopeDependenciesHIR } from "../HIR/PropagateScopeDepsHIR";
99100

100101
export type CompilerPipelineValue =
101102
| { kind: "ast"; name: string; value: CodegenFunction }
@@ -306,6 +307,13 @@ function* runWithEnvironment(
306307
});
307308
assertTerminalSuccessorsExist(hir);
308309
assertTerminalPredsExist(hir);
310+
311+
propagateScopeDependenciesHIR(hir);
312+
yield log({
313+
kind: "hir",
314+
name: "PropagateScopeDependenciesHIR",
315+
value: hir,
316+
});
309317
}
310318

311319
const reactiveFunction = buildReactiveFunction(hir);
@@ -359,16 +367,15 @@ function* runWithEnvironment(
359367
name: "FlattenScopesWithHooks",
360368
value: reactiveFunction,
361369
});
362-
}
370+
assertScopeInstructionsWithinScopes(reactiveFunction);
363371

364-
assertScopeInstructionsWithinScopes(reactiveFunction);
365-
366-
propagateScopeDependencies(reactiveFunction);
367-
yield log({
368-
kind: "reactive",
369-
name: "PropagateScopeDependencies",
370-
value: reactiveFunction,
371-
});
372+
propagateScopeDependencies(reactiveFunction);
373+
yield log({
374+
kind: "reactive",
375+
name: "PropagateScopeDependencies",
376+
value: reactiveFunction,
377+
});
378+
}
372379

373380
pruneNonEscapingScopes(reactiveFunction);
374381
yield log({
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { CompilerError } from "../CompilerError";
9+
import { GeneratedSource, Identifier, ReactiveScopeDependency } from "../HIR";
10+
import { printIdentifier } from "../HIR/PrintHIR";
11+
import { ReactiveScopePropertyDependency } from "../ReactiveScopes/DeriveMinimalDependencies";
12+
13+
const ENABLE_DEBUG_INVARIANTS = true;
14+
15+
/*
16+
* Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional
17+
* dependencies, preserving granular accesses when possible.
18+
*
19+
* Correctness properties:
20+
* - All dependencies to a ReactiveBlock must be tracked.
21+
* We can always truncate a dependency's path to a subpath, due to Forget assuming
22+
* deep immutability. If the value produced by a subpath has not changed, then
23+
* dependency must have not changed.
24+
* i.e. props.a === $[..] implies props.a.b === $[..]
25+
*
26+
* Note the inverse is not true, but this only means a false positive (we run the
27+
* reactive block more than needed).
28+
* i.e. props.a !== $[..] does not imply props.a.b !== $[..]
29+
*
30+
* - The dependencies of a finalized ReactiveBlock must be all safe to access
31+
* unconditionally (i.e. preserve program semantics with respect to nullthrows).
32+
* If a dependency is only accessed within a conditional, we must track the nearest
33+
* unconditionally accessed subpath instead.
34+
* @param initialDeps
35+
* @returns
36+
*/
37+
export class ReactiveScopeDependencyTreeHIR {
38+
#roots: Map<Identifier, DependencyNode> = new Map();
39+
40+
#getOrCreateRoot(identifier: Identifier, isNonNull: boolean): DependencyNode {
41+
// roots can always be accessed unconditionally in JS
42+
let rootNode = this.#roots.get(identifier);
43+
44+
if (rootNode === undefined) {
45+
rootNode = {
46+
properties: new Map(),
47+
accessType: isNonNull
48+
? PropertyAccessType.NonNullAccess
49+
: PropertyAccessType.MaybeNullAccess,
50+
};
51+
this.#roots.set(identifier, rootNode);
52+
}
53+
return rootNode;
54+
}
55+
56+
addDependency(dep: ReactiveScopePropertyDependency): void {
57+
const { path, optionalPath } = dep;
58+
let currNode = this.#getOrCreateRoot(dep.identifier, false);
59+
60+
const accessType = PropertyAccessType.MaybeNullAccess;
61+
62+
currNode.accessType = merge(currNode.accessType, accessType);
63+
64+
for (const property of path) {
65+
// all properties read 'on the way' to a dependency are marked as 'access'
66+
let currChild = getOrMakeProperty(currNode, property);
67+
currChild.accessType = merge(currChild.accessType, accessType);
68+
currNode = currChild;
69+
}
70+
71+
if (optionalPath.length === 0) {
72+
/*
73+
* If this property does not have a conditional path (i.e. a.b.c), the
74+
* final property node should be marked as an conditional/unconditional
75+
* `dependency` as based on control flow.
76+
*/
77+
currNode.accessType = merge(
78+
currNode.accessType,
79+
PropertyAccessType.MaybeNullDependency
80+
);
81+
} else {
82+
/*
83+
* Technically, we only depend on whether unconditional path `dep.path`
84+
* is nullish (not its actual value). As long as we preserve the nullthrows
85+
* behavior of `dep.path`, we can keep it as an access (and not promote
86+
* to a dependency).
87+
* See test `reduce-reactive-cond-memberexpr-join` for example.
88+
*/
89+
90+
/*
91+
* If this property has an optional path (i.e. a?.b.c), all optional
92+
* nodes should be marked accordingly.
93+
*/
94+
for (const property of optionalPath) {
95+
let currChild = getOrMakeProperty(currNode, property);
96+
currChild.accessType = merge(
97+
currChild.accessType,
98+
PropertyAccessType.MaybeNullAccess
99+
);
100+
currNode = currChild;
101+
}
102+
103+
// The final node should be marked as a conditional dependency.
104+
currNode.accessType = merge(
105+
currNode.accessType,
106+
PropertyAccessType.MaybeNullDependency
107+
);
108+
}
109+
}
110+
111+
markNodesNonNull(dep: ReactiveScopePropertyDependency): void {
112+
const accessType = PropertyAccessType.NonNullAccess;
113+
let currNode = this.#roots.get(dep.identifier);
114+
115+
let cursor = 0;
116+
while (currNode != null && cursor < dep.path.length) {
117+
currNode.accessType = merge(currNode.accessType, accessType);
118+
currNode = currNode.properties.get(dep.path[cursor++]);
119+
}
120+
if (currNode != null) {
121+
currNode.accessType = merge(currNode.accessType, accessType);
122+
}
123+
}
124+
/**
125+
* Derive a set of minimal dependencies that are safe to
126+
* access unconditionally (with respect to nullthrows behavior)
127+
*/
128+
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
129+
const results = new Set<ReactiveScopeDependency>();
130+
for (const [rootId, rootNode] of this.#roots.entries()) {
131+
if (ENABLE_DEBUG_INVARIANTS) {
132+
assertWellFormedTree(rootNode);
133+
}
134+
const deps = deriveMinimalDependenciesInSubtree(rootNode, []);
135+
136+
for (const dep of deps) {
137+
results.add({
138+
identifier: rootId,
139+
path: dep.path,
140+
});
141+
}
142+
}
143+
144+
return results;
145+
}
146+
147+
/*
148+
* Prints dependency tree to string for debugging.
149+
* @param includeAccesses
150+
* @returns string representation of DependencyTree
151+
*/
152+
printDeps(includeAccesses: boolean): string {
153+
let res: Array<Array<string>> = [];
154+
155+
for (const [rootId, rootNode] of this.#roots.entries()) {
156+
const rootResults = printSubtree(rootNode, includeAccesses).map(
157+
(result) => `${printIdentifier(rootId)}.${result}`
158+
);
159+
res.push(rootResults);
160+
}
161+
return res.flat().join("\n");
162+
}
163+
}
164+
165+
enum PropertyAccessType {
166+
MaybeNullAccess = "MaybeNullAccess",
167+
NonNullAccess = "NonNullAccess",
168+
MaybeNullDependency = "MaybeNullDependency",
169+
NonNullDependency = "NonNullDependency",
170+
}
171+
172+
const MIN_ACCESS_TYPE = PropertyAccessType.MaybeNullAccess;
173+
function isNonNull(access: PropertyAccessType): boolean {
174+
return (
175+
access === PropertyAccessType.NonNullAccess ||
176+
access === PropertyAccessType.NonNullDependency
177+
);
178+
}
179+
function isDependency(access: PropertyAccessType): boolean {
180+
return (
181+
access === PropertyAccessType.MaybeNullDependency ||
182+
access === PropertyAccessType.NonNullDependency
183+
);
184+
}
185+
186+
function merge(
187+
access1: PropertyAccessType,
188+
access2: PropertyAccessType
189+
): PropertyAccessType {
190+
const resultIsNonNull = isNonNull(access1) || isNonNull(access2);
191+
const resultIsDependency = isDependency(access1) || isDependency(access2);
192+
193+
/*
194+
* Straightforward merge.
195+
* This can be represented as bitwise OR, but is written out for readability
196+
*
197+
* Observe that `NonNullAccess | MaybeNullDependency` produces an
198+
* unconditionally accessed conditional dependency. We currently use these
199+
* as we use unconditional dependencies. (i.e. to codegen change variables)
200+
*/
201+
if (resultIsNonNull) {
202+
if (resultIsDependency) {
203+
return PropertyAccessType.NonNullDependency;
204+
} else {
205+
return PropertyAccessType.NonNullAccess;
206+
}
207+
} else {
208+
if (resultIsDependency) {
209+
return PropertyAccessType.MaybeNullDependency;
210+
} else {
211+
return PropertyAccessType.MaybeNullAccess;
212+
}
213+
}
214+
}
215+
216+
type DependencyNode = {
217+
properties: Map<string, DependencyNode>;
218+
accessType: PropertyAccessType;
219+
};
220+
221+
type ReduceResultNode = {
222+
path: Array<string>;
223+
};
224+
function assertWellFormedTree(node: DependencyNode): void {
225+
let nonNullInChildren = false;
226+
for (const childNode of node.properties.values()) {
227+
assertWellFormedTree(childNode);
228+
nonNullInChildren ||= isNonNull(childNode.accessType);
229+
}
230+
if (nonNullInChildren) {
231+
CompilerError.invariant(isNonNull(node.accessType), {
232+
reason:
233+
"[DeriveMinimialDependencies] Not well formed tree, unexpected nonnull node",
234+
description: node.accessType,
235+
loc: GeneratedSource,
236+
});
237+
}
238+
}
239+
function deriveMinimalDependenciesInSubtree(
240+
node: DependencyNode,
241+
path: Array<string>
242+
): Array<ReduceResultNode> {
243+
if (isDependency(node.accessType)) {
244+
/**
245+
* If this node is a dependency, we truncate the subtree
246+
* and return this node. e.g. deps=[`obj.a`, `obj.a.b`]
247+
* reduces to deps=[`obj.a`]
248+
*/
249+
return [{ path }];
250+
} else {
251+
if (isNonNull(node.accessType)) {
252+
/*
253+
* Only recurse into subtree dependencies if this node
254+
* is known to be non-null.
255+
*/
256+
const result: Array<ReduceResultNode> = [];
257+
for (const [childName, childNode] of node.properties) {
258+
result.push(
259+
...deriveMinimalDependenciesInSubtree(childNode, [...path, childName])
260+
);
261+
}
262+
return result;
263+
} else {
264+
/*
265+
* This only occurs when this subtree contains a dependency,
266+
* but this node is potentially nullish. As we currently
267+
* don't record optional property paths as scope dependencies,
268+
* we truncate and record this node as a dependency.
269+
*/
270+
return [{ path }];
271+
}
272+
}
273+
}
274+
275+
/*
276+
* Demote all unconditional accesses + dependencies in subtree to the
277+
* conditional equivalent, mutating subtree in place.
278+
* @param subtree unconditional node representing a subtree of dependencies
279+
*/
280+
function _demoteSubtreeToConditional(subtree: DependencyNode): void {
281+
const stack: Array<DependencyNode> = [subtree];
282+
283+
let node;
284+
while ((node = stack.pop()) !== undefined) {
285+
const { accessType, properties } = node;
286+
if (!isNonNull(accessType)) {
287+
// A conditionally accessed node should not have unconditional children
288+
continue;
289+
}
290+
node.accessType = isDependency(accessType)
291+
? PropertyAccessType.MaybeNullDependency
292+
: PropertyAccessType.MaybeNullAccess;
293+
294+
for (const childNode of properties.values()) {
295+
if (isNonNull(accessType)) {
296+
/*
297+
* No conditional node can have an unconditional node as a child, so
298+
* we only process childNode if it is unconditional
299+
*/
300+
stack.push(childNode);
301+
}
302+
}
303+
}
304+
}
305+
306+
function printSubtree(
307+
node: DependencyNode,
308+
includeAccesses: boolean
309+
): Array<string> {
310+
const results: Array<string> = [];
311+
for (const [propertyName, propertyNode] of node.properties) {
312+
if (includeAccesses || isDependency(propertyNode.accessType)) {
313+
results.push(`${propertyName} (${propertyNode.accessType})`);
314+
}
315+
const propertyResults = printSubtree(propertyNode, includeAccesses);
316+
results.push(
317+
...propertyResults.map((result) => `${propertyName}.${result}`)
318+
);
319+
}
320+
return results;
321+
}
322+
323+
function getOrMakeProperty(
324+
node: DependencyNode,
325+
property: string
326+
): DependencyNode {
327+
let child = node.properties.get(property);
328+
if (child == null) {
329+
child = {
330+
properties: new Map(),
331+
accessType: MIN_ACCESS_TYPE,
332+
};
333+
node.properties.set(property, child);
334+
}
335+
return child;
336+
}
337+
338+
function mapNonNull<T extends NonNullable<V>, V, U>(
339+
arr: Array<U>,
340+
fn: (arg0: U) => T | undefined | null
341+
): Array<T> | null {
342+
const result = [];
343+
for (let i = 0; i < arr.length; i++) {
344+
const element = fn(arr[i]);
345+
if (element) {
346+
result.push(element);
347+
} else {
348+
return null;
349+
}
350+
}
351+
return result;
352+
}

0 commit comments

Comments
 (0)