@samitouri / QOS-React / commits / 0751fac747

[compiler] Optional chaining for dependencies (HIR rewrite)

Adds HIR version of `PropagateScopeDeps` to handle optional chaining. Internally, this improves memoization on ~4% of compiled files (internal links: [1](https://www.internalfb.com/intern/paste/P1610406497/)) Summarizing the changes in this PR. 1. `CollectOptionalChainDependencies` recursively traverses optional blocks down to the base. From the base, we build up a set of `baseIdentifier.propertyA?.propertyB` mappings. The tricky bit here is that optional blocks sometimes reference other optional blocks that are *not* part of the same chain e.g. a(c?.d)?.d. See code + comments in `traverseOptionalBlock` for how we avoid concatenating unrelated blocks. 2. Adding optional chains into non-null object calculation. (Note that marking `a?.b` as 'non-null' means that `a?.b.c` is safe to evaluate, *not* `(a?.b).c`. Happy to rename this / reword comments accordingly if there's a better term) This pass is split into two stages. (1) collecting non-null objects by block and (2) propagating non-null objects across blocks. The only significant change here was to (2). We add an extra reduce step `X=Reduce(Union(X, Intersect(X_neighbors)))` to merge optional and non-optional nodes (e.g. nonNulls=`{a, a?.b}` reduces to `{a, a.b}`) 3. Adding optional chains into dependency calculation. This was the trickiest. We need to take the "maximal" property chain as a dependency. Prior to this PR, we avoided taking subpaths e.g. `a.b` of `a.b.c` as dependencies by only visiting non-PropertyLoad/LoadLocal instructions. This effectively only recorded the property-path at site-of-use. Unfortunately, this *quite* doesn't work for optional chains for a few reasons: - We would need to skip relevant `StoreLocal`/`Branch terminal` instructions (but only those within optional blocks that have been successfully read). - Given an optional chain, either (1) only a subpath or (2) the entire path can be represented as a PropertyLoad. We cannot directly add the last hoistable optional-block as a dependency as MethodCalls are an edge case e.g. given a?.b.c(), we should depend on `a?.b`, not `a?.b.c` This means that we add its dependency at either the innermost unhoistable optional-block or when encountering it within its phi-join. 4. Handle optional chains in DeriveMinimalDependenciesHIR. This was also a bit tricky to formulate. Ideally, we would avoid a 2^3 case join (cond | uncond cfg, optional | not optional load, access | dependency). This PR attempts to simplify by building two trees 1. First add each hoistable path into a tree containing `Optional | NonOptional` nodes. 2. Then add each dependency into another tree containing `Optional | NonOptional`, `Access | Dependency` nodes, truncating the dependency at the earliest non-hoistable node (i.e. non-matching pair when walking the hoistable tree) ghstack-source-id: a2170f26280dfbf65a4893d8a658f863a0fd0c88 Pull Request resolved: https://github.com/facebook/react/pull/31037

Mofei Zhang committed Oct 2, 2024 at 12:53 UTC 0751fac747452af8c0494900b4afa7c56ee7b32c
37 files changed +2221 -407
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+108 -20
@@ -1,6 +1,12 @@
1 import {CompilerError} from '../CompilerError';
2 import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
3 -import {Set_intersect, Set_union, getOrInsertDefault} from '../Utils/utils';
3 +import {
4 + Set_equal,
5 + Set_filter,
6 + Set_intersect,
7 + Set_union,
8 + getOrInsertDefault,
9 +} from '../Utils/utils';
10 import {
11 BasicBlock,
12 BlockId,
@@ -15,9 +21,9 @@ import {
21 } from './HIR';
22
23 /**
18 - * Helper function for `PropagateScopeDependencies`.
19 - * Uses control flow graph analysis to determine which `Identifier`s can
20 - * be assumed to be non-null objects, on a per-block basis.
24 + * Helper function for `PropagateScopeDependencies`. Uses control flow graph
25 + * analysis to determine which `Identifier`s can be assumed to be non-null
26 + * objects, on a per-block basis.
27 *
28 * Here is an example:
29 * ```js
@@ -42,15 +48,16 @@ import {
48 * }
49 * ```
50 *
45 - * Note that we currently do NOT account for mutable / declaration range
46 - * when doing the CFG-based traversal, producing results that are technically
51 + * Note that we currently do NOT account for mutable / declaration range when
52 + * doing the CFG-based traversal, producing results that are technically
53 * incorrect but filtered by PropagateScopeDeps (which only takes dependencies
54 * on constructed value -- i.e. a scope's dependencies must have mutable ranges
55 * ending earlier than the scope start).
56 *
51 - * Take this example, this function will infer x.foo.bar as non-nullable for bb0,
52 - * via the intersection of bb1 & bb2 which in turn comes from bb3. This is technically
53 - * incorrect bb0 is before / during x's mutable range.
57 + * Take this example, this function will infer x.foo.bar as non-nullable for
58 + * bb0, via the intersection of bb1 & bb2 which in turn comes from bb3. This is
59 + * technically incorrect bb0 is before / during x's mutable range.
60 + * ```
61 * bb0:
62 * const x = ...;
63 * if cond then bb1 else bb2
@@ -62,15 +69,30 @@ import {
69 * goto bb3:
70 * bb3:
71 * x.foo.bar
72 + * ```
73 + *
74 + * @param fn
75 + * @param temporaries sidemap of identifier -> baseObject.a.b paths. Does not
76 + * contain optional chains.
77 + * @param hoistableFromOptionals sidemap of optionalBlock -> baseObject?.a
78 + * optional paths for which it's safe to evaluate non-optional loads (see
79 + * CollectOptionalChainDependencies).
80 + * @returns
81 */
82 export function collectHoistablePropertyLoads(
83 fn: HIRFunction,
84 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
85 + hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
86 ): ReadonlyMap<ScopeId, BlockInfo> {
87 const registry = new PropertyPathRegistry();
88
72 - const nodes = collectNonNullsInBlocks(fn, temporaries, registry);
73 - propagateNonNull(fn, nodes);
89 + const nodes = collectNonNullsInBlocks(
90 + fn,
91 + temporaries,
92 + hoistableFromOptionals,
93 + registry,
94 + );
95 + propagateNonNull(fn, nodes, registry);
96
97 const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
98 for (const [_, block] of fn.body.blocks) {
@@ -96,17 +118,21 @@ export type BlockInfo = {
118 */
119 type RootNode = {
120 properties: Map<string, PropertyPathNode>;
121 + optionalProperties: Map<string, PropertyPathNode>;
122 parent: null;
123 // Recorded to make later computations simpler
124 fullPath: ReactiveScopeDependency;
125 + hasOptional: boolean;
126 root: IdentifierId;
127 };
128
129 type PropertyPathNode =
130 | {
131 properties: Map<string, PropertyPathNode>;
132 + optionalProperties: Map<string, PropertyPathNode>;
133 parent: PropertyPathNode;
134 fullPath: ReactiveScopeDependency;
135 + hasOptional: boolean;
136 }
137 | RootNode;
138
@@ -124,10 +150,12 @@ class PropertyPathRegistry {
150 rootNode = {
151 root: identifier.id,
152 properties: new Map(),
153 + optionalProperties: new Map(),
154 fullPath: {
155 identifier,
156 path: [],
157 },
158 + hasOptional: false,
159 parent: null,
160 };
161 this.roots.set(identifier.id, rootNode);
@@ -139,23 +167,20 @@ class PropertyPathRegistry {
167 parent: PropertyPathNode,
168 entry: DependencyPathEntry,
169 ): PropertyPathNode {
142 - if (entry.optional) {
143 - CompilerError.throwTodo({
144 - reason: 'handle optional nodes',
145 - loc: GeneratedSource,
146 - });
147 - }
148 - let child = parent.properties.get(entry.property);
170 + const map = entry.optional ? parent.optionalProperties : parent.properties;
171 + let child = map.get(entry.property);
172 if (child == null) {
173 child = {
174 properties: new Map(),
175 + optionalProperties: new Map(),
176 parent: parent,
177 fullPath: {
178 identifier: parent.fullPath.identifier,
179 path: parent.fullPath.path.concat(entry),
180 },
181 + hasOptional: parent.hasOptional || entry.optional,
182 };
158 - parent.properties.set(entry.property, child);
183 + map.set(entry.property, child);
184 }
185 return child;
186 }
@@ -216,6 +241,7 @@ function addNonNullPropertyPath(
241 function collectNonNullsInBlocks(
242 fn: HIRFunction,
243 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
244 + hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
245 registry: PropertyPathRegistry,
246 ): ReadonlyMap<BlockId, BlockInfo> {
247 /**
@@ -252,6 +278,13 @@ function collectNonNullsInBlocks(
278 const assumedNonNullObjects = new Set<PropertyPathNode>(
279 knownNonNullIdentifiers,
280 );
281 +
282 + const maybeOptionalChain = hoistableFromOptionals.get(block.id);
283 + if (maybeOptionalChain != null) {
284 + assumedNonNullObjects.add(
285 + registry.getOrCreateProperty(maybeOptionalChain),
286 + );
287 + }
288 for (const instr of block.instructions) {
289 if (instr.value.kind === 'PropertyLoad') {
290 const source = temporaries.get(instr.value.object.identifier.id) ?? {
@@ -303,6 +336,7 @@ function collectNonNullsInBlocks(
336 function propagateNonNull(
337 fn: HIRFunction,
338 nodes: ReadonlyMap<BlockId, BlockInfo>,
339 + registry: PropertyPathRegistry,
340 ): void {
341 const blockSuccessors = new Map<BlockId, Set<BlockId>>();
342 const terminalPreds = new Set<BlockId>();
@@ -388,10 +422,17 @@ function propagateNonNull(
422
423 const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
424 const mergedObjects = Set_union(prevObjects, neighborAccesses);
425 + reduceMaybeOptionalChains(mergedObjects, registry);
426
427 assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
428 traversalState.set(nodeId, 'done');
394 - changed ||= prevObjects.size !== mergedObjects.size;
429 + /**
430 + * Note that it's not sufficient to compare set sizes since
431 + * reduceMaybeOptionalChains may replace optional-chain loads with
432 + * unconditional loads. This could in turn change `assumedNonNullObjects` of
433 + * downstream blocks and backedges.
434 + */
435 + changed ||= !Set_equal(prevObjects, mergedObjects);
436 return changed;
437 }
438 const traversalState = new Map<BlockId, 'done' | 'active'>();
@@ -440,3 +481,50 @@ export function assertNonNull<T extends NonNullable<U>, U>(
481 });
482 return value;
483 }
484 +
485 +/**
486 + * Any two optional chains with different operations . vs ?. but the same set of
487 + * property strings paths de-duplicates.
488 + *
489 + * Intuitively: given <base>?.b, we know <base> to be either hoistable or not.
490 + * If unconditional reads from <base> are hoistable, we can replace all
491 + * <base>?.PROPERTY_STRING subpaths with <base>.PROPERTY_STRING
492 + */
493 +function reduceMaybeOptionalChains(
494 + nodes: Set<PropertyPathNode>,
495 + registry: PropertyPathRegistry,
496 +): void {
497 + let optionalChainNodes = Set_filter(nodes, n => n.hasOptional);
498 + if (optionalChainNodes.size === 0) {
499 + return;
500 + }
501 + let changed: boolean;
502 + do {
503 + changed = false;
504 +
505 + for (const original of optionalChainNodes) {
506 + let {identifier, path: origPath} = original.fullPath;
507 + let currNode: PropertyPathNode =
508 + registry.getOrCreateIdentifier(identifier);
509 + for (let i = 0; i < origPath.length; i++) {
510 + const entry = origPath[i];
511 + // If the base is known to be non-null, replace with a non-optional load
512 + const nextEntry: DependencyPathEntry =
513 + entry.optional && nodes.has(currNode)
514 + ? {property: entry.property, optional: false}
515 + : entry;
516 + currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
517 + currNode,
518 + nextEntry,
519 + );
520 + }
521 + if (currNode !== original) {
522 + changed = true;
523 + optionalChainNodes.delete(original);
524 + optionalChainNodes.add(currNode);
525 + nodes.delete(original);
526 + nodes.add(currNode);
527 + }
528 + }
529 + } while (changed);
530 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts new
+382
@@ -0,0 +1,382 @@
1 +import {CompilerError} from '..';
2 +import {assertNonNull} from './CollectHoistablePropertyLoads';
3 +import {
4 + BlockId,
5 + BasicBlock,
6 + InstructionId,
7 + IdentifierId,
8 + ReactiveScopeDependency,
9 + BranchTerminal,
10 + TInstruction,
11 + PropertyLoad,
12 + StoreLocal,
13 + GotoVariant,
14 + TBasicBlock,
15 + OptionalTerminal,
16 + HIRFunction,
17 + DependencyPathEntry,
18 +} from './HIR';
19 +import {printIdentifier} from './PrintHIR';
20 +
21 +export function collectOptionalChainSidemap(
22 + fn: HIRFunction,
23 +): OptionalChainSidemap {
24 + const context: OptionalTraversalContext = {
25 + blocks: fn.body.blocks,
26 + seenOptionals: new Set(),
27 + processedInstrsInOptional: new Set(),
28 + temporariesReadInOptional: new Map(),
29 + hoistableObjects: new Map(),
30 + };
31 + for (const [_, block] of fn.body.blocks) {
32 + if (
33 + block.terminal.kind === 'optional' &&
34 + !context.seenOptionals.has(block.id)
35 + ) {
36 + traverseOptionalBlock(
37 + block as TBasicBlock<OptionalTerminal>,
38 + context,
39 + null,
40 + );
41 + }
42 + }
43 +
44 + return {
45 + temporariesReadInOptional: context.temporariesReadInOptional,
46 + processedInstrsInOptional: context.processedInstrsInOptional,
47 + hoistableObjects: context.hoistableObjects,
48 + };
49 +}
50 +export type OptionalChainSidemap = {
51 + /**
52 + * Stores the correct property mapping (e.g. `a?.b` instead of `a.b`) for
53 + * dependency calculation. Note that we currently do not store anything on
54 + * outer phi nodes.
55 + */
56 + temporariesReadInOptional: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
57 + /**
58 + * Records instructions (PropertyLoads, StoreLocals, and test terminals)
59 + * processed in this pass. When extracting dependencies in
60 + * PropagateScopeDependencies, these instructions are skipped.
61 + *
62 + * E.g. given a?.b
63 + * ```
64 + * bb0
65 + * $0 = LoadLocal 'a'
66 + * test $0 then=bb1 <- Avoid adding dependencies from these instructions, as
67 + * bb1 the sidemap produced by readOptionalBlock already maps
68 + * $1 = PropertyLoad $0.'b' <- $1 and $2 back to a?.b. Instead, we want to add a?.b
69 + * StoreLocal $2 = $1 <- as a dependency when $1 or $2 are later used in either
70 + * - an unhoistable expression within an outer optional
71 + * block e.g. MethodCall
72 + * - a phi node (if the entire optional value is hoistable)
73 + * ```
74 + *
75 + * Note that mapping blockIds to their evaluated dependency path does not
76 + * work, since values produced by inner optional chains may be referenced in
77 + * outer ones
78 + * ```
79 + * a?.b.c()
80 + * ->
81 + * bb0
82 + * $0 = LoadLocal 'a'
83 + * test $0 then=bb1
84 + * bb1
85 + * $1 = PropertyLoad $0.'b'
86 + * StoreLocal $2 = $1
87 + * goto bb2
88 + * bb2
89 + * test $2 then=bb3
90 + * bb3:
91 + * $3 = PropertyLoad $2.'c'
92 + * StoreLocal $4 = $3
93 + * goto bb4
94 + * bb4
95 + * test $4 then=bb5
96 + * bb5:
97 + * $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4!
98 + * ```
99 + */
100 + processedInstrsInOptional: ReadonlySet<InstructionId>;
101 + /**
102 + * Records optional chains for which we can safely evaluate non-optional
103 + * PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
104 + * the optional terminal in bb1.
105 + * ```js
106 + * bb1:
107 + * ...
108 + * Optional optional=false test=bb2 fallth=...
109 + * bb2:
110 + * Optional optional=true test=bb3 fallth=...
111 + * ...
112 + * ```
113 + */
114 + hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
115 +};
116 +
117 +type OptionalTraversalContext = {
118 + blocks: ReadonlyMap<BlockId, BasicBlock>;
119 +
120 + // Track optional blocks to avoid outer calls into nested optionals
121 + seenOptionals: Set<BlockId>;
122 +
123 + processedInstrsInOptional: Set<InstructionId>;
124 + temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
125 + hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
126 +};
127 +
128 +/**
129 + * Match the consequent and alternate blocks of an optional.
130 + * @returns propertyload computed by the consequent block, or null if the
131 + * consequent block is not a simple PropertyLoad.
132 + */
133 +function matchOptionalTestBlock(
134 + terminal: BranchTerminal,
135 + blocks: ReadonlyMap<BlockId, BasicBlock>,
136 +): {
137 + consequentId: IdentifierId;
138 + property: string;
139 + propertyId: IdentifierId;
140 + storeLocalInstrId: InstructionId;
141 + consequentGoto: BlockId;
142 +} | null {
143 + const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
144 + if (
145 + consequentBlock.instructions.length === 2 &&
146 + consequentBlock.instructions[0].value.kind === 'PropertyLoad' &&
147 + consequentBlock.instructions[1].value.kind === 'StoreLocal'
148 + ) {
149 + const propertyLoad: TInstruction<PropertyLoad> = consequentBlock
150 + .instructions[0] as TInstruction<PropertyLoad>;
151 + const storeLocal: StoreLocal = consequentBlock.instructions[1].value;
152 + const storeLocalInstrId = consequentBlock.instructions[1].id;
153 + CompilerError.invariant(
154 + propertyLoad.value.object.identifier.id === terminal.test.identifier.id,
155 + {
156 + reason:
157 + '[OptionalChainDeps] Inconsistent optional chaining property load',
158 + description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
159 + loc: propertyLoad.loc,
160 + },
161 + );
162 +
163 + CompilerError.invariant(
164 + storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
165 + {
166 + reason: '[OptionalChainDeps] Unexpected storeLocal',
167 + loc: propertyLoad.loc,
168 + },
169 + );
170 + if (
171 + consequentBlock.terminal.kind !== 'goto' ||
172 + consequentBlock.terminal.variant !== GotoVariant.Break
173 + ) {
174 + return null;
175 + }
176 + const alternate = assertNonNull(blocks.get(terminal.alternate));
177 +
178 + CompilerError.invariant(
179 + alternate.instructions.length === 2 &&
180 + alternate.instructions[0].value.kind === 'Primitive' &&
181 + alternate.instructions[1].value.kind === 'StoreLocal',
182 + {
183 + reason: 'Unexpected alternate structure',
184 + loc: terminal.loc,
185 + },
186 + );
187 +
188 + return {
189 + consequentId: storeLocal.lvalue.place.identifier.id,
190 + property: propertyLoad.value.property,
191 + propertyId: propertyLoad.lvalue.identifier.id,
192 + storeLocalInstrId,
193 + consequentGoto: consequentBlock.terminal.block,
194 + };
195 + }
196 + return null;
197 +}
198 +
199 +/**
200 + * Traverse into the optional block and all transitively referenced blocks to
201 + * collect sidemaps of optional chain dependencies.
202 + *
203 + * @returns the IdentifierId representing the optional block if the block and
204 + * all transitively referenced optional blocks precisely represent a chain of
205 + * property loads. If any part of the optional chain is not hoistable, returns
206 + * null.
207 + */
208 +function traverseOptionalBlock(
209 + optional: TBasicBlock<OptionalTerminal>,
210 + context: OptionalTraversalContext,
211 + outerAlternate: BlockId | null,
212 +): IdentifierId | null {
213 + context.seenOptionals.add(optional.id);
214 + const maybeTest = context.blocks.get(optional.terminal.test)!;
215 + let test: BranchTerminal;
216 + let baseObject: ReactiveScopeDependency;
217 + if (maybeTest.terminal.kind === 'branch') {
218 + CompilerError.invariant(optional.terminal.optional, {
219 + reason: '[OptionalChainDeps] Expect base case to be always optional',
220 + loc: optional.terminal.loc,
221 + });
222 + /**
223 + * Optional base expressions are currently within value blocks which cannot
224 + * be interrupted by scope boundaries. As such, the only dependencies we can
225 + * hoist out of optional chains are property load chains with no intervening
226 + * instructions.
227 + *
228 + * Ideally, we would be able to flatten base instructions out of optional
229 + * blocks, but this would require changes to HIR.
230 + *
231 + * For now, only match base expressions that are straightforward
232 + * PropertyLoad chains
233 + */
234 + if (
235 + maybeTest.instructions.length === 0 ||
236 + maybeTest.instructions[0].value.kind !== 'LoadLocal'
237 + ) {
238 + return null;
239 + }
240 + const path: Array<DependencyPathEntry> = [];
241 + for (let i = 1; i < maybeTest.instructions.length; i++) {
242 + const instrVal = maybeTest.instructions[i].value;
243 + const prevInstr = maybeTest.instructions[i - 1];
244 + if (
245 + instrVal.kind === 'PropertyLoad' &&
246 + instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
247 + ) {
248 + path.push({property: instrVal.property, optional: false});
249 + } else {
250 + return null;
251 + }
252 + }
253 + CompilerError.invariant(
254 + maybeTest.terminal.test.identifier.id ===
255 + maybeTest.instructions.at(-1)!.lvalue.identifier.id,
256 + {
257 + reason: '[OptionalChainDeps] Unexpected test expression',
258 + loc: maybeTest.terminal.loc,
259 + },
260 + );
261 + baseObject = {
262 + identifier: maybeTest.instructions[0].value.place.identifier,
263 + path,
264 + };
265 + test = maybeTest.terminal;
266 + } else if (maybeTest.terminal.kind === 'optional') {
267 + /**
268 + * This is either
269 + * - <inner_optional>?.property (optional=true)
270 + * - <inner_optional>.property (optional=false)
271 + * - <inner_optional> <other operation>
272 + * - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
273 + */
274 + const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
275 + if (testBlock!.terminal.kind !== 'branch') {
276 + /**
277 + * Fallthrough of the inner optional should be a block with no
278 + * instructions, terminating with Test($<temporary written to from
279 + * StoreLocal>)
280 + */
281 + CompilerError.throwTodo({
282 + reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
283 + loc: maybeTest.terminal.loc,
284 + });
285 + }
286 + /**
287 + * Recurse into inner optional blocks to collect inner optional-chain
288 + * expressions, regardless of whether we can match the outer one to a
289 + * PropertyLoad.
290 + */
291 + const innerOptional = traverseOptionalBlock(
292 + maybeTest as TBasicBlock<OptionalTerminal>,
293 + context,
294 + testBlock.terminal.alternate,
295 + );
296 + if (innerOptional == null) {
297 + return null;
298 + }
299 +
300 + /**
301 + * Check that the inner optional is part of the same optional-chain as the
302 + * outer one. This is not guaranteed, e.g. given a(c?.d)?.d
303 + * ```
304 + * bb0:
305 + * Optional test=bb1
306 + * bb1:
307 + * $0 = LoadLocal a <-- part 1 of the outer optional-chaining base
308 + * Optional test=bb2 fallth=bb5 <-- start of optional chain for c?.d
309 + * bb2:
310 + * ... (optional chain for c?.d)
311 + * ...
312 + * bb5:
313 + * $1 = phi(c.d, undefined) <-- part 2 (continuation) of the outer optional-base
314 + * $2 = Call $0($1)
315 + * Branch $2 ...
316 + * ```
317 + */
318 + if (testBlock.terminal.test.identifier.id !== innerOptional) {
319 + return null;
320 + }
321 +
322 + if (!optional.terminal.optional) {
323 + /**
324 + * If this is an non-optional load participating in an optional chain
325 + * (e.g. loading the `c` property in `a?.b.c`), record that PropertyLoads
326 + * from the inner optional value are hoistable.
327 + */
328 + context.hoistableObjects.set(
329 + optional.id,
330 + assertNonNull(context.temporariesReadInOptional.get(innerOptional)),
331 + );
332 + }
333 + baseObject = assertNonNull(
334 + context.temporariesReadInOptional.get(innerOptional),
335 + );
336 + test = testBlock.terminal;
337 + } else {
338 + return null;
339 + }
340 +
341 + if (test.alternate === outerAlternate) {
342 + CompilerError.invariant(optional.instructions.length === 0, {
343 + reason:
344 + '[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
345 + 'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
346 + loc: optional.terminal.loc,
347 + });
348 + }
349 + const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
350 + if (!matchConsequentResult) {
351 + // Optional chain consequent is not hoistable e.g. a?.[computed()]
352 + return null;
353 + }
354 + CompilerError.invariant(
355 + matchConsequentResult.consequentGoto === optional.terminal.fallthrough,
356 + {
357 + reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
358 + description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
359 + loc: optional.terminal.loc,
360 + },
361 + );
362 + const load = {
363 + identifier: baseObject.identifier,
364 + path: [
365 + ...baseObject.path,
366 + {
367 + property: matchConsequentResult.property,
368 + optional: optional.terminal.optional,
369 + },
370 + ],
371 + };
372 + context.processedInstrsInOptional.add(
373 + matchConsequentResult.storeLocalInstrId,
374 + );
375 + context.processedInstrsInOptional.add(test.id);
376 + context.temporariesReadInOptional.set(
377 + matchConsequentResult.consequentId,
378 + load,
379 + );
380 + context.temporariesReadInOptional.set(matchConsequentResult.propertyId, load);
381 + return matchConsequentResult.consequentId;
382 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts
+227 -140
@@ -6,97 +6,173 @@
6 */
7
8 import {CompilerError} from '../CompilerError';
9 -import {GeneratedSource, Identifier, ReactiveScopeDependency} from '../HIR';
9 +import {
10 + DependencyPathEntry,
11 + GeneratedSource,
12 + Identifier,
13 + ReactiveScopeDependency,
14 +} from '../HIR';
15 import {printIdentifier} from '../HIR/PrintHIR';
16 import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies';
17
13 -const ENABLE_DEBUG_INVARIANTS = true;
14 -
18 /**
19 * Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR
20 * for detailed explanation.
21 */
22 export class ReactiveScopeDependencyTreeHIR {
20 - #roots: Map<Identifier, DependencyNode> = new Map();
23 + /**
24 + * Paths from which we can hoist PropertyLoads. If an `identifier`,
25 + * `identifier.path`, or `identifier?.path` is in this map, it is safe to
26 + * evaluate (non-optional) PropertyLoads from.
27 + */
28 + #hoistableObjects: Map<Identifier, HoistableNode> = new Map();
29 + #deps: Map<Identifier, DependencyNode> = new Map();
30 +
31 + /**
32 + * @param hoistableObjects a set of paths from which we can safely evaluate
33 + * PropertyLoads. Note that we expect these to not contain duplicates (e.g.
34 + * both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges
35 + * duplicates when traversing the CFG.
36 + */
37 + constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
38 + for (const {path, identifier} of hoistableObjects) {
39 + let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
40 + identifier,
41 + this.#hoistableObjects,
42 + path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
43 + );
44
22 - #getOrCreateRoot(
45 + for (let i = 0; i < path.length; i++) {
46 + const prevAccessType = currNode.properties.get(
47 + path[i].property,
48 + )?.accessType;
49 + const accessType =
50 + i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull';
51 + CompilerError.invariant(
52 + prevAccessType == null || prevAccessType === accessType,
53 + {
54 + reason: 'Conflicting access types',
55 + loc: GeneratedSource,
56 + },
57 + );
58 + let nextNode = currNode.properties.get(path[i].property);
59 + if (nextNode == null) {
60 + nextNode = {
61 + properties: new Map(),
62 + accessType,
63 + };
64 + currNode.properties.set(path[i].property, nextNode);
65 + }
66 + currNode = nextNode;
67 + }
68 + }
69 + }
70 +
71 + static #getOrCreateRoot<T extends string>(
72 identifier: Identifier,
24 - accessType: PropertyAccessType,
25 - ): DependencyNode {
73 + roots: Map<Identifier, TreeNode<T>>,
74 + defaultAccessType: T,
75 + ): TreeNode<T> {
76 // roots can always be accessed unconditionally in JS
27 - let rootNode = this.#roots.get(identifier);
77 + let rootNode = roots.get(identifier);
78
79 if (rootNode === undefined) {
80 rootNode = {
81 properties: new Map(),
32 - accessType,
82 + accessType: defaultAccessType,
83 };
34 - this.#roots.set(identifier, rootNode);
84 + roots.set(identifier, rootNode);
85 }
86 return rootNode;
87 }
88
89 + /**
90 + * Join a dependency with `#hoistableObjects` to record the hoistable
91 + * dependency. This effectively truncates @param dep to its maximal
92 + * safe-to-evaluate subpath
93 + */
94 addDependency(dep: ReactiveScopePropertyDependency): void {
40 - const {path} = dep;
41 - let currNode = this.#getOrCreateRoot(dep.identifier, MIN_ACCESS_TYPE);
42 -
43 - const accessType = PropertyAccessType.Access;
44 -
45 - currNode.accessType = merge(currNode.accessType, accessType);
46 -
47 - for (const property of path) {
48 - // all properties read 'on the way' to a dependency are marked as 'access'
49 - let currChild = makeOrMergeProperty(
50 - currNode,
51 - property.property,
52 - accessType,
53 - );
54 - currNode = currChild;
55 - }
56 -
57 - /*
58 - * If this property does not have a conditional path (i.e. a.b.c), the
59 - * final property node should be marked as an conditional/unconditional
60 - * `dependency` as based on control flow.
61 - */
62 - currNode.accessType = merge(
63 - currNode.accessType,
64 - PropertyAccessType.Dependency,
95 + const {identifier, path} = dep;
96 + let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
97 + identifier,
98 + this.#deps,
99 + PropertyAccessType.UnconditionalAccess,
100 );
66 - }
101 + /**
102 + * hoistableCursor is null if depCursor is not an object we can hoist
103 + * property reads from otherwise, it represents the same node in the
104 + * hoistable / cfg-informed tree
105 + */
106 + let hoistableCursor: HoistableNode | undefined =
107 + this.#hoistableObjects.get(identifier);
108
68 - markNodesNonNull(dep: ReactiveScopePropertyDependency): void {
69 - const accessType = PropertyAccessType.NonNullAccess;
70 - let currNode = this.#roots.get(dep.identifier);
109 + // All properties read 'on the way' to a dependency are marked as 'access'
110 + for (const entry of path) {
111 + let nextHoistableCursor: HoistableNode | undefined;
112 + let nextDepCursor: DependencyNode;
113 + if (entry.optional) {
114 + /**
115 + * No need to check the access type since we can match both optional or non-optionals
116 + * in the hoistable
117 + * e.g. a?.b<rest> is hoistable if a.b<rest> is hoistable
118 + */
119 + if (hoistableCursor != null) {
120 + nextHoistableCursor = hoistableCursor?.properties.get(entry.property);
121 + }
122
72 - let cursor = 0;
73 - while (currNode != null && cursor < dep.path.length) {
74 - currNode.accessType = merge(currNode.accessType, accessType);
75 - currNode = currNode.properties.get(dep.path[cursor++].property);
76 - }
77 - if (currNode != null) {
78 - currNode.accessType = merge(currNode.accessType, accessType);
123 + let accessType;
124 + if (
125 + hoistableCursor != null &&
126 + hoistableCursor.accessType === 'NonNull'
127 + ) {
128 + /**
129 + * For an optional chain dep `a?.b`: if the hoistable tree only
130 + * contains `a`, we can keep either `a?.b` or 'a.b' as a dependency.
131 + * (note that we currently do the latter for perf)
132 + */
133 + accessType = PropertyAccessType.UnconditionalAccess;
134 + } else {
135 + /**
136 + * Given that it's safe to evaluate `depCursor` and optional load
137 + * never throws, it's also safe to evaluate `depCursor?.entry`
138 + */
139 + accessType = PropertyAccessType.OptionalAccess;
140 + }
141 + nextDepCursor = makeOrMergeProperty(
142 + depCursor,
143 + entry.property,
144 + accessType,
145 + );
146 + } else if (
147 + hoistableCursor != null &&
148 + hoistableCursor.accessType === 'NonNull'
149 + ) {
150 + nextHoistableCursor = hoistableCursor.properties.get(entry.property);
151 + nextDepCursor = makeOrMergeProperty(
152 + depCursor,
153 + entry.property,
154 + PropertyAccessType.UnconditionalAccess,
155 + );
156 + } else {
157 + /**
158 + * Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from
159 + */
160 + break;
161 + }
162 + depCursor = nextDepCursor;
163 + hoistableCursor = nextHoistableCursor;
164 }
165 + // mark the final node as a dependency
166 + depCursor.accessType = merge(
167 + depCursor.accessType,
168 + PropertyAccessType.OptionalDependency,
169 + );
170 }
171
82 - /**
83 - * Derive a set of minimal dependencies that are safe to
84 - * access unconditionally (with respect to nullthrows behavior)
85 - */
172 deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
173 const results = new Set<ReactiveScopeDependency>();
88 - for (const [rootId, rootNode] of this.#roots.entries()) {
89 - if (ENABLE_DEBUG_INVARIANTS) {
90 - assertWellFormedTree(rootNode);
91 - }
92 - const deps = deriveMinimalDependenciesInSubtree(rootNode, []);
93 -
94 - for (const dep of deps) {
95 - results.add({
96 - identifier: rootId,
97 - path: dep.path.map(s => ({property: s, optional: false})),
98 - });
99 - }
174 + for (const [rootId, rootNode] of this.#deps.entries()) {
175 + collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
176 }
177
178 return results;
@@ -110,7 +186,7 @@ export class ReactiveScopeDependencyTreeHIR {
186 printDeps(includeAccesses: boolean): string {
187 let res: Array<Array<string>> = [];
188
113 - for (const [rootId, rootNode] of this.#roots.entries()) {
189 + for (const [rootId, rootNode] of this.#deps.entries()) {
190 const rootResults = printSubtree(rootNode, includeAccesses).map(
191 result => `${printIdentifier(rootId)}.${result}`,
192 );
@@ -118,31 +194,64 @@ export class ReactiveScopeDependencyTreeHIR {
194 }
195 return res.flat().join('\n');
196 }
197 +
198 + static debug<T extends string>(roots: Map<Identifier, TreeNode<T>>): string {
199 + const buf: Array<string> = [`tree() [`];
200 + for (const [rootId, rootNode] of roots) {
201 + buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
202 + this.#debugImpl(buf, rootNode, 1);
203 + }
204 + buf.push(']');
205 + return buf.length > 2 ? buf.join('\n') : buf.join('');
206 + }
207 +
208 + static #debugImpl<T extends string>(
209 + buf: Array<string>,
210 + node: TreeNode<T>,
211 + depth: number = 0,
212 + ): void {
213 + for (const [property, childNode] of node.properties) {
214 + buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
215 + this.#debugImpl(buf, childNode, depth + 1);
216 + }
217 + }
218 }
219
220 +/*
221 + * Enum representing the access type of single property on a parent object.
222 + * We distinguish on two independent axes:
223 + * Optional / Unconditional:
224 + * - whether this property is an optional load (within an optional chain)
225 + * Access / Dependency:
226 + * - Access: this property is read on the path of a dependency. We do not
227 + * need to track change variables for accessed properties. Tracking accesses
228 + * helps Forget do more granular dependency tracking.
229 + * - Dependency: this property is read as a dependency and we must track changes
230 + * to it for correctness.
231 + * ```javascript
232 + * // props.a is a dependency here and must be tracked
233 + * deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
234 + * // props.a is just an access here and does not need to be tracked
235 + * deps: {props.a.b} ---> minimalDeps: {props.a.b}
236 + * ```
237 + */
238 enum PropertyAccessType {
124 - Access = 'Access',
125 - NonNullAccess = 'NonNullAccess',
126 - Dependency = 'Dependency',
127 - NonNullDependency = 'NonNullDependency',
239 + OptionalAccess = 'OptionalAccess',
240 + UnconditionalAccess = 'UnconditionalAccess',
241 + OptionalDependency = 'OptionalDependency',
242 + UnconditionalDependency = 'UnconditionalDependency',
243 }
244
130 -const MIN_ACCESS_TYPE = PropertyAccessType.Access;
131 -/**
132 - * "NonNull" means that PropertyReads from a node are side-effect free,
133 - * as the node is (1) immutable and (2) has unconditional propertyloads
134 - * somewhere in the cfg.
135 - */
136 -function isNonNull(access: PropertyAccessType): boolean {
245 +function isOptional(access: PropertyAccessType): boolean {
246 return (
138 - access === PropertyAccessType.NonNullAccess ||
139 - access === PropertyAccessType.NonNullDependency
247 + access === PropertyAccessType.OptionalAccess ||
248 + access === PropertyAccessType.OptionalDependency
249 );
250 }
251 function isDependency(access: PropertyAccessType): boolean {
252 return (
144 - access === PropertyAccessType.Dependency ||
145 - access === PropertyAccessType.NonNullDependency
253 + access === PropertyAccessType.OptionalDependency ||
254 + access === PropertyAccessType.UnconditionalDependency
255 );
256 }
257
@@ -150,92 +259,70 @@ function merge(
259 access1: PropertyAccessType,
260 access2: PropertyAccessType,
261 ): PropertyAccessType {
153 - const resultisNonNull = isNonNull(access1) || isNonNull(access2);
262 + const resultIsUnconditional = !(isOptional(access1) && isOptional(access2));
263 const resultIsDependency = isDependency(access1) || isDependency(access2);
264
265 /*
266 * Straightforward merge.
267 * This can be represented as bitwise OR, but is written out for readability
268 *
160 - * Observe that `NonNullAccess | Dependency` produces an
269 + * Observe that `UnconditionalAccess | ConditionalDependency` produces an
270 * unconditionally accessed conditional dependency. We currently use these
271 * as we use unconditional dependencies. (i.e. to codegen change variables)
272 */
164 - if (resultisNonNull) {
273 + if (resultIsUnconditional) {
274 if (resultIsDependency) {
166 - return PropertyAccessType.NonNullDependency;
275 + return PropertyAccessType.UnconditionalDependency;
276 } else {
168 - return PropertyAccessType.NonNullAccess;
277 + return PropertyAccessType.UnconditionalAccess;
278 }
279 } else {
280 + // result is optional
281 if (resultIsDependency) {
172 - return PropertyAccessType.Dependency;
282 + return PropertyAccessType.OptionalDependency;
283 } else {
174 - return PropertyAccessType.Access;
284 + return PropertyAccessType.OptionalAccess;
285 }
286 }
287 }
288
179 -type DependencyNode = {
180 - properties: Map<string, DependencyNode>;
181 - accessType: PropertyAccessType;
289 +type TreeNode<T extends string> = {
290 + properties: Map<string, TreeNode<T>>;
291 + accessType: T;
292 };
293 +type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
294 +type DependencyNode = TreeNode<PropertyAccessType>;
295
184 -type ReduceResultNode = {
185 - path: Array<string>;
186 -};
187 -
188 -function assertWellFormedTree(node: DependencyNode): void {
189 - let nonNullInChildren = false;
190 - for (const childNode of node.properties.values()) {
191 - assertWellFormedTree(childNode);
192 - nonNullInChildren ||= isNonNull(childNode.accessType);
193 - }
194 - if (nonNullInChildren) {
195 - CompilerError.invariant(isNonNull(node.accessType), {
196 - reason:
197 - '[DeriveMinimialDependencies] Not well formed tree, unexpected non-null node',
198 - description: node.accessType,
199 - loc: GeneratedSource,
200 - });
201 - }
202 -}
203 -
204 -function deriveMinimalDependenciesInSubtree(
296 +/**
297 + * TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
298 + * longer have conditionally accessed nodes, we can simplify
299 + *
300 + * Recursively calculates minimal dependencies in a subtree.
301 + * @param node DependencyNode representing a dependency subtree.
302 + * @returns a minimal list of dependencies in this subtree.
303 + */
304 +function collectMinimalDependenciesInSubtree(
305 node: DependencyNode,
206 - path: Array<string>,
207 -): Array<ReduceResultNode> {
306 + rootIdentifier: Identifier,
307 + path: Array<DependencyPathEntry>,
308 + results: Set<ReactiveScopeDependency>,
309 +): void {
310 if (isDependency(node.accessType)) {
209 - /**
210 - * If this node is a dependency, we truncate the subtree
211 - * and return this node. e.g. deps=[`obj.a`, `obj.a.b`]
212 - * reduces to deps=[`obj.a`]
213 - */
214 - return [{path}];
311 + results.add({identifier: rootIdentifier, path});
312 } else {
216 - if (isNonNull(node.accessType)) {
217 - /*
218 - * Only recurse into subtree dependencies if this node
219 - * is known to be non-null.
220 - */
221 - const result: Array<ReduceResultNode> = [];
222 - for (const [childName, childNode] of node.properties) {
223 - result.push(
224 - ...deriveMinimalDependenciesInSubtree(childNode, [
225 - ...path,
226 - childName,
227 - ]),
228 - );
229 - }
230 - return result;
231 - } else {
232 - /*
233 - * This only occurs when this subtree contains a dependency,
234 - * but this node is potentially nullish. As we currently
235 - * don't record optional property paths as scope dependencies,
236 - * we truncate and record this node as a dependency.
237 - */
238 - return [{path}];
313 + for (const [childName, childNode] of node.properties) {
314 + collectMinimalDependenciesInSubtree(
315 + childNode,
316 + rootIdentifier,
317 + [
318 + ...path,
319 + {
320 + property: childName,
321 + optional: isOptional(childNode.accessType),
322 + },
323 + ],
324 + results,
325 + );
326 }
327 }
328 }
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+1
@@ -367,6 +367,7 @@ export type BasicBlock = {
367 preds: Set<BlockId>;
368 phis: Set<Phi>;
369 };
370 +export type TBasicBlock<T extends Terminal> = BasicBlock & {terminal: T};
371
372 /*
373 * Terminal nodes generally represent statements that affect control flow, such as
compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts
+67 -44
@@ -17,10 +17,7 @@ import {
17 areEqualPaths,
18 IdentifierId,
19 } from './HIR';
20 -import {
21 - BlockInfo,
22 - collectHoistablePropertyLoads,
23 -} from './CollectHoistablePropertyLoads';
20 +import {collectHoistablePropertyLoads} from './CollectHoistablePropertyLoads';
21 import {
22 ScopeBlockTraversal,
23 eachInstructionOperand,
@@ -32,37 +29,61 @@ import {Stack, empty} from '../Utils/Stack';
29 import {CompilerError} from '../CompilerError';
30 import {Iterable_some} from '../Utils/utils';
31 import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
32 +import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
33
34 export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
35 const usedOutsideDeclaringScope =
36 findTemporariesUsedOutsideDeclaringScope(fn);
37 const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope);
38 + const {
39 + temporariesReadInOptional,
40 + processedInstrsInOptional,
41 + hoistableObjects,
42 + } = collectOptionalChainSidemap(fn);
43
41 - const hoistablePropertyLoads = collectHoistablePropertyLoads(fn, temporaries);
44 + const hoistablePropertyLoads = collectHoistablePropertyLoads(
45 + fn,
46 + temporaries,
47 + hoistableObjects,
48 + );
49
50 const scopeDeps = collectDependencies(
51 fn,
52 usedOutsideDeclaringScope,
46 - temporaries,
53 + new Map([...temporaries, ...temporariesReadInOptional]),
54 + processedInstrsInOptional,
55 );
56
57 /**
58 * Derive the minimal set of hoistable dependencies for each scope.
59 */
60 for (const [scope, deps] of scopeDeps) {
53 - const tree = new ReactiveScopeDependencyTreeHIR();
61 + if (deps.length === 0) {
62 + continue;
63 + }
64
65 /**
56 - * Step 1: Add every dependency used by this scope (e.g. `a.b.c`)
66 + * Step 1: Find hoistable accesses, given the basic block in which the scope
67 + * begins.
68 + */
69 + const hoistables = hoistablePropertyLoads.get(scope.id);
70 + CompilerError.invariant(hoistables != null, {
71 + reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
72 + loc: GeneratedSource,
73 + });
74 + /**
75 + * Step 2: Calculate hoistable dependencies.
76 */
77 + const tree = new ReactiveScopeDependencyTreeHIR(
78 + [...hoistables.assumedNonNullObjects].map(o => o.fullPath),
79 + );
80 for (const dep of deps) {
81 tree.addDependency({...dep});
82 }
83 +
84 /**
62 - * Step 2: Mark hoistable dependencies, given the basic block in
63 - * which the scope begins.
85 + * Step 3: Reduce dependencies to a minimal set.
86 */
65 - recordHoistablePropertyReads(hoistablePropertyLoads, scope.id, tree);
87 const candidates = tree.deriveMinimalDependencies();
88 for (const candidateDep of candidates) {
89 if (
@@ -201,7 +222,12 @@ function collectTemporariesSidemap(
222 );
223
224 if (value.kind === 'PropertyLoad' && !usedOutside) {
204 - const property = getProperty(value.object, value.property, temporaries);
225 + const property = getProperty(
226 + value.object,
227 + value.property,
228 + false,
229 + temporaries,
230 + );
231 temporaries.set(lvalue.identifier.id, property);
232 } else if (
233 value.kind === 'LoadLocal' &&
@@ -222,6 +248,7 @@ function collectTemporariesSidemap(
248 function getProperty(
249 object: Place,
250 propertyName: string,
251 + optional: boolean,
252 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
253 ): ReactiveScopeDependency {
254 /*
@@ -253,15 +280,12 @@ function getProperty(
280 if (resolvedDependency == null) {
281 property = {
282 identifier: object.identifier,
256 - path: [{property: propertyName, optional: false}],
283 + path: [{property: propertyName, optional}],
284 };
285 } else {
286 property = {
287 identifier: resolvedDependency.identifier,
261 - path: [
262 - ...resolvedDependency.path,
263 - {property: propertyName, optional: false},
264 - ],
288 + path: [...resolvedDependency.path, {property: propertyName, optional}],
289 };
290 }
291 return property;
@@ -409,8 +433,13 @@ class Context {
433 );
434 }
435
412 - visitProperty(object: Place, property: string): void {
413 - const nextDependency = getProperty(object, property, this.#temporaries);
436 + visitProperty(object: Place, property: string, optional: boolean): void {
437 + const nextDependency = getProperty(
438 + object,
439 + property,
440 + optional,
441 + this.#temporaries,
442 + );
443 this.visitDependency(nextDependency);
444 }
445
@@ -489,7 +518,7 @@ function handleInstruction(instr: Instruction, context: Context): void {
518 }
519 } else if (value.kind === 'PropertyLoad') {
520 if (context.isUsedOutsideDeclaringScope(lvalue)) {
492 - context.visitProperty(value.object, value.property);
521 + context.visitProperty(value.object, value.property, false);
522 }
523 } else if (value.kind === 'StoreLocal') {
524 context.visitOperand(value.value);
@@ -544,6 +573,7 @@ function collectDependencies(
573 fn: HIRFunction,
574 usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
575 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
576 + processedInstrsInOptional: ReadonlySet<InstructionId>,
577 ): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
578 const context = new Context(usedOutsideDeclaringScope, temporaries);
579
@@ -572,33 +602,26 @@ function collectDependencies(
602 context.exitScope(scopeBlockInfo.scope, scopeBlockInfo?.pruned);
603 }
604
605 + // Record referenced optional chains in phis
606 + for (const phi of block.phis) {
607 + for (const operand of phi.operands) {
608 + const maybeOptionalChain = temporaries.get(operand[1].id);
609 + if (maybeOptionalChain) {
610 + context.visitDependency(maybeOptionalChain);
611 + }
612 + }
613 + }
614 for (const instr of block.instructions) {
576 - handleInstruction(instr, context);
615 + if (!processedInstrsInOptional.has(instr.id)) {
616 + handleInstruction(instr, context);
617 + }
618 }
578 - for (const place of eachTerminalOperand(block.terminal)) {
579 - context.visitOperand(place);
619 +
620 + if (!processedInstrsInOptional.has(block.terminal.id)) {
621 + for (const place of eachTerminalOperand(block.terminal)) {
622 + context.visitOperand(place);
623 + }
624 }
625 }
626 return context.deps;
627 }
584 -
585 -/**
586 - * Compute the set of hoistable property reads.
587 - */
588 -function recordHoistablePropertyReads(
589 - nodes: ReadonlyMap<ScopeId, BlockInfo>,
590 - scopeId: ScopeId,
591 - tree: ReactiveScopeDependencyTreeHIR,
592 -): void {
593 - const node = nodes.get(scopeId);
594 - CompilerError.invariant(node != null, {
595 - reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
596 - loc: GeneratedSource,
597 - });
598 -
599 - for (const item of node.assumedNonNullObjects) {
600 - tree.markNodesNonNull({
601 - ...item.fullPath,
602 - });
603 - }
604 -}
compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts
+24
@@ -82,6 +82,17 @@ export function getOrInsertDefault<U, V>(
82 return defaultValue;
83 }
84 }
85 +export function Set_equal<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
86 + if (a.size !== b.size) {
87 + return false;
88 + }
89 + for (const item of a) {
90 + if (!b.has(item)) {
91 + return false;
92 + }
93 + }
94 + return true;
95 +}
96
97 export function Set_union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
98 const union = new Set<T>(a);
@@ -128,6 +139,19 @@ export function nonNull<T extends NonNullable<U>, U>(
139 return value != null;
140 }
141
142 +export function Set_filter<T>(
143 + source: ReadonlySet<T>,
144 + fn: (arg: T) => boolean,
145 +): Set<T> {
146 + const result = new Set<T>();
147 + for (const entry of source) {
148 + if (fn(entry)) {
149 + result.add(entry);
150 + }
151 + }
152 + return result;
153 +}
154 +
155 export function hasNode<T>(
156 input: NodePath<T | null | undefined>,
157 ): input is NodePath<NonNullable<T>> {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.expect.md new
+71
@@ -0,0 +1,71 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({a}) {
6 + let x = [];
7 + x.push(a?.b.c?.d.e);
8 + x.push(a.b?.c.d?.e);
9 + return x;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [{a: null}],
15 + sequentialRenders: [
16 + {a: null},
17 + {a: null},
18 + {a: {}},
19 + {a: {b: {c: {d: {e: 42}}}}},
20 + {a: {b: {c: {d: {e: 43}}}}},
21 + {a: {b: {c: {d: {e: undefined}}}}},
22 + {a: {b: undefined}},
23 + ],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { c as _c } from "react/compiler-runtime";
32 +function useFoo(t0) {
33 + const $ = _c(2);
34 + const { a } = t0;
35 + let x;
36 + if ($[0] !== a.b.c.d) {
37 + x = [];
38 + x.push(a?.b.c?.d.e);
39 + x.push(a.b?.c.d?.e);
40 + $[0] = a.b.c.d;
41 + $[1] = x;
42 + } else {
43 + x = $[1];
44 + }
45 + return x;
46 +}
47 +
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: useFoo,
50 + params: [{ a: null }],
51 + sequentialRenders: [
52 + { a: null },
53 + { a: null },
54 + { a: {} },
55 + { a: { b: { c: { d: { e: 42 } } } } },
56 + { a: { b: { c: { d: { e: 43 } } } } },
57 + { a: { b: { c: { d: { e: undefined } } } } },
58 + { a: { b: undefined } },
59 + ],
60 +};
61 +
62 +```
63 +
64 +### Eval output
65 +(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
66 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
67 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
68 +[42,42]
69 +[43,43]
70 +[null,null]
71 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.ts new
+20
@@ -0,0 +1,20 @@
1 +function useFoo({a}) {
2 + let x = [];
3 + x.push(a?.b.c?.d.e);
4 + x.push(a.b?.c.d?.e);
5 + return x;
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: useFoo,
10 + params: [{a: null}],
11 + sequentialRenders: [
12 + {a: null},
13 + {a: null},
14 + {a: {}},
15 + {a: {b: {c: {d: {e: 42}}}}},
16 + {a: {b: {c: {d: {e: 43}}}}},
17 + {a: {b: {c: {d: {e: undefined}}}}},
18 + {a: {b: undefined}},
19 + ],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.expect.md new
+229
@@ -0,0 +1,229 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {identity} from 'shared-runtime';
6 +
7 +/**
8 + * identity(...)?.toString() is the outer optional, and prop?.value is the inner
9 + * one.
10 + * Note that prop?.
11 + */
12 +function useFoo({
13 + prop1,
14 + prop2,
15 + prop3,
16 + prop4,
17 + prop5,
18 + prop6,
19 +}: {
20 + prop1: null | {value: number};
21 + prop2: null | {inner: {value: number}};
22 + prop3: null | {fn: (val: any) => NonNullable<object>};
23 + prop4: null | {inner: {value: number}};
24 + prop5: null | {fn: (val: any) => NonNullable<object>};
25 + prop6: null | {inner: {value: number}};
26 +}) {
27 + // prop1?.value should be hoisted as the dependency of x
28 + const x = identity(prop1?.value)?.toString();
29 +
30 + // prop2?.inner.value should be hoisted as the dependency of y
31 + const y = identity(prop2?.inner.value)?.toString();
32 +
33 + // prop3 and prop4?.inner should be hoisted as the dependency of z
34 + const z = prop3?.fn(prop4?.inner.value).toString();
35 +
36 + // prop5 and prop6?.inner should be hoisted as the dependency of zz
37 + const zz = prop5?.fn(prop6?.inner.value)?.toString();
38 + return [x, y, z, zz];
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: useFoo,
43 + params: [
44 + {
45 + prop1: null,
46 + prop2: null,
47 + prop3: null,
48 + prop4: null,
49 + prop5: null,
50 + prop6: null,
51 + },
52 + ],
53 + sequentialRenders: [
54 + {
55 + prop1: null,
56 + prop2: null,
57 + prop3: null,
58 + prop4: null,
59 + prop5: null,
60 + prop6: null,
61 + },
62 + {
63 + prop1: {value: 2},
64 + prop2: {inner: {value: 3}},
65 + prop3: {fn: identity},
66 + prop4: {inner: {value: 4}},
67 + prop5: {fn: identity},
68 + prop6: {inner: {value: 4}},
69 + },
70 + {
71 + prop1: {value: 2},
72 + prop2: {inner: {value: 3}},
73 + prop3: {fn: identity},
74 + prop4: {inner: {value: 4}},
75 + prop5: {fn: identity},
76 + prop6: {inner: {value: undefined}},
77 + },
78 + {
79 + prop1: {value: 2},
80 + prop2: {inner: {value: undefined}},
81 + prop3: {fn: identity},
82 + prop4: {inner: {value: undefined}},
83 + prop5: {fn: identity},
84 + prop6: {inner: {value: undefined}},
85 + },
86 + {
87 + prop1: {value: 2},
88 + prop2: {},
89 + prop3: {fn: identity},
90 + prop4: {},
91 + prop5: {fn: identity},
92 + prop6: {inner: {value: undefined}},
93 + },
94 + ],
95 +};
96 +
97 +```
98 +
99 +## Code
100 +
101 +```javascript
102 +import { c as _c } from "react/compiler-runtime";
103 +import { identity } from "shared-runtime";
104 +
105 +/**
106 + * identity(...)?.toString() is the outer optional, and prop?.value is the inner
107 + * one.
108 + * Note that prop?.
109 + */
110 +function useFoo(t0) {
111 + const $ = _c(15);
112 + const { prop1, prop2, prop3, prop4, prop5, prop6 } = t0;
113 + let t1;
114 + if ($[0] !== prop1?.value) {
115 + t1 = identity(prop1?.value)?.toString();
116 + $[0] = prop1?.value;
117 + $[1] = t1;
118 + } else {
119 + t1 = $[1];
120 + }
121 + const x = t1;
122 + let t2;
123 + if ($[2] !== prop2?.inner) {
124 + t2 = identity(prop2?.inner.value)?.toString();
125 + $[2] = prop2?.inner;
126 + $[3] = t2;
127 + } else {
128 + t2 = $[3];
129 + }
130 + const y = t2;
131 + let t3;
132 + if ($[4] !== prop3 || $[5] !== prop4) {
133 + t3 = prop3?.fn(prop4?.inner.value).toString();
134 + $[4] = prop3;
135 + $[5] = prop4;
136 + $[6] = t3;
137 + } else {
138 + t3 = $[6];
139 + }
140 + const z = t3;
141 + let t4;
142 + if ($[7] !== prop5 || $[8] !== prop6) {
143 + t4 = prop5?.fn(prop6?.inner.value)?.toString();
144 + $[7] = prop5;
145 + $[8] = prop6;
146 + $[9] = t4;
147 + } else {
148 + t4 = $[9];
149 + }
150 + const zz = t4;
151 + let t5;
152 + if ($[10] !== x || $[11] !== y || $[12] !== z || $[13] !== zz) {
153 + t5 = [x, y, z, zz];
154 + $[10] = x;
155 + $[11] = y;
156 + $[12] = z;
157 + $[13] = zz;
158 + $[14] = t5;
159 + } else {
160 + t5 = $[14];
161 + }
162 + return t5;
163 +}
164 +
165 +export const FIXTURE_ENTRYPOINT = {
166 + fn: useFoo,
167 + params: [
168 + {
169 + prop1: null,
170 + prop2: null,
171 + prop3: null,
172 + prop4: null,
173 + prop5: null,
174 + prop6: null,
175 + },
176 + ],
177 +
178 + sequentialRenders: [
179 + {
180 + prop1: null,
181 + prop2: null,
182 + prop3: null,
183 + prop4: null,
184 + prop5: null,
185 + prop6: null,
186 + },
187 + {
188 + prop1: { value: 2 },
189 + prop2: { inner: { value: 3 } },
190 + prop3: { fn: identity },
191 + prop4: { inner: { value: 4 } },
192 + prop5: { fn: identity },
193 + prop6: { inner: { value: 4 } },
194 + },
195 + {
196 + prop1: { value: 2 },
197 + prop2: { inner: { value: 3 } },
198 + prop3: { fn: identity },
199 + prop4: { inner: { value: 4 } },
200 + prop5: { fn: identity },
201 + prop6: { inner: { value: undefined } },
202 + },
203 + {
204 + prop1: { value: 2 },
205 + prop2: { inner: { value: undefined } },
206 + prop3: { fn: identity },
207 + prop4: { inner: { value: undefined } },
208 + prop5: { fn: identity },
209 + prop6: { inner: { value: undefined } },
210 + },
211 + {
212 + prop1: { value: 2 },
213 + prop2: {},
214 + prop3: { fn: identity },
215 + prop4: {},
216 + prop5: { fn: identity },
217 + prop6: { inner: { value: undefined } },
218 + },
219 + ],
220 +};
221 +
222 +```
223 +
224 +### Eval output
225 +(kind: ok) [null,null,null,null]
226 +["2","3","4","4"]
227 +["2","3","4",null]
228 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'toString') ]]
229 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'value') ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.ts new
+91
@@ -0,0 +1,91 @@
1 +import {identity} from 'shared-runtime';
2 +
3 +/**
4 + * identity(...)?.toString() is the outer optional, and prop?.value is the inner
5 + * one.
6 + * Note that prop?.
7 + */
8 +function useFoo({
9 + prop1,
10 + prop2,
11 + prop3,
12 + prop4,
13 + prop5,
14 + prop6,
15 +}: {
16 + prop1: null | {value: number};
17 + prop2: null | {inner: {value: number}};
18 + prop3: null | {fn: (val: any) => NonNullable<object>};
19 + prop4: null | {inner: {value: number}};
20 + prop5: null | {fn: (val: any) => NonNullable<object>};
21 + prop6: null | {inner: {value: number}};
22 +}) {
23 + // prop1?.value should be hoisted as the dependency of x
24 + const x = identity(prop1?.value)?.toString();
25 +
26 + // prop2?.inner.value should be hoisted as the dependency of y
27 + const y = identity(prop2?.inner.value)?.toString();
28 +
29 + // prop3 and prop4?.inner should be hoisted as the dependency of z
30 + const z = prop3?.fn(prop4?.inner.value).toString();
31 +
32 + // prop5 and prop6?.inner should be hoisted as the dependency of zz
33 + const zz = prop5?.fn(prop6?.inner.value)?.toString();
34 + return [x, y, z, zz];
35 +}
36 +
37 +export const FIXTURE_ENTRYPOINT = {
38 + fn: useFoo,
39 + params: [
40 + {
41 + prop1: null,
42 + prop2: null,
43 + prop3: null,
44 + prop4: null,
45 + prop5: null,
46 + prop6: null,
47 + },
48 + ],
49 + sequentialRenders: [
50 + {
51 + prop1: null,
52 + prop2: null,
53 + prop3: null,
54 + prop4: null,
55 + prop5: null,
56 + prop6: null,
57 + },
58 + {
59 + prop1: {value: 2},
60 + prop2: {inner: {value: 3}},
61 + prop3: {fn: identity},
62 + prop4: {inner: {value: 4}},
63 + prop5: {fn: identity},
64 + prop6: {inner: {value: 4}},
65 + },
66 + {
67 + prop1: {value: 2},
68 + prop2: {inner: {value: 3}},
69 + prop3: {fn: identity},
70 + prop4: {inner: {value: 4}},
71 + prop5: {fn: identity},
72 + prop6: {inner: {value: undefined}},
73 + },
74 + {
75 + prop1: {value: 2},
76 + prop2: {inner: {value: undefined}},
77 + prop3: {fn: identity},
78 + prop4: {inner: {value: undefined}},
79 + prop5: {fn: identity},
80 + prop6: {inner: {value: undefined}},
81 + },
82 + {
83 + prop1: {value: 2},
84 + prop2: {},
85 + prop3: {fn: identity},
86 + prop4: {},
87 + prop5: {fn: identity},
88 + prop6: {inner: {value: undefined}},
89 + },
90 + ],
91 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md
+73 -23
@@ -3,12 +3,29 @@
3
4 ```javascript
5 // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
6 -function Component(props) {
6 +import {identity, ValidateMemoization} from 'shared-runtime';
7 +import {useMemo} from 'react';
8 +
9 +function Component({arg}) {
10 const data = useMemo(() => {
8 - return props?.items.edges?.nodes.map();
9 - }, [props?.items.edges?.nodes]);
10 - return <Foo data={data} />;
11 + return arg?.items.edges?.nodes.map(identity);
12 + }, [arg?.items.edges?.nodes]);
13 + return (
14 + <ValidateMemoization inputs={[arg?.items.edges?.nodes]} output={data} />
15 + );
16 }
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{arg: null}],
20 + sequentialRenders: [
21 + {arg: null},
22 + {arg: null},
23 + {arg: {items: {edges: null}}},
24 + {arg: {items: {edges: null}}},
25 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
26 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
27 + ],
28 +};
29
30 ```
31
@@ -16,33 +33,66 @@ function Component(props) {
33
34 ```javascript
35 import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
19 -function Component(props) {
20 - const $ = _c(4);
36 +import { identity, ValidateMemoization } from "shared-runtime";
37 +import { useMemo } from "react";
38 +
39 +function Component(t0) {
40 + const $ = _c(7);
41 + const { arg } = t0;
42
22 - props?.items.edges?.nodes;
23 - let t0;
43 + arg?.items.edges?.nodes;
44 let t1;
25 - if ($[0] !== props?.items.edges?.nodes) {
26 - t1 = props?.items.edges?.nodes.map();
27 - $[0] = props?.items.edges?.nodes;
28 - $[1] = t1;
45 + let t2;
46 + if ($[0] !== arg?.items.edges?.nodes) {
47 + t2 = arg?.items.edges?.nodes.map(identity);
48 + $[0] = arg?.items.edges?.nodes;
49 + $[1] = t2;
50 } else {
30 - t1 = $[1];
51 + t2 = $[1];
52 }
32 - t0 = t1;
33 - const data = t0;
34 - let t2;
35 - if ($[2] !== data) {
36 - t2 = <Foo data={data} />;
37 - $[2] = data;
38 - $[3] = t2;
53 + t1 = t2;
54 + const data = t1;
55 +
56 + const t3 = arg?.items.edges?.nodes;
57 + let t4;
58 + if ($[2] !== t3) {
59 + t4 = [t3];
60 + $[2] = t3;
61 + $[3] = t4;
62 + } else {
63 + t4 = $[3];
64 + }
65 + let t5;
66 + if ($[4] !== t4 || $[5] !== data) {
67 + t5 = <ValidateMemoization inputs={t4} output={data} />;
68 + $[4] = t4;
69 + $[5] = data;
70 + $[6] = t5;
71 } else {
40 - t2 = $[3];
72 + t5 = $[6];
73 }
42 - return t2;
74 + return t5;
75 }
76
77 +export const FIXTURE_ENTRYPOINT = {
78 + fn: Component,
79 + params: [{ arg: null }],
80 + sequentialRenders: [
81 + { arg: null },
82 + { arg: null },
83 + { arg: { items: { edges: null } } },
84 + { arg: { items: { edges: null } } },
85 + { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } },
86 + { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } },
87 + ],
88 +};
89 +
90 ```
91
92 ### Eval output
48 -(kind: exception) Fixture not implemented
\ No newline at end of file
93 +(kind: ok) <div>{"inputs":[null]}</div>
94 +<div>{"inputs":[null]}</div>
95 +<div>{"inputs":[null]}</div>
96 +<div>{"inputs":[null]}</div>
97 +<div>{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}</div>
98 +<div>{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js
+21 -4
@@ -1,7 +1,24 @@
1 // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 -function Component(props) {
2 +import {identity, ValidateMemoization} from 'shared-runtime';
3 +import {useMemo} from 'react';
4 +
5 +function Component({arg}) {
6 const data = useMemo(() => {
4 - return props?.items.edges?.nodes.map();
5 - }, [props?.items.edges?.nodes]);
6 - return <Foo data={data} />;
7 + return arg?.items.edges?.nodes.map(identity);
8 + }, [arg?.items.edges?.nodes]);
9 + return (
10 + <ValidateMemoization inputs={[arg?.items.edges?.nodes]} output={data} />
11 + );
12 }
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{arg: null}],
16 + sequentialRenders: [
17 + {arg: null},
18 + {arg: null},
19 + {arg: {items: {edges: null}}},
20 + {arg: {items: {edges: null}}},
21 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
22 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
23 + ],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md
+55 -27
@@ -4,15 +4,27 @@
4 ```javascript
5 // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
6 import {ValidateMemoization} from 'shared-runtime';
7 -function Component(props) {
7 +import {useMemo} from 'react';
8 +function Component({arg}) {
9 const data = useMemo(() => {
10 const x = [];
10 - x.push(props?.items);
11 + x.push(arg?.items);
12 return x;
12 - }, [props?.items]);
13 - return <ValidateMemoization inputs={[props?.items]} output={data} />;
13 + }, [arg?.items]);
14 + return <ValidateMemoization inputs={[arg?.items]} output={data} />;
15 }
16
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{arg: {items: 2}}],
20 + sequentialRenders: [
21 + {arg: {items: 2}},
22 + {arg: {items: 2}},
23 + {arg: null},
24 + {arg: null},
25 + ],
26 +};
27 +
28 ```
29
30 ## Code
@@ -20,44 +32,60 @@ function Component(props) {
32 ```javascript
33 import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
34 import { ValidateMemoization } from "shared-runtime";
23 -function Component(props) {
35 +import { useMemo } from "react";
36 +function Component(t0) {
37 const $ = _c(7);
38 + const { arg } = t0;
39
26 - props?.items;
27 - let t0;
40 + arg?.items;
41 + let t1;
42 let x;
29 - if ($[0] !== props?.items) {
43 + if ($[0] !== arg?.items) {
44 x = [];
31 - x.push(props?.items);
32 - $[0] = props?.items;
45 + x.push(arg?.items);
46 + $[0] = arg?.items;
47 $[1] = x;
48 } else {
49 x = $[1];
50 }
37 - t0 = x;
38 - const data = t0;
39 - const t1 = props?.items;
40 - let t2;
41 - if ($[2] !== t1) {
42 - t2 = [t1];
43 - $[2] = t1;
44 - $[3] = t2;
51 + t1 = x;
52 + const data = t1;
53 + const t2 = arg?.items;
54 + let t3;
55 + if ($[2] !== t2) {
56 + t3 = [t2];
57 + $[2] = t2;
58 + $[3] = t3;
59 } else {
46 - t2 = $[3];
60 + t3 = $[3];
61 }
48 - let t3;
49 - if ($[4] !== t2 || $[5] !== data) {
50 - t3 = <ValidateMemoization inputs={t2} output={data} />;
51 - $[4] = t2;
62 + let t4;
63 + if ($[4] !== t3 || $[5] !== data) {
64 + t4 = <ValidateMemoization inputs={t3} output={data} />;
65 + $[4] = t3;
66 $[5] = data;
53 - $[6] = t3;
67 + $[6] = t4;
68 } else {
55 - t3 = $[6];
69 + t4 = $[6];
70 }
57 - return t3;
71 + return t4;
72 }
73
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: Component,
76 + params: [{ arg: { items: 2 } }],
77 + sequentialRenders: [
78 + { arg: { items: 2 } },
79 + { arg: { items: 2 } },
80 + { arg: null },
81 + { arg: null },
82 + ],
83 +};
84 +
85 ```
86
87 ### Eval output
63 -(kind: exception) Fixture not implemented
\ No newline at end of file
88 +(kind: ok) <div>{"inputs":[2],"output":[2]}</div>
89 +<div>{"inputs":[2],"output":[2]}</div>
90 +<div>{"inputs":[null],"output":[null]}</div>
91 +<div>{"inputs":[null],"output":[null]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js
+16 -4
@@ -1,10 +1,22 @@
1 // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 import {ValidateMemoization} from 'shared-runtime';
3 -function Component(props) {
3 +import {useMemo} from 'react';
4 +function Component({arg}) {
5 const data = useMemo(() => {
6 const x = [];
6 - x.push(props?.items);
7 + x.push(arg?.items);
8 return x;
8 - }, [props?.items]);
9 - return <ValidateMemoization inputs={[props?.items]} output={data} />;
9 + }, [arg?.items]);
10 + return <ValidateMemoization inputs={[arg?.items]} output={data} />;
11 }
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{arg: {items: 2}}],
16 + sequentialRenders: [
17 + {arg: {items: 2}},
18 + {arg: {items: 2}},
19 + {arg: null},
20 + {arg: null},
21 + ],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-call-chain-in-optional.expect.md
+1 -1
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPONT = {
26 2 | function useFoo(props: {value: {x: string; y: string} | null}) {
27 3 | const value = props.value;
28 > 4 | return createArray(value?.x, value?.y)?.join(', ');
29 - | ^^^^^^^^ Todo: Unexpected terminal kind `optional` for optional test block (4:4)
29 + | ^^^^^^^^ Todo: Unexpected terminal kind `optional` for optional fallthrough block (4:4)
30 5 | }
31 6 |
32 7 | function createArray<T>(...args: Array<T>): Array<T> {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md deleted
-32
@@ -1,32 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
6 -function Component(props) {
7 - const data = useMemo(() => {
8 - return props?.items.edges?.nodes.map();
9 - }, [props?.items.edges?.nodes]);
10 - return <Foo data={data} />;
11 -}
12 -
13 -```
14 -
15 -
16 -## Error
17 -
18 -```
19 - 1 | // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
20 - 2 | function Component(props) {
21 -> 3 | const data = useMemo(() => {
22 - | ^^^^^^^
23 -> 4 | return props?.items.edges?.nodes.map();
24 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25 -> 5 | }, [props?.items.edges?.nodes]);
26 - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:5)
27 - 6 | return <Foo data={data} />;
28 - 7 | }
29 - 8 |
30 -```
31 -
32 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.js deleted
-7
@@ -1,7 +0,0 @@
1 -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
2 -function Component(props) {
3 - const data = useMemo(() => {
4 - return props?.items.edges?.nodes.map();
5 - }, [props?.items.edges?.nodes]);
6 - return <Foo data={data} />;
7 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md deleted
-42
@@ -1,42 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
6 -import {ValidateMemoization} from 'shared-runtime';
7 -function Component(props) {
8 - const data = useMemo(() => {
9 - const x = [];
10 - x.push(props?.items);
11 - x.push(props.items);
12 - return x;
13 - }, [props.items]);
14 - return <ValidateMemoization inputs={[props.items]} output={data} />;
15 -}
16 -
17 -```
18 -
19 -
20 -## Error
21 -
22 -```
23 - 2 | import {ValidateMemoization} from 'shared-runtime';
24 - 3 | function Component(props) {
25 -> 4 | const data = useMemo(() => {
26 - | ^^^^^^^
27 -> 5 | const x = [];
28 - | ^^^^^^^^^^^^^^^^^
29 -> 6 | x.push(props?.items);
30 - | ^^^^^^^^^^^^^^^^^
31 -> 7 | x.push(props.items);
32 - | ^^^^^^^^^^^^^^^^^
33 -> 8 | return x;
34 - | ^^^^^^^^^^^^^^^^^
35 -> 9 | }, [props.items]);
36 - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:9)
37 - 10 | return <ValidateMemoization inputs={[props.items]} output={data} />;
38 - 11 | }
39 - 12 |
40 -```
41 -
42 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md deleted
-39
@@ -1,39 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
6 -import {ValidateMemoization} from 'shared-runtime';
7 -function Component(props) {
8 - const data = useMemo(() => {
9 - const x = [];
10 - x.push(props?.items);
11 - return x;
12 - }, [props?.items]);
13 - return <ValidateMemoization inputs={[props?.items]} output={data} />;
14 -}
15 -
16 -```
17 -
18 -
19 -## Error
20 -
21 -```
22 - 2 | import {ValidateMemoization} from 'shared-runtime';
23 - 3 | function Component(props) {
24 -> 4 | const data = useMemo(() => {
25 - | ^^^^^^^
26 -> 5 | const x = [];
27 - | ^^^^^^^^^^^^^^^^^
28 -> 6 | x.push(props?.items);
29 - | ^^^^^^^^^^^^^^^^^
30 -> 7 | return x;
31 - | ^^^^^^^^^^^^^^^^^
32 -> 8 | }, [props?.items]);
33 - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:8)
34 - 9 | return <ValidateMemoization inputs={[props?.items]} output={data} />;
35 - 10 | }
36 - 11 |
37 -```
38 -
39 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.js deleted
-10
@@ -1,10 +0,0 @@
1 -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
2 -import {ValidateMemoization} from 'shared-runtime';
3 -function Component(props) {
4 - const data = useMemo(() => {
5 - const x = [];
6 - x.push(props?.items);
7 - return x;
8 - }, [props?.items]);
9 - return <ValidateMemoization inputs={[props?.items]} output={data} />;
10 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.expect.md new
+74
@@ -0,0 +1,74 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enablePropagateDepsInHIR
6 +
7 +function useFoo({a}) {
8 + let x = [];
9 + x.push(a?.b.c?.d.e);
10 + x.push(a.b?.c.d?.e);
11 + return x;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [{a: null}],
17 + sequentialRenders: [
18 + {a: null},
19 + {a: null},
20 + {a: {}},
21 + {a: {b: {c: {d: {e: 42}}}}},
22 + {a: {b: {c: {d: {e: 43}}}}},
23 + {a: {b: {c: {d: {e: undefined}}}}},
24 + {a: {b: undefined}},
25 + ],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
34 +
35 +function useFoo(t0) {
36 + const $ = _c(2);
37 + const { a } = t0;
38 + let x;
39 + if ($[0] !== a.b.c.d.e) {
40 + x = [];
41 + x.push(a?.b.c?.d.e);
42 + x.push(a.b?.c.d?.e);
43 + $[0] = a.b.c.d.e;
44 + $[1] = x;
45 + } else {
46 + x = $[1];
47 + }
48 + return x;
49 +}
50 +
51 +export const FIXTURE_ENTRYPOINT = {
52 + fn: useFoo,
53 + params: [{ a: null }],
54 + sequentialRenders: [
55 + { a: null },
56 + { a: null },
57 + { a: {} },
58 + { a: { b: { c: { d: { e: 42 } } } } },
59 + { a: { b: { c: { d: { e: 43 } } } } },
60 + { a: { b: { c: { d: { e: undefined } } } } },
61 + { a: { b: undefined } },
62 + ],
63 +};
64 +
65 +```
66 +
67 +### Eval output
68 +(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
69 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
70 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
71 +[42,42]
72 +[43,43]
73 +[null,null]
74 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.ts new
+22
@@ -0,0 +1,22 @@
1 +// @enablePropagateDepsInHIR
2 +
3 +function useFoo({a}) {
4 + let x = [];
5 + x.push(a?.b.c?.d.e);
6 + x.push(a.b?.c.d?.e);
7 + return x;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: useFoo,
12 + params: [{a: null}],
13 + sequentialRenders: [
14 + {a: null},
15 + {a: null},
16 + {a: {}},
17 + {a: {b: {c: {d: {e: 42}}}}},
18 + {a: {b: {c: {d: {e: 43}}}}},
19 + {a: {b: {c: {d: {e: undefined}}}}},
20 + {a: {b: undefined}},
21 + ],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.expect.md new
+232
@@ -0,0 +1,232 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enablePropagateDepsInHIR
6 +
7 +import {identity} from 'shared-runtime';
8 +
9 +/**
10 + * identity(...)?.toString() is the outer optional, and prop?.value is the inner
11 + * one.
12 + * Note that prop?.
13 + */
14 +function useFoo({
15 + prop1,
16 + prop2,
17 + prop3,
18 + prop4,
19 + prop5,
20 + prop6,
21 +}: {
22 + prop1: null | {value: number};
23 + prop2: null | {inner: {value: number}};
24 + prop3: null | {fn: (val: any) => NonNullable<object>};
25 + prop4: null | {inner: {value: number}};
26 + prop5: null | {fn: (val: any) => NonNullable<object>};
27 + prop6: null | {inner: {value: number}};
28 +}) {
29 + // prop1?.value should be hoisted as the dependency of x
30 + const x = identity(prop1?.value)?.toString();
31 +
32 + // prop2?.inner.value should be hoisted as the dependency of y
33 + const y = identity(prop2?.inner.value)?.toString();
34 +
35 + // prop3 and prop4?.inner should be hoisted as the dependency of z
36 + const z = prop3?.fn(prop4?.inner.value).toString();
37 +
38 + // prop5 and prop6?.inner should be hoisted as the dependency of zz
39 + const zz = prop5?.fn(prop6?.inner.value)?.toString();
40 + return [x, y, z, zz];
41 +}
42 +
43 +export const FIXTURE_ENTRYPOINT = {
44 + fn: useFoo,
45 + params: [
46 + {
47 + prop1: null,
48 + prop2: null,
49 + prop3: null,
50 + prop4: null,
51 + prop5: null,
52 + prop6: null,
53 + },
54 + ],
55 + sequentialRenders: [
56 + {
57 + prop1: null,
58 + prop2: null,
59 + prop3: null,
60 + prop4: null,
61 + prop5: null,
62 + prop6: null,
63 + },
64 + {
65 + prop1: {value: 2},
66 + prop2: {inner: {value: 3}},
67 + prop3: {fn: identity},
68 + prop4: {inner: {value: 4}},
69 + prop5: {fn: identity},
70 + prop6: {inner: {value: 4}},
71 + },
72 + {
73 + prop1: {value: 2},
74 + prop2: {inner: {value: 3}},
75 + prop3: {fn: identity},
76 + prop4: {inner: {value: 4}},
77 + prop5: {fn: identity},
78 + prop6: {inner: {value: undefined}},
79 + },
80 + {
81 + prop1: {value: 2},
82 + prop2: {inner: {value: undefined}},
83 + prop3: {fn: identity},
84 + prop4: {inner: {value: undefined}},
85 + prop5: {fn: identity},
86 + prop6: {inner: {value: undefined}},
87 + },
88 + {
89 + prop1: {value: 2},
90 + prop2: {},
91 + prop3: {fn: identity},
92 + prop4: {},
93 + prop5: {fn: identity},
94 + prop6: {inner: {value: undefined}},
95 + },
96 + ],
97 +};
98 +
99 +```
100 +
101 +## Code
102 +
103 +```javascript
104 +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
105 +
106 +import { identity } from "shared-runtime";
107 +
108 +/**
109 + * identity(...)?.toString() is the outer optional, and prop?.value is the inner
110 + * one.
111 + * Note that prop?.
112 + */
113 +function useFoo(t0) {
114 + const $ = _c(15);
115 + const { prop1, prop2, prop3, prop4, prop5, prop6 } = t0;
116 + let t1;
117 + if ($[0] !== prop1?.value) {
118 + t1 = identity(prop1?.value)?.toString();
119 + $[0] = prop1?.value;
120 + $[1] = t1;
121 + } else {
122 + t1 = $[1];
123 + }
124 + const x = t1;
125 + let t2;
126 + if ($[2] !== prop2?.inner.value) {
127 + t2 = identity(prop2?.inner.value)?.toString();
128 + $[2] = prop2?.inner.value;
129 + $[3] = t2;
130 + } else {
131 + t2 = $[3];
132 + }
133 + const y = t2;
134 + let t3;
135 + if ($[4] !== prop3 || $[5] !== prop4?.inner) {
136 + t3 = prop3?.fn(prop4?.inner.value).toString();
137 + $[4] = prop3;
138 + $[5] = prop4?.inner;
139 + $[6] = t3;
140 + } else {
141 + t3 = $[6];
142 + }
143 + const z = t3;
144 + let t4;
145 + if ($[7] !== prop5 || $[8] !== prop6?.inner) {
146 + t4 = prop5?.fn(prop6?.inner.value)?.toString();
147 + $[7] = prop5;
148 + $[8] = prop6?.inner;
149 + $[9] = t4;
150 + } else {
151 + t4 = $[9];
152 + }
153 + const zz = t4;
154 + let t5;
155 + if ($[10] !== x || $[11] !== y || $[12] !== z || $[13] !== zz) {
156 + t5 = [x, y, z, zz];
157 + $[10] = x;
158 + $[11] = y;
159 + $[12] = z;
160 + $[13] = zz;
161 + $[14] = t5;
162 + } else {
163 + t5 = $[14];
164 + }
165 + return t5;
166 +}
167 +
168 +export const FIXTURE_ENTRYPOINT = {
169 + fn: useFoo,
170 + params: [
171 + {
172 + prop1: null,
173 + prop2: null,
174 + prop3: null,
175 + prop4: null,
176 + prop5: null,
177 + prop6: null,
178 + },
179 + ],
180 +
181 + sequentialRenders: [
182 + {
183 + prop1: null,
184 + prop2: null,
185 + prop3: null,
186 + prop4: null,
187 + prop5: null,
188 + prop6: null,
189 + },
190 + {
191 + prop1: { value: 2 },
192 + prop2: { inner: { value: 3 } },
193 + prop3: { fn: identity },
194 + prop4: { inner: { value: 4 } },
195 + prop5: { fn: identity },
196 + prop6: { inner: { value: 4 } },
197 + },
198 + {
199 + prop1: { value: 2 },
200 + prop2: { inner: { value: 3 } },
201 + prop3: { fn: identity },
202 + prop4: { inner: { value: 4 } },
203 + prop5: { fn: identity },
204 + prop6: { inner: { value: undefined } },
205 + },
206 + {
207 + prop1: { value: 2 },
208 + prop2: { inner: { value: undefined } },
209 + prop3: { fn: identity },
210 + prop4: { inner: { value: undefined } },
211 + prop5: { fn: identity },
212 + prop6: { inner: { value: undefined } },
213 + },
214 + {
215 + prop1: { value: 2 },
216 + prop2: {},
217 + prop3: { fn: identity },
218 + prop4: {},
219 + prop5: { fn: identity },
220 + prop6: { inner: { value: undefined } },
221 + },
222 + ],
223 +};
224 +
225 +```
226 +
227 +### Eval output
228 +(kind: ok) [null,null,null,null]
229 +["2","3","4","4"]
230 +["2","3","4",null]
231 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'toString') ]]
232 +[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'value') ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.ts new
+93
@@ -0,0 +1,93 @@
1 +// @enablePropagateDepsInHIR
2 +
3 +import {identity} from 'shared-runtime';
4 +
5 +/**
6 + * identity(...)?.toString() is the outer optional, and prop?.value is the inner
7 + * one.
8 + * Note that prop?.
9 + */
10 +function useFoo({
11 + prop1,
12 + prop2,
13 + prop3,
14 + prop4,
15 + prop5,
16 + prop6,
17 +}: {
18 + prop1: null | {value: number};
19 + prop2: null | {inner: {value: number}};
20 + prop3: null | {fn: (val: any) => NonNullable<object>};
21 + prop4: null | {inner: {value: number}};
22 + prop5: null | {fn: (val: any) => NonNullable<object>};
23 + prop6: null | {inner: {value: number}};
24 +}) {
25 + // prop1?.value should be hoisted as the dependency of x
26 + const x = identity(prop1?.value)?.toString();
27 +
28 + // prop2?.inner.value should be hoisted as the dependency of y
29 + const y = identity(prop2?.inner.value)?.toString();
30 +
31 + // prop3 and prop4?.inner should be hoisted as the dependency of z
32 + const z = prop3?.fn(prop4?.inner.value).toString();
33 +
34 + // prop5 and prop6?.inner should be hoisted as the dependency of zz
35 + const zz = prop5?.fn(prop6?.inner.value)?.toString();
36 + return [x, y, z, zz];
37 +}
38 +
39 +export const FIXTURE_ENTRYPOINT = {
40 + fn: useFoo,
41 + params: [
42 + {
43 + prop1: null,
44 + prop2: null,
45 + prop3: null,
46 + prop4: null,
47 + prop5: null,
48 + prop6: null,
49 + },
50 + ],
51 + sequentialRenders: [
52 + {
53 + prop1: null,
54 + prop2: null,
55 + prop3: null,
56 + prop4: null,
57 + prop5: null,
58 + prop6: null,
59 + },
60 + {
61 + prop1: {value: 2},
62 + prop2: {inner: {value: 3}},
63 + prop3: {fn: identity},
64 + prop4: {inner: {value: 4}},
65 + prop5: {fn: identity},
66 + prop6: {inner: {value: 4}},
67 + },
68 + {
69 + prop1: {value: 2},
70 + prop2: {inner: {value: 3}},
71 + prop3: {fn: identity},
72 + prop4: {inner: {value: 4}},
73 + prop5: {fn: identity},
74 + prop6: {inner: {value: undefined}},
75 + },
76 + {
77 + prop1: {value: 2},
78 + prop2: {inner: {value: undefined}},
79 + prop3: {fn: identity},
80 + prop4: {inner: {value: undefined}},
81 + prop5: {fn: identity},
82 + prop6: {inner: {value: undefined}},
83 + },
84 + {
85 + prop1: {value: 2},
86 + prop2: {},
87 + prop3: {fn: identity},
88 + prop4: {},
89 + prop5: {fn: identity},
90 + prop6: {inner: {value: undefined}},
91 + },
92 + ],
93 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md new
+98
@@ -0,0 +1,98 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
6 +import {identity, ValidateMemoization} from 'shared-runtime';
7 +import {useMemo} from 'react';
8 +
9 +function Component({arg}) {
10 + const data = useMemo(() => {
11 + return arg?.items.edges?.nodes.map(identity);
12 + }, [arg?.items.edges?.nodes]);
13 + return (
14 + <ValidateMemoization inputs={[arg?.items.edges?.nodes]} output={data} />
15 + );
16 +}
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{arg: null}],
20 + sequentialRenders: [
21 + {arg: null},
22 + {arg: null},
23 + {arg: {items: {edges: null}}},
24 + {arg: {items: {edges: null}}},
25 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
26 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
27 + ],
28 +};
29 +
30 +```
31 +
32 +## Code
33 +
34 +```javascript
35 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
36 +import { identity, ValidateMemoization } from "shared-runtime";
37 +import { useMemo } from "react";
38 +
39 +function Component(t0) {
40 + const $ = _c(7);
41 + const { arg } = t0;
42 +
43 + arg?.items.edges?.nodes;
44 + let t1;
45 + let t2;
46 + if ($[0] !== arg?.items.edges?.nodes) {
47 + t2 = arg?.items.edges?.nodes.map(identity);
48 + $[0] = arg?.items.edges?.nodes;
49 + $[1] = t2;
50 + } else {
51 + t2 = $[1];
52 + }
53 + t1 = t2;
54 + const data = t1;
55 +
56 + const t3 = arg?.items.edges?.nodes;
57 + let t4;
58 + if ($[2] !== t3) {
59 + t4 = [t3];
60 + $[2] = t3;
61 + $[3] = t4;
62 + } else {
63 + t4 = $[3];
64 + }
65 + let t5;
66 + if ($[4] !== t4 || $[5] !== data) {
67 + t5 = <ValidateMemoization inputs={t4} output={data} />;
68 + $[4] = t4;
69 + $[5] = data;
70 + $[6] = t5;
71 + } else {
72 + t5 = $[6];
73 + }
74 + return t5;
75 +}
76 +
77 +export const FIXTURE_ENTRYPOINT = {
78 + fn: Component,
79 + params: [{ arg: null }],
80 + sequentialRenders: [
81 + { arg: null },
82 + { arg: null },
83 + { arg: { items: { edges: null } } },
84 + { arg: { items: { edges: null } } },
85 + { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } },
86 + { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } },
87 + ],
88 +};
89 +
90 +```
91 +
92 +### Eval output
93 +(kind: ok) <div>{"inputs":[null]}</div>
94 +<div>{"inputs":[null]}</div>
95 +<div>{"inputs":[null]}</div>
96 +<div>{"inputs":[null]}</div>
97 +<div>{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}</div>
98 +<div>{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.js new
+24
@@ -0,0 +1,24 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
2 +import {identity, ValidateMemoization} from 'shared-runtime';
3 +import {useMemo} from 'react';
4 +
5 +function Component({arg}) {
6 + const data = useMemo(() => {
7 + return arg?.items.edges?.nodes.map(identity);
8 + }, [arg?.items.edges?.nodes]);
9 + return (
10 + <ValidateMemoization inputs={[arg?.items.edges?.nodes]} output={data} />
11 + );
12 +}
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{arg: null}],
16 + sequentialRenders: [
17 + {arg: null},
18 + {arg: null},
19 + {arg: {items: {edges: null}}},
20 + {arg: {items: {edges: null}}},
21 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
22 + {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}},
23 + ],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md new
+62
@@ -0,0 +1,62 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
6 +import {ValidateMemoization} from 'shared-runtime';
7 +function Component(props) {
8 + const data = useMemo(() => {
9 + const x = [];
10 + x.push(props?.items);
11 + x.push(props.items);
12 + return x;
13 + }, [props.items]);
14 + return <ValidateMemoization inputs={[props.items]} output={data} />;
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
23 +import { ValidateMemoization } from "shared-runtime";
24 +function Component(props) {
25 + const $ = _c(7);
26 + let t0;
27 + let x;
28 + if ($[0] !== props.items) {
29 + x = [];
30 + x.push(props?.items);
31 + x.push(props.items);
32 + $[0] = props.items;
33 + $[1] = x;
34 + } else {
35 + x = $[1];
36 + }
37 + t0 = x;
38 + const data = t0;
39 + let t1;
40 + if ($[2] !== props.items) {
41 + t1 = [props.items];
42 + $[2] = props.items;
43 + $[3] = t1;
44 + } else {
45 + t1 = $[3];
46 + }
47 + let t2;
48 + if ($[4] !== t1 || $[5] !== data) {
49 + t2 = <ValidateMemoization inputs={t1} output={data} />;
50 + $[4] = t1;
51 + $[5] = data;
52 + $[6] = t2;
53 + } else {
54 + t2 = $[6];
55 + }
56 + return t2;
57 +}
58 +
59 +```
60 +
61 +### Eval output
62 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md new
+91
@@ -0,0 +1,91 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
6 +import {ValidateMemoization} from 'shared-runtime';
7 +import {useMemo} from 'react';
8 +function Component({arg}) {
9 + const data = useMemo(() => {
10 + const x = [];
11 + x.push(arg?.items);
12 + return x;
13 + }, [arg?.items]);
14 + return <ValidateMemoization inputs={[arg?.items]} output={data} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{arg: {items: 2}}],
20 + sequentialRenders: [
21 + {arg: {items: 2}},
22 + {arg: {items: 2}},
23 + {arg: null},
24 + {arg: null},
25 + ],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
34 +import { ValidateMemoization } from "shared-runtime";
35 +import { useMemo } from "react";
36 +function Component(t0) {
37 + const $ = _c(7);
38 + const { arg } = t0;
39 +
40 + arg?.items;
41 + let t1;
42 + let x;
43 + if ($[0] !== arg?.items) {
44 + x = [];
45 + x.push(arg?.items);
46 + $[0] = arg?.items;
47 + $[1] = x;
48 + } else {
49 + x = $[1];
50 + }
51 + t1 = x;
52 + const data = t1;
53 + const t2 = arg?.items;
54 + let t3;
55 + if ($[2] !== t2) {
56 + t3 = [t2];
57 + $[2] = t2;
58 + $[3] = t3;
59 + } else {
60 + t3 = $[3];
61 + }
62 + let t4;
63 + if ($[4] !== t3 || $[5] !== data) {
64 + t4 = <ValidateMemoization inputs={t3} output={data} />;
65 + $[4] = t3;
66 + $[5] = data;
67 + $[6] = t4;
68 + } else {
69 + t4 = $[6];
70 + }
71 + return t4;
72 +}
73 +
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: Component,
76 + params: [{ arg: { items: 2 } }],
77 + sequentialRenders: [
78 + { arg: { items: 2 } },
79 + { arg: { items: 2 } },
80 + { arg: null },
81 + { arg: null },
82 + ],
83 +};
84 +
85 +```
86 +
87 +### Eval output
88 +(kind: ok) <div>{"inputs":[2],"output":[2]}</div>
89 +<div>{"inputs":[2],"output":[2]}</div>
90 +<div>{"inputs":[null],"output":[null]}</div>
91 +<div>{"inputs":[null],"output":[null]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.js new
+22
@@ -0,0 +1,22 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
2 +import {ValidateMemoization} from 'shared-runtime';
3 +import {useMemo} from 'react';
4 +function Component({arg}) {
5 + const data = useMemo(() => {
6 + const x = [];
7 + x.push(arg?.items);
8 + return x;
9 + }, [arg?.items]);
10 + return <ValidateMemoization inputs={[arg?.items]} output={data} />;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{arg: {items: 2}}],
16 + sequentialRenders: [
17 + {arg: {items: 2}},
18 + {arg: {items: 2}},
19 + {arg: null},
20 + {arg: null},
21 + ],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md
+2 -2
@@ -16,9 +16,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
16 function Component(props) {
17 const $ = _c(2);
18 let t0;
19 - if ($[0] !== props.post.feedback.comments) {
19 + if ($[0] !== props.post.feedback.comments?.edges) {
20 t0 = props.post.feedback.comments?.edges?.map(render);
21 - $[0] = props.post.feedback.comments;
21 + $[0] = props.post.feedback.comments?.edges;
22 $[1] = t0;
23 } else {
24 t0 = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md
+2 -2
@@ -31,10 +31,10 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
31 function Component(props) {
32 const $ = _c(2);
33 let x;
34 - if ($[0] !== props.a) {
34 + if ($[0] !== props.a?.b) {
35 x = [];
36 x.push(props.a?.b);
37 - $[0] = props.a;
37 + $[0] = props.a?.b;
38 $[1] = x;
39 } else {
40 x = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md
+2 -2
@@ -46,11 +46,11 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
46 function Component(props) {
47 const $ = _c(2);
48 let x;
49 - if ($[0] !== props.a) {
49 + if ($[0] !== props.a.b) {
50 x = [];
51 x.push(props.a?.b);
52 x.push(props.a.b.c);
53 - $[0] = props.a;
53 + $[0] = props.a.b;
54 $[1] = x;
55 } else {
56 x = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md
+15 -6
@@ -22,16 +22,25 @@ export const FIXTURE_ENTRYPOINT = {
22 ```javascript
23 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
24 function Component(props) {
25 - const $ = _c(2);
25 + const $ = _c(5);
26 let x;
27 - if ($[0] !== props.items) {
27 + if ($[0] !== props.items?.length || $[1] !== props.items?.edges) {
28 x = [];
29 x.push(props.items?.length);
30 - x.push(props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []);
31 - $[0] = props.items;
32 - $[1] = x;
30 + let t0;
31 + if ($[3] !== props.items?.edges) {
32 + t0 = props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? [];
33 + $[3] = props.items?.edges;
34 + $[4] = t0;
35 + } else {
36 + t0 = $[4];
37 + }
38 + x.push(t0);
39 + $[0] = props.items?.length;
40 + $[1] = props.items?.edges;
41 + $[2] = x;
42 } else {
34 - x = $[1];
43 + x = $[2];
44 }
45 return x;
46 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.expect.md new
+72
@@ -0,0 +1,72 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enablePropagateDepsInHIR
6 +import {identity} from 'shared-runtime';
7 +
8 +/**
9 + * Very contrived text fixture showing that it's technically incorrect to merge
10 + * a conditional dependency (e.g. dep.path in `cond ? dep.path : ...`) and an
11 + * unconditionally evaluated optional chain (`dep?.path`).
12 + *
13 + *
14 + * when screen is non-null, useFoo returns { title: null } or "(not null)"
15 + * when screen is null, useFoo throws
16 + */
17 +function useFoo({screen}: {screen: null | undefined | {title_text: null}}) {
18 + return screen?.title_text != null
19 + ? '(not null)'
20 + : identity({title: screen.title_text});
21 +}
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [{screen: null}],
25 + sequentialRenders: [{screen: {title_bar: undefined}}, {screen: null}],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
34 +import { identity } from "shared-runtime";
35 +
36 +/**
37 + * Very contrived text fixture showing that it's technically incorrect to merge
38 + * a conditional dependency (e.g. dep.path in `cond ? dep.path : ...`) and an
39 + * unconditionally evaluated optional chain (`dep?.path`).
40 + *
41 + *
42 + * when screen is non-null, useFoo returns { title: null } or "(not null)"
43 + * when screen is null, useFoo throws
44 + */
45 +function useFoo(t0) {
46 + const $ = _c(2);
47 + const { screen } = t0;
48 + let t1;
49 + if ($[0] !== screen) {
50 + t1 =
51 + screen?.title_text != null
52 + ? "(not null)"
53 + : identity({ title: screen.title_text });
54 + $[0] = screen;
55 + $[1] = t1;
56 + } else {
57 + t1 = $[1];
58 + }
59 + return t1;
60 +}
61 +
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: useFoo,
64 + params: [{ screen: null }],
65 + sequentialRenders: [{ screen: { title_bar: undefined } }, { screen: null }],
66 +};
67 +
68 +```
69 +
70 +### Eval output
71 +(kind: ok) {}
72 +[[ (exception in render) TypeError: Cannot read properties of null (reading 'title_text') ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.ts new
+22
@@ -0,0 +1,22 @@
1 +// @enablePropagateDepsInHIR
2 +import {identity} from 'shared-runtime';
3 +
4 +/**
5 + * Very contrived text fixture showing that it's technically incorrect to merge
6 + * a conditional dependency (e.g. dep.path in `cond ? dep.path : ...`) and an
7 + * unconditionally evaluated optional chain (`dep?.path`).
8 + *
9 + *
10 + * when screen is non-null, useFoo returns { title: null } or "(not null)"
11 + * when screen is null, useFoo throws
12 + */
13 +function useFoo({screen}: {screen: null | undefined | {title_text: null}}) {
14 + return screen?.title_text != null
15 + ? '(not null)'
16 + : identity({title: screen.title_text});
17 +}
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{screen: null}],
21 + sequentialRenders: [{screen: {title_bar: undefined}}, {screen: null}],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md
+2 -2
@@ -24,14 +24,14 @@ function HomeDiscoStoreItemTileRating(props) {
24 const $ = _c(4);
25 const item = useFragment();
26 let count;
27 - if ($[0] !== item) {
27 + if ($[0] !== item?.aggregates) {
28 count = 0;
29 const aggregates = item?.aggregates || [];
30 aggregates.forEach((aggregate) => {
31 count = count + (aggregate.count || 0);
32 count;
33 });
34 - $[0] = item;
34 + $[0] = item?.aggregates;
35 $[1] = count;
36 } else {
37 count = $[1];