@samitouri / QOS-React / commits / 7475d568da

[wip][compiler] Infer optional dependencies

Updates PropagateScopeDeps and DeriveMinimalDeps to understand optional dependency paths (`a?.b`). There a few key pieces to this: In PropagateScopeDeps we jump through some hoops to work around the awkward structure of nested OptionalExpressions. This is much easier in HIR form, but I managed to get this pretty close and i think it will be landable with further cleanup. A good chunk of this is avoiding prematurely registering a value as a dependency - there are a bunch of indirections in the ReactiveFunction structure: ``` t0 = OptionalExpression SequenceExpression t0 = Sequence ... LoadLocal t0 ``` Where if at any point we call `visitOperand()` we'll prematurely register a dependency instead of declareProperty(). The other bit is that optionals can be optional=false for nested member expressions where not all the parts are actually optional (`foo.bar?.bar.call()`). And of course, parts of an optional chain can still be conditional even when optional=true (for example the `x` in `foo.bar?.[x]?.baz`). Not all of this is tested yet so there are likely bugs still. The other bit is DeriveMinimalDeps, which is thankfully easier. We add OptionalAccess and OptionalDep and update the merge and reducing logic for these cases. There is probably still more to update though, for things like merging subtrees. There are a lot of ternaries that assume a result can be exactly one of two states (conditional/unconditional, dependency/access) and these assumptions don't hold anymore. I'd like to refactor to dependency/access separate from conditional/optional/unconditional. Also, the reducing logic isn't quite right: once a child is optional we keep inferring all the parents as optional too, losing some precision. I need to adjust the reducing logic to let children decide whether their path token is optional or not. ghstack-source-id: 207842ac64560cf0f93ec96eb9ae1f17c62493ac Pull Request resolved: https://github.com/facebook/react/pull/30819

