@samitouri / QOS-React / commits / 206df66e70

[compiler][rewrite] PropagateScopeDeps hir rewrite

Resubmission of #30079 -- core logic unchanged, but needed to rebase past #30573 ### Quick background #### Temporaries The compiler currently treats temporaries and named variables (e.g. `x`) differently in this pass. - named variables may be reassigned (in fact, since we're running after LeaveSSA, a single named identifier's IdentifierId may map to multiple `Identifier` instances -- each with its own scope and mutable range) - temporaries are replaced with their represented expressions during codegen. This is correct (mostly correct, see #29878) as we're careful to always lower the correct evaluation semantics. However, since we rewrite reactive scopes entirely (to if/else blocks), we need to track temporaries that a scope produces in `ReactiveScope.declarations` and later promote them to named variables. In the same example, $4, $5, and $6 need to be promoted: $2 ->`t0`, $5 ->`t1`, and $6 ->`t2`. ```js [1] $2 = LoadGlobal(global) foo [2] $3 = LoadLocal bar$1 scope 0: [3] $4 = Call $2(<unknown> $3) scope 1: [4] $5 = Object { } scope 2: [5] $6 = Object { a: $4, b: $5 } [6] $8 = StoreLocal Const x$7 = $6 ``` #### Dependencies `ReactiveScope.dependencies` records the set of (read-only) values that a reactive scope is dependent on. This is currently limited to just variables (named variables from source and promoted temporaries) and property-loads. All dependencies we record need to be hoistable -- i.e. reordered to just before the ReactiveScope begins. Not all PropertyLoads are hoistable. In this example, we should not evaluate `obj.a.b` without before creating x and checking `objIsNull`. ```js // reduce-reactive-deps/no-uncond.js function useFoo({ obj, objIsNull }) { const x = []; if (isFalse(objIsNull)) { x.push(obj.a.b); } return x; } ``` While other memoization strategies with different constraints exist, the current compiler requires that `ReactiveScope.dependencies` be re-orderable to the beginning of the reactive scope. But.. `PropertyLoad`s from null values will throw `TypeError`. This means that evaluating hoisted dependencies should throw if and only if the source program throws. (It is also a bug if source throws and compiler output does not throw. See https://github.com/facebook/react-forget/pull/2709) --- ### Rough high level overview 1. Pass 1 Walk over instructions to gather every temporary used outside of its defining scope (same as ReactiveFunction version). These determine the sidemaps we produce, as temporaries used outside of their declaring scopes get promoted to named variables later (and are not considered hoistable rvals). 2. Pass 2 (collectTemporariesSidemap) Walk over instructions to generate a sidemap of temporary identifier -> named variable and property path (e.g. `$3 -> {obj: props, path: ["a", "b"]}`) 2. Pass 2 (collectHoistablePropertyLoads) a. Build a sidemap of block -> accessed variables and properties (e.g. `bb0 -> [ {obj: props, path: ["a", "b"]} ]`) b. Propagate "non-nullness" i.e. variables and properties for which we can safely evaluate `PropertyLoad`. A basic block can unconditionally read from identifier X if any of the following applies: - the block itself reads from identifier X - all predecessors of the block read from identifier X - all successors of the block read from identifier X 4. Pass 3: (collectDependencies) Walks over instructions again to record dependencies and declarations, using the previously produced sidemaps. We do not record any control-flow here 5. Merge every scope's recorded dependencies with the set of hoistable PropertyLoads Tested by syncing internally and (1) checking compilation output differences ([internal link](https://www.internalfb.com/intern/everpaste/?handle=GPCfUBt_HCoy_S4EAJDVFJyJJMR0bsIXAAAB)), running internally e2e tests ([internal link](https://fburl.com/sandcastle/cs5mlkxq)) --- ### Followups: 1. Rewrite function expression deps This change produces much more optimal output as the compiler now uses the function CFG to understand which variables / paths are assumed to be non-null. However, it may exacerbate [this function-expr hoisting bug](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.tsx). A short term fix here is to simply call some form of `collectNonNullObjects` on every function expression to find hoistable variable / paths. In the longer term, we should refactor out `FunctionExpression.deps`. 2. Enable optional paths (a) don't count optional load temporaries as dependencies (e.g. `collectOptionalLoadRValues(...)`). (b) record optional paths in both collectHoistablePropertyLoads and dependency collection ghstack-source-id: 2507f6ea751dce09ad1dccd353ae6fc7cf411582 Pull Request resolved: https://github.com/facebook/react/pull/30894

Mofei Zhang committed Sep 12, 2024 at 16:59 UTC 206df66e70652e85711c3177ce1a0459609a7771
67 files changed +2848 -484
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+17 -6
@@ -101,6 +101,7 @@ import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
101 import {lowerContextAccess} from '../Optimization/LowerContextAccess';
102 import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
103 import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
104 +import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
105
106 export type CompilerPipelineValue =
107 | {kind: 'ast'; name: string; value: CodegenFunction}
@@ -341,6 +342,14 @@ function* runWithEnvironment(
342 });
343 assertTerminalSuccessorsExist(hir);
344 assertTerminalPredsExist(hir);
345 + if (env.config.enablePropagateDepsInHIR) {
346 + propagateScopeDependenciesHIR(hir);
347 + yield log({
348 + kind: 'hir',
349 + name: 'PropagateScopeDependenciesHIR',
350 + value: hir,
351 + });
352 + }
353
354 const reactiveFunction = buildReactiveFunction(hir);
355 yield log({
@@ -359,12 +368,14 @@ function* runWithEnvironment(
368 });
369 assertScopeInstructionsWithinScopes(reactiveFunction);
370
362 - propagateScopeDependencies(reactiveFunction);
363 - yield log({
364 - kind: 'reactive',
365 - name: 'PropagateScopeDependencies',
366 - value: reactiveFunction,
367 - });
371 + if (!env.config.enablePropagateDepsInHIR) {
372 + propagateScopeDependencies(reactiveFunction);
373 + yield log({
374 + kind: 'reactive',
375 + name: 'PropagateScopeDependencies',
376 + value: reactiveFunction,
377 + });
378 + }
379
380 pruneNonEscapingScopes(reactiveFunction);
381 yield log({
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts new
+469
@@ -0,0 +1,469 @@
1 +import {CompilerError} from '../CompilerError';
2 +import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
3 +import {Set_intersect, Set_union, getOrInsertDefault} from '../Utils/utils';
4 +import {
5 + BasicBlock,
6 + BlockId,
7 + GeneratedSource,
8 + HIRFunction,
9 + Identifier,
10 + IdentifierId,
11 + Place,
12 + ReactiveScopeDependency,
13 + ScopeId,
14 +} from './HIR';
15 +
16 +/**
17 + * Helper function for `PropagateScopeDependencies`.
18 + * Uses control flow graph analysis to determine which `Identifier`s can
19 + * be assumed to be non-null objects, on a per-block basis.
20 + *
21 + * Here is an example:
22 + * ```js
23 + * function useFoo(x, y, z) {
24 + * // NOT safe to hoist PropertyLoads here
25 + * if (...) {
26 + * // safe to hoist loads from x
27 + * read(x.a);
28 + * return;
29 + * }
30 + * // safe to hoist loads from y, z
31 + * read(y.b);
32 + * if (...) {
33 + * // safe to hoist loads from y, z
34 + * read(z.a);
35 + * } else {
36 + * // safe to hoist loads from y, z
37 + * read(z.b);
38 + * }
39 + * // safe to hoist loads from y, z
40 + * return;
41 + * }
42 + * ```
43 + *
44 + * Note that we currently do NOT account for mutable / declaration range
45 + * when doing the CFG-based traversal, producing results that are technically
46 + * incorrect but filtered by PropagateScopeDeps (which only takes dependencies
47 + * on constructed value -- i.e. a scope's dependencies must have mutable ranges
48 + * ending earlier than the scope start).
49 + *
50 + * Take this example, this function will infer x.foo.bar as non-nullable for bb0,
51 + * via the intersection of bb1 & bb2 which in turn comes from bb3. This is technically
52 + * incorrect bb0 is before / during x's mutable range.
53 + * bb0:
54 + * const x = ...;
55 + * if cond then bb1 else bb2
56 + * bb1:
57 + * ...
58 + * goto bb3
59 + * bb2:
60 + * ...
61 + * goto bb3:
62 + * bb3:
63 + * x.foo.bar
64 + */
65 +export function collectHoistablePropertyLoads(
66 + fn: HIRFunction,
67 + temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
68 +): ReadonlyMap<ScopeId, BlockInfo> {
69 + const nodes = collectPropertyLoadsInBlocks(fn, temporaries);
70 + propagateNonNull(fn, nodes);
71 +
72 + const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
73 + for (const [_, block] of fn.body.blocks) {
74 + if (block.terminal.kind === 'scope') {
75 + nodesKeyedByScopeId.set(
76 + block.terminal.scope.id,
77 + nodes.get(block.terminal.block)!,
78 + );
79 + }
80 + }
81 +
82 + return nodesKeyedByScopeId;
83 +}
84 +
85 +export type BlockInfo = {
86 + block: BasicBlock;
87 + assumedNonNullObjects: ReadonlySet<PropertyLoadNode>;
88 +};
89 +
90 +export function getProperty(
91 + object: Place,
92 + propertyName: string,
93 + temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
94 +): ReactiveScopeDependency {
95 + /*
96 + * (1) Get the base object either from the temporary sidemap (e.g. a LoadLocal)
97 + * or a deep copy of an existing property dependency.
98 + * Example 1:
99 + * $0 = LoadLocal x
100 + * $1 = PropertyLoad $0.y
101 + * getProperty($0, ...) -> resolvedObject = x, resolvedDependency = null
102 + *
103 + * Example 2:
104 + * $0 = LoadLocal x
105 + * $1 = PropertyLoad $0.y
106 + * $2 = PropertyLoad $1.z
107 + * getProperty($1, ...) -> resolvedObject = null, resolvedDependency = x.y
108 + *
109 + * Example 3:
110 + * $0 = Call(...)
111 + * $1 = PropertyLoad $0.y
112 + * getProperty($0, ...) -> resolvedObject = null, resolvedDependency = null
113 + */
114 + const resolvedDependency = temporaries.get(object.identifier.id);
115 +
116 + /**
117 + * (2) Push the last PropertyLoad
118 + * TODO(mofeiZ): understand optional chaining
119 + */
120 + let property: ReactiveScopeDependency;
121 + if (resolvedDependency == null) {
122 + property = {
123 + identifier: object.identifier,
124 + path: [{property: propertyName, optional: false}],
125 + };
126 + } else {
127 + property = {
128 + identifier: resolvedDependency.identifier,
129 + path: [
130 + ...resolvedDependency.path,
131 + {property: propertyName, optional: false},
132 + ],
133 + };
134 + }
135 + return property;
136 +}
137 +
138 +export function resolveTemporary(
139 + place: Place,
140 + temporaries: ReadonlyMap<IdentifierId, Identifier>,
141 +): Identifier {
142 + return temporaries.get(place.identifier.id) ?? place.identifier;
143 +}
144 +
145 +/**
146 + * Tree data structure to dedupe property loads (e.g. a.b.c)
147 + * and make computing sets intersections simpler.
148 + */
149 +type RootNode = {
150 + properties: Map<string, PropertyLoadNode>;
151 + parent: null;
152 + // Recorded to make later computations simpler
153 + fullPath: ReactiveScopeDependency;
154 + root: Identifier;
155 +};
156 +
157 +type PropertyLoadNode =
158 + | {
159 + properties: Map<string, PropertyLoadNode>;
160 + parent: PropertyLoadNode;
161 + fullPath: ReactiveScopeDependency;
162 + }
163 + | RootNode;
164 +
165 +class Tree {
166 + roots: Map<Identifier, RootNode> = new Map();
167 +
168 + #getOrCreateRoot(identifier: Identifier): PropertyLoadNode {
169 + /**
170 + * Reads from a statically scoped variable are always safe in JS,
171 + * with the exception of TDZ (not addressed by this pass).
172 + */
173 + let rootNode = this.roots.get(identifier);
174 +
175 + if (rootNode === undefined) {
176 + rootNode = {
177 + root: identifier,
178 + properties: new Map(),
179 + fullPath: {
180 + identifier,
181 + path: [],
182 + },
183 + parent: null,
184 + };
185 + this.roots.set(identifier, rootNode);
186 + }
187 + return rootNode;
188 + }
189 +
190 + static #getOrCreateProperty(
191 + node: PropertyLoadNode,
192 + property: string,
193 + ): PropertyLoadNode {
194 + let child = node.properties.get(property);
195 + if (child == null) {
196 + child = {
197 + properties: new Map(),
198 + parent: node,
199 + fullPath: {
200 + identifier: node.fullPath.identifier,
201 + path: node.fullPath.path.concat([{property, optional: false}]),
202 + },
203 + };
204 + node.properties.set(property, child);
205 + }
206 + return child;
207 + }
208 +
209 + getPropertyLoadNode(n: ReactiveScopeDependency): PropertyLoadNode {
210 + CompilerError.invariant(n.path.length > 0, {
211 + reason:
212 + '[CollectHoistablePropertyLoads] Expected property node, found root node',
213 + loc: GeneratedSource,
214 + });
215 + /**
216 + * We add ReactiveScopeDependencies according to instruction ordering,
217 + * so all subpaths of a PropertyLoad should already exist
218 + * (e.g. a.b is added before a.b.c),
219 + */
220 + let currNode = this.#getOrCreateRoot(n.identifier);
221 + for (let i = 0; i < n.path.length - 1; i++) {
222 + currNode = assertNonNull(currNode.properties.get(n.path[i].property));
223 + }
224 +
225 + return Tree.#getOrCreateProperty(currNode, n.path.at(-1)!.property);
226 + }
227 +}
228 +
229 +function collectPropertyLoadsInBlocks(
230 + fn: HIRFunction,
231 + temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
232 +): ReadonlyMap<BlockId, BlockInfo> {
233 + /**
234 + * Due to current limitations of mutable range inference, there are edge cases in
235 + * which we infer known-immutable values (e.g. props or hook params) to have a
236 + * mutable range and scope.
237 + * (see `destructure-array-declaration-to-context-var` fixture)
238 + * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
239 + * is being rewritten to HIR).
240 + */
241 + const knownImmutableIdentifiers = new Set<Identifier>();
242 + if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
243 + for (const p of fn.params) {
244 + if (p.kind === 'Identifier') {
245 + knownImmutableIdentifiers.add(p.identifier);
246 + }
247 + }
248 + }
249 + const tree = new Tree();
250 + const nodes = new Map<BlockId, BlockInfo>();
251 + for (const [_, block] of fn.body.blocks) {
252 + const assumedNonNullObjects = new Set<PropertyLoadNode>();
253 + for (const instr of block.instructions) {
254 + if (instr.value.kind === 'PropertyLoad') {
255 + const property = getProperty(
256 + instr.value.object,
257 + instr.value.property,
258 + temporaries,
259 + );
260 + const propertyNode = tree.getPropertyLoadNode(property);
261 + const object = instr.value.object.identifier;
262 + /**
263 + * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
264 + * are not valid with respect to current instruction id numbering.
265 + * We use attached reactive scope ranges as a proxy for mutable range, but this
266 + * is an overestimate as (1) scope ranges merge and align to form valid program
267 + * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
268 + * non-mutable identifiers.
269 + *
270 + * See comment at top of function for why we track known immutable identifiers.
271 + */
272 + const isMutableAtInstr =
273 + object.mutableRange.end > object.mutableRange.start + 1 &&
274 + object.scope != null &&
275 + inRange(instr, object.scope.range);
276 + if (
277 + !isMutableAtInstr ||
278 + knownImmutableIdentifiers.has(propertyNode.fullPath.identifier)
279 + ) {
280 + let curr = propertyNode.parent;
281 + while (curr != null) {
282 + assumedNonNullObjects.add(curr);
283 + curr = curr.parent;
284 + }
285 + }
286 + }
287 + // TODO handle destructuring
288 + }
289 +
290 + nodes.set(block.id, {
291 + block,
292 + assumedNonNullObjects,
293 + });
294 + }
295 + return nodes;
296 +}
297 +
298 +function propagateNonNull(
299 + fn: HIRFunction,
300 + nodes: ReadonlyMap<BlockId, BlockInfo>,
301 +): void {
302 + const blockSuccessors = new Map<BlockId, Set<BlockId>>();
303 + const terminalPreds = new Set<BlockId>();
304 +
305 + for (const [blockId, block] of fn.body.blocks) {
306 + for (const pred of block.preds) {
307 + getOrInsertDefault(blockSuccessors, pred, new Set()).add(blockId);
308 + }
309 + if (block.terminal.kind === 'throw' || block.terminal.kind === 'return') {
310 + terminalPreds.add(blockId);
311 + }
312 + }
313 +
314 + /**
315 + * In the context of a control flow graph, the identifiers that a block
316 + * can assume are non-null can be calculated from the following:
317 + * X = Union(Intersect(X_neighbors), X)
318 + */
319 + function recursivelyPropagateNonNull(
320 + nodeId: BlockId,
321 + direction: 'forward' | 'backward',
322 + traversalState: Map<BlockId, 'active' | 'done'>,
323 + nonNullObjectsByBlock: Map<BlockId, ReadonlySet<PropertyLoadNode>>,
324 + ): boolean {
325 + /**
326 + * Avoid re-visiting computed or currently active nodes, which can
327 + * occur when the control flow graph has backedges.
328 + */
329 + if (traversalState.has(nodeId)) {
330 + return false;
331 + }
332 + traversalState.set(nodeId, 'active');
333 +
334 + const node = nodes.get(nodeId);
335 + if (node == null) {
336 + CompilerError.invariant(false, {
337 + reason: `Bad node ${nodeId}, kind: ${direction}`,
338 + loc: GeneratedSource,
339 + });
340 + }
341 + const neighbors = Array.from(
342 + direction === 'backward'
343 + ? (blockSuccessors.get(nodeId) ?? [])
344 + : node.block.preds,
345 + );
346 +
347 + let changed = false;
348 + for (const pred of neighbors) {
349 + if (!traversalState.has(pred)) {
350 + const neighborChanged = recursivelyPropagateNonNull(
351 + pred,
352 + direction,
353 + traversalState,
354 + nonNullObjectsByBlock,
355 + );
356 + changed ||= neighborChanged;
357 + }
358 + }
359 + /**
360 + * Note that a predecessor / successor can only be active (status != 'done')
361 + * if it is a self-loop or other transitive cycle. Active neighbors can be
362 + * filtered out (i.e. not included in the intersection)
363 + * Example: self loop.
364 + * X = Union(Intersect(X, ...X_other_neighbors), X)
365 + *
366 + * Example: transitive cycle through node Y, for some Y that is a
367 + * predecessor / successor of X.
368 + * X = Union(
369 + * Intersect(
370 + * Union(Intersect(X, ...Y_other_neighbors), Y),
371 + * ...X_neighbors
372 + * ),
373 + * X
374 + * )
375 + *
376 + * Non-active neighbors with no recorded results can occur due to backedges.
377 + * it's not safe to assume they can be filtered out (e.g. not included in
378 + * the intersection)
379 + */
380 + const neighborAccesses = Set_intersect(
381 + Array.from(neighbors)
382 + .filter(n => traversalState.get(n) === 'done')
383 + .map(n => assertNonNull(nonNullObjectsByBlock.get(n))),
384 + );
385 +
386 + const prevObjects = assertNonNull(nonNullObjectsByBlock.get(nodeId));
387 + const newObjects = Set_union(prevObjects, neighborAccesses);
388 +
389 + nonNullObjectsByBlock.set(nodeId, newObjects);
390 + traversalState.set(nodeId, 'done');
391 + changed ||= prevObjects.size !== newObjects.size;
392 + return changed;
393 + }
394 + const fromEntry = new Map<BlockId, ReadonlySet<PropertyLoadNode>>();
395 + const fromExit = new Map<BlockId, ReadonlySet<PropertyLoadNode>>();
396 + for (const [blockId, blockInfo] of nodes) {
397 + fromEntry.set(blockId, blockInfo.assumedNonNullObjects);
398 + fromExit.set(blockId, blockInfo.assumedNonNullObjects);
399 + }
400 + const traversalState = new Map<BlockId, 'done' | 'active'>();
401 + const reversedBlocks = [...fn.body.blocks];
402 + reversedBlocks.reverse();
403 +
404 + let i = 0;
405 + let changed;
406 + do {
407 + i++;
408 + changed = false;
409 + for (const [blockId] of fn.body.blocks) {
410 + const forwardChanged = recursivelyPropagateNonNull(
411 + blockId,
412 + 'forward',
413 + traversalState,
414 + fromEntry,
415 + );
416 + changed ||= forwardChanged;
417 + }
418 + traversalState.clear();
419 + for (const [blockId] of reversedBlocks) {
420 + const backwardChanged = recursivelyPropagateNonNull(
421 + blockId,
422 + 'backward',
423 + traversalState,
424 + fromExit,
425 + );
426 + changed ||= backwardChanged;
427 + }
428 + traversalState.clear();
429 + } while (changed);
430 +
431 + /**
432 + * TODO: validate against meta internal code, then remove in future PR.
433 + * Currently cannot come up with a case that requires fixed-point iteration.
434 + */
435 + CompilerError.invariant(i <= 2, {
436 + reason: 'require fixed-point iteration',
437 + description: `#iterations = ${i}`,
438 + loc: GeneratedSource,
439 + });
440 +
441 + CompilerError.invariant(
442 + fromEntry.size === fromExit.size && fromEntry.size === nodes.size,
443 + {
444 + reason:
445 + 'bad sizes after calculating fromEntry + fromExit ' +
446 + `${fromEntry.size} ${fromExit.size} ${nodes.size}`,
447 + loc: GeneratedSource,
448 + },
449 + );
450 +
451 + for (const [id, node] of nodes) {
452 + node.assumedNonNullObjects = Set_union(
453 + assertNonNull(fromEntry.get(id)),
454 + assertNonNull(fromExit.get(id)),
455 + );
456 + }
457 +}
458 +
459 +function assertNonNull<T extends NonNullable<U>, U>(
460 + value: T | null | undefined,
461 + source?: string,
462 +): T {
463 + CompilerError.invariant(value != null, {
464 + reason: 'Unexpected null',
465 + description: source != null ? `(from ${source})` : null,
466 + loc: GeneratedSource,
467 + });
468 + return value;
469 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts new
+267
@@ -0,0 +1,267 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {CompilerError} from '../CompilerError';
9 +import {GeneratedSource, Identifier, ReactiveScopeDependency} from '../HIR';
10 +import {printIdentifier} from '../HIR/PrintHIR';
11 +import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies';
12 +
13 +const ENABLE_DEBUG_INVARIANTS = true;
14 +
15 +/**
16 + * Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR
17 + * for detailed explanation.
18 + */
19 +export class ReactiveScopeDependencyTreeHIR {
20 + #roots: Map<Identifier, DependencyNode> = new Map();
21 +
22 + #getOrCreateRoot(identifier: Identifier, isNonNull: boolean): DependencyNode {
23 + // roots can always be accessed unconditionally in JS
24 + let rootNode = this.#roots.get(identifier);
25 +
26 + if (rootNode === undefined) {
27 + rootNode = {
28 + properties: new Map(),
29 + accessType: isNonNull
30 + ? PropertyAccessType.NonNullAccess
31 + : PropertyAccessType.Access,
32 + };
33 + this.#roots.set(identifier, rootNode);
34 + }
35 + return rootNode;
36 + }
37 +
38 + addDependency(dep: ReactiveScopePropertyDependency): void {
39 + const {path} = dep;
40 + let currNode = this.#getOrCreateRoot(dep.identifier, false);
41 +
42 + const accessType = PropertyAccessType.Access;
43 +
44 + currNode.accessType = merge(currNode.accessType, accessType);
45 +
46 + for (const property of path) {
47 + // all properties read 'on the way' to a dependency are marked as 'access'
48 + let currChild = getOrMakeProperty(currNode, property.property);
49 + currChild.accessType = merge(currChild.accessType, accessType);
50 + currNode = currChild;
51 + }
52 +
53 + /*
54 + * If this property does not have a conditional path (i.e. a.b.c), the
55 + * final property node should be marked as an conditional/unconditional
56 + * `dependency` as based on control flow.
57 + */
58 + currNode.accessType = merge(
59 + currNode.accessType,
60 + PropertyAccessType.Dependency,
61 + );
62 + }
63 +
64 + markNodesNonNull(dep: ReactiveScopePropertyDependency): void {
65 + const accessType = PropertyAccessType.NonNullAccess;
66 + let currNode = this.#roots.get(dep.identifier);
67 +
68 + let cursor = 0;
69 + while (currNode != null && cursor < dep.path.length) {
70 + currNode.accessType = merge(currNode.accessType, accessType);
71 + currNode = currNode.properties.get(dep.path[cursor++].property);
72 + }
73 + if (currNode != null) {
74 + currNode.accessType = merge(currNode.accessType, accessType);
75 + }
76 + }
77 +
78 + /**
79 + * Derive a set of minimal dependencies that are safe to
80 + * access unconditionally (with respect to nullthrows behavior)
81 + */
82 + deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
83 + const results = new Set<ReactiveScopeDependency>();
84 + for (const [rootId, rootNode] of this.#roots.entries()) {
85 + if (ENABLE_DEBUG_INVARIANTS) {
86 + assertWellFormedTree(rootNode);
87 + }
88 + const deps = deriveMinimalDependenciesInSubtree(rootNode, []);
89 +
90 + for (const dep of deps) {
91 + results.add({
92 + identifier: rootId,
93 + path: dep.path.map(s => ({property: s, optional: false})),
94 + });
95 + }
96 + }
97 +
98 + return results;
99 + }
100 +
101 + /*
102 + * Prints dependency tree to string for debugging.
103 + * @param includeAccesses
104 + * @returns string representation of DependencyTree
105 + */
106 + printDeps(includeAccesses: boolean): string {
107 + let res: Array<Array<string>> = [];
108 +
109 + for (const [rootId, rootNode] of this.#roots.entries()) {
110 + const rootResults = printSubtree(rootNode, includeAccesses).map(
111 + result => `${printIdentifier(rootId)}.${result}`,
112 + );
113 + res.push(rootResults);
114 + }
115 + return res.flat().join('\n');
116 + }
117 +}
118 +
119 +enum PropertyAccessType {
120 + Access = 'Access',
121 + NonNullAccess = 'NonNullAccess',
122 + Dependency = 'Dependency',
123 + NonNullDependency = 'NonNullDependency',
124 +}
125 +
126 +const MIN_ACCESS_TYPE = PropertyAccessType.Access;
127 +/**
128 + * "NonNull" means that PropertyReads from a node are side-effect free,
129 + * as the node is (1) immutable and (2) has unconditional propertyloads
130 + * somewhere in the cfg.
131 + */
132 +function isNonNull(access: PropertyAccessType): boolean {
133 + return (
134 + access === PropertyAccessType.NonNullAccess ||
135 + access === PropertyAccessType.NonNullDependency
136 + );
137 +}
138 +function isDependency(access: PropertyAccessType): boolean {
139 + return (
140 + access === PropertyAccessType.Dependency ||
141 + access === PropertyAccessType.NonNullDependency
142 + );
143 +}
144 +
145 +function merge(
146 + access1: PropertyAccessType,
147 + access2: PropertyAccessType,
148 +): PropertyAccessType {
149 + const resultisNonNull = isNonNull(access1) || isNonNull(access2);
150 + const resultIsDependency = isDependency(access1) || isDependency(access2);
151 +
152 + /*
153 + * Straightforward merge.
154 + * This can be represented as bitwise OR, but is written out for readability
155 + *
156 + * Observe that `NonNullAccess | Dependency` produces an
157 + * unconditionally accessed conditional dependency. We currently use these
158 + * as we use unconditional dependencies. (i.e. to codegen change variables)
159 + */
160 + if (resultisNonNull) {
161 + if (resultIsDependency) {
162 + return PropertyAccessType.NonNullDependency;
163 + } else {
164 + return PropertyAccessType.NonNullAccess;
165 + }
166 + } else {
167 + if (resultIsDependency) {
168 + return PropertyAccessType.Dependency;
169 + } else {
170 + return PropertyAccessType.Access;
171 + }
172 + }
173 +}
174 +
175 +type DependencyNode = {
176 + properties: Map<string, DependencyNode>;
177 + accessType: PropertyAccessType;
178 +};
179 +
180 +type ReduceResultNode = {
181 + path: Array<string>;
182 +};
183 +
184 +function assertWellFormedTree(node: DependencyNode): void {
185 + let nonNullInChildren = false;
186 + for (const childNode of node.properties.values()) {
187 + assertWellFormedTree(childNode);
188 + nonNullInChildren ||= isNonNull(childNode.accessType);
189 + }
190 + if (nonNullInChildren) {
191 + CompilerError.invariant(isNonNull(node.accessType), {
192 + reason:
193 + '[DeriveMinimialDependencies] Not well formed tree, unexpected non-null node',
194 + description: node.accessType,
195 + loc: GeneratedSource,
196 + });
197 + }
198 +}
199 +
200 +function deriveMinimalDependenciesInSubtree(
201 + node: DependencyNode,
202 + path: Array<string>,
203 +): Array<ReduceResultNode> {
204 + if (isDependency(node.accessType)) {
205 + /**
206 + * If this node is a dependency, we truncate the subtree
207 + * and return this node. e.g. deps=[`obj.a`, `obj.a.b`]
208 + * reduces to deps=[`obj.a`]
209 + */
210 + return [{path}];
211 + } else {
212 + if (isNonNull(node.accessType)) {
213 + /*
214 + * Only recurse into subtree dependencies if this node
215 + * is known to be non-null.
216 + */
217 + const result: Array<ReduceResultNode> = [];
218 + for (const [childName, childNode] of node.properties) {
219 + result.push(
220 + ...deriveMinimalDependenciesInSubtree(childNode, [
221 + ...path,
222 + childName,
223 + ]),
224 + );
225 + }
226 + return result;
227 + } else {
228 + /*
229 + * This only occurs when this subtree contains a dependency,
230 + * but this node is potentially nullish. As we currently
231 + * don't record optional property paths as scope dependencies,
232 + * we truncate and record this node as a dependency.
233 + */
234 + return [{path}];
235 + }
236 + }
237 +}
238 +
239 +function printSubtree(
240 + node: DependencyNode,
241 + includeAccesses: boolean,
242 +): Array<string> {
243 + const results: Array<string> = [];
244 + for (const [propertyName, propertyNode] of node.properties) {
245 + if (includeAccesses || isDependency(propertyNode.accessType)) {
246 + results.push(`${propertyName} (${propertyNode.accessType})`);
247 + }
248 + const propertyResults = printSubtree(propertyNode, includeAccesses);
249 + results.push(...propertyResults.map(result => `${propertyName}.${result}`));
250 + }
251 + return results;
252 +}
253 +
254 +function getOrMakeProperty(
255 + node: DependencyNode,
256 + property: string,
257 +): DependencyNode {
258 + let child = node.properties.get(property);
259 + if (child == null) {
260 + child = {
261 + properties: new Map(),
262 + accessType: MIN_ACCESS_TYPE,
263 + };
264 + node.properties.set(property, child);
265 + }
266 + return child;
267 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts new
+557
@@ -0,0 +1,557 @@
1 +import {
2 + ScopeId,
3 + HIRFunction,
4 + Place,
5 + Instruction,
6 + ReactiveScopeDependency,
7 + Identifier,
8 + ReactiveScope,
9 + isObjectMethodType,
10 + isRefValueType,
11 + isUseRefType,
12 + makeInstructionId,
13 + InstructionId,
14 + InstructionKind,
15 + GeneratedSource,
16 + DeclarationId,
17 + areEqualPaths,
18 + IdentifierId,
19 +} from './HIR';
20 +import {
21 + BlockInfo,
22 + collectHoistablePropertyLoads,
23 + getProperty,
24 +} from './CollectHoistablePropertyLoads';
25 +import {
26 + ScopeBlockTraversal,
27 + eachInstructionOperand,
28 + eachInstructionValueOperand,
29 + eachPatternOperand,
30 + eachTerminalOperand,
31 +} from './visitors';
32 +import {Stack, empty} from '../Utils/Stack';
33 +import {CompilerError} from '../CompilerError';
34 +import {Iterable_some} from '../Utils/utils';
35 +import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
36 +
37 +export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
38 + const usedOutsideDeclaringScope =
39 + findTemporariesUsedOutsideDeclaringScope(fn);
40 + const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope);
41 +
42 + const hoistablePropertyLoads = collectHoistablePropertyLoads(fn, temporaries);
43 +
44 + const scopeDeps = collectDependencies(
45 + fn,
46 + usedOutsideDeclaringScope,
47 + temporaries,
48 + );
49 +
50 + /**
51 + * Derive the minimal set of hoistable dependencies for each scope.
52 + */
53 + for (const [scope, deps] of scopeDeps) {
54 + const tree = new ReactiveScopeDependencyTreeHIR();
55 +
56 + /**
57 + * Step 1: Add every dependency used by this scope (e.g. `a.b.c`)
58 + */
59 + for (const dep of deps) {
60 + tree.addDependency({...dep});
61 + }
62 + /**
63 + * Step 2: Mark hoistable dependencies, given the basic block in
64 + * which the scope begins.
65 + */
66 + recordHoistablePropertyReads(hoistablePropertyLoads, scope.id, tree);
67 + const candidates = tree.deriveMinimalDependencies();
68 + for (const candidateDep of candidates) {
69 + if (
70 + !Iterable_some(
71 + scope.dependencies,
72 + existingDep =>
73 + existingDep.identifier.declarationId ===
74 + candidateDep.identifier.declarationId &&
75 + areEqualPaths(existingDep.path, candidateDep.path),
76 + )
77 + )
78 + scope.dependencies.add(candidateDep);
79 + }
80 + }
81 +}
82 +
83 +function findTemporariesUsedOutsideDeclaringScope(
84 + fn: HIRFunction,
85 +): ReadonlySet<DeclarationId> {
86 + /*
87 + * tracks all relevant LoadLocal and PropertyLoad lvalues
88 + * and the scope where they are defined
89 + */
90 + const declarations = new Map<DeclarationId, ScopeId>();
91 + const prunedScopes = new Set<ScopeId>();
92 + const scopeTraversal = new ScopeBlockTraversal();
93 + const usedOutsideDeclaringScope = new Set<DeclarationId>();
94 +
95 + function handlePlace(place: Place): void {
96 + const declaringScope = declarations.get(place.identifier.declarationId);
97 + if (
98 + declaringScope != null &&
99 + !scopeTraversal.isScopeActive(declaringScope) &&
100 + !prunedScopes.has(declaringScope)
101 + ) {
102 + // Declaring scope is not active === used outside declaring scope
103 + usedOutsideDeclaringScope.add(place.identifier.declarationId);
104 + }
105 + }
106 +
107 + function handleInstruction(instr: Instruction): void {
108 + const scope = scopeTraversal.currentScope;
109 + if (scope == null || prunedScopes.has(scope)) {
110 + return;
111 + }
112 + switch (instr.value.kind) {
113 + case 'LoadLocal':
114 + case 'LoadContext':
115 + case 'PropertyLoad': {
116 + declarations.set(instr.lvalue.identifier.declarationId, scope);
117 + break;
118 + }
119 + default: {
120 + break;
121 + }
122 + }
123 + }
124 +
125 + for (const [blockId, block] of fn.body.blocks) {
126 + scopeTraversal.recordScopes(block);
127 + const scopeStartInfo = scopeTraversal.blockInfos.get(blockId);
128 + if (scopeStartInfo?.kind === 'begin' && scopeStartInfo.pruned) {
129 + prunedScopes.add(scopeStartInfo.scope.id);
130 + }
131 + for (const instr of block.instructions) {
132 + for (const place of eachInstructionOperand(instr)) {
133 + handlePlace(place);
134 + }
135 + handleInstruction(instr);
136 + }
137 +
138 + for (const place of eachTerminalOperand(block.terminal)) {
139 + handlePlace(place);
140 + }
141 + }
142 + return usedOutsideDeclaringScope;
143 +}
144 +
145 +/**
146 + * @returns mapping of LoadLocal and PropertyLoad to the source of the load.
147 + * ```js
148 + * // source
149 + * foo(a.b);
150 + *
151 + * // HIR: a potential sidemap is {0: a, 1: a.b, 2: foo}
152 + * $0 = LoadLocal 'a'
153 + * $1 = PropertyLoad $0, 'b'
154 + * $2 = LoadLocal 'foo'
155 + * $3 = CallExpression $2($1)
156 + * ```
157 + * Only map LoadLocal and PropertyLoad lvalues to their source if we know that
158 + * reordering the read (from the time-of-load to time-of-use) is valid.
159 + *
160 + * If a LoadLocal or PropertyLoad instruction is within the reactive scope range
161 + * (a proxy for mutable range) of the load source, later instructions may
162 + * reassign / mutate the source value. Since it's incorrect to reorder these
163 + * load instructions to after their scope ranges, we also do not store them in
164 + * identifier sidemaps.
165 + *
166 + * Take this example (from fixture
167 + * `evaluation-order-mutate-call-after-dependency-load`)
168 + * ```js
169 + * // source
170 + * function useFoo(arg) {
171 + * const arr = [1, 2, 3, ...arg];
172 + * return [
173 + * arr.length,
174 + * arr.push(0)
175 + * ];
176 + * }
177 + *
178 + * // IR pseudocode
179 + * scope @0 {
180 + * $0 = arr = ArrayExpression [1, 2, 3, ...arg]
181 + * $1 = arr.length
182 + * $2 = arr.push(0)
183 + * }
184 + * scope @1 {
185 + * $3 = ArrayExpression [$1, $2]
186 + * }
187 + * ```
188 + * Here, it's invalid for scope@1 to take `arr.length` as a dependency instead
189 + * of $1, as the evaluation of `arr.length` changes between instructions $1 and
190 + * $3. We do not track $1 -> arr.length in this case.
191 + */
192 +function collectTemporariesSidemap(
193 + fn: HIRFunction,
194 + usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
195 +): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
196 + const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
197 + for (const [_, block] of fn.body.blocks) {
198 + for (const instr of block.instructions) {
199 + const {value, lvalue} = instr;
200 + const usedOutside = usedOutsideDeclaringScope.has(
201 + lvalue.identifier.declarationId,
202 + );
203 +
204 + if (value.kind === 'PropertyLoad' && !usedOutside) {
205 + const property = getProperty(value.object, value.property, temporaries);
206 + temporaries.set(lvalue.identifier.id, property);
207 + } else if (
208 + value.kind === 'LoadLocal' &&
209 + lvalue.identifier.name == null &&
210 + value.place.identifier.name !== null &&
211 + !usedOutside
212 + ) {
213 + temporaries.set(lvalue.identifier.id, {
214 + identifier: value.place.identifier,
215 + path: [],
216 + });
217 + }
218 + }
219 + }
220 + return temporaries;
221 +}
222 +
223 +type Decl = {
224 + id: InstructionId;
225 + scope: Stack<ReactiveScope>;
226 +};
227 +
228 +class Context {
229 + #declarations: Map<DeclarationId, Decl> = new Map();
230 + #reassignments: Map<Identifier, Decl> = new Map();
231 +
232 + #scopes: Stack<ReactiveScope> = empty();
233 + // Reactive dependencies used in the current reactive scope.
234 + #dependencies: Stack<Array<ReactiveScopeDependency>> = empty();
235 + deps: Map<ReactiveScope, Array<ReactiveScopeDependency>> = new Map();
236 +
237 + #temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
238 + #temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
239 +
240 + constructor(
241 + temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
242 + temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
243 + ) {
244 + this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
245 + this.#temporaries = temporaries;
246 + }
247 +
248 + enterScope(scope: ReactiveScope): void {
249 + // Set context for new scope
250 + this.#dependencies = this.#dependencies.push([]);
251 + this.#scopes = this.#scopes.push(scope);
252 + }
253 +
254 + exitScope(scope: ReactiveScope, pruned: boolean): void {
255 + // Save dependencies we collected from the exiting scope
256 + const scopedDependencies = this.#dependencies.value;
257 + CompilerError.invariant(scopedDependencies != null, {
258 + reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
259 + loc: scope.loc,
260 + });
261 +
262 + // Restore context of previous scope
263 + this.#scopes = this.#scopes.pop();
264 + this.#dependencies = this.#dependencies.pop();
265 +
266 + /*
267 + * Collect dependencies we recorded for the exiting scope and propagate
268 + * them upward using the same rules as normal dependency collection.
269 + * Child scopes may have dependencies on values created within the outer
270 + * scope, which necessarily cannot be dependencies of the outer scope.
271 + */
272 + for (const dep of scopedDependencies) {
273 + if (this.#checkValidDependency(dep)) {
274 + this.#dependencies.value?.push(dep);
275 + }
276 + }
277 +
278 + if (!pruned) {
279 + this.deps.set(scope, scopedDependencies);
280 + }
281 + }
282 +
283 + isUsedOutsideDeclaringScope(place: Place): boolean {
284 + return this.#temporariesUsedOutsideScope.has(
285 + place.identifier.declarationId,
286 + );
287 + }
288 +
289 + /*
290 + * Records where a value was declared, and optionally, the scope where the value originated from.
291 + * This is later used to determine if a dependency should be added to a scope; if the current
292 + * scope we are visiting is the same scope where the value originates, it can't be a dependency
293 + * on itself.
294 + */
295 + declare(identifier: Identifier, decl: Decl): void {
296 + if (!this.#declarations.has(identifier.declarationId)) {
297 + this.#declarations.set(identifier.declarationId, decl);
298 + }
299 + this.#reassignments.set(identifier, decl);
300 + }
301 +
302 + // Checks if identifier is a valid dependency in the current scope
303 + #checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
304 + // ref.current access is not a valid dep
305 + if (
306 + isUseRefType(maybeDependency.identifier) &&
307 + maybeDependency.path.at(0)?.property === 'current'
308 + ) {
309 + return false;
310 + }
311 +
312 + // ref value is not a valid dep
313 + if (isRefValueType(maybeDependency.identifier)) {
314 + return false;
315 + }
316 +
317 + /*
318 + * object methods are not deps because they will be codegen'ed back in to
319 + * the object literal.
320 + */
321 + if (isObjectMethodType(maybeDependency.identifier)) {
322 + return false;
323 + }
324 +
325 + const identifier = maybeDependency.identifier;
326 + /*
327 + * If this operand is used in a scope, has a dynamic value, and was defined
328 + * before this scope, then its a dependency of the scope.
329 + */
330 + const currentDeclaration =
331 + this.#reassignments.get(identifier) ??
332 + this.#declarations.get(identifier.declarationId);
333 + const currentScope = this.currentScope.value;
334 + return (
335 + currentScope != null &&
336 + currentDeclaration !== undefined &&
337 + currentDeclaration.id < currentScope.range.start
338 + );
339 + }
340 +
341 + #isScopeActive(scope: ReactiveScope): boolean {
342 + if (this.#scopes === null) {
343 + return false;
344 + }
345 + return this.#scopes.find(state => state === scope);
346 + }
347 +
348 + get currentScope(): Stack<ReactiveScope> {
349 + return this.#scopes;
350 + }
351 +
352 + visitOperand(place: Place): void {
353 + /*
354 + * if this operand is a temporary created for a property load, try to resolve it to
355 + * the expanded Place. Fall back to using the operand as-is.
356 + */
357 + this.visitDependency(
358 + this.#temporaries.get(place.identifier.id) ?? {
359 + identifier: place.identifier,
360 + path: [],
361 + },
362 + );
363 + }
364 +
365 + visitProperty(object: Place, property: string): void {
366 + const nextDependency = getProperty(object, property, this.#temporaries);
367 + this.visitDependency(nextDependency);
368 + }
369 +
370 + visitDependency(maybeDependency: ReactiveScopeDependency): void {
371 + /*
372 + * Any value used after its originally defining scope has concluded must be added as an
373 + * output of its defining scope. Regardless of whether its a const or not,
374 + * some later code needs access to the value. If the current
375 + * scope we are visiting is the same scope where the value originates, it can't be a dependency
376 + * on itself.
377 + */
378 +
379 + /*
380 + * if originalDeclaration is undefined here, then this is not a local var
381 + * (all decls e.g. `let x;` should be initialized in BuildHIR)
382 + */
383 + const originalDeclaration = this.#declarations.get(
384 + maybeDependency.identifier.declarationId,
385 + );
386 + if (
387 + originalDeclaration !== undefined &&
388 + originalDeclaration.scope.value !== null
389 + ) {
390 + originalDeclaration.scope.each(scope => {
391 + if (
392 + !this.#isScopeActive(scope) &&
393 + !Iterable_some(
394 + scope.declarations.values(),
395 + decl =>
396 + decl.identifier.declarationId ===
397 + maybeDependency.identifier.declarationId,
398 + )
399 + ) {
400 + scope.declarations.set(maybeDependency.identifier.id, {
401 + identifier: maybeDependency.identifier,
402 + scope: originalDeclaration.scope.value!,
403 + });
404 + }
405 + });
406 + }
407 +
408 + if (this.#checkValidDependency(maybeDependency)) {
409 + this.#dependencies.value!.push(maybeDependency);
410 + }
411 + }
412 +
413 + /*
414 + * Record a variable that is declared in some other scope and that is being reassigned in the
415 + * current one as a {@link ReactiveScope.reassignments}
416 + */
417 + visitReassignment(place: Place): void {
418 + const currentScope = this.currentScope.value;
419 + if (
420 + currentScope != null &&
421 + !Iterable_some(
422 + currentScope.reassignments,
423 + identifier =>
424 + identifier.declarationId === place.identifier.declarationId,
425 + ) &&
426 + this.#checkValidDependency({identifier: place.identifier, path: []})
427 + ) {
428 + currentScope.reassignments.add(place.identifier);
429 + }
430 + }
431 +}
432 +
433 +function handleInstruction(instr: Instruction, context: Context): void {
434 + const {id, value, lvalue} = instr;
435 + if (value.kind === 'LoadLocal') {
436 + if (
437 + value.place.identifier.name === null ||
438 + lvalue.identifier.name !== null ||
439 + context.isUsedOutsideDeclaringScope(lvalue)
440 + ) {
441 + context.visitOperand(value.place);
442 + }
443 + } else if (value.kind === 'PropertyLoad') {
444 + if (context.isUsedOutsideDeclaringScope(lvalue)) {
445 + context.visitProperty(value.object, value.property);
446 + }
447 + } else if (value.kind === 'StoreLocal') {
448 + context.visitOperand(value.value);
449 + if (value.lvalue.kind === InstructionKind.Reassign) {
450 + context.visitReassignment(value.lvalue.place);
451 + }
452 + context.declare(value.lvalue.place.identifier, {
453 + id,
454 + scope: context.currentScope,
455 + });
456 + } else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
457 + /*
458 + * Some variables may be declared and never initialized. We need
459 + * to retain (and hoist) these declarations if they are included
460 + * in a reactive scope. One approach is to simply add all `DeclareLocal`s
461 + * as scope declarations.
462 + */
463 +
464 + /*
465 + * We add context variable declarations here, not at `StoreContext`, since
466 + * context Store / Loads are modeled as reads and mutates to the underlying
467 + * variable reference (instead of through intermediate / inlined temporaries)
468 + */
469 + context.declare(value.lvalue.place.identifier, {
470 + id,
471 + scope: context.currentScope,
472 + });
473 + } else if (value.kind === 'Destructure') {
474 + context.visitOperand(value.value);
475 + for (const place of eachPatternOperand(value.lvalue.pattern)) {
476 + if (value.lvalue.kind === InstructionKind.Reassign) {
477 + context.visitReassignment(place);
478 + }
479 + context.declare(place.identifier, {
480 + id,
481 + scope: context.currentScope,
482 + });
483 + }
484 + } else {
485 + for (const operand of eachInstructionValueOperand(value)) {
486 + context.visitOperand(operand);
487 + }
488 + }
489 +
490 + context.declare(lvalue.identifier, {
491 + id,
492 + scope: context.currentScope,
493 + });
494 +}
495 +
496 +function collectDependencies(
497 + fn: HIRFunction,
498 + usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
499 + temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
500 +): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
501 + const context = new Context(usedOutsideDeclaringScope, temporaries);
502 +
503 + for (const param of fn.params) {
504 + if (param.kind === 'Identifier') {
505 + context.declare(param.identifier, {
506 + id: makeInstructionId(0),
507 + scope: empty(),
508 + });
509 + } else {
510 + context.declare(param.place.identifier, {
511 + id: makeInstructionId(0),
512 + scope: empty(),
513 + });
514 + }
515 + }
516 +
517 + const scopeTraversal = new ScopeBlockTraversal();
518 +
519 + for (const [blockId, block] of fn.body.blocks) {
520 + scopeTraversal.recordScopes(block);
521 + const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId);
522 + if (scopeBlockInfo?.kind === 'begin') {
523 + context.enterScope(scopeBlockInfo.scope);
524 + } else if (scopeBlockInfo?.kind === 'end') {
525 + context.exitScope(scopeBlockInfo.scope, scopeBlockInfo?.pruned);
526 + }
527 +
528 + for (const instr of block.instructions) {
529 + handleInstruction(instr, context);
530 + }
531 + for (const place of eachTerminalOperand(block.terminal)) {
532 + context.visitOperand(place);
533 + }
534 + }
535 + return context.deps;
536 +}
537 +
538 +/**
539 + * Compute the set of hoistable property reads.
540 + */
541 +function recordHoistablePropertyReads(
542 + nodes: ReadonlyMap<ScopeId, BlockInfo>,
543 + scopeId: ScopeId,
544 + tree: ReactiveScopeDependencyTreeHIR,
545 +): void {
546 + const node = nodes.get(scopeId);
547 + CompilerError.invariant(node != null, {
548 + reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
549 + loc: GeneratedSource,
550 + });
551 +
552 + for (const item of node.assumedNonNullObjects) {
553 + tree.markNodesNonNull({
554 + ...item.fullPath,
555 + });
556 + }
557 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts
+73
@@ -6,7 +6,9 @@
6 */
7
8 import {assertExhaustive} from '../Utils/utils';
9 +import {CompilerError} from '..';
10 import {
11 + BasicBlock,
12 BlockId,
13 Instruction,
14 InstructionValue,
@@ -14,7 +16,9 @@ import {
16 Pattern,
17 Place,
18 ReactiveInstruction,
19 + ReactiveScope,
20 ReactiveValue,
21 + ScopeId,
22 SpreadPattern,
23 Terminal,
24 } from './HIR';
@@ -1149,3 +1153,72 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
1153 }
1154 }
1155 }
1156 +
1157 +/**
1158 + * Helper class for traversing scope blocks in HIR-form.
1159 + */
1160 +export class ScopeBlockTraversal {
1161 + // Live stack of active scopes
1162 + #activeScopes: Array<ScopeId> = [];
1163 + blockInfos: Map<
1164 + BlockId,
1165 + | {
1166 + kind: 'end';
1167 + scope: ReactiveScope;
1168 + pruned: boolean;
1169 + }
1170 + | {
1171 + kind: 'begin';
1172 + scope: ReactiveScope;
1173 + pruned: boolean;
1174 + fallthrough: BlockId;
1175 + }
1176 + > = new Map();
1177 +
1178 + recordScopes(block: BasicBlock): void {
1179 + const blockInfo = this.blockInfos.get(block.id);
1180 + if (blockInfo?.kind === 'begin') {
1181 + this.#activeScopes.push(blockInfo.scope.id);
1182 + } else if (blockInfo?.kind === 'end') {
1183 + const top = this.#activeScopes.at(-1);
1184 + CompilerError.invariant(blockInfo.scope.id === top, {
1185 + reason:
1186 + 'Expected traversed block fallthrough to match top-most active scope',
1187 + loc: block.instructions[0]?.loc ?? block.terminal.id,
1188 + });
1189 + this.#activeScopes.pop();
1190 + }
1191 +
1192 + if (
1193 + block.terminal.kind === 'scope' ||
1194 + block.terminal.kind === 'pruned-scope'
1195 + ) {
1196 + CompilerError.invariant(
1197 + !this.blockInfos.has(block.terminal.block) &&
1198 + !this.blockInfos.has(block.terminal.fallthrough),
1199 + {
1200 + reason: 'Expected unique scope blocks and fallthroughs',
1201 + loc: block.terminal.loc,
1202 + },
1203 + );
1204 + this.blockInfos.set(block.terminal.block, {
1205 + kind: 'begin',
1206 + scope: block.terminal.scope,
1207 + pruned: block.terminal.kind === 'pruned-scope',
1208 + fallthrough: block.terminal.fallthrough,
1209 + });
1210 + this.blockInfos.set(block.terminal.fallthrough, {
1211 + kind: 'end',
1212 + scope: block.terminal.scope,
1213 + pruned: block.terminal.kind === 'pruned-scope',
1214 + });
1215 + }
1216 + }
1217 +
1218 + isScopeActive(scopeId: ScopeId): boolean {
1219 + return this.#activeScopes.indexOf(scopeId) !== -1;
1220 + }
1221 + get currentScope(): ScopeId | null {
1222 + return this.#activeScopes.at(-1) ?? null;
1223 + }
1224 +}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts
+10 -2
@@ -13,6 +13,8 @@ import {
13 HIRFunction,
14 Identifier,
15 Instruction,
16 + InstructionId,
17 + MutableRange,
18 Place,
19 ReactiveScope,
20 makeInstructionId,
@@ -186,8 +188,14 @@ function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation {
188 }
189
190 // Is the operand mutable at this given instruction
189 -export function isMutable({id}: Instruction, place: Place): boolean {
190 - const range = place.identifier.mutableRange;
191 +export function isMutable(instr: {id: InstructionId}, place: Place): boolean {
192 + return inRange(instr, place.identifier.mutableRange);
193 +}
194 +
195 +export function inRange(
196 + {id}: {id: InstructionId},
197 + range: MutableRange,
198 +): boolean {
199 return id >= range.start && id < range.end;
200 }
201
compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts
+23 -6
@@ -83,16 +83,33 @@ export function getOrInsertDefault<U, V>(
83 }
84 }
85
86 -export function Set_union<T>(a: Set<T>, b: Set<T>): Set<T> {
87 - const union = new Set<T>();
88 - for (const item of a) {
89 - if (b.has(item)) {
90 - union.add(item);
91 - }
86 +export function Set_union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
87 + const union = new Set<T>(a);
88 + for (const item of b) {
89 + union.add(item);
90 }
91 return union;
92 }
93
94 +export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
95 + if (sets.length === 0 || sets.some(s => s.size === 0)) {
96 + return new Set();
97 + } else if (sets.length === 1) {
98 + return new Set(sets[0]);
99 + }
100 + const result: Set<T> = new Set();
101 + const first = sets[0];
102 + outer: for (const e of first) {
103 + for (let i = 1; i < sets.length; i++) {
104 + if (!sets[i].has(e)) {
105 + continue outer;
106 + }
107 + }
108 + result.add(e);
109 + }
110 + return result;
111 +}
112 +
113 export function Iterable_some<T>(
114 iter: Iterable<T>,
115 pred: (item: T) => boolean,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md
+3 -3
@@ -10,7 +10,7 @@ function Component(props) {
10 x = identity(props.value[0]);
11 };
12 foo();
13 - return {x};
13 + return <div>{x}</div>;
14 }
15
16 export const FIXTURE_ENTRYPOINT = {
@@ -45,7 +45,7 @@ function Component(props) {
45 const t0 = x;
46 let t1;
47 if ($[2] !== t0) {
48 - t1 = { x: t0 };
48 + t1 = <div>{t0}</div>;
49 $[2] = t0;
50 $[3] = t1;
51 } else {
@@ -62,4 +62,4 @@ export const FIXTURE_ENTRYPOINT = {
62 ```
63
64 ### Eval output
65 -(kind: ok) {"x":42}
\ No newline at end of file
65 +(kind: ok) <div>42</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.js
+1 -1
@@ -6,7 +6,7 @@ function Component(props) {
6 x = identity(props.value[0]);
7 };
8 foo();
9 - return {x};
9 + return <div>{x}</div>;
10 }
11
12 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * Test that we preserve order of evaluation on the following case scope@0
7 + * ```js
8 + * // simplified HIR
9 + * scope@0
10 + * ...
11 + * $0 = arr.length
12 + * $1 = arr.push(...)
13 + *
14 + * scope@1 <-- here we should depend on $0 (the value of the property load before the
15 + * mutable call)
16 + * [$0, $1]
17 + * ```
18 + */
19 +function useFoo(source: Array<number>): [number, number] {
20 + const arr = [1, 2, 3, ...source];
21 + return [arr.length, arr.push(0)];
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: useFoo,
26 + params: [[5, 6]],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime"; /**
35 + * Test that we preserve order of evaluation on the following case scope@0
36 + * ```js
37 + * // simplified HIR
38 + * scope@0
39 + * ...
40 + * $0 = arr.length
41 + * $1 = arr.push(...)
42 + *
43 + * scope@1 <-- here we should depend on $0 (the value of the property load before the
44 + * mutable call)
45 + * [$0, $1]
46 + * ```
47 + */
48 +function useFoo(source) {
49 + const $ = _c(6);
50 + let t0;
51 + let t1;
52 + if ($[0] !== source) {
53 + const arr = [1, 2, 3, ...source];
54 + t0 = arr.length;
55 + t1 = arr.push(0);
56 + $[0] = source;
57 + $[1] = t0;
58 + $[2] = t1;
59 + } else {
60 + t0 = $[1];
61 + t1 = $[2];
62 + }
63 + let t2;
64 + if ($[3] !== t0 || $[4] !== t1) {
65 + t2 = [t0, t1];
66 + $[3] = t0;
67 + $[4] = t1;
68 + $[5] = t2;
69 + } else {
70 + t2 = $[5];
71 + }
72 + return t2;
73 +}
74 +
75 +export const FIXTURE_ENTRYPOINT = {
76 + fn: useFoo,
77 + params: [[5, 6]],
78 +};
79 +
80 +```
81 +
82 +### Eval output
83 +(kind: ok) [5,6]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.ts new
+23
@@ -0,0 +1,23 @@
1 +/**
2 + * Test that we preserve order of evaluation on the following case scope@0
3 + * ```js
4 + * // simplified HIR
5 + * scope@0
6 + * ...
7 + * $0 = arr.length
8 + * $1 = arr.push(...)
9 + *
10 + * scope@1 <-- here we should depend on $0 (the value of the property load before the
11 + * mutable call)
12 + * [$0, $1]
13 + * ```
14 + */
15 +function useFoo(source: Array<number>): [number, number] {
16 + const arr = [1, 2, 3, ...source];
17 + return [arr.length, arr.push(0)];
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [[5, 6]],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * Test that we preserve order of evaluation on the following case scope@0
7 + * ```js
8 + * // simplified HIR
9 + * scope@0
10 + * ...
11 + * $0 = arr.length
12 + * $1 = arr.length = 0
13 + *
14 + * scope@1 <-- here we should depend on $0 (the value of the property load before the
15 + * property store)
16 + * [$0, $1]
17 + * ```
18 + */
19 +function useFoo(source: Array<number>): [number, number] {
20 + const arr = [1, 2, 3, ...source];
21 + return [arr.length, (arr.length = 0)];
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: useFoo,
26 + params: [[5, 6]],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime"; /**
35 + * Test that we preserve order of evaluation on the following case scope@0
36 + * ```js
37 + * // simplified HIR
38 + * scope@0
39 + * ...
40 + * $0 = arr.length
41 + * $1 = arr.length = 0
42 + *
43 + * scope@1 <-- here we should depend on $0 (the value of the property load before the
44 + * property store)
45 + * [$0, $1]
46 + * ```
47 + */
48 +function useFoo(source) {
49 + const $ = _c(6);
50 + let t0;
51 + let t1;
52 + if ($[0] !== source) {
53 + const arr = [1, 2, 3, ...source];
54 + t0 = arr.length;
55 + t1 = arr.length = 0;
56 + $[0] = source;
57 + $[1] = t0;
58 + $[2] = t1;
59 + } else {
60 + t0 = $[1];
61 + t1 = $[2];
62 + }
63 + let t2;
64 + if ($[3] !== t0 || $[4] !== t1) {
65 + t2 = [t0, t1];
66 + $[3] = t0;
67 + $[4] = t1;
68 + $[5] = t2;
69 + } else {
70 + t2 = $[5];
71 + }
72 + return t2;
73 +}
74 +
75 +export const FIXTURE_ENTRYPOINT = {
76 + fn: useFoo,
77 + params: [[5, 6]],
78 +};
79 +
80 +```
81 +
82 +### Eval output
83 +(kind: ok) [5,0]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.ts new
+23
@@ -0,0 +1,23 @@
1 +/**
2 + * Test that we preserve order of evaluation on the following case scope@0
3 + * ```js
4 + * // simplified HIR
5 + * scope@0
6 + * ...
7 + * $0 = arr.length
8 + * $1 = arr.length = 0
9 + *
10 + * scope@1 <-- here we should depend on $0 (the value of the property load before the
11 + * property store)
12 + * [$0, $1]
13 + * ```
14 + */
15 +function useFoo(source: Array<number>): [number, number] {
16 + const arr = [1, 2, 3, ...source];
17 + return [arr.length, (arr.length = 0)];
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [[5, 6]],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.expect.md new
+67
@@ -0,0 +1,67 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Component({props}) {
9 + const f = () => props.a.b;
10 +
11 + return <Stringify f={props == null ? () => {} : f} />;
12 +}
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{props: null}],
16 +};
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false
24 +import { Stringify } from "shared-runtime";
25 +
26 +function Component(t0) {
27 + const $ = _c(7);
28 + const { props } = t0;
29 + let t1;
30 + if ($[0] !== props) {
31 + t1 = () => props.a.b;
32 + $[0] = props;
33 + $[1] = t1;
34 + } else {
35 + t1 = $[1];
36 + }
37 + const f = t1;
38 + let t2;
39 + if ($[2] !== props || $[3] !== f) {
40 + t2 = props == null ? _temp : f;
41 + $[2] = props;
42 + $[3] = f;
43 + $[4] = t2;
44 + } else {
45 + t2 = $[4];
46 + }
47 + let t3;
48 + if ($[5] !== t2) {
49 + t3 = <Stringify f={t2} />;
50 + $[5] = t2;
51 + $[6] = t3;
52 + } else {
53 + t3 = $[6];
54 + }
55 + return t3;
56 +}
57 +function _temp() {}
58 +
59 +export const FIXTURE_ENTRYPOINT = {
60 + fn: Component,
61 + params: [{ props: null }],
62 +};
63 +
64 +```
65 +
66 +### Eval output
67 +(kind: ok) <div>{"f":"[[ function params=0 ]]"}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.tsx new
+12
@@ -0,0 +1,12 @@
1 +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Component({props}) {
5 + const f = () => props.a.b;
6 +
7 + return <Stringify f={props == null ? () => {} : f} />;
8 +}
9 +export const FIXTURE_ENTRYPOINT = {
10 + fn: Component,
11 + params: [{props: null}],
12 +};
"b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.expect.md" renamed
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @enableTreatFunctionDepsAsConditional
5 +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false
6 function Component(props) {
7 function getLength() {
8 return props.bar.length;
@@ -21,7 +21,7 @@ export const FIXTURE_ENTRYPOINT = {
21 ## Code
22
23 ```javascript
24 -import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional
24 +import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false
25 function Component(props) {
26 const $ = _c(5);
27 let t0;
"b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" renamed
+1 -1
@@ -1,4 +1,4 @@
1 -// @enableTreatFunctionDepsAsConditional
1 +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false
2 function Component(props) {
3 function getLength() {
4 return props.bar.length;
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 new
+32
@@ -0,0 +1,32 @@
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 renamed
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 new
+42
@@ -0,0 +1,42 @@
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-with-unconditional.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md new
+39
@@ -0,0 +1,39 @@
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 renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional-optional.expect.md new
+50
@@ -0,0 +1,50 @@
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 + if (props.cond) {
12 + x.push(props?.items);
13 + }
14 + return x;
15 + }, [props?.items, props.cond]);
16 + return (
17 + <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
18 + );
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 + 2 | import {ValidateMemoization} from 'shared-runtime';
28 + 3 | function Component(props) {
29 +> 4 | const data = useMemo(() => {
30 + | ^^^^^^^
31 +> 5 | const x = [];
32 + | ^^^^^^^^^^^^^^^^^
33 +> 6 | x.push(props?.items);
34 + | ^^^^^^^^^^^^^^^^^
35 +> 7 | if (props.cond) {
36 + | ^^^^^^^^^^^^^^^^^
37 +> 8 | x.push(props?.items);
38 + | ^^^^^^^^^^^^^^^^^
39 +> 9 | }
40 + | ^^^^^^^^^^^^^^^^^
41 +> 10 | return x;
42 + | ^^^^^^^^^^^^^^^^^
43 +> 11 | }, [props?.items, props.cond]);
44 + | ^^^^ 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:11)
45 + 12 | return (
46 + 13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
47 + 14 | );
48 +```
49 +
50 +
\ 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-with-conditional-optional.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional.expect.md new
+50
@@ -0,0 +1,50 @@
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 + if (props.cond) {
12 + x.push(props.items);
13 + }
14 + return x;
15 + }, [props?.items, props.cond]);
16 + return (
17 + <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
18 + );
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 + 2 | import {ValidateMemoization} from 'shared-runtime';
28 + 3 | function Component(props) {
29 +> 4 | const data = useMemo(() => {
30 + | ^^^^^^^
31 +> 5 | const x = [];
32 + | ^^^^^^^^^^^^^^^^^
33 +> 6 | x.push(props?.items);
34 + | ^^^^^^^^^^^^^^^^^
35 +> 7 | if (props.cond) {
36 + | ^^^^^^^^^^^^^^^^^
37 +> 8 | x.push(props.items);
38 + | ^^^^^^^^^^^^^^^^^
39 +> 9 | }
40 + | ^^^^^^^^^^^^^^^^^
41 +> 10 | return x;
42 + | ^^^^^^^^^^^^^^^^^
43 +> 11 | }, [props?.items, props.cond]);
44 + | ^^^^ 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:11)
45 + 12 | return (
46 + 13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
47 + 14 | );
48 +```
49 +
50 +
\ 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-with-conditional.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md deleted
-48
@@ -1,48 +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 -## Code
16 -
17 -```javascript
18 -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
19 -function Component(props) {
20 - const $ = _c(4);
21 -
22 - props?.items.edges?.nodes;
23 - let t0;
24 - 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;
29 - } else {
30 - t1 = $[1];
31 - }
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;
39 - } else {
40 - t2 = $[3];
41 - }
42 - return t2;
43 -}
44 -
45 -```
46 -
47 -### Eval output
48 -(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.expect.md deleted
-62
@@ -1,62 +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 -## 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.expect.md deleted
-63
@@ -1,63 +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 -## Code
19 -
20 -```javascript
21 -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
22 -import { ValidateMemoization } from "shared-runtime";
23 -function Component(props) {
24 - const $ = _c(7);
25 -
26 - props?.items;
27 - let t0;
28 - let x;
29 - if ($[0] !== props?.items) {
30 - x = [];
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 - const t1 = props?.items;
40 - let t2;
41 - if ($[2] !== t1) {
42 - t2 = [t1];
43 - $[2] = t1;
44 - $[3] = t2;
45 - } else {
46 - t2 = $[3];
47 - }
48 - let t3;
49 - if ($[4] !== t2 || $[5] !== data) {
50 - t3 = <ValidateMemoization inputs={t2} output={data} />;
51 - $[4] = t2;
52 - $[5] = data;
53 - $[6] = t3;
54 - } else {
55 - t3 = $[6];
56 - }
57 - return t3;
58 -}
59 -
60 -```
61 -
62 -### Eval output
63 -(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-with-conditional-optional.expect.md deleted
-74
@@ -1,74 +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 - if (props.cond) {
12 - x.push(props?.items);
13 - }
14 - return x;
15 - }, [props?.items, props.cond]);
16 - return (
17 - <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
18 - );
19 -}
20 -
21 -```
22 -
23 -## Code
24 -
25 -```javascript
26 -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
27 -import { ValidateMemoization } from "shared-runtime";
28 -function Component(props) {
29 - const $ = _c(9);
30 -
31 - props?.items;
32 - let t0;
33 - let x;
34 - if ($[0] !== props?.items || $[1] !== props.cond) {
35 - x = [];
36 - x.push(props?.items);
37 - if (props.cond) {
38 - x.push(props?.items);
39 - }
40 - $[0] = props?.items;
41 - $[1] = props.cond;
42 - $[2] = x;
43 - } else {
44 - x = $[2];
45 - }
46 - t0 = x;
47 - const data = t0;
48 -
49 - const t1 = props?.items;
50 - let t2;
51 - if ($[3] !== t1 || $[4] !== props.cond) {
52 - t2 = [t1, props.cond];
53 - $[3] = t1;
54 - $[4] = props.cond;
55 - $[5] = t2;
56 - } else {
57 - t2 = $[5];
58 - }
59 - let t3;
60 - if ($[6] !== t2 || $[7] !== data) {
61 - t3 = <ValidateMemoization inputs={t2} output={data} />;
62 - $[6] = t2;
63 - $[7] = data;
64 - $[8] = t3;
65 - } else {
66 - t3 = $[8];
67 - }
68 - return t3;
69 -}
70 -
71 -```
72 -
73 -### Eval output
74 -(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-with-conditional.expect.md deleted
-74
@@ -1,74 +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 - if (props.cond) {
12 - x.push(props.items);
13 - }
14 - return x;
15 - }, [props?.items, props.cond]);
16 - return (
17 - <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
18 - );
19 -}
20 -
21 -```
22 -
23 -## Code
24 -
25 -```javascript
26 -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR
27 -import { ValidateMemoization } from "shared-runtime";
28 -function Component(props) {
29 - const $ = _c(9);
30 -
31 - props?.items;
32 - let t0;
33 - let x;
34 - if ($[0] !== props?.items || $[1] !== props.cond) {
35 - x = [];
36 - x.push(props?.items);
37 - if (props.cond) {
38 - x.push(props.items);
39 - }
40 - $[0] = props?.items;
41 - $[1] = props.cond;
42 - $[2] = x;
43 - } else {
44 - x = $[2];
45 - }
46 - t0 = x;
47 - const data = t0;
48 -
49 - const t1 = props?.items;
50 - let t2;
51 - if ($[3] !== t1 || $[4] !== props.cond) {
52 - t2 = [t1, props.cond];
53 - $[3] = t1;
54 - $[4] = props.cond;
55 - $[5] = t2;
56 - } else {
57 - t2 = $[5];
58 - }
59 - let t3;
60 - if ($[6] !== t2 || $[7] !== data) {
61 - t3 = <ValidateMemoization inputs={t2} output={data} />;
62 - $[6] = t2;
63 - $[7] = data;
64 - $[8] = t3;
65 - } else {
66 - t3 = $[8];
67 - }
68 - return t3;
69 -}
70 -
71 -```
72 -
73 -### Eval output
74 -(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/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?.b) {
34 + if ($[0] !== props.a) {
35 x = [];
36 x.push(props.a?.b);
37 - $[0] = props.a?.b;
37 + $[0] = props.a;
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/join-uncond-scopes-cond-deps.expect.md
+4 -11
@@ -63,20 +63,13 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
63 import { CONST_TRUE, setProperty } from "shared-runtime";
64
65 function useJoinCondDepsInUncondScopes(props) {
66 - const $ = _c(4);
66 + const $ = _c(2);
67 let t0;
68 if ($[0] !== props.a.b) {
69 const y = {};
70 - let x;
71 - if ($[2] !== props) {
72 - x = {};
73 - if (CONST_TRUE) {
74 - setProperty(x, props.a.b);
75 - }
76 - $[2] = props;
77 - $[3] = x;
78 - } else {
79 - x = $[3];
70 + const x = {};
71 + if (CONST_TRUE) {
72 + setProperty(x, props.a.b);
73 }
74
75 setProperty(y, props.a.b);
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.b) {
49 + if ($[0] !== props.a) {
50 x = [];
51 x.push(props.a?.b);
52 x.push(props.a.b.c);
53 - $[0] = props.a.b;
53 + $[0] = props.a;
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
+6 -15
@@ -22,25 +22,16 @@ export const FIXTURE_ENTRYPOINT = {
22 ```javascript
23 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
24 function Component(props) {
25 - const $ = _c(5);
25 + const $ = _c(2);
26 let x;
27 - if ($[0] !== props.items?.length || $[1] !== props.items?.edges) {
27 + if ($[0] !== props.items) {
28 x = [];
29 x.push(props.items?.length);
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;
30 + x.push(props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []);
31 + $[0] = props.items;
32 + $[1] = x;
33 } else {
43 - x = $[2];
34 + x = $[1];
35 }
36 return x;
37 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/promote-uncond.expect.md
+7 -6
@@ -36,19 +36,20 @@ import { identity } from "shared-runtime";
36
37 // and promote it to an unconditional dependency.
38 function usePromoteUnconditionalAccessToDependency(props, other) {
39 - const $ = _c(3);
39 + const $ = _c(4);
40 let x;
41 - if ($[0] !== props.a || $[1] !== other) {
41 + if ($[0] !== props.a.a.a || $[1] !== props.a.b || $[2] !== other) {
42 x = {};
43 x.a = props.a.a.a;
44 if (identity(other)) {
45 x.c = props.a.b.c;
46 }
47 - $[0] = props.a;
48 - $[1] = other;
49 - $[2] = x;
47 + $[0] = props.a.a.a;
48 + $[1] = props.a.b;
49 + $[2] = other;
50 + $[3] = x;
51 } else {
51 - x = $[2];
52 + x = $[3];
53 }
54 return x;
55 }
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?.aggregates) {
27 + if ($[0] !== item) {
28 count = 0;
29 const aggregates = item?.aggregates || [];
30 aggregates.forEach((aggregate) => {
31 count = count + (aggregate.count || 0);
32 count;
33 });
34 - $[0] = item?.aggregates;
34 + $[0] = item;
35 $[1] = count;
36 } else {
37 count = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-cascading-eliminated-phis.expect.md
+17 -8
@@ -37,10 +37,16 @@ export const FIXTURE_ENTRYPOINT = {
37 ```javascript
38 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
39 function Component(props) {
40 - const $ = _c(4);
40 + const $ = _c(7);
41 let x = 0;
42 let values;
43 - if ($[0] !== props || $[1] !== x) {
43 + if (
44 + $[0] !== props.a ||
45 + $[1] !== props.b ||
46 + $[2] !== props.c ||
47 + $[3] !== props.d ||
48 + $[4] !== x
49 + ) {
50 values = [];
51 const y = props.a || props.b;
52 values.push(y);
@@ -54,13 +60,16 @@ function Component(props) {
60 }
61
62 values.push(x);
57 - $[0] = props;
58 - $[1] = x;
59 - $[2] = values;
60 - $[3] = x;
63 + $[0] = props.a;
64 + $[1] = props.b;
65 + $[2] = props.c;
66 + $[3] = props.d;
67 + $[4] = x;
68 + $[5] = values;
69 + $[6] = x;
70 } else {
62 - values = $[2];
63 - x = $[3];
71 + values = $[5];
72 + x = $[6];
73 }
74 return values;
75 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-leave-case.expect.md
+6 -5
@@ -40,9 +40,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
40 import { Stringify } from "shared-runtime";
41
42 function Component(props) {
43 - const $ = _c(2);
43 + const $ = _c(3);
44 let t0;
45 - if ($[0] !== props) {
45 + if ($[0] !== props.p0 || $[1] !== props.p1) {
46 const x = [];
47 let y;
48 if (props.p0) {
@@ -56,10 +56,11 @@ function Component(props) {
56 {y}
57 </Stringify>
58 );
59 - $[0] = props;
60 - $[1] = t0;
59 + $[0] = props.p0;
60 + $[1] = props.p1;
61 + $[2] = t0;
62 } else {
62 - t0 = $[1];
63 + t0 = $[2];
64 }
65 return t0;
66 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction-with-mutation.expect.md
+7 -5
@@ -32,17 +32,19 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
32 import { mutate } from "shared-runtime";
33
34 function useFoo(props) {
35 - const $ = _c(2);
35 + const $ = _c(4);
36 let x;
37 - if ($[0] !== props) {
37 + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
38 x = [];
39 x.push(props.bar);
40 props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
41 mutate(x);
42 - $[0] = props;
43 - $[1] = x;
42 + $[0] = props.bar;
43 + $[1] = props.cond;
44 + $[2] = props.foo;
45 + $[3] = x;
46 } else {
45 - x = $[1];
47 + x = $[3];
48 }
49 return x;
50 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction.expect.md
+6 -5
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
27 ```javascript
28 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
29 function useFoo(props) {
30 - const $ = _c(4);
30 + const $ = _c(5);
31 let x;
32 if ($[0] !== props.bar) {
33 x = [];
@@ -37,12 +37,13 @@ function useFoo(props) {
37 } else {
38 x = $[1];
39 }
40 - if ($[2] !== props) {
40 + if ($[2] !== props.cond || $[3] !== props.foo) {
41 props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
42 - $[2] = props;
43 - $[3] = x;
42 + $[2] = props.cond;
43 + $[3] = props.foo;
44 + $[4] = x;
45 } else {
45 - x = $[3];
46 + x = $[4];
47 }
48 return x;
49 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-with-mutation.expect.md
+7 -5
@@ -32,17 +32,19 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
32 import { mutate } from "shared-runtime";
33
34 function useFoo(props) {
35 - const $ = _c(2);
35 + const $ = _c(4);
36 let x;
37 - if ($[0] !== props) {
37 + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
38 x = [];
39 x.push(props.bar);
40 props.cond ? ((x = []), x.push(props.foo)) : null;
41 mutate(x);
42 - $[0] = props;
43 - $[1] = x;
42 + $[0] = props.bar;
43 + $[1] = props.cond;
44 + $[2] = props.foo;
45 + $[3] = x;
46 } else {
45 - x = $[1];
47 + x = $[3];
48 }
49 return x;
50 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary.expect.md
+6 -5
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
27 ```javascript
28 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
29 function useFoo(props) {
30 - const $ = _c(4);
30 + const $ = _c(5);
31 let x;
32 if ($[0] !== props.bar) {
33 x = [];
@@ -37,12 +37,13 @@ function useFoo(props) {
37 } else {
38 x = $[1];
39 }
40 - if ($[2] !== props) {
40 + if ($[2] !== props.cond || $[3] !== props.foo) {
41 props.cond ? ((x = []), x.push(props.foo)) : null;
42 - $[2] = props;
43 - $[3] = x;
42 + $[2] = props.cond;
43 + $[3] = props.foo;
44 + $[4] = x;
45 } else {
45 - x = $[3];
46 + x = $[4];
47 }
48 return x;
49 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary-with-mutation.expect.md
+7 -5
@@ -32,17 +32,19 @@ export const FIXTURE_ENTRYPOINT = {
32 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
33 import { arrayPush } from "shared-runtime";
34 function useFoo(props) {
35 - const $ = _c(2);
35 + const $ = _c(4);
36 let x;
37 - if ($[0] !== props) {
37 + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
38 x = [];
39 x.push(props.bar);
40 props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
41 arrayPush(x, 4);
42 - $[0] = props;
43 - $[1] = x;
42 + $[0] = props.bar;
43 + $[1] = props.cond;
44 + $[2] = props.foo;
45 + $[3] = x;
46 } else {
45 - x = $[1];
47 + x = $[3];
48 }
49 return x;
50 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary.expect.md
+7 -5
@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
29 ```javascript
30 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
31 function useFoo(props) {
32 - const $ = _c(4);
32 + const $ = _c(6);
33 let x;
34 if ($[0] !== props.bar) {
35 x = [];
@@ -39,12 +39,14 @@ function useFoo(props) {
39 } else {
40 x = $[1];
41 }
42 - if ($[2] !== props) {
42 + if ($[2] !== props.cond || $[3] !== props.foo || $[4] !== props.bar) {
43 props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
44 - $[2] = props;
45 - $[3] = x;
44 + $[2] = props.cond;
45 + $[3] = props.foo;
46 + $[4] = props.bar;
47 + $[5] = x;
48 } else {
47 - x = $[3];
49 + x = $[5];
50 }
51 return x;
52 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-via-destructuring-with-mutation.expect.md
+7 -5
@@ -36,9 +36,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
36 import { mutate } from "shared-runtime";
37
38 function useFoo(props) {
39 - const $ = _c(2);
39 + const $ = _c(4);
40 let x;
41 - if ($[0] !== props) {
41 + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
42 ({ x } = { x: [] });
43 x.push(props.bar);
44 if (props.cond) {
@@ -47,10 +47,12 @@ function useFoo(props) {
47 }
48
49 mutate(x);
50 - $[0] = props;
51 - $[1] = x;
50 + $[0] = props.bar;
51 + $[1] = props.cond;
52 + $[2] = props.foo;
53 + $[3] = x;
54 } else {
53 - x = $[1];
55 + x = $[3];
56 }
57 return x;
58 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-with-mutation.expect.md
+7 -5
@@ -36,9 +36,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
36 import { mutate } from "shared-runtime";
37
38 function useFoo(props) {
39 - const $ = _c(2);
39 + const $ = _c(4);
40 let x;
41 - if ($[0] !== props) {
41 + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
42 x = [];
43 x.push(props.bar);
44 if (props.cond) {
@@ -47,10 +47,12 @@ function useFoo(props) {
47 }
48
49 mutate(x);
50 - $[0] = props;
51 - $[1] = x;
50 + $[0] = props.bar;
51 + $[1] = props.cond;
52 + $[2] = props.foo;
53 + $[3] = x;
54 } else {
53 - x = $[1];
55 + x = $[3];
56 }
57 return x;
58 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch-non-final-default.expect.md
+16 -15
@@ -34,10 +34,10 @@ function Component(props) {
34 ```javascript
35 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
36 function Component(props) {
37 - const $ = _c(7);
37 + const $ = _c(8);
38 let y;
39 let t0;
40 - if ($[0] !== props) {
40 + if ($[0] !== props.p0 || $[1] !== props.p2) {
41 const x = [];
42 bb0: switch (props.p0) {
43 case 1: {
@@ -46,11 +46,11 @@ function Component(props) {
46 case true: {
47 x.push(props.p2);
48 let t1;
49 - if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
49 + if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
50 t1 = [];
51 - $[3] = t1;
51 + $[4] = t1;
52 } else {
53 - t1 = $[3];
53 + t1 = $[4];
54 }
55 y = t1;
56 }
@@ -63,23 +63,24 @@ function Component(props) {
63 }
64
65 t0 = <Component data={x} />;
66 - $[0] = props;
67 - $[1] = y;
68 - $[2] = t0;
66 + $[0] = props.p0;
67 + $[1] = props.p2;
68 + $[2] = y;
69 + $[3] = t0;
70 } else {
70 - y = $[1];
71 - t0 = $[2];
71 + y = $[2];
72 + t0 = $[3];
73 }
74 const child = t0;
75 y.push(props.p4);
76 let t1;
76 - if ($[4] !== y || $[5] !== child) {
77 + if ($[5] !== y || $[6] !== child) {
78 t1 = <Component data={y}>{child}</Component>;
78 - $[4] = y;
79 - $[5] = child;
80 - $[6] = t1;
79 + $[5] = y;
80 + $[6] = child;
81 + $[7] = t1;
82 } else {
82 - t1 = $[6];
83 + t1 = $[7];
84 }
85 return t1;
86 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch.expect.md
+14 -12
@@ -29,10 +29,10 @@ function Component(props) {
29 ```javascript
30 import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
31 function Component(props) {
32 - const $ = _c(6);
32 + const $ = _c(8);
33 let y;
34 let t0;
35 - if ($[0] !== props) {
35 + if ($[0] !== props.p0 || $[1] !== props.p2 || $[2] !== props.p3) {
36 const x = [];
37 switch (props.p0) {
38 case true: {
@@ -45,23 +45,25 @@ function Component(props) {
45 }
46
47 t0 = <Component data={x} />;
48 - $[0] = props;
49 - $[1] = y;
50 - $[2] = t0;
48 + $[0] = props.p0;
49 + $[1] = props.p2;
50 + $[2] = props.p3;
51 + $[3] = y;
52 + $[4] = t0;
53 } else {
52 - y = $[1];
53 - t0 = $[2];
54 + y = $[3];
55 + t0 = $[4];
56 }
57 const child = t0;
58 y.push(props.p4);
59 let t1;
58 - if ($[3] !== y || $[4] !== child) {
60 + if ($[5] !== y || $[6] !== child) {
61 t1 = <Component data={y}>{child}</Component>;
60 - $[3] = y;
61 - $[4] = child;
62 - $[5] = t1;
62 + $[5] = y;
63 + $[6] = child;
64 + $[7] = t1;
65 } else {
64 - t1 = $[5];
66 + t1 = $[7];
67 }
68 return t1;
69 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-mutate-outer-value.expect.md
+12 -4
@@ -29,9 +29,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
29 const { shallowCopy, throwErrorWithMessage } = require("shared-runtime");
30
31 function Component(props) {
32 - const $ = _c(3);
32 + const $ = _c(5);
33 let x;
34 - if ($[0] !== props.a) {
34 + if ($[0] !== props) {
35 x = [];
36 try {
37 let t0;
@@ -43,9 +43,17 @@ function Component(props) {
43 }
44 x.push(t0);
45 } catch {
46 - x.push(shallowCopy({ a: props.a }));
46 + let t0;
47 + if ($[3] !== props.a) {
48 + t0 = shallowCopy({ a: props.a });
49 + $[3] = props.a;
50 + $[4] = t0;
51 + } else {
52 + t0 = $[4];
53 + }
54 + x.push(t0);
55 }
48 - $[0] = props.a;
56 + $[0] = props;
57 $[1] = x;
58 } else {
59 x = $[1];
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md
+5 -6
@@ -32,9 +32,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
32 const { throwInput } = require("shared-runtime");
33
34 function Component(props) {
35 - const $ = _c(3);
35 + const $ = _c(2);
36 let x;
37 - if ($[0] !== props.y || $[1] !== props.e) {
37 + if ($[0] !== props) {
38 try {
39 const y = [];
40 y.push(props.y);
@@ -44,11 +44,10 @@ function Component(props) {
44 e.push(props.e);
45 x = e;
46 }
47 - $[0] = props.y;
48 - $[1] = props.e;
49 - $[2] = x;
47 + $[0] = props;
48 + $[1] = x;
49 } else {
51 - x = $[2];
50 + x = $[1];
51 }
52 return x;
53 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md
+5 -6
@@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
31 const { throwInput } = require("shared-runtime");
32
33 function Component(props) {
34 - const $ = _c(3);
34 + const $ = _c(2);
35 let t0;
36 - if ($[0] !== props.y || $[1] !== props.e) {
36 + if ($[0] !== props) {
37 t0 = Symbol.for("react.early_return_sentinel");
38 bb0: {
39 try {
@@ -47,11 +47,10 @@ function Component(props) {
47 break bb0;
48 }
49 }
50 - $[0] = props.y;
51 - $[1] = props.e;
52 - $[2] = t0;
50 + $[0] = props;
51 + $[1] = t0;
52 } else {
54 - t0 = $[2];
53 + t0 = $[1];
54 }
55 if (t0 !== Symbol.for("react.early_return_sentinel")) {
56 return t0;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/useMemo-multiple-if-else.expect.md
+15 -7
@@ -34,11 +34,16 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
34 import { useMemo } from "react";
35
36 function Component(props) {
37 - const $ = _c(3);
37 + const $ = _c(6);
38 let t0;
39 bb0: {
40 let y;
41 - if ($[0] !== props) {
41 + if (
42 + $[0] !== props.cond ||
43 + $[1] !== props.a ||
44 + $[2] !== props.cond2 ||
45 + $[3] !== props.b
46 + ) {
47 y = [];
48 if (props.cond) {
49 y.push(props.a);
@@ -49,12 +54,15 @@ function Component(props) {
54 }
55
56 y.push(props.b);
52 - $[0] = props;
53 - $[1] = y;
54 - $[2] = t0;
57 + $[0] = props.cond;
58 + $[1] = props.a;
59 + $[2] = props.cond2;
60 + $[3] = props.b;
61 + $[4] = y;
62 + $[5] = t0;
63 } else {
56 - y = $[1];
57 - t0 = $[2];
64 + y = $[4];
65 + t0 = $[5];
66 }
67 t0 = y;
68 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.expect.md new
+107
@@ -0,0 +1,107 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {makeObject_Primitives, setPropertyByKey} from 'shared-runtime';
6 +
7 +function useFoo({value, cond}) {
8 + let x: any = makeObject_Primitives();
9 + if (cond) {
10 + setPropertyByKey(x, 'a', null);
11 + } else {
12 + setPropertyByKey(x, 'a', {b: 2});
13 + }
14 +
15 + /**
16 + * y should take a dependency on `x`, not `x.a.b` here
17 + */
18 + const y = [];
19 + if (!cond) {
20 + y.push(x.a.b);
21 + }
22 +
23 + x = makeObject_Primitives();
24 + setPropertyByKey(x, 'a', {b: value});
25 +
26 + return [y, x.a.b];
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: useFoo,
31 + params: [{value: 3, cond: true}],
32 + sequentialRenders: [
33 + {value: 3, cond: true},
34 + {value: 3, cond: false},
35 + ],
36 +};
37 +
38 +```
39 +
40 +## Code
41 +
42 +```javascript
43 +import { c as _c } from "react/compiler-runtime";
44 +import { makeObject_Primitives, setPropertyByKey } from "shared-runtime";
45 +
46 +function useFoo(t0) {
47 + const $ = _c(10);
48 + const { value, cond } = t0;
49 + let x;
50 + if ($[0] !== cond) {
51 + x = makeObject_Primitives();
52 + if (cond) {
53 + setPropertyByKey(x, "a", null);
54 + } else {
55 + setPropertyByKey(x, "a", { b: 2 });
56 + }
57 + $[0] = cond;
58 + $[1] = x;
59 + } else {
60 + x = $[1];
61 + }
62 + let y;
63 + if ($[2] !== cond || $[3] !== x) {
64 + y = [];
65 + if (!cond) {
66 + y.push(x.a.b);
67 + }
68 + $[2] = cond;
69 + $[3] = x;
70 + $[4] = y;
71 + } else {
72 + y = $[4];
73 + }
74 + if ($[5] !== value) {
75 + x = makeObject_Primitives();
76 + setPropertyByKey(x, "a", { b: value });
77 + $[5] = value;
78 + $[6] = x;
79 + } else {
80 + x = $[6];
81 + }
82 + let t1;
83 + if ($[7] !== y || $[8] !== x.a.b) {
84 + t1 = [y, x.a.b];
85 + $[7] = y;
86 + $[8] = x.a.b;
87 + $[9] = t1;
88 + } else {
89 + t1 = $[9];
90 + }
91 + return t1;
92 +}
93 +
94 +export const FIXTURE_ENTRYPOINT = {
95 + fn: useFoo,
96 + params: [{ value: 3, cond: true }],
97 + sequentialRenders: [
98 + { value: 3, cond: true },
99 + { value: 3, cond: false },
100 + ],
101 +};
102 +
103 +```
104 +
105 +### Eval output
106 +(kind: ok) [[],3]
107 +[[2],3]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.tsx new
+32
@@ -0,0 +1,32 @@
1 +import {makeObject_Primitives, setPropertyByKey} from 'shared-runtime';
2 +
3 +function useFoo({value, cond}) {
4 + let x: any = makeObject_Primitives();
5 + if (cond) {
6 + setPropertyByKey(x, 'a', null);
7 + } else {
8 + setPropertyByKey(x, 'a', {b: 2});
9 + }
10 +
11 + /**
12 + * y should take a dependency on `x`, not `x.a.b` here
13 + */
14 + const y = [];
15 + if (!cond) {
16 + y.push(x.a.b);
17 + }
18 +
19 + x = makeObject_Primitives();
20 + setPropertyByKey(x, 'a', {b: value});
21 +
22 + return [y, x.a.b];
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: useFoo,
27 + params: [{value: 3, cond: true}],
28 + sequentialRenders: [
29 + {value: 3, cond: true},
30 + {value: 3, cond: false},
31 + ],
32 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.expect.md new
+96
@@ -0,0 +1,96 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {identity, shallowCopy, Stringify, useIdentity} from 'shared-runtime';
6 +
7 +type HasA = {kind: 'hasA'; a: {value: number}};
8 +type HasC = {kind: 'hasC'; c: {value: number}};
9 +function Foo({cond}: {cond: boolean}) {
10 + let x: HasA | HasC = shallowCopy({kind: 'hasA', a: {value: 2}});
11 + /**
12 + * This read of x.a.value is outside of x's identifier mutable
13 + * range + scope range. We mark this ssa instance (x_@0) as having
14 + * a non-null object property `x.a`.
15 + */
16 + Math.max(x.a.value, 2);
17 + if (cond) {
18 + x = shallowCopy({kind: 'hasC', c: {value: 3}});
19 + }
20 +
21 + /**
22 + * Since this x (x_@2 = phi(x_@0, x_@1)) is a different ssa instance,
23 + * we cannot safely hoist a read of `x.a.value`
24 + */
25 + return <Stringify val={!cond && [(x as HasA).a.value + 2]} />;
26 +}
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: Foo,
29 + params: [{cond: false}],
30 + sequentialRenders: [{cond: false}, {cond: true}],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { c as _c } from "react/compiler-runtime";
39 +import { identity, shallowCopy, Stringify, useIdentity } from "shared-runtime";
40 +
41 +type HasA = { kind: "hasA"; a: { value: number } };
42 +type HasC = { kind: "hasC"; c: { value: number } };
43 +function Foo(t0) {
44 + const $ = _c(7);
45 + const { cond } = t0;
46 + let t1;
47 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
48 + t1 = shallowCopy({ kind: "hasA", a: { value: 2 } });
49 + $[0] = t1;
50 + } else {
51 + t1 = $[0];
52 + }
53 + let x = t1;
54 +
55 + Math.max(x.a.value, 2);
56 + if (cond) {
57 + let t2;
58 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
59 + t2 = shallowCopy({ kind: "hasC", c: { value: 3 } });
60 + $[1] = t2;
61 + } else {
62 + t2 = $[1];
63 + }
64 + x = t2;
65 + }
66 + let t2;
67 + if ($[2] !== cond || $[3] !== x) {
68 + t2 = !cond && [(x as HasA).a.value + 2];
69 + $[2] = cond;
70 + $[3] = x;
71 + $[4] = t2;
72 + } else {
73 + t2 = $[4];
74 + }
75 + let t3;
76 + if ($[5] !== t2) {
77 + t3 = <Stringify val={t2} />;
78 + $[5] = t2;
79 + $[6] = t3;
80 + } else {
81 + t3 = $[6];
82 + }
83 + return t3;
84 +}
85 +
86 +export const FIXTURE_ENTRYPOINT = {
87 + fn: Foo,
88 + params: [{ cond: false }],
89 + sequentialRenders: [{ cond: false }, { cond: true }],
90 +};
91 +
92 +```
93 +
94 +### Eval output
95 +(kind: ok) <div>{"val":[4]}</div>
96 +<div>{"val":false}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.tsx new
+27
@@ -0,0 +1,27 @@
1 +import {identity, shallowCopy, Stringify, useIdentity} from 'shared-runtime';
2 +
3 +type HasA = {kind: 'hasA'; a: {value: number}};
4 +type HasC = {kind: 'hasC'; c: {value: number}};
5 +function Foo({cond}: {cond: boolean}) {
6 + let x: HasA | HasC = shallowCopy({kind: 'hasA', a: {value: 2}});
7 + /**
8 + * This read of x.a.value is outside of x's identifier mutable
9 + * range + scope range. We mark this ssa instance (x_@0) as having
10 + * a non-null object property `x.a`.
11 + */
12 + Math.max(x.a.value, 2);
13 + if (cond) {
14 + x = shallowCopy({kind: 'hasC', c: {value: 3}});
15 + }
16 +
17 + /**
18 + * Since this x (x_@2 = phi(x_@0, x_@1)) is a different ssa instance,
19 + * we cannot safely hoist a read of `x.a.value`
20 + */
21 + return <Stringify val={!cond && [(x as HasA).a.value + 2]} />;
22 +}
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Foo,
25 + params: [{cond: false}],
26 + sequentialRenders: [{cond: false}, {cond: true}],
27 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.expect.md new
+114
@@ -0,0 +1,114 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {
6 + identity,
7 + makeObject_Primitives,
8 + setPropertyByKey,
9 +} from 'shared-runtime';
10 +
11 +/**
12 + * A bit of an edge case, but we could further optimize here by merging
13 + * re-orderability of nodes across phis.
14 + */
15 +function useFoo(cond) {
16 + let x;
17 + if (cond) {
18 + /** start of scope for x_@0 */
19 + x = {};
20 + setPropertyByKey(x, 'a', {b: 2});
21 + /** end of scope for x_@0 */
22 + Math.max(x.a.b, 0);
23 + } else {
24 + /** start of scope for x_@1 */
25 + x = makeObject_Primitives();
26 + setPropertyByKey(x, 'a', {b: 3});
27 + /** end of scope for x_@1 */
28 + Math.max(x.a.b, 0);
29 + }
30 + /**
31 + * At this point, we have a phi node.
32 + * x_@2 = phi(x_@0, x_@1)
33 + *
34 + * We can assume that both x_@0 and x_@1 both have non-null `x.a` properties,
35 + * so we can infer that x_@2 does as well.
36 + */
37 +
38 + // Here, y should take a dependency on `x.a.b`
39 + const y = [];
40 + if (identity(cond)) {
41 + y.push(x.a.b);
42 + }
43 + return y;
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: useFoo,
48 + params: [true],
49 +};
50 +
51 +```
52 +
53 +## Code
54 +
55 +```javascript
56 +import { c as _c } from "react/compiler-runtime";
57 +import {
58 + identity,
59 + makeObject_Primitives,
60 + setPropertyByKey,
61 +} from "shared-runtime";
62 +
63 +/**
64 + * A bit of an edge case, but we could further optimize here by merging
65 + * re-orderability of nodes across phis.
66 + */
67 +function useFoo(cond) {
68 + const $ = _c(5);
69 + let x;
70 + if (cond) {
71 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
72 + x = {};
73 + setPropertyByKey(x, "a", { b: 2 });
74 + $[0] = x;
75 + } else {
76 + x = $[0];
77 + }
78 +
79 + Math.max(x.a.b, 0);
80 + } else {
81 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
82 + x = makeObject_Primitives();
83 + setPropertyByKey(x, "a", { b: 3 });
84 + $[1] = x;
85 + } else {
86 + x = $[1];
87 + }
88 +
89 + Math.max(x.a.b, 0);
90 + }
91 + let y;
92 + if ($[2] !== cond || $[3] !== x) {
93 + y = [];
94 + if (identity(cond)) {
95 + y.push(x.a.b);
96 + }
97 + $[2] = cond;
98 + $[3] = x;
99 + $[4] = y;
100 + } else {
101 + y = $[4];
102 + }
103 + return y;
104 +}
105 +
106 +export const FIXTURE_ENTRYPOINT = {
107 + fn: useFoo,
108 + params: [true],
109 +};
110 +
111 +```
112 +
113 +### Eval output
114 +(kind: ok) [2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.ts new
+45
@@ -0,0 +1,45 @@
1 +import {
2 + identity,
3 + makeObject_Primitives,
4 + setPropertyByKey,
5 +} from 'shared-runtime';
6 +
7 +/**
8 + * A bit of an edge case, but we could further optimize here by merging
9 + * re-orderability of nodes across phis.
10 + */
11 +function useFoo(cond) {
12 + let x;
13 + if (cond) {
14 + /** start of scope for x_@0 */
15 + x = {};
16 + setPropertyByKey(x, 'a', {b: 2});
17 + /** end of scope for x_@0 */
18 + Math.max(x.a.b, 0);
19 + } else {
20 + /** start of scope for x_@1 */
21 + x = makeObject_Primitives();
22 + setPropertyByKey(x, 'a', {b: 3});
23 + /** end of scope for x_@1 */
24 + Math.max(x.a.b, 0);
25 + }
26 + /**
27 + * At this point, we have a phi node.
28 + * x_@2 = phi(x_@0, x_@1)
29 + *
30 + * We can assume that both x_@0 and x_@1 both have non-null `x.a` properties,
31 + * so we can infer that x_@2 does as well.
32 + */
33 +
34 + // Here, y should take a dependency on `x.a.b`
35 + const y = [];
36 + if (identity(cond)) {
37 + y.push(x.a.b);
38 + }
39 + return y;
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: useFoo,
44 + params: [true],
45 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.expect.md new
+105
@@ -0,0 +1,105 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// x.a.b was accessed unconditionally within the mutable range of x.
6 +// As a result, we cannot infer anything about whether `x` or `x.a`
7 +// may be null. This means that it's not safe to hoist reads from x
8 +// (e.g. take `x.a` or `x.a.b` as a dependency).
9 +
10 +import {identity, makeObject_Primitives, setProperty} from 'shared-runtime';
11 +
12 +function Component({cond, other}) {
13 + const x = makeObject_Primitives();
14 + setProperty(x, {b: 3, other}, 'a');
15 + identity(x.a.b);
16 + if (!cond) {
17 + x.a = null;
18 + }
19 +
20 + const y = [identity(cond) && x.a.b];
21 + return y;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Component,
26 + params: [{cond: false}],
27 + sequentialRenders: [
28 + {cond: false},
29 + {cond: false},
30 + {cond: false, other: 8},
31 + {cond: true},
32 + {cond: true},
33 + ],
34 +};
35 +
36 +```
37 +
38 +## Code
39 +
40 +```javascript
41 +import { c as _c } from "react/compiler-runtime"; // x.a.b was accessed unconditionally within the mutable range of x.
42 +// As a result, we cannot infer anything about whether `x` or `x.a`
43 +// may be null. This means that it's not safe to hoist reads from x
44 +// (e.g. take `x.a` or `x.a.b` as a dependency).
45 +
46 +import { identity, makeObject_Primitives, setProperty } from "shared-runtime";
47 +
48 +function Component(t0) {
49 + const $ = _c(8);
50 + const { cond, other } = t0;
51 + let x;
52 + if ($[0] !== other || $[1] !== cond) {
53 + x = makeObject_Primitives();
54 + setProperty(x, { b: 3, other }, "a");
55 + identity(x.a.b);
56 + if (!cond) {
57 + x.a = null;
58 + }
59 + $[0] = other;
60 + $[1] = cond;
61 + $[2] = x;
62 + } else {
63 + x = $[2];
64 + }
65 + let t1;
66 + if ($[3] !== cond || $[4] !== x) {
67 + t1 = identity(cond) && x.a.b;
68 + $[3] = cond;
69 + $[4] = x;
70 + $[5] = t1;
71 + } else {
72 + t1 = $[5];
73 + }
74 + let t2;
75 + if ($[6] !== t1) {
76 + t2 = [t1];
77 + $[6] = t1;
78 + $[7] = t2;
79 + } else {
80 + t2 = $[7];
81 + }
82 + const y = t2;
83 + return y;
84 +}
85 +
86 +export const FIXTURE_ENTRYPOINT = {
87 + fn: Component,
88 + params: [{ cond: false }],
89 + sequentialRenders: [
90 + { cond: false },
91 + { cond: false },
92 + { cond: false, other: 8 },
93 + { cond: true },
94 + { cond: true },
95 + ],
96 +};
97 +
98 +```
99 +
100 +### Eval output
101 +(kind: ok) [false]
102 +[false]
103 +[false]
104 +[null]
105 +[null]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.js new
+30
@@ -0,0 +1,30 @@
1 +// x.a.b was accessed unconditionally within the mutable range of x.
2 +// As a result, we cannot infer anything about whether `x` or `x.a`
3 +// may be null. This means that it's not safe to hoist reads from x
4 +// (e.g. take `x.a` or `x.a.b` as a dependency).
5 +
6 +import {identity, makeObject_Primitives, setProperty} from 'shared-runtime';
7 +
8 +function Component({cond, other}) {
9 + const x = makeObject_Primitives();
10 + setProperty(x, {b: 3, other}, 'a');
11 + identity(x.a.b);
12 + if (!cond) {
13 + x.a = null;
14 + }
15 +
16 + const y = [identity(cond) && x.a.b];
17 + return y;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{cond: false}],
23 + sequentialRenders: [
24 + {cond: false},
25 + {cond: false},
26 + {cond: false, other: 8},
27 + {cond: true},
28 + {cond: true},
29 + ],
30 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.expect.md new
+73
@@ -0,0 +1,73 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify} from 'shared-runtime';
6 +import {makeArray} from 'shared-runtime';
7 +
8 +/**
9 + * Here, we don't need to memoize Stringify as it is a read off of a global.
10 + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
11 + * and avoid adding them to `temporariesUsedOutsideDefiningScope`.
12 + */
13 +function Component({num}: {num: number}) {
14 + const arr = makeArray(num);
15 + return <Stringify value={arr.push(num)}></Stringify>;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{num: 2}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime";
29 +import { Stringify } from "shared-runtime";
30 +import { makeArray } from "shared-runtime";
31 +
32 +/**
33 + * Here, we don't need to memoize Stringify as it is a read off of a global.
34 + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
35 + * and avoid adding them to `temporariesUsedOutsideDefiningScope`.
36 + */
37 +function Component(t0) {
38 + const $ = _c(6);
39 + const { num } = t0;
40 + let T0;
41 + let t1;
42 + if ($[0] !== num) {
43 + const arr = makeArray(num);
44 + T0 = Stringify;
45 + t1 = arr.push(num);
46 + $[0] = num;
47 + $[1] = T0;
48 + $[2] = t1;
49 + } else {
50 + T0 = $[1];
51 + t1 = $[2];
52 + }
53 + let t2;
54 + if ($[3] !== T0 || $[4] !== t1) {
55 + t2 = <T0 value={t1} />;
56 + $[3] = T0;
57 + $[4] = t1;
58 + $[5] = t2;
59 + } else {
60 + t2 = $[5];
61 + }
62 + return t2;
63 +}
64 +
65 +export const FIXTURE_ENTRYPOINT = {
66 + fn: Component,
67 + params: [{ num: 2 }],
68 +};
69 +
70 +```
71 +
72 +### Eval output
73 +(kind: ok) <div>{"value":2}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.tsx new
+17
@@ -0,0 +1,17 @@
1 +import {Stringify} from 'shared-runtime';
2 +import {makeArray} from 'shared-runtime';
3 +
4 +/**
5 + * Here, we don't need to memoize Stringify as it is a read off of a global.
6 + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
7 + * and avoid adding them to `temporariesUsedOutsideDefiningScope`.
8 + */
9 +function Component({num}: {num: number}) {
10 + const arr = makeArray(num);
11 + return <Stringify value={arr.push(num)}></Stringify>;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{num: 2}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.expect.md new
+78
@@ -0,0 +1,78 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import * as SharedRuntime from 'shared-runtime';
6 +import {makeArray} from 'shared-runtime';
7 +
8 +/**
9 + * Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad
10 + * off of a global.
11 + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
12 + * and avoid adding them to `temporariesUsedOutsideDefiningScope`.
13 + */
14 +function Component({num}: {num: number}) {
15 + const arr = makeArray(num);
16 + return (
17 + <SharedRuntime.Stringify value={arr.push(num)}></SharedRuntime.Stringify>
18 + );
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{num: 2}],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { c as _c } from "react/compiler-runtime";
32 +import * as SharedRuntime from "shared-runtime";
33 +import { makeArray } from "shared-runtime";
34 +
35 +/**
36 + * Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad
37 + * off of a global.
38 + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
39 + * and avoid adding them to `temporariesUsedOutsideDefiningScope`.
40 + */
41 +function Component(t0) {
42 + const $ = _c(6);
43 + const { num } = t0;
44 + let T0;
45 + let t1;
46 + if ($[0] !== num) {
47 + const arr = makeArray(num);
48 +
49 + T0 = SharedRuntime.Stringify;
50 + t1 = arr.push(num);
51 + $[0] = num;
52 + $[1] = T0;
53 + $[2] = t1;
54 + } else {
55 + T0 = $[1];
56 + t1 = $[2];
57 + }
58 + let t2;
59 + if ($[3] !== T0 || $[4] !== t1) {
60 + t2 = <T0 value={t1} />;
61 + $[3] = T0;
62 + $[4] = t1;
63 + $[5] = t2;
64 + } else {
65 + t2 = $[5];
66 + }
67 + return t2;
68 +}
69 +
70 +export const FIXTURE_ENTRYPOINT = {
71 + fn: Component,
72 + params: [{ num: 2 }],
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: ok) <div>{"value":2}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.tsx new
+20
@@ -0,0 +1,20 @@
1 +import * as SharedRuntime from 'shared-runtime';
2 +import {makeArray} from 'shared-runtime';
3 +
4 +/**
5 + * Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad
6 + * off of a global.
7 + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
8 + * and avoid adding them to `temporariesUsedOutsideDefiningScope`.
9 + */
10 +function Component({num}: {num: number}) {
11 + const arr = makeArray(num);
12 + return (
13 + <SharedRuntime.Stringify value={arr.push(num)}></SharedRuntime.Stringify>
14 + );
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{num: 2}],
20 +};
compiler/packages/snap/src/sprout/shared-runtime.ts
+10 -1
@@ -98,6 +98,15 @@ export function setProperty(arg: any, property: any): void {
98 }
99 }
100
101 +export function setPropertyByKey<
102 + T,
103 + TKey extends keyof T,
104 + TProperty extends T[TKey],
105 +>(arg: T, key: TKey, property: TProperty): T {
106 + arg[key] = property;
107 + return arg;
108 +}
109 +
110 export function arrayPush<T>(arr: Array<T>, ...values: Array<T>): void {
111 arr.push(...values);
112 }
@@ -125,7 +134,7 @@ export function calculateExpensiveNumber(x: number): number {
134 /**
135 * Functions that do not mutate their parameters
136 */
128 -export function shallowCopy(obj: object): object {
137 +export function shallowCopy<T extends object>(obj: T): T {
138 return Object.assign({}, obj);
139 }
140