Joe Savona committed Aug 28, 2024 at 10:52 UTC 7475d568da137b661ce23edc24446871d58c67ef
18 files changed +755 -128
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+8
@@ -224,6 +224,14 @@ const EnvironmentConfigSchema = z.object({
224
225 enableReactiveScopesInHIR: z.boolean().default(true),
226
227 + /**
228 + * Enables inference of optional dependency chains. Without this flag
229 + * a property chain such as `props?.items?.foo` will infer as a dep on
230 + * just `props`. With this flag enabled, we'll infer that full path as
231 + * the dependency.
232 + */
233 + enableOptionalDependencies: z.boolean().default(false),
234 +
235 /*
236 * Enable validation of hooks to partially check that the component honors the rules of hooks.
237 * When disabled, the component is assumed to follow the rules (though the Babel plugin looks
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+1 -1
@@ -191,7 +191,7 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
191 case 'branch': {
192 value = `[${terminal.id}] Branch (${printPlace(terminal.test)}) then:bb${
193 terminal.consequent
194 - } else:bb${terminal.alternate}`;
194 + } else:bb${terminal.alternate} fallthrough:bb${terminal.fallthrough}`;
195 break;
196 }
197 case 'logical': {
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+12 -2
@@ -1446,9 +1446,19 @@ function codegenDependency(
1446 dependency: ReactiveScopeDependency,
1447 ): t.Expression {
1448 let object: t.Expression = convertIdentifier(dependency.identifier);
1449 - if (dependency.path !== null) {
1449 + if (dependency.path.length !== 0) {
1450 + const hasOptional = dependency.path.some(path => path.optional);
1451 for (const path of dependency.path) {
1451 - object = t.memberExpression(object, t.identifier(path.property));
1452 + if (hasOptional) {
1453 + object = t.optionalMemberExpression(
1454 + object,
1455 + t.identifier(path.property),
1456 + false,
1457 + path.optional,
1458 + );
1459 + } else {
1460 + object = t.memberExpression(object, t.identifier(path.property));
1461 + }
1462 }
1463 }
1464 return object;
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts
+131 -34
@@ -60,13 +60,14 @@ export class ReactiveScopeDependencyTree {
60 const {path} = dep;
61 let currNode = this.#getOrCreateRoot(dep.identifier);
62
63 - const accessType = inConditional
64 - ? PropertyAccessType.ConditionalAccess
65 - : PropertyAccessType.UnconditionalAccess;
66 -
63 for (const item of path) {
64 // all properties read 'on the way' to a dependency are marked as 'access'
65 let currChild = getOrMakeProperty(currNode, item.property);
66 + const accessType = inConditional
67 + ? PropertyAccessType.ConditionalAccess
68 + : item.optional
69 + ? PropertyAccessType.OptionalAccess
70 + : PropertyAccessType.UnconditionalAccess;
71 currChild.accessType = merge(currChild.accessType, accessType);
72 currNode = currChild;
73 }
@@ -77,7 +78,9 @@ export class ReactiveScopeDependencyTree {
78 */
79 const depType = inConditional
80 ? PropertyAccessType.ConditionalDependency
80 - : PropertyAccessType.UnconditionalDependency;
81 + : isOptional(currNode.accessType)
82 + ? PropertyAccessType.OptionalDependency
83 + : PropertyAccessType.UnconditionalDependency;
84
85 currNode.accessType = merge(currNode.accessType, depType);
86 }
@@ -85,10 +88,12 @@ export class ReactiveScopeDependencyTree {
88 deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
89 const results = new Set<ReactiveScopeDependency>();
90 for (const [rootId, rootNode] of this.#roots.entries()) {
88 - const deps = deriveMinimalDependenciesInSubtree(rootNode);
91 + const deps = deriveMinimalDependenciesInSubtree(rootNode, null);
92 CompilerError.invariant(
93 deps.every(
91 - dep => dep.accessType === PropertyAccessType.UnconditionalDependency,
94 + dep =>
95 + dep.accessType === PropertyAccessType.UnconditionalDependency ||
96 + dep.accessType == PropertyAccessType.OptionalDependency,
97 ),
98 {
99 reason:
@@ -173,6 +178,27 @@ export class ReactiveScopeDependencyTree {
178 }
179 return res.flat().join('\n');
180 }
181 +
182 + debug(): string {
183 + const buf: Array<string> = [`tree() [`];
184 + for (const [rootId, rootNode] of this.#roots) {
185 + buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
186 + this.#debugImpl(buf, rootNode, 1);
187 + }
188 + buf.push(']');
189 + return buf.length > 2 ? buf.join('\n') : buf.join('');
190 + }
191 +
192 + #debugImpl(
193 + buf: Array<string>,
194 + node: DependencyNode,
195 + depth: number = 0,
196 + ): void {
197 + for (const [property, childNode] of node.properties) {
198 + buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
199 + this.#debugImpl(buf, childNode, depth + 1);
200 + }
201 + }
202 }
203
204 /*
@@ -196,8 +222,10 @@ export class ReactiveScopeDependencyTree {
222 */
223 enum PropertyAccessType {
224 ConditionalAccess = 'ConditionalAccess',
225 + OptionalAccess = 'OptionalAccess',
226 UnconditionalAccess = 'UnconditionalAccess',
227 ConditionalDependency = 'ConditionalDependency',
228 + OptionalDependency = 'OptionalDependency',
229 UnconditionalDependency = 'UnconditionalDependency',
230 }
231
@@ -211,9 +239,16 @@ function isUnconditional(access: PropertyAccessType): boolean {
239 function isDependency(access: PropertyAccessType): boolean {
240 return (
241 access === PropertyAccessType.ConditionalDependency ||
242 + access === PropertyAccessType.OptionalDependency ||
243 access === PropertyAccessType.UnconditionalDependency
244 );
245 }
246 +function isOptional(access: PropertyAccessType): boolean {
247 + return (
248 + access === PropertyAccessType.OptionalAccess ||
249 + access === PropertyAccessType.OptionalDependency
250 + );
251 +}
252
253 function merge(
254 access1: PropertyAccessType,
@@ -222,6 +257,7 @@ function merge(
257 const resultIsUnconditional =
258 isUnconditional(access1) || isUnconditional(access2);
259 const resultIsDependency = isDependency(access1) || isDependency(access2);
260 + const resultIsOptional = isOptional(access1) || isOptional(access2);
261
262 /*
263 * Straightforward merge.
@@ -237,6 +273,12 @@ function merge(
273 } else {
274 return PropertyAccessType.UnconditionalAccess;
275 }
276 + } else if (resultIsOptional) {
277 + if (resultIsDependency) {
278 + return PropertyAccessType.OptionalDependency;
279 + } else {
280 + return PropertyAccessType.OptionalAccess;
281 + }
282 } else {
283 if (resultIsDependency) {
284 return PropertyAccessType.ConditionalDependency;
@@ -256,19 +298,34 @@ type ReduceResultNode = {
298 accessType: PropertyAccessType;
299 };
300
259 -const promoteUncondResult = [
260 - {
301 +function promoteResult(
302 + accessType: PropertyAccessType,
303 + path: {property: string; optional: boolean} | null,
304 +): Array<ReduceResultNode> {
305 + const result: ReduceResultNode = {
306 relativePath: [],
262 - accessType: PropertyAccessType.UnconditionalDependency,
263 - },
264 -];
307 + accessType,
308 + };
309 + if (path !== null) {
310 + result.relativePath.push(path);
311 + }
312 + return [result];
313 +}
314
266 -const promoteCondResult = [
267 - {
268 - relativePath: [],
269 - accessType: PropertyAccessType.ConditionalDependency,
270 - },
271 -];
315 +function prependPath(
316 + results: Array<ReduceResultNode>,
317 + path: {property: string; optional: boolean} | null,
318 +): Array<ReduceResultNode> {
319 + if (path === null) {
320 + return results;
321 + }
322 + return results.map(result => {
323 + return {
324 + accessType: result.accessType,
325 + relativePath: [path, ...result.relativePath],
326 + };
327 + });
328 +}
329
330 /*
331 * Recursively calculates minimal dependencies in a subtree.
@@ -277,42 +334,76 @@ const promoteCondResult = [
334 */
335 function deriveMinimalDependenciesInSubtree(
336 dep: DependencyNode,
337 + property: string | null,
338 ): Array<ReduceResultNode> {
339 const results: Array<ReduceResultNode> = [];
340 for (const [childName, childNode] of dep.properties) {
283 - const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
284 - ({relativePath, accessType}) => {
285 - return {
286 - relativePath: [
287 - {property: childName, optional: false},
288 - ...relativePath,
289 - ],
290 - accessType,
291 - };
292 - },
341 + const childResult = deriveMinimalDependenciesInSubtree(
342 + childNode,
343 + childName,
344 );
345 results.push(...childResult);
346 }
347
348 switch (dep.accessType) {
349 case PropertyAccessType.UnconditionalDependency: {
299 - return promoteUncondResult;
350 + return promoteResult(
351 + PropertyAccessType.UnconditionalDependency,
352 + property !== null ? {property, optional: false} : null,
353 + );
354 }
355 case PropertyAccessType.UnconditionalAccess: {
356 if (
357 results.every(
358 ({accessType}) =>
305 - accessType === PropertyAccessType.UnconditionalDependency,
359 + accessType === PropertyAccessType.UnconditionalDependency ||
360 + accessType === PropertyAccessType.OptionalDependency,
361 )
362 ) {
363 // all children are unconditional dependencies, return them to preserve granularity
309 - return results;
364 + return prependPath(
365 + results,
366 + property !== null ? {property, optional: false} : null,
367 + );
368 } else {
369 /*
370 * at least one child is accessed conditionally, so this node needs to be promoted to
371 * unconditional dependency
372 */
315 - return promoteUncondResult;
373 + return promoteResult(
374 + PropertyAccessType.UnconditionalDependency,
375 + property !== null ? {property, optional: false} : null,
376 + );
377 + }
378 + }
379 + case PropertyAccessType.OptionalDependency: {
380 + return promoteResult(
381 + PropertyAccessType.OptionalDependency,
382 + property !== null ? {property, optional: true} : null,
383 + );
384 + }
385 + case PropertyAccessType.OptionalAccess: {
386 + if (
387 + results.every(
388 + ({accessType}) =>
389 + accessType === PropertyAccessType.UnconditionalDependency ||
390 + accessType === PropertyAccessType.OptionalDependency,
391 + )
392 + ) {
393 + // all children are unconditional dependencies, return them to preserve granularity
394 + return prependPath(
395 + results,
396 + property !== null ? {property, optional: true} : null,
397 + );
398 + } else {
399 + /*
400 + * at least one child is accessed conditionally, so this node needs to be promoted to
401 + * unconditional dependency
402 + */
403 + return promoteResult(
404 + PropertyAccessType.OptionalDependency,
405 + property !== null ? {property, optional: true} : null,
406 + );
407 }
408 }
409 case PropertyAccessType.ConditionalAccess:
@@ -328,13 +419,19 @@ function deriveMinimalDependenciesInSubtree(
419 * unconditional access.
420 * Truncate results of child nodes here, since we shouldn't access them anyways
421 */
331 - return promoteCondResult;
422 + return promoteResult(
423 + PropertyAccessType.ConditionalDependency,
424 + property !== null ? {property, optional: true} : null,
425 + );
426 } else {
427 /*
428 * at least one child is accessed unconditionally, so this node can be promoted to
429 * unconditional dependency
430 */
337 - return promoteUncondResult;
431 + return promoteResult(
432 + PropertyAccessType.UnconditionalDependency,
433 + property !== null ? {property, optional: true} : null,
434 + );
435 }
436 }
437 default: {
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts
+1 -1
@@ -113,7 +113,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
113 const identifier =
114 printIdentifier(dependency.identifier) +
115 printType(dependency.identifier.type);
116 - return `${identifier}${dependency.path.map(token => `.${token.property}`).join('')}`;
116 + return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`;
117 }
118
119 export function printReactiveInstructions(
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts
+220 -51
@@ -6,6 +6,7 @@
6 */
7
8 import {CompilerError} from '../CompilerError';
9 +import {Environment} from '../HIR';
10 import {
11 areEqualPaths,
12 BlockId,
@@ -22,6 +23,7 @@ import {
23 PrunedReactiveScopeBlock,
24 ReactiveFunction,
25 ReactiveInstruction,
26 + ReactiveOptionalCallValue,
27 ReactiveScope,
28 ReactiveScopeBlock,
29 ReactiveScopeDependency,
@@ -65,11 +67,7 @@ export function propagateScopeDependencies(fn: ReactiveFunction): void {
67 });
68 }
69 }
68 - visitReactiveFunction(
69 - fn,
70 - new PropagationVisitor(fn.env.config.enableTreatFunctionDepsAsConditional),
71 - context,
72 - );
70 + visitReactiveFunction(fn, new PropagationVisitor(fn.env), context);
71 }
72
73 type TemporariesUsedOutsideDefiningScope = {
@@ -465,6 +463,7 @@ class Context {
463 #getProperty(
464 object: Place,
465 property: string,
466 + optional: boolean,
467 ): ReactiveScopePropertyDependency {
468 const resolvedObject = this.resolveTemporary(object);
469 const resolvedDependency = this.#properties.get(resolvedObject.identifier);
@@ -485,13 +484,18 @@ class Context {
484 };
485 }
486
488 - objectDependency.path.push({property, optional: false});
487 + objectDependency.path.push({property, optional});
488
489 return objectDependency;
490 }
491
493 - declareProperty(lvalue: Place, object: Place, property: string): void {
494 - const nextDependency = this.#getProperty(object, property);
492 + declareProperty(
493 + lvalue: Place,
494 + object: Place,
495 + property: string,
496 + optional: boolean,
497 + ): void {
498 + const nextDependency = this.#getProperty(object, property, optional);
499 this.#properties.set(lvalue.identifier, nextDependency);
500 }
501
@@ -571,8 +575,8 @@ class Context {
575 this.visitDependency(dependency);
576 }
577
574 - visitProperty(object: Place, property: string): void {
575 - const nextDependency = this.#getProperty(object, property);
578 + visitProperty(object: Place, property: string, optional: boolean): void {
579 + const nextDependency = this.#getProperty(object, property, optional);
580 this.visitDependency(nextDependency);
581 }
582
@@ -671,12 +675,11 @@ class Context {
675 }
676
677 class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
674 - enableTreatFunctionDepsAsConditional = false;
678 + env: Environment;
679
676 - constructor(enableTreatFunctionDepsAsConditional: boolean) {
680 + constructor(env: Environment) {
681 super();
678 - this.enableTreatFunctionDepsAsConditional =
679 - enableTreatFunctionDepsAsConditional;
682 + this.env = env;
683 }
684
685 override visitScope(scope: ReactiveScopeBlock, context: Context): void {
@@ -744,51 +747,212 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
747 });
748 }
749
750 + extractOptionalProperty(
751 + context: Context,
752 + optionalValue: ReactiveOptionalCallValue,
753 + lvalue: Place,
754 + ): {
755 + lvalue: Place;
756 + object: Place;
757 + property: string;
758 + optional: boolean;
759 + } | null {
760 + const sequence = optionalValue.value;
761 + CompilerError.invariant(sequence.kind === 'SequenceExpression', {
762 + reason: 'Expected OptionalExpression value to be a SequenceExpression',
763 + description: `Found a \`${sequence.kind}\``,
764 + loc: sequence.loc,
765 + });
766 + /**
767 + * Base case: inner `<variable> "." or "?."" <property>`
768 + *```
769 + * <lvalue> = OptionalExpression optional=true (`optionalValue` is here)
770 + * Sequence (`sequence` is here)
771 + * t0 = LoadLocal <variable>
772 + * Sequence
773 + * t1 = PropertyLoad t0 . <property>
774 + * LoadLocal t1
775 + * ```
776 + */
777 + if (
778 + sequence.instructions.length === 1 &&
779 + sequence.instructions[0].value.kind === 'LoadLocal' &&
780 + sequence.instructions[0].lvalue !== null &&
781 + sequence.instructions[0].value.place.identifier.name !== null &&
782 + !context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) &&
783 + sequence.value.kind === 'SequenceExpression' &&
784 + sequence.value.instructions.length === 1 &&
785 + sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
786 + sequence.value.instructions[0].value.object.identifier.id ===
787 + sequence.instructions[0].lvalue.identifier.id &&
788 + sequence.value.instructions[0].lvalue !== null &&
789 + sequence.value.value.kind === 'LoadLocal' &&
790 + sequence.value.value.place.identifier.id ===
791 + sequence.value.instructions[0].lvalue.identifier.id
792 + ) {
793 + context.declareTemporary(
794 + sequence.instructions[0].lvalue,
795 + sequence.instructions[0].value.place,
796 + );
797 + const propertyLoad = sequence.value.instructions[0].value;
798 + return {
799 + lvalue,
800 + object: propertyLoad.object,
801 + property: propertyLoad.property,
802 + optional: optionalValue.optional,
803 + };
804 + }
805 + /**
806 + * Composed case: `<base-case> "." or "?." <property>`
807 + *
808 + * This case is convoluted, note how `t0` appears as an lvalue *twice*
809 + * and then is an operand of an intermediate LoadLocal and then the
810 + * object of the final PropertyLoad:
811 + *
812 + * ```
813 + * <lvalue> = OptionalExpression optional=false (`optionalValue` is here)
814 + * Sequence (`sequence` is here)
815 + * t0 = Sequence
816 + * t0 =
817 + * <nested>
818 + * LoadLocal t0
819 + * Sequence
820 + * t1 = PropertyLoad t0. <property>
821 + * LoadLocal t1
822 + * ```
823 + */
824 + if (
825 + sequence.instructions.length === 1 &&
826 + sequence.instructions[0].value.kind === 'SequenceExpression' &&
827 + sequence.instructions[0].value.instructions.length === 1 &&
828 + sequence.instructions[0].value.instructions[0].lvalue !== null &&
829 + sequence.instructions[0].value.instructions[0].value.kind ===
830 + 'OptionalExpression' &&
831 + sequence.instructions[0].value.value.kind === 'LoadLocal' &&
832 + sequence.instructions[0].value.value.place.identifier.id ===
833 + sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
834 + sequence.value.kind === 'SequenceExpression' &&
835 + sequence.value.instructions.length === 1 &&
836 + sequence.value.instructions[0].lvalue !== null &&
837 + sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
838 + sequence.value.instructions[0].value.object.identifier.id ===
839 + sequence.instructions[0].value.value.place.identifier.id &&
840 + sequence.value.value.kind === 'LoadLocal' &&
841 + sequence.value.value.place.identifier.id ===
842 + sequence.value.instructions[0].lvalue.identifier.id
843 + ) {
844 + const {lvalue: innerLvalue, value: innerOptional} =
845 + sequence.instructions[0].value.instructions[0];
846 + const innerProperty = this.extractOptionalProperty(
847 + context,
848 + innerOptional,
849 + innerLvalue,
850 + );
851 + if (innerProperty === null) {
852 + return null;
853 + }
854 + context.declareProperty(
855 + innerProperty.lvalue,
856 + innerProperty.object,
857 + innerProperty.property,
858 + innerProperty.optional,
859 + );
860 + const propertyLoad = sequence.value.instructions[0].value;
861 + return {
862 + lvalue,
863 + object: propertyLoad.object,
864 + property: propertyLoad.property,
865 + optional: optionalValue.optional,
866 + };
867 + }
868 + return null;
869 + }
870 +
871 + visitOptionalExpression(
872 + context: Context,
873 + id: InstructionId,
874 + value: ReactiveOptionalCallValue,
875 + lvalue: Place | null,
876 + ): void {
877 + /**
878 + * If this is the first optional=true optional in a recursive OptionalExpression
879 + * subtree, we check to see if the subtree is of the form:
880 + * ```
881 + * NestedOptional =
882 + * `<variable> . / ?. <property>`
883 + * `<nested-optional> . / ?. <property>`
884 + * ```
885 + *
886 + * Ie strictly a chain like `foo?.bar?.baz` or `a?.b.c`. If the subtree contains
887 + * any other types of expressions - for example `foo?.[makeKey(a)]` - then this
888 + * will return null and we'll go to the default handling below.
889 + *
890 + * If the tree does match the NestedOptional shape, then we'll have recorded
891 + * a sequence of declareProperty calls, and the final visitProperty call here
892 + * will record that optional chain as a dependency (since we know it's about
893 + * to be referenced via its lvalue which is non-null).
894 + */
895 + if (
896 + lvalue !== null &&
897 + value.optional &&
898 + this.env.config.enableOptionalDependencies
899 + ) {
900 + const inner = this.extractOptionalProperty(context, value, lvalue);
901 + if (inner !== null) {
902 + context.visitProperty(inner.object, inner.property, inner.optional);
903 + return;
904 + }
905 + }
906 +
907 + // Otherwise we treat everything after the optional as conditional
908 + const inner = value.value;
909 + /*
910 + * OptionalExpression value is a SequenceExpression where the instructions
911 + * represent the code prior to the `?` and the final value represents the
912 + * conditional code that follows.
913 + */
914 + CompilerError.invariant(inner.kind === 'SequenceExpression', {
915 + reason: 'Expected OptionalExpression value to be a SequenceExpression',
916 + description: `Found a \`${value.kind}\``,
917 + loc: value.loc,
918 + suggestions: null,
919 + });
920 + // Instructions are the unconditionally executed portion before the `?`
921 + for (const instr of inner.instructions) {
922 + this.visitInstruction(instr, context);
923 + }
924 + // The final value is the conditional portion following the `?`
925 + context.enterConditional(() => {
926 + this.visitReactiveValue(context, id, inner.value, null);
927 + });
928 + }
929 +
930 visitReactiveValue(
931 context: Context,
932 id: InstructionId,
933 value: ReactiveValue,
934 + lvalue: Place | null,
935 ): void {
936 switch (value.kind) {
937 case 'OptionalExpression': {
754 - const inner = value.value;
755 - /*
756 - * OptionalExpression value is a SequenceExpression where the instructions
757 - * represent the code prior to the `?` and the final value represents the
758 - * conditional code that follows.
759 - */
760 - CompilerError.invariant(inner.kind === 'SequenceExpression', {
761 - reason:
762 - 'Expected OptionalExpression value to be a SequenceExpression',
763 - description: `Found a \`${value.kind}\``,
764 - loc: value.loc,
765 - suggestions: null,
766 - });
767 - // Instructions are the unconditionally executed portion before the `?`
768 - for (const instr of inner.instructions) {
769 - this.visitInstruction(instr, context);
770 - }
771 - // The final value is the conditional portion following the `?`
772 - context.enterConditional(() => {
773 - this.visitReactiveValue(context, id, inner.value);
774 - });
938 + this.visitOptionalExpression(context, id, value, lvalue);
939 break;
940 }
941 case 'LogicalExpression': {
778 - this.visitReactiveValue(context, id, value.left);
942 + this.visitReactiveValue(context, id, value.left, null);
943 context.enterConditional(() => {
780 - this.visitReactiveValue(context, id, value.right);
944 + this.visitReactiveValue(context, id, value.right, null);
945 });
946 break;
947 }
948 case 'ConditionalExpression': {
785 - this.visitReactiveValue(context, id, value.test);
949 + this.visitReactiveValue(context, id, value.test, null);
950
951 const consequentDeps = context.enterConditional(() => {
788 - this.visitReactiveValue(context, id, value.consequent);
952 + this.visitReactiveValue(context, id, value.consequent, null);
953 });
954 const alternateDeps = context.enterConditional(() => {
791 - this.visitReactiveValue(context, id, value.alternate);
955 + this.visitReactiveValue(context, id, value.alternate, null);
956 });
957 context.promoteDepsFromExhaustiveConditionals([
958 consequentDeps,
@@ -804,7 +968,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
968 break;
969 }
970 case 'FunctionExpression': {
807 - if (this.enableTreatFunctionDepsAsConditional) {
971 + if (this.env.config.enableTreatFunctionDepsAsConditional) {
972 context.enterConditional(() => {
973 for (const operand of eachInstructionValueOperand(value)) {
974 context.visitOperand(operand);
@@ -851,9 +1015,9 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
1015 }
1016 } else if (value.kind === 'PropertyLoad') {
1017 if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) {
854 - context.declareProperty(lvalue, value.object, value.property);
1018 + context.declareProperty(lvalue, value.object, value.property, false);
1019 } else {
856 - context.visitProperty(value.object, value.property);
1020 + context.visitProperty(value.object, value.property, false);
1021 }
1022 } else if (value.kind === 'StoreLocal') {
1023 context.visitOperand(value.value);
@@ -896,7 +1060,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
1060 });
1061 }
1062 } else {
899 - this.visitReactiveValue(context, id, value);
1063 + this.visitReactiveValue(context, id, value, lvalue);
1064 }
1065 }
1066
@@ -947,25 +1111,30 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
1111 break;
1112 }
1113 case 'for': {
950 - this.visitReactiveValue(context, terminal.id, terminal.init);
951 - this.visitReactiveValue(context, terminal.id, terminal.test);
1114 + this.visitReactiveValue(context, terminal.id, terminal.init, null);
1115 + this.visitReactiveValue(context, terminal.id, terminal.test, null);
1116 context.enterConditional(() => {
1117 this.visitBlock(terminal.loop, context);
1118 if (terminal.update !== null) {
955 - this.visitReactiveValue(context, terminal.id, terminal.update);
1119 + this.visitReactiveValue(
1120 + context,
1121 + terminal.id,
1122 + terminal.update,
1123 + null,
1124 + );
1125 }
1126 });
1127 break;
1128 }
1129 case 'for-of': {
961 - this.visitReactiveValue(context, terminal.id, terminal.init);
1130 + this.visitReactiveValue(context, terminal.id, terminal.init, null);
1131 context.enterConditional(() => {
1132 this.visitBlock(terminal.loop, context);
1133 });
1134 break;
1135 }
1136 case 'for-in': {
968 - this.visitReactiveValue(context, terminal.id, terminal.init);
1137 + this.visitReactiveValue(context, terminal.id, terminal.init, null);
1138 context.enterConditional(() => {
1139 this.visitBlock(terminal.loop, context);
1140 });
@@ -974,12 +1143,12 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
1143 case 'do-while': {
1144 this.visitBlock(terminal.loop, context);
1145 context.enterConditional(() => {
977 - this.visitReactiveValue(context, terminal.id, terminal.test);
1146 + this.visitReactiveValue(context, terminal.id, terminal.test, null);
1147 });
1148 break;
1149 }
1150 case 'while': {
982 - this.visitReactiveValue(context, terminal.id, terminal.test);
1151 + this.visitReactiveValue(context, terminal.id, terminal.test, null);
1152 context.enterConditional(() => {
1153 this.visitBlock(terminal.loop, context);
1154 });
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md deleted
-32
@@ -1,32 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees
6 -function Component(props) {
7 - const data = useMemo(() => {
8 - return props.items?.edges?.nodes ?? [];
9 - }, [props.items?.edges?.nodes]);
10 - return <Foo data={data} />;
11 -}
12 -
13 -```
14 -
15 -
16 -## Error
17 -
18 -```
19 - 1 | // @validatePreserveExistingMemoizationGuarantees
20 - 2 | function Component(props) {
21 -> 3 | const data = useMemo(() => {
22 - | ^^^^^^^
23 -> 4 | return props.items?.edges?.nodes ?? [];
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/error.todo-optional-member-expression-as-memo-dep.js deleted
-7
@@ -1,7 +0,0 @@
1 -// @validatePreserveExistingMemoizationGuarantees
2 -function Component(props) {
3 - const data = useMemo(() => {
4 - return props.items?.edges?.nodes ?? [];
5 - }, [props.items?.edges?.nodes]);
6 - return <Foo data={data} />;
7 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md new
+48
@@ -0,0 +1,48 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
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
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/optional-member-expression-as-memo-dep.js new
+7
@@ -0,0 +1,7 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 +function Component(props) {
3 + const data = useMemo(() => {
4 + return props?.items.edges?.nodes.map();
5 + }, [props?.items.edges?.nodes]);
6 + return <Foo data={data} />;
7 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md new
+65
@@ -0,0 +1,65 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
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
23 +import { ValidateMemoization } from "shared-runtime";
24 +function Component(props) {
25 + const $ = _c(7);
26 +
27 + props?.items;
28 + let t0;
29 + let x;
30 + if ($[0] !== props.items) {
31 + x = [];
32 + x.push(props?.items);
33 + x.push(props.items);
34 + $[0] = props.items;
35 + $[1] = x;
36 + } else {
37 + x = $[1];
38 + }
39 + t0 = x;
40 + const data = t0;
41 + const t1 = props?.items;
42 + let t2;
43 + if ($[2] !== t1) {
44 + t2 = [t1];
45 + $[2] = t1;
46 + $[3] = t2;
47 + } else {
48 + t2 = $[3];
49 + }
50 + let t3;
51 + if ($[4] !== t2 || $[5] !== data) {
52 + t3 = <ValidateMemoization inputs={t2} output={data} />;
53 + $[4] = t2;
54 + $[5] = data;
55 + $[6] = t3;
56 + } else {
57 + t3 = $[6];
58 + }
59 + return t3;
60 +}
61 +
62 +```
63 +
64 +### Eval output
65 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js new
+11
@@ -0,0 +1,11 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 +import {ValidateMemoization} from 'shared-runtime';
3 +function Component(props) {
4 + const data = useMemo(() => {
5 + const x = [];
6 + x.push(props?.items);
7 + x.push(props.items);
8 + return x;
9 + }, [props?.items]);
10 + return <ValidateMemoization inputs={[props?.items]} output={data} />;
11 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md new
+63
@@ -0,0 +1,63 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
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
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/optional-member-expression-single.js new
+10
@@ -0,0 +1,10 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 +import {ValidateMemoization} from 'shared-runtime';
3 +function Component(props) {
4 + const data = useMemo(() => {
5 + const x = [];
6 + x.push(props?.items);
7 + return x;
8 + }, [props?.items]);
9 + return <ValidateMemoization inputs={[props?.items]} output={data} />;
10 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md new
+74
@@ -0,0 +1,74 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
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
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/optional-member-expression-with-conditional-optional.js new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 +import {ValidateMemoization} from 'shared-runtime';
3 +function Component(props) {
4 + const data = useMemo(() => {
5 + const x = [];
6 + x.push(props?.items);
7 + if (props.cond) {
8 + x.push(props?.items);
9 + }
10 + return x;
11 + }, [props?.items, props.cond]);
12 + return (
13 + <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 + );
15 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md new
+74
@@ -0,0 +1,74 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
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
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/optional-member-expression-with-conditional.js new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies
2 +import {ValidateMemoization} from 'shared-runtime';
3 +function Component(props) {
4 + const data = useMemo(() => {
5 + const x = [];
6 + x.push(props?.items);
7 + if (props.cond) {
8 + x.push(props.items);
9 + }
10 + return x;
11 + }, [props?.items, props.cond]);
12 + return (
13 + <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 + );
15 +}