@samitouri / QOS-React-1 / commits / cea84a41bc

validatePreserveExistingMemoizationGuarantees ensures compiler preserves subset of dependencies from source

validatePreserveExistingMemoizationGuarantees ensures compiler preserves subset of dependencies from source --- `validatePreserveExistingMemoizationGuarantees` previously checked - manual memoization dependencies and declarations (the returned value) do not "lose" memoization due to inferred mutations ``` function useFoo() { const y = {}; // bail out because we infer that y cannot be a dependency of x as its mutableRange // extends beyond const x = useMemo(() => maybeMutate(y), [y]); // similarly, bail out if we find that x or y are mutated here return x; } ``` - manual memoization deps and decls do not get deopted due to hook calls ``` function useBar() { const x = getArray(); useHook(); mutate(x); return useCallback(() => [x], [x]); } ``` This PR updates `validatePreserveExistingMemoizationGuarantees` with the following correctness conditions: *major change* All inferred dependencies of reactive scopes between `StartMemoize` and `StopMemoize` instructions (e.g. scopes containing manual memoization code) must either: 1. be produced from earlier within the same manual memoization block 2. exactly match an element of depslist from source This assumes that the source codebase mostly follows the `exhaustive-deps` lint rule, which ensures that deps lists are (1) simple expressions composing of reads from named identifiers + property loads and (2) exactly match deps usages in the useMemo/useCallback itself. --- Validated that this does not change source by running internally on ~50k files (no validation on `main`, no validation on this PR, and validation on this PR).

Mofei Zhang committed Mar 18, 2024 at 14:50 UTC cea84a41bc6f3bb0909cd25859d471b0a2a3181a
115 files changed +3875 -125
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+28 -1
@@ -632,14 +632,41 @@ export type Phi = {
632 type: Type;
633 };
634
635 +/**
636 + * Valid ManualMemoDependencies are always of the form
637 + * `sourceDeclaredVariable.a.b?.c`, since this is documented
638 + * and enforced by the `react-hooks/exhaustive-deps` rule.
639 + *
640 + * `root` must either reference a ValidatedIdentifier or a global
641 + * variable.
642 + */
643 +export type ManualMemoDependency = {
644 + root:
645 + | {
646 + kind: "NamedLocal";
647 + value: Place;
648 + }
649 + | { kind: "Global"; identifierName: string };
650 + path: Array<string>;
651 +};
652 +
653 export type StartMemoize = {
654 kind: "StartMemoize";
637 - deps: Array<Place>;
655 + // Start/FinishMemoize markers should have matching ids
656 + manualMemoId: number;
657 + /**
658 + * deps-list from source code, or null if one was not provided
659 + * (e.g. useMemo without a second arg)
660 + */
661 + deps: Array<ManualMemoDependency> | null;
662 loc: SourceLocation;
663 };
664 export type FinishMemoize = {
665 kind: "FinishMemoize";
666 + // Start/FinishMemoize markers should have matching ids
667 + manualMemoId: number;
668 decl: Place;
669 + pruned?: true;
670 loc: SourceLocation;
671 };
672
compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+24 -3
@@ -19,6 +19,7 @@ import type {
19 Instruction,
20 InstructionValue,
21 LValue,
22 + ManualMemoDependency,
23 MutableRange,
24 ObjectMethod,
25 ObjectPropertyKey,
@@ -601,9 +602,10 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
602 break;
603 }
604 case "StartMemoize": {
604 - value = `StartMemoize deps=${instrValue.deps.map((dep) =>
605 - printPlace(dep)
606 - )}`;
605 + value = `StartMemoize deps=${
606 + instrValue.deps?.map((dep) => printManualMemoDependency(dep, false)) ??
607 + "(none)"
608 + }`;
609 break;
610 }
611 case "FinishMemoize": {
@@ -744,6 +746,25 @@ function printScope(scope: ReactiveScope | null): string {
746 return `${scope !== null ? `_@${scope.id}` : ""}`;
747 }
748
749 +export function printManualMemoDependency(
750 + val: ManualMemoDependency,
751 + nameOnly: boolean
752 +): string {
753 + let rootStr;
754 + if (val.root.kind === "Global") {
755 + rootStr = val.root.identifierName;
756 + } else {
757 + CompilerError.invariant(val.root.value.identifier.name?.kind === "named", {
758 + reason: "DepsValidation: expected named local variable in depslist",
759 + suggestions: null,
760 + loc: val.root.value.loc,
761 + });
762 + rootStr = nameOnly
763 + ? val.root.value.identifier.name.value
764 + : printIdentifier(val.root.value.identifier);
765 + }
766 + return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`;
767 +}
768 export function printType(type: Type): string {
769 if (type.kind === "Type") return "";
770 // TODO(mofeiZ): add debugName for generated ids
compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts
+12 -4
@@ -217,8 +217,12 @@ export function* eachInstructionValueOperand(
217 break;
218 }
219 case "StartMemoize": {
220 - for (const dep of instrValue.deps) {
221 - yield dep;
220 + if (instrValue.deps != null) {
221 + for (const dep of instrValue.deps) {
222 + if (dep.root.kind === "NamedLocal") {
223 + yield dep.root.value;
224 + }
225 + }
226 }
227 break;
228 }
@@ -528,8 +532,12 @@ export function mapInstructionValueOperands(
532 break;
533 }
534 case "StartMemoize": {
531 - for (let i = 0; i < instrValue.deps.length; i++) {
532 - instrValue.deps[i] = fn(instrValue.deps[i]);
535 + if (instrValue.deps != null) {
536 + for (const dep of instrValue.deps) {
537 + if (dep.root.kind === "NamedLocal") {
538 + dep.root.value = fn(dep.root.value);
539 + }
540 + }
541 }
542 break;
543 }
compiler/packages/babel-plugin-react-forget/src/Inference/DropManualMemoization.ts
+163 -39
@@ -16,8 +16,10 @@ import {
16 IdentifierId,
17 Instruction,
18 InstructionId,
19 + InstructionValue,
20 LoadGlobal,
21 LoadLocal,
22 + ManualMemoDependency,
23 MethodCall,
24 Place,
25 PropertyLoad,
@@ -28,7 +30,6 @@ import {
30 makeInstructionId,
31 } from "../HIR";
32 import { createTemporaryPlace, markInstructionIds } from "../HIR/HIRBuilder";
31 -import { eachInstructionValueOperand } from "../HIR/visitors";
33
34 type ManualMemoCallee = {
35 kind: "useMemo" | "useCallback";
@@ -39,14 +40,84 @@ type IdentifierSidemap = {
40 functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
41 manualMemos: Map<IdentifierId, ManualMemoCallee>;
42 react: Set<IdentifierId>;
43 + maybeDepsLists: Map<IdentifierId, Array<Place>>;
44 + maybeDeps: Map<IdentifierId, ManualMemoDependency>;
45 };
46
47 +/**
48 + * Collect loads from named variables and property reads from @value
49 + * into `maybeDeps`
50 + * Returns the variable + property reads represented by @instr
51 + */
52 +export function collectMaybeMemoDependencies(
53 + value: InstructionValue,
54 + maybeDeps: Map<IdentifierId, ManualMemoDependency>
55 +): ManualMemoDependency | null {
56 + switch (value.kind) {
57 + case "LoadGlobal": {
58 + return {
59 + root: {
60 + kind: "Global",
61 + identifierName: value.name,
62 + },
63 + path: [],
64 + };
65 + }
66 + case "PropertyLoad": {
67 + const object = maybeDeps.get(value.object.identifier.id);
68 + if (object != null) {
69 + return {
70 + root: object.root,
71 + path: [...object.path, value.property],
72 + };
73 + }
74 + break;
75 + }
76 +
77 + case "LoadLocal":
78 + case "LoadContext": {
79 + const source = maybeDeps.get(value.place.identifier.id);
80 + if (source != null) {
81 + return source;
82 + } else if (
83 + value.place.identifier.name != null &&
84 + value.place.identifier.name.kind === "named"
85 + ) {
86 + return {
87 + root: {
88 + kind: "NamedLocal",
89 + value: { ...value.place },
90 + },
91 + path: [],
92 + };
93 + }
94 + break;
95 + }
96 + case "StoreLocal": {
97 + /*
98 + * Value blocks rely on StoreLocal to populate their return value.
99 + * We need to track these as optional property chains are valid in
100 + * source depslists
101 + */
102 + const lvalue = value.lvalue.place.identifier;
103 + const rvalue = value.value.identifier.id;
104 + const aliased = maybeDeps.get(rvalue);
105 + if (aliased != null && lvalue.name?.kind !== "named") {
106 + maybeDeps.set(lvalue.id, aliased);
107 + return aliased;
108 + }
109 + break;
110 + }
111 + }
112 + return null;
113 +}
114 +
115 function collectTemporaries(
116 instr: Instruction,
117 env: Environment,
118 sidemap: IdentifierSidemap
119 ): void {
49 - const { value } = instr;
120 + const { value, lvalue } = instr;
121 switch (value.kind) {
122 case "FunctionExpression": {
123 sidemap.functions.set(
@@ -80,14 +151,29 @@ function collectTemporaries(
151 }
152 break;
153 }
154 + case "ArrayExpression": {
155 + if (value.elements.every((e) => e.kind === "Identifier")) {
156 + sidemap.maybeDepsLists.set(
157 + instr.lvalue.identifier.id,
158 + value.elements as Array<Place>
159 + );
160 + }
161 + break;
162 + }
163 + }
164 + const maybeDep = collectMaybeMemoDependencies(value, sidemap.maybeDeps);
165 + // We don't expect named lvalues during this pass (unlike ValidatePreservingManualMemo)
166 + if (maybeDep != null) {
167 + sidemap.maybeDeps.set(lvalue.identifier.id, maybeDep);
168 }
169 }
170
171 function makeManualMemoizationMarkers(
172 fnExpr: Place,
173 env: Environment,
89 - depsList: Array<Place>,
90 - memoDecl: Place
174 + depsList: Array<ManualMemoDependency> | null,
175 + memoDecl: Place,
176 + manualMemoId: number
177 ): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
178 return [
179 {
@@ -95,6 +181,7 @@ function makeManualMemoizationMarkers(
181 lvalue: createTemporaryPlace(env),
182 value: {
183 kind: "StartMemoize",
184 + manualMemoId,
185 /*
186 * Use deps list from source instead of inferred deps
187 * as dependencies
@@ -109,6 +196,7 @@ function makeManualMemoizationMarkers(
196 lvalue: createTemporaryPlace(env),
197 value: {
198 kind: "FinishMemoize",
199 + manualMemoId,
200 decl: { ...memoDecl },
201 loc: fnExpr.loc,
202 },
@@ -183,11 +271,13 @@ function getManualMemoizationReplacement(
271
272 function extractManualMemoizationArgs(
273 instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
186 - kind: "useCallback" | "useMemo"
274 + kind: "useCallback" | "useMemo",
275 + sidemap: IdentifierSidemap
276 ): {
277 fnPlace: Place;
278 + depsList: Array<ManualMemoDependency> | null;
279 } {
190 - const [fnPlace] = instr.value.args as Array<
280 + const [fnPlace, depsListPlace] = instr.value.args as Array<
281 Place | SpreadPattern | undefined
282 >;
283 if (fnPlace == null) {
@@ -197,15 +287,40 @@ function extractManualMemoizationArgs(
287 suggestions: null,
288 });
289 }
200 - if (fnPlace?.kind !== "Identifier") {
290 + if (fnPlace?.kind !== "Identifier" || depsListPlace?.kind === "Spread") {
291 CompilerError.throwInvalidReact({
292 reason: `Unexpected arguments to ${kind} call`,
293 loc: instr.value.loc,
294 suggestions: null,
295 });
296 }
297 + let depsList: Array<ManualMemoDependency> | null = null;
298 + if (depsListPlace != null) {
299 + const maybeDepsList = sidemap.maybeDepsLists.get(
300 + depsListPlace.identifier.id
301 + );
302 + if (maybeDepsList == null) {
303 + CompilerError.throwInvalidReact({
304 + reason: `Expected the dependency list for ${kind} to be an array literal without rest spreads`,
305 + suggestions: null,
306 + loc: depsListPlace.loc,
307 + });
308 + }
309 + depsList = maybeDepsList.map((dep) => {
310 + const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
311 + if (maybeDep == null) {
312 + CompilerError.throwInvalidReact({
313 + reason: `Expected the dependency list for ${kind} to be an array of simple expressions`,
314 + suggestions: null,
315 + loc: dep.loc,
316 + });
317 + }
318 + return maybeDep;
319 + });
320 + }
321 return {
322 fnPlace,
323 + depsList,
324 };
325 }
326
@@ -225,7 +340,10 @@ export function dropManualMemoization(func: HIRFunction): void {
340 functions: new Map(),
341 manualMemos: new Map(),
342 react: new Set(),
343 + maybeDeps: new Map(),
344 + maybeDepsLists: new Map(),
345 };
346 + let nextManualMemoId = 0;
347
348 /**
349 * Phase 1:
@@ -238,10 +356,7 @@ export function dropManualMemoization(func: HIRFunction): void {
356 */
357 const queuedInserts: Map<
358 InstructionId,
241 - {
242 - kind: "before" | "after";
243 - value: TInstruction<StartMemoize> | TInstruction<FinishMemoize>;
244 - }
359 + TInstruction<StartMemoize> | TInstruction<FinishMemoize>
360 > = new Map();
361 for (const [_, block] of func.body.blocks) {
362 for (let i = 0; i < block.instructions.length; i++) {
@@ -257,9 +372,10 @@ export function dropManualMemoization(func: HIRFunction): void {
372
373 const manualMemo = sidemap.manualMemos.get(id);
374 if (manualMemo != null) {
260 - const { fnPlace } = extractManualMemoizationArgs(
375 + const { fnPlace, depsList } = extractManualMemoizationArgs(
376 instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
262 - manualMemo.kind
377 + manualMemo.kind,
378 + sidemap
379 );
380 instr.value = getManualMemoizationReplacement(
381 fnPlace,
@@ -267,11 +383,22 @@ export function dropManualMemoization(func: HIRFunction): void {
383 manualMemo.kind
384 );
385 if (isValidationEnabled) {
270 - const inlineMemoFn = sidemap.functions.get(fnPlace.identifier.id);
271 - if (inlineMemoFn == null) {
386 + /**
387 + * Explicitly bail out when we encounter manual memoization
388 + * without inline instructions, as our current validation
389 + * assumes that source depslists closely match inferred deps
390 + * due to the `exhaustive-deps` lint rule (which only provides
391 + * diagnostics for inline memo functions)
392 + * ```js
393 + * useMemo(opaqueFn, [dep1, dep2]);
394 + * ```
395 + * While we could handle this by diffing reactive scope deps
396 + * of the opaque arg against the source depslist, this pattern
397 + * is rare and likely sketchy.
398 + */
399 + if (!sidemap.functions.has(fnPlace.identifier.id)) {
400 CompilerError.throwInvalidReact({
273 - reason:
274 - "DepsValidation: Expected function literal as manual memoization callback",
401 + reason: `Expected the first argument of ${manualMemo.kind} to be an inline function expression`,
402 suggestions: [],
403 loc: fnPlace.loc,
404 });
@@ -290,24 +417,26 @@ export function dropManualMemoization(func: HIRFunction): void {
417 const [startMarker, finishMarker] = makeManualMemoizationMarkers(
418 fnPlace,
419 func.env,
293 - // Next PR will replace this with depslist from source
294 - [...eachInstructionValueOperand(inlineMemoFn.value)],
295 - memoDecl
420 + depsList,
421 + memoDecl,
422 + nextManualMemoId++
423 );
424
298 - /*
299 - * This PR reorders startMarker to right before the inlineMemoFn
300 - * since startMarker references inlineMemoFn.deps.
301 - * Next PR will move startMarker earlier, to after the `useMemo`/
302 - * `useCallback` load itself (as it also changes startMarker to
303 - * not reference lowered deps anymore).
425 + /**
426 + * Insert StartMarker right after the `useMemo`/`useCallback` load to
427 + * ensure all temporaries created when lowering the inline fn expression
428 + * are included.
429 + * e.g.
430 + * ```
431 + * 0: LoadGlobal useMemo
432 + * 1: StartMarker deps=[var]
433 + * 2: t0 = LoadContext [var]
434 + * 3: function deps=t0
435 + * ...
436 + * ```
437 */
305 - queuedInserts.set(inlineMemoFn.id, {
306 - kind: "before",
307 - value: startMarker,
308 - });
309 - queuedInserts.set(instr.id, { kind: "after", value: finishMarker });
310 - continue;
438 + queuedInserts.set(manualMemo.loadInstr.id, startMarker);
439 + queuedInserts.set(instr.id, finishMarker);
440 }
441 }
442 } else {
@@ -328,13 +457,8 @@ export function dropManualMemoization(func: HIRFunction): void {
457 const insertInstr = queuedInserts.get(instr.id);
458 if (insertInstr != null) {
459 nextInstructions = nextInstructions ?? block.instructions.slice(0, i);
331 - if (insertInstr.kind === "before") {
332 - nextInstructions.push(insertInstr.value);
333 - nextInstructions.push(instr);
334 - } else {
335 - nextInstructions.push(instr);
336 - nextInstructions.push(insertInstr.value);
337 - }
460 + nextInstructions.push(instr);
461 + nextInstructions.push(insertInstr);
462 } else if (nextInstructions != null) {
463 nextInstructions.push(instr);
464 }
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts
+1 -1
@@ -83,7 +83,7 @@ export function writeReactiveBlock(
83 writer.writeLine("}");
84 }
85
86 -function printDependency(dependency: ReactiveScopeDependency): string {
86 +export function printDependency(dependency: ReactiveScopeDependency): string {
87 const identifier =
88 printIdentifier(dependency.identifier) +
89 printType(dependency.identifier.type);
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+1 -1
@@ -933,7 +933,7 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
933 identifier.scope !== null &&
934 this.prunedScopes.has(identifier.scope.id)
935 ) {
936 - return { kind: "remove" };
936 + instruction.value.pruned = true;
937 }
938 }
939
compiler/packages/babel-plugin-react-forget/src/Validation/ValidatePreservedManualMemoization.ts
+287 -15
@@ -5,16 +5,25 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { CompilerError, ErrorSeverity } from "..";
8 +import { CompilerError, Effect, ErrorSeverity } from "..";
9 import {
10 + GeneratedSource,
11 Identifier,
12 + IdentifierId,
13 Instruction,
14 + InstructionValue,
15 + ManualMemoDependency,
16 + Place,
17 ReactiveFunction,
18 ReactiveInstruction,
19 ReactiveScopeBlock,
20 + ReactiveScopeDependency,
21 + ReactiveValue,
22 ScopeId,
23 } from "../HIR";
24 +import { printManualMemoDependency } from "../HIR/PrintHIR";
25 import { eachInstructionValueOperand } from "../HIR/visitors";
26 +import { collectMaybeMemoDependencies } from "../Inference/DropManualMemoization";
27 import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables";
28 import {
29 ReactiveFunctionVisitor,
@@ -29,22 +38,250 @@ import {
38 * was pruned.
39 */
40 export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
32 - const errors = new CompilerError();
33 - visitReactiveFunction(fn, new Visitor(), errors);
34 - if (errors.hasErrors()) {
35 - throw errors;
41 + const state = {
42 + errors: new CompilerError(),
43 + manualMemoState: null,
44 + };
45 + visitReactiveFunction(fn, new Visitor(), state);
46 + if (state.errors.hasErrors()) {
47 + throw state.errors;
48 }
49 }
50
39 -class Visitor extends ReactiveFunctionVisitor<CompilerError> {
51 +type ManualMemoBlockState = {
52 + /**
53 + * Values produced within manual memoization blocks.
54 + * We track these to ensure our inferred dependencies are
55 + * produced before the manual memo block starts
56 + *
57 + * As an example:
58 + * ```js
59 + * // source
60 + * const result = useMemo(() => {
61 + * return [makeObject(input1), input2],
62 + * }, [input1, input2]);
63 + * ```
64 + * Here, we record inferred dependencies as [input1, input2]
65 + * but not t0
66 + * ```js
67 + * // StartMemoize
68 + * let t0;
69 + * if ($[0] != input1) {
70 + * t0 = makeObject(input1);
71 + * // ...
72 + * } else { ... }
73 + *
74 + * let result;
75 + * if ($[1] != t0 || $[2] != input2) {
76 + * result = [t0, input2];
77 + * } else { ... }
78 + * ```
79 + */
80 + decls: Set<IdentifierId>;
81 +
82 + /*
83 + * normalized depslist from useMemo/useCallback
84 + * callsite in source
85 + */
86 + depsFromSource: Array<ManualMemoDependency> | null;
87 + manualMemoId: number;
88 +};
89 +
90 +type VisitorState = {
91 + errors: CompilerError;
92 + manualMemoState: ManualMemoBlockState | null;
93 +};
94 +
95 +function prettyPrintScopeDependency(val: ReactiveScopeDependency): string {
96 + let rootStr;
97 + if (val.identifier.name?.kind === "named") {
98 + rootStr = val.identifier.name.value;
99 + } else {
100 + rootStr = "[unnamed]";
101 + }
102 + return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`;
103 +}
104 +function depsEqual(
105 + dep1: ManualMemoDependency,
106 + dep2: ManualMemoDependency
107 +): boolean {
108 + const rootsEqual =
109 + (dep1.root.kind === "Global" &&
110 + dep2.root.kind === "Global" &&
111 + dep1.root.identifierName === dep2.root.identifierName) ||
112 + (dep1.root.kind === "NamedLocal" &&
113 + dep2.root.kind === "NamedLocal" &&
114 + dep1.root.value.identifier.id === dep2.root.value.identifier.id);
115 + return (
116 + rootsEqual &&
117 + dep1.path.length === dep2.path.length &&
118 + dep1.path.every((val, idx) => val === dep2.path[idx])
119 + );
120 +}
121 +
122 +function validateInferredDep(
123 + dep: ReactiveScopeDependency,
124 + temporaries: Map<IdentifierId, ManualMemoDependency>,
125 + declsWithinMemoBlock: Set<IdentifierId>,
126 + validDepsInMemoBlock: Array<ManualMemoDependency>,
127 + errorState: CompilerError
128 +): void {
129 + let normalizedDep: ManualMemoDependency;
130 + const maybeNormalizedRoot = temporaries.get(dep.identifier.id);
131 + if (maybeNormalizedRoot != null) {
132 + normalizedDep = {
133 + root: maybeNormalizedRoot.root,
134 + path: [...maybeNormalizedRoot.path, ...dep.path],
135 + };
136 + } else {
137 + CompilerError.invariant(dep.identifier.name?.kind === "named", {
138 + reason:
139 + "ValidatePreservedManualMemoization: expected scope dependency to be named",
140 + loc: GeneratedSource,
141 + suggestions: null,
142 + });
143 + normalizedDep = {
144 + root: {
145 + kind: "NamedLocal",
146 + value: {
147 + kind: "Identifier",
148 + identifier: dep.identifier,
149 + loc: GeneratedSource,
150 + effect: Effect.Read,
151 + reactive: false,
152 + },
153 + },
154 + path: [...dep.path],
155 + };
156 + }
157 + for (const originalDep of validDepsInMemoBlock) {
158 + if (depsEqual(originalDep, normalizedDep)) {
159 + return;
160 + }
161 + }
162 + for (const decl of declsWithinMemoBlock) {
163 + const normalizedDecl = temporaries.get(decl);
164 + if (normalizedDecl != null && depsEqual(normalizedDecl, normalizedDep)) {
165 + return;
166 + } else if (
167 + normalizedDep.root.kind === "NamedLocal" &&
168 + decl === normalizedDep.root.value.identifier.id
169 + ) {
170 + return;
171 + }
172 + }
173 + errorState.push({
174 + severity: ErrorSeverity.Todo,
175 + reason:
176 + "Could not preserve manual memoization because an inferred dependency does not match the dependency list in source",
177 + description: `The inferred dependency was \`${prettyPrintScopeDependency(
178 + dep
179 + )}\`, but the source dependencies were [${validDepsInMemoBlock
180 + .map((dep) => printManualMemoDependency(dep, true))
181 + .join(", ")}]`,
182 + loc: GeneratedSource,
183 + suggestions: null,
184 + });
185 +}
186 +
187 +class Visitor extends ReactiveFunctionVisitor<VisitorState> {
188 scopes: Set<ScopeId> = new Set();
189 + scopeMapping = new Map();
190 + temporaries: Map<IdentifierId, ManualMemoDependency> = new Map();
191 +
192 + collectMaybeMemoDependencies(
193 + value: ReactiveValue,
194 + state: VisitorState
195 + ): ManualMemoDependency | null {
196 + switch (value.kind) {
197 + case "SequenceExpression": {
198 + for (const instr of value.instructions) {
199 + this.visitInstruction(instr, state);
200 + }
201 + const result = this.collectMaybeMemoDependencies(value.value, state);
202 +
203 + return result;
204 + }
205 + case "OptionalExpression": {
206 + return this.collectMaybeMemoDependencies(value.value, state);
207 + }
208 + case "ReactiveFunctionValue":
209 + case "ConditionalExpression":
210 + case "LogicalExpression": {
211 + return null;
212 + }
213 + default: {
214 + const dep = collectMaybeMemoDependencies(value, this.temporaries);
215 + if (value.kind === "StoreLocal" || value.kind === "StoreContext") {
216 + const storeTarget = value.lvalue.place;
217 + state.manualMemoState?.decls.add(storeTarget.identifier.id);
218 + if (storeTarget.identifier.name?.kind === "named" && dep == null) {
219 + const dep: ManualMemoDependency = {
220 + root: {
221 + kind: "NamedLocal",
222 + value: storeTarget,
223 + },
224 + path: [],
225 + };
226 + this.temporaries.set(storeTarget.identifier.id, dep);
227 + return dep;
228 + }
229 + }
230 + return dep;
231 + }
232 + }
233 + }
234 +
235 + recordTemporaries(instr: ReactiveInstruction, state: VisitorState): void {
236 + const temporaries = this.temporaries;
237 + const { value } = instr;
238 + const lvalId = instr.lvalue?.identifier.id;
239 + if (lvalId != null && temporaries.has(lvalId)) {
240 + return;
241 + }
242 + const isNamedLocal =
243 + lvalId != null && instr.lvalue?.identifier.name?.kind === "named";
244 + if (isNamedLocal && state.manualMemoState != null) {
245 + state.manualMemoState.decls.add(lvalId);
246 + }
247 +
248 + const maybeDep = this.collectMaybeMemoDependencies(value, state);
249 + if (lvalId != null) {
250 + if (maybeDep != null) {
251 + temporaries.set(lvalId, maybeDep);
252 + } else if (isNamedLocal) {
253 + temporaries.set(lvalId, {
254 + root: {
255 + kind: "NamedLocal",
256 + value: { ...(instr.lvalue as Place) },
257 + },
258 + path: [],
259 + });
260 + }
261 + }
262 + }
263
264 override visitScope(
265 scopeBlock: ReactiveScopeBlock,
44 - state: CompilerError
266 + state: VisitorState
267 ): void {
268 this.traverseScope(scopeBlock, state);
269
270 + if (
271 + state.manualMemoState != null &&
272 + state.manualMemoState.depsFromSource != null
273 + ) {
274 + for (const dep of scopeBlock.scope.dependencies) {
275 + validateInferredDep(
276 + dep,
277 + this.temporaries,
278 + state.manualMemoState.decls,
279 + state.manualMemoState.depsFromSource,
280 + state.errors
281 + );
282 + }
283 + }
284 +
285 /*
286 * Record scopes that exist in the AST so we can later check to see if
287 * effect dependencies which should be memoized (have a scope assigned)
@@ -69,19 +306,54 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
306
307 override visitInstruction(
308 instruction: ReactiveInstruction,
72 - state: CompilerError
309 + state: VisitorState
310 ): void {
311 this.traverseInstruction(instruction, state);
75 - if (
76 - instruction.value.kind === "StartMemoize" ||
77 - instruction.value.kind === "FinishMemoize"
78 - ) {
79 - for (const value of eachInstructionValueOperand(instruction.value)) {
312 + this.recordTemporaries(instruction, state);
313 + if (instruction.value.kind === "StartMemoize") {
314 + let depsFromSource: Array<ManualMemoDependency> | null = null;
315 + if (instruction.value.deps != null) {
316 + depsFromSource = instruction.value.deps;
317 + }
318 + CompilerError.invariant(state.manualMemoState == null, {
319 + reason: "Unexpected nested StartMemoize instructions",
320 + description: `Bad manual memoization ids: ${state.manualMemoState?.manualMemoId}, ${instruction.value.manualMemoId}`,
321 + loc: instruction.value.loc,
322 + suggestions: null,
323 + });
324 +
325 + state.manualMemoState = {
326 + decls: new Set(),
327 + depsFromSource,
328 + manualMemoId: instruction.value.manualMemoId,
329 + };
330 + }
331 + if (instruction.value.kind === "FinishMemoize") {
332 + CompilerError.invariant(
333 + state.manualMemoState != null &&
334 + state.manualMemoState.manualMemoId === instruction.value.manualMemoId,
335 + {
336 + reason: "Unexpected mismatch between StartMemoize and FinishMemoize",
337 + description: `Encountered StartMemoize id=${state.manualMemoState?.manualMemoId} followed by FinishMemoize id=${instruction.value.manualMemoId}`,
338 + loc: instruction.value.loc,
339 + suggestions: null,
340 + }
341 + );
342 + state.manualMemoState = null;
343 + }
344 +
345 + const isDep = instruction.value.kind === "StartMemoize";
346 + const isDecl =
347 + instruction.value.kind === "FinishMemoize" && !instruction.value.pruned;
348 + if (isDep || isDecl) {
349 + for (const value of eachInstructionValueOperand(
350 + instruction.value as InstructionValue
351 + )) {
352 if (
353 isMutable(instruction as Instruction, value) ||
82 - isUnmemoized(value.identifier, this.scopes)
354 + (isDecl && isUnmemoized(value.identifier, this.scopes))
355 ) {
84 - state.push({
356 + state.errors.push({
357 reason:
358 "This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized",
359 description: null,
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.expect.md deleted
-53
@@ -1,53 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function App({ text, hasDeps }) {
6 - const resolvedText = useMemo(
7 - () => {
8 - return text.toUpperCase();
9 - },
10 - hasDeps ? null : [text] // should be DCE'd
11 - );
12 - return resolvedText;
13 -}
14 -
15 -export const FIXTURE_ENTRYPOINT = {
16 - fn: App,
17 - params: ["TodoAdd"],
18 - isComponent: "TodoAdd",
19 -};
20 -
21 -```
22 -
23 -## Code
24 -
25 -```javascript
26 -import { unstable_useMemoCache as useMemoCache } from "react";
27 -function App(t0) {
28 - const $ = useMemoCache(2);
29 - const { text, hasDeps } = t0;
30 -
31 - hasDeps ? null : [text];
32 - let t1;
33 - let t2;
34 - if ($[0] !== text) {
35 - t2 = text.toUpperCase();
36 - $[0] = text;
37 - $[1] = t2;
38 - } else {
39 - t2 = $[1];
40 - }
41 - t1 = t2;
42 - const resolvedText = t1;
43 - return resolvedText;
44 -}
45 -
46 -export const FIXTURE_ENTRYPOINT = {
47 - fn: App,
48 - params: ["TodoAdd"],
49 - isComponent: "TodoAdd",
50 -};
51 -
52 -```
53 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md
-2
@@ -46,8 +46,6 @@ export const FIXTURE_ENTRYPOINT = {
46 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47 > 11 | });
48 | ^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11)
49 -
50 -[ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11)
49 12 |
50 13 | // The ref is modified later, extending its range and preventing memoization of onChange
51 14 | const reset = () => {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md
-2
@@ -43,8 +43,6 @@ export const FIXTURE_ENTRYPOINT = {
43 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44 > 11 | });
45 | ^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11)
46 -
47 -[ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11)
46 12 |
47 13 | // The ref is modified later, extending its range and preventing memoization of onChange
48 14 | ref.current.inner = null;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useMemo } from "react";
6 +
7 +// react-hooks-deps would error on this code (complex expression in depslist),
8 +// so Forget could bailout here
9 +function App({ text, hasDeps }) {
10 + const resolvedText = useMemo(
11 + () => {
12 + return text.toUpperCase();
13 + },
14 + hasDeps ? null : [text] // should be DCE'd
15 + );
16 + return resolvedText;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: App,
21 + params: ["TodoAdd"],
22 + isComponent: "TodoAdd",
23 +};
24 +
25 +```
26 +
27 +
28 +## Error
29 +
30 +```
31 + 8 | return text.toUpperCase();
32 + 9 | },
33 +> 10 | hasDeps ? null : [text] // should be DCE'd
34 + | ^^^^^^^^^^^^^^^^^^^^^^^ [ReactForget] InvalidReact: Expected the dependency list for useMemo to be an array literal without rest spreads (10:10)
35 + 11 | );
36 + 12 | return resolvedText;
37 + 13 | }
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.ts renamed
+4
@@ -1,3 +1,7 @@
1 +import { useMemo } from "react";
2 +
3 +// react-hooks-deps would error on this code (complex expression in depslist),
4 +// so Forget could bailout here
5 function App({ text, hasDeps }) {
6 const resolvedText = useMemo(
7 () => {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.expect.md new
+28
@@ -0,0 +1,28 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useCallback } from "react";
8 +
9 +// False positive as more specific memoization always results
10 +// in fewer memo block executions.
11 +// Precisely:
12 +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
13 +// x.y.z_new != x.y.z_prev does imply x_new != x_prev
14 +// One fix would be to depend on optional chains
15 +function useHook(x) {
16 + return useCallback(() => [x.y.z], [x]);
17 +}
18 +
19 +```
20 +
21 +
22 +## Error
23 +
24 +```
25 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x.y.z`, but the source dependencies were [x]
26 +```
27 +
28 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.ts new
+13
@@ -0,0 +1,13 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useCallback } from "react";
4 +
5 +// False positive as more specific memoization always results
6 +// in fewer memo block executions.
7 +// Precisely:
8 +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
9 +// x.y.z_new != x.y.z_prev does imply x_new != x_prev
10 +// One fix would be to depend on optional chains
11 +function useHook(x) {
12 + return useCallback(() => [x.y.z], [x]);
13 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.expect.md new
+27
@@ -0,0 +1,27 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +// False positive as more specific memoization always results
10 +// in fewer memo block executions.
11 +// Precisely:
12 +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
13 +// x.y.z_new != x.y.z_prev does imply x_new != x_prev
14 +function useHook(x) {
15 + return useMemo(() => [x.y.z], [x]);
16 +}
17 +
18 +```
19 +
20 +
21 +## Error
22 +
23 +```
24 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x.y.z`, but the source dependencies were [x]
25 +```
26 +
27 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.ts new
+12
@@ -0,0 +1,12 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +// False positive as more specific memoization always results
6 +// in fewer memo block executions.
7 +// Precisely:
8 +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
9 +// x.y.z_new != x.y.z_prev does imply x_new != x_prev
10 +function useHook(x) {
11 + return useMemo(() => [x.y.z], [x]);
12 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.expect.md new
+45
@@ -0,0 +1,45 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { identity } from "shared-runtime";
8 +
9 +// This is a false positive as Forget's inferred memoization
10 +// invalidates strictly less than source. We currently do not
11 +// track transitive deps / invalidations of manual memo deps
12 +// because of implementation complexity
13 +function useFoo() {
14 + const val = [1, 2, 3];
15 +
16 + return useMemo(() => {
17 + return identity(val);
18 + }, [val]);
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: useFoo,
23 + params: [],
24 +};
25 +
26 +```
27 +
28 +
29 +## Error
30 +
31 +```
32 + 10 | const val = [1, 2, 3];
33 + 11 |
34 +> 12 | return useMemo(() => {
35 + | ^^^^^^^
36 +> 13 | return identity(val);
37 + | ^^^^^^^^^^^^^^^^^^^^^^^^^
38 +> 14 | }, [val]);
39 + | ^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (12:14)
40 + 15 | }
41 + 16 |
42 + 17 | export const FIXTURE_ENTRYPOINT = {
43 +```
44 +
45 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.ts new
+20
@@ -0,0 +1,20 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { identity } from "shared-runtime";
4 +
5 +// This is a false positive as Forget's inferred memoization
6 +// invalidates strictly less than source. We currently do not
7 +// track transitive deps / invalidations of manual memo deps
8 +// because of implementation complexity
9 +function useFoo() {
10 + const val = [1, 2, 3];
11 +
12 + return useMemo(() => {
13 + return identity(val);
14 + }, [val]);
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useFoo,
19 + params: [],
20 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.expect.md new
+44
@@ -0,0 +1,44 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useCallback } from "react";
8 +import { makeArray } from "shared-runtime";
9 +
10 +// This case is already unsound in source, so we can safely bailout
11 +function Foo(props) {
12 + let x = [];
13 + x.push(props);
14 +
15 + // makeArray() is captured, but depsList contains [props]
16 + const cb = useCallback(() => [x], [x]);
17 +
18 + x = makeArray();
19 +
20 + return cb;
21 +}
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Foo,
24 + params: [{}],
25 +};
26 +
27 +```
28 +
29 +
30 +## Error
31 +
32 +```
33 + 10 |
34 + 11 | // makeArray() is captured, but depsList contains [props]
35 +> 12 | const cb = useCallback(() => [x], [x]);
36 + | ^^^^^^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (12:12)
37 +
38 +[ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (12:12)
39 + 13 |
40 + 14 | x = makeArray();
41 + 15 |
42 +```
43 +
44 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.ts new
+21
@@ -0,0 +1,21 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useCallback } from "react";
4 +import { makeArray } from "shared-runtime";
5 +
6 +// This case is already unsound in source, so we can safely bailout
7 +function Foo(props) {
8 + let x = [];
9 + x.push(props);
10 +
11 + // makeArray() is captured, but depsList contains [props]
12 + const cb = useCallback(() => [x], [x]);
13 +
14 + x = makeArray();
15 +
16 + return cb;
17 +}
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Foo,
20 + params: [{}],
21 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +
8 +function useHook(maybeRef) {
9 + return useCallback(() => {
10 + return [maybeRef.current];
11 + }, [maybeRef]);
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `maybeRef.current`, but the source dependencies were [maybeRef]
21 +```
22 +
23 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.ts new
+8
@@ -0,0 +1,8 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +
4 +function useHook(maybeRef) {
5 + return useCallback(() => {
6 + return [maybeRef.current];
7 + }, [maybeRef]);
8 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +
8 +function useHook(maybeRef, shouldRead) {
9 + return useMemo(() => {
10 + return () => [maybeRef.current];
11 + }, [shouldRead, maybeRef]);
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `maybeRef.current`, but the source dependencies were [shouldRead, maybeRef]
21 +```
22 +
23 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.ts new
+8
@@ -0,0 +1,8 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +
4 +function useHook(maybeRef, shouldRead) {
5 + return useMemo(() => {
6 + return () => [maybeRef.current];
7 + }, [shouldRead, maybeRef]);
8 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useCallback } from "react";
8 +
9 +// False positive:
10 +// We currently bail out on this because we don't understand
11 +// that `() => [x]` gets pruned because `x` always invalidates.
12 +function useFoo(props) {
13 + const x = [];
14 + useHook();
15 + x.push(props);
16 +
17 + return useCallback(() => [x], [x]);
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{}],
23 +};
24 +
25 +```
26 +
27 +
28 +## Error
29 +
30 +```
31 + 11 | x.push(props);
32 + 12 |
33 +> 13 | return useCallback(() => [x], [x]);
34 + | ^^^^^^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (13:13)
35 + 14 | }
36 + 15 |
37 + 16 | export const FIXTURE_ENTRYPOINT = {
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.ts new
+19
@@ -0,0 +1,19 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useCallback } from "react";
4 +
5 +// False positive:
6 +// We currently bail out on this because we don't understand
7 +// that `() => [x]` gets pruned because `x` always invalidates.
8 +function useFoo(props) {
9 + const x = [];
10 + useHook();
11 + x.push(props);
12 +
13 + return useCallback(() => [x], [x]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{}],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.expect.md new
+27
@@ -0,0 +1,27 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +// This is technically a false positive, but source is already breaking
8 +// `exhaustive-deps` lint rule (and can be considered invalid).
9 +function useHook(x) {
10 + const aliasedX = x;
11 + const aliasedProp = x.y.z;
12 +
13 + return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]);
14 +}
15 +
16 +```
17 +
18 +
19 +## Error
20 +
21 +```
22 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `aliasedX`, but the source dependencies were [x, aliasedProp]
23 +
24 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x.y.z`, but the source dependencies were [x, aliasedProp]
25 +```
26 +
27 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.ts new
+10
@@ -0,0 +1,10 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +// This is technically a false positive, but source is already breaking
4 +// `exhaustive-deps` lint rule (and can be considered invalid).
5 +function useHook(x) {
6 + const aliasedX = x;
7 + const aliasedProp = x.y.z;
8 +
9 + return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]);
10 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md new
+31
@@ -0,0 +1,31 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +
8 +function Component({ propA, propB }) {
9 + return useCallback(() => {
10 + return {
11 + value: propB?.x.y,
12 + other: propA,
13 + };
14 + }, [propA, propB.x.y]);
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{ propA: 2, propB: { x: { y: [] } } }],
20 +};
21 +
22 +```
23 +
24 +
25 +## Error
26 +
27 +```
28 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propB`, but the source dependencies were [propA, propB.x.y]
29 +```
30 +
31 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.ts new
+16
@@ -0,0 +1,16 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +
4 +function Component({ propA, propB }) {
5 + return useCallback(() => {
6 + return {
7 + value: propB?.x.y,
8 + other: propA,
9 + };
10 + }, [propA, propB.x.y]);
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{ propA: 2, propB: { x: { y: [] } } }],
16 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.expect.md new
+30
@@ -0,0 +1,30 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +import { mutate } from "shared-runtime";
8 +
9 +function Component({ propA, propB }) {
10 + return useCallback(() => {
11 + const x = {};
12 + if (propA?.a) {
13 + mutate(x);
14 + return {
15 + value: propB.x.y,
16 + };
17 + }
18 + }, [propA?.a, propB.x.y]);
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]
28 +```
29 +
30 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.ts new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +import { mutate } from "shared-runtime";
4 +
5 +function Component({ propA, propB }) {
6 + return useCallback(() => {
7 + const x = {};
8 + if (propA?.a) {
9 + mutate(x);
10 + return {
11 + value: propB.x.y,
12 + };
13 + }
14 + }, [propA?.a, propB.x.y]);
15 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +
8 +function Component({ propA }) {
9 + return useCallback(() => {
10 + return propA.x();
11 + }, [propA.x]);
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propA`, but the source dependencies were [propA.x]
21 +```
22 +
23 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.ts new
+8
@@ -0,0 +1,8 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +
4 +function Component({ propA }) {
5 + return useCallback(() => {
6 + return propA.x();
7 + }, [propA.x]);
8 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.expect.md new
+25
@@ -0,0 +1,25 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +// This is technically a false positive, but source is already breaking
8 +// `exhaustive-deps` lint rule (and can be considered invalid).
9 +function useHook(x) {
10 + const aliasedX = x;
11 + const aliasedProp = x.y.z;
12 +
13 + return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]);
14 +}
15 +
16 +```
17 +
18 +
19 +## Error
20 +
21 +```
22 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x`, but the source dependencies were [aliasedX, aliasedProp]
23 +```
24 +
25 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.ts new
+10
@@ -0,0 +1,10 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +// This is technically a false positive, but source is already breaking
4 +// `exhaustive-deps` lint rule (and can be considered invalid).
5 +function useHook(x) {
6 + const aliasedX = x;
7 + const aliasedProp = x.y.z;
8 +
9 + return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]);
10 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +import { makeArray } from "shared-runtime";
9 +
10 +// We currently only recognize "hoistable" values (e.g. variable reads
11 +// and property loads from named variables) in the source depslist.
12 +// This makes validation logic simpler and follows the same constraints
13 +// from the eslint react-hooks-deps plugin.
14 +function Foo(props) {
15 + const x = makeArray(props);
16 + // react-hooks-deps lint would already fail here
17 + return useMemo(() => [x[0]], [x[0]]);
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{ val: 1 }],
23 +};
24 +
25 +```
26 +
27 +
28 +## Error
29 +
30 +```
31 + 11 | const x = makeArray(props);
32 + 12 | // react-hooks-deps lint would already fail here
33 +> 13 | return useMemo(() => [x[0]], [x[0]]);
34 + | ^^^^ [ReactForget] InvalidReact: Expected the dependency list for useMemo to be an array of simple expressions (13:13)
35 + 14 | }
36 + 15 |
37 + 16 | export const FIXTURE_ENTRYPOINT = {
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.ts new
+19
@@ -0,0 +1,19 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +import { makeArray } from "shared-runtime";
5 +
6 +// We currently only recognize "hoistable" values (e.g. variable reads
7 +// and property loads from named variables) in the source depslist.
8 +// This makes validation logic simpler and follows the same constraints
9 +// from the eslint react-hooks-deps plugin.
10 +function Foo(props) {
11 + const x = makeArray(props);
12 + // react-hooks-deps lint would already fail here
13 + return useMemo(() => [x[0]], [x[0]]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Foo,
18 + params: [{ val: 1 }],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md new
+32
@@ -0,0 +1,32 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { mutate } from "shared-runtime";
8 +
9 +function Component({ propA, propB }) {
10 + return useMemo(() => {
11 + const x = {};
12 + if (propA?.a) {
13 + mutate(x);
14 + return {
15 + value: propB.x.y,
16 + };
17 + }
18 + }, [propA?.a, propB.x.y]);
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]
28 +
29 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propB`, but the source dependencies were [propA.a, propB.x.y]
30 +```
31 +
32 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.ts new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { mutate } from "shared-runtime";
4 +
5 +function Component({ propA, propB }) {
6 + return useMemo(() => {
7 + const x = {};
8 + if (propA?.a) {
9 + mutate(x);
10 + return {
11 + value: propB.x.y,
12 + };
13 + }
14 + }, [propA?.a, propB.x.y]);
15 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.expect.md new
+32
@@ -0,0 +1,32 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { identity, mutate } from "shared-runtime";
8 +
9 +function Component({ propA, propB }) {
10 + return useMemo(() => {
11 + const x = {};
12 + if (identity(null) ?? propA.a) {
13 + mutate(x);
14 + return {
15 + value: propB.x.y,
16 + };
17 + }
18 + }, [propA.a, propB.x.y]);
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]
28 +
29 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propB`, but the source dependencies were [propA.a, propB.x.y]
30 +```
31 +
32 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.ts new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { identity, mutate } from "shared-runtime";
4 +
5 +function Component({ propA, propB }) {
6 + return useMemo(() => {
7 + const x = {};
8 + if (identity(null) ?? propA.a) {
9 + mutate(x);
10 + return {
11 + value: propB.x.y,
12 + };
13 + }
14 + }, [propA.a, propB.x.y]);
15 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.expect.md new
+25
@@ -0,0 +1,25 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +
8 +function Component({ propA }) {
9 + return useMemo(() => {
10 + return {
11 + value: propA.x().y,
12 + };
13 + }, [propA.x]);
14 +}
15 +
16 +```
17 +
18 +
19 +## Error
20 +
21 +```
22 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propA`, but the source dependencies were [propA.x]
23 +```
24 +
25 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.ts new
+10
@@ -0,0 +1,10 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +
4 +function Component({ propA }) {
5 + return useMemo(() => {
6 + return {
7 + value: propA.x().y,
8 + };
9 + }, [propA.x]);
10 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +
8 +function Component({ propA }) {
9 + return useMemo(() => {
10 + return propA.x();
11 + }, [propA.x]);
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `propA`, but the source dependencies were [propA.x]
21 +```
22 +
23 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.ts new
+8
@@ -0,0 +1,8 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +
4 +function Component({ propA }) {
5 + return useMemo(() => {
6 + return propA.x();
7 + }, [propA.x]);
8 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md new
+36
@@ -0,0 +1,36 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +// Here, Forget infers that the memo block dependency is input1
10 +// 1. StartMemoize is emitted before the function expression
11 +// (and thus before the depslist arg and its rvalues)
12 +// 2. x and y's overlapping reactive scopes forces y's reactive
13 +// scope to be extended to after the `mutate(x)` call, after
14 +// the StartMemoize instruction.
15 +// While this is technically a false positive, this example would
16 +// already fail the exhaustive-deps eslint rule.
17 +function useFoo(input1) {
18 + const x = {};
19 + const y = [input1];
20 + const memoized = useMemo(() => {
21 + return [y];
22 + }, [(mutate(x), y)]);
23 +
24 + return [x, memoized];
25 +}
26 +
27 +```
28 +
29 +
30 +## Error
31 +
32 +```
33 +[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `input1`, but the source dependencies were [y]
34 +```
35 +
36 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.ts new
+21
@@ -0,0 +1,21 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +// Here, Forget infers that the memo block dependency is input1
6 +// 1. StartMemoize is emitted before the function expression
7 +// (and thus before the depslist arg and its rvalues)
8 +// 2. x and y's overlapping reactive scopes forces y's reactive
9 +// scope to be extended to after the `mutate(x)` call, after
10 +// the StartMemoize instruction.
11 +// While this is technically a false positive, this example would
12 +// already fail the exhaustive-deps eslint rule.
13 +function useFoo(input1) {
14 + const x = {};
15 + const y = [input1];
16 + const memoized = useMemo(() => {
17 + return [y];
18 + }, [(mutate(x), y)]);
19 +
20 + return [x, memoized];
21 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.expect.md new
+32
@@ -0,0 +1,32 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +// We technically do not need to bailout here if we can check
8 +// `someHelper`'s reactive deps are a subset of depslist from
9 +// source. This check is somewhat incompatible with our current
10 +// representation of manual memoization in HIR, so we bail out
11 +// for now.
12 +function Component(props) {
13 + const x = useMemo(someHelper, []);
14 + return x;
15 +}
16 +
17 +```
18 +
19 +
20 +## Error
21 +
22 +```
23 + 7 | // for now.
24 + 8 | function Component(props) {
25 +> 9 | const x = useMemo(someHelper, []);
26 + | ^^^^^^^^^^ [ReactForget] InvalidReact: Expected the first argument of useMemo to be an inline function expression (9:9)
27 + 10 | return x;
28 + 11 | }
29 + 12 |
30 +```
31 +
32 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.js new
+11
@@ -0,0 +1,11 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +// We technically do not need to bailout here if we can check
4 +// `someHelper`'s reactive deps are a subset of depslist from
5 +// source. This check is somewhat incompatible with our current
6 +// representation of manual memoization in HIR, so we bail out
7 +// for now.
8 +function Component(props) {
9 + const x = useMemo(someHelper, []);
10 + return x;
11 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md new
+54
@@ -0,0 +1,54 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +// This is currently considered valid because we don't ensure that every
10 +// instruction within manual memoization gets assigned to a reactive scope
11 +// (i.e. inferred non-mutable or non-escaping values don't get memoized)
12 +function useFoo({ minWidth, styles, setStyles }) {
13 + useMemo(() => {
14 + if (styles.width > minWidth) {
15 + setStyles(styles);
16 + }
17 + }, [styles, minWidth, setStyles]);
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ minWidth: 2, styles: { width: 1 }, setStyles: () => {} }],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +// @validatePreserveExistingMemoizationGuarantees
31 +
32 +import { useMemo } from "react";
33 +
34 +// This is currently considered valid because we don't ensure that every
35 +// instruction within manual memoization gets assigned to a reactive scope
36 +// (i.e. inferred non-mutable or non-escaping values don't get memoized)
37 +function useFoo(t0) {
38 + const { minWidth, styles, setStyles } = t0;
39 + let t1;
40 + if (styles.width > minWidth) {
41 + setStyles(styles);
42 + }
43 + t1 = undefined;
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: useFoo,
48 + params: [{ minWidth: 2, styles: { width: 1 }, setStyles: () => {} }],
49 +};
50 +
51 +```
52 +
53 +### Eval output
54 +(kind: ok)
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.ts new
+19
@@ -0,0 +1,19 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +// This is currently considered valid because we don't ensure that every
6 +// instruction within manual memoization gets assigned to a reactive scope
7 +// (i.e. inferred non-mutable or non-escaping values don't get memoized)
8 +function useFoo({ minWidth, styles, setStyles }) {
9 + useMemo(() => {
10 + if (styles.width > minWidth) {
11 + setStyles(styles);
12 + }
13 + }, [styles, minWidth, setStyles]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ minWidth: 2, styles: { width: 1 }, setStyles: () => {} }],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md new
+62
@@ -0,0 +1,62 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +// Todo: we currently only generate a `constVal` declaration when
10 +// validatePreserveExistingMemoizationGuarantees is enabled, as the
11 +// StartMemoize instruction uses `constVal`.
12 +// Fix is to rewrite StartMemoize instructions to remove constant
13 +// propagated values
14 +function useFoo() {
15 + const constVal = 0;
16 +
17 + return useMemo(() => [constVal], [constVal]);
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +// @validatePreserveExistingMemoizationGuarantees
31 +
32 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
33 +
34 +// Todo: we currently only generate a `constVal` declaration when
35 +// validatePreserveExistingMemoizationGuarantees is enabled, as the
36 +// StartMemoize instruction uses `constVal`.
37 +// Fix is to rewrite StartMemoize instructions to remove constant
38 +// propagated values
39 +function useFoo() {
40 + const $ = useMemoCache(1);
41 + const constVal = 0;
42 + let t0;
43 + let t1;
44 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
45 + t1 = [0];
46 + $[0] = t1;
47 + } else {
48 + t1 = $[0];
49 + }
50 + t0 = t1;
51 + return t0;
52 +}
53 +
54 +export const FIXTURE_ENTRYPOINT = {
55 + fn: useFoo,
56 + params: [{}],
57 +};
58 +
59 +```
60 +
61 +### Eval output
62 +(kind: ok) [0]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.ts new
+19
@@ -0,0 +1,19 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +// Todo: we currently only generate a `constVal` declaration when
6 +// validatePreserveExistingMemoizationGuarantees is enabled, as the
7 +// StartMemoize instruction uses `constVal`.
8 +// Fix is to rewrite StartMemoize instructions to remove constant
9 +// propagated values
10 +function useFoo() {
11 + const constVal = 0;
12 +
13 + return useMemo(() => [constVal], [constVal]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{}],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.expect.md new
+54
@@ -0,0 +1,54 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +import { sum } from "shared-runtime";
8 +
9 +function Component({ propA, propB }) {
10 + const x = propB.x.y;
11 + return useCallback(() => {
12 + return sum(propA.x, x);
13 + }, [propA.x, x]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +// @validatePreserveExistingMemoizationGuarantees
27 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
28 +import { sum } from "shared-runtime";
29 +
30 +function Component(t0) {
31 + const $ = useMemoCache(3);
32 + const { propA, propB } = t0;
33 + const x = propB.x.y;
34 + let t1;
35 + if ($[0] !== propA.x || $[1] !== x) {
36 + t1 = () => sum(propA.x, x);
37 + $[0] = propA.x;
38 + $[1] = x;
39 + $[2] = t1;
40 + } else {
41 + t1 = $[2];
42 + }
43 + return t1;
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: Component,
48 + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
49 +};
50 +
51 +```
52 +
53 +### Eval output
54 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.ts new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +import { sum } from "shared-runtime";
4 +
5 +function Component({ propA, propB }) {
6 + const x = propB.x.y;
7 + return useCallback(() => {
8 + return sum(propA.x, x);
9 + }, [propA.x, x]);
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
15 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md new
+81
@@ -0,0 +1,81 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +import { Stringify } from "shared-runtime";
8 +
9 +function Foo(props) {
10 + let contextVar;
11 + if (props.cond) {
12 + contextVar = { val: 2 };
13 + } else {
14 + contextVar = {};
15 + }
16 +
17 + const cb = useCallback(() => [contextVar.val], [contextVar.val]);
18 +
19 + return <Stringify cb={cb} shouldInvokeFns={true} />;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Foo,
24 + params: [{ cond: true }],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +// @validatePreserveExistingMemoizationGuarantees
33 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
34 +import { Stringify } from "shared-runtime";
35 +
36 +function Foo(props) {
37 + const $ = useMemoCache(6);
38 + let contextVar;
39 + if ($[0] !== props.cond) {
40 + if (props.cond) {
41 + contextVar = { val: 2 };
42 + } else {
43 + contextVar = {};
44 + }
45 + $[0] = props.cond;
46 + $[1] = contextVar;
47 + } else {
48 + contextVar = $[1];
49 + }
50 +
51 + const t0 = contextVar;
52 + let t1;
53 + if ($[2] !== t0.val) {
54 + t1 = () => [contextVar.val];
55 + $[2] = t0.val;
56 + $[3] = t1;
57 + } else {
58 + t1 = $[3];
59 + }
60 + contextVar;
61 + const cb = t1;
62 + let t2;
63 + if ($[4] !== cb) {
64 + t2 = <Stringify cb={cb} shouldInvokeFns={true} />;
65 + $[4] = cb;
66 + $[5] = t2;
67 + } else {
68 + t2 = $[5];
69 + }
70 + return t2;
71 +}
72 +
73 +export const FIXTURE_ENTRYPOINT = {
74 + fn: Foo,
75 + params: [{ cond: true }],
76 +};
77 +
78 +```
79 +
80 +### Eval output
81 +(kind: ok) <div>{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx new
+21
@@ -0,0 +1,21 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +import { Stringify } from "shared-runtime";
4 +
5 +function Foo(props) {
6 + let contextVar;
7 + if (props.cond) {
8 + contextVar = { val: 2 };
9 + } else {
10 + contextVar = {};
11 + }
12 +
13 + const cb = useCallback(() => [contextVar.val], [contextVar.val]);
14 +
15 + return <Stringify cb={cb} shouldInvokeFns={true} />;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Foo,
20 + params: [{ cond: true }],
21 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md new
+71
@@ -0,0 +1,71 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useCallback } from "react";
8 +import { makeArray } from "shared-runtime";
9 +
10 +// This case is fine, as all reassignments happen before the useCallback
11 +function Foo(props) {
12 + let x = [];
13 + x.push(props);
14 + x = makeArray();
15 +
16 + const cb = useCallback(() => [x], [x]);
17 +
18 + return cb;
19 +}
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +// @validatePreserveExistingMemoizationGuarantees
31 +
32 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
33 +import { makeArray } from "shared-runtime";
34 +
35 +// This case is fine, as all reassignments happen before the useCallback
36 +function Foo(props) {
37 + const $ = useMemoCache(4);
38 + let x;
39 + if ($[0] !== props) {
40 + x = [];
41 + x.push(props);
42 + x = makeArray();
43 + $[0] = props;
44 + $[1] = x;
45 + } else {
46 + x = $[1];
47 + }
48 +
49 + const t0 = x;
50 + let t1;
51 + if ($[2] !== t0) {
52 + t1 = () => [x];
53 + $[2] = t0;
54 + $[3] = t1;
55 + } else {
56 + t1 = $[3];
57 + }
58 + x;
59 + const cb = t1;
60 + return cb;
61 +}
62 +
63 +export const FIXTURE_ENTRYPOINT = {
64 + fn: Foo,
65 + params: [{}],
66 +};
67 +
68 +```
69 +
70 +### Eval output
71 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.ts new
+19
@@ -0,0 +1,19 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useCallback } from "react";
4 +import { makeArray } from "shared-runtime";
5 +
6 +// This case is fine, as all reassignments happen before the useCallback
7 +function Foo(props) {
8 + let x = [];
9 + x.push(props);
10 + x = makeArray();
11 +
12 + const cb = useCallback(() => [x], [x]);
13 +
14 + return cb;
15 +}
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Foo,
18 + params: [{}],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md new
+58
@@ -0,0 +1,58 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +
8 +function Component({ propA, propB }) {
9 + return useCallback(() => {
10 + if (propA) {
11 + return {
12 + value: propB.x.y,
13 + };
14 + }
15 + }, [propA, propB.x.y]);
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ propA: 1, propB: { x: { y: [] } } }],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +// @validatePreserveExistingMemoizationGuarantees
29 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
30 +
31 +function Component(t0) {
32 + const $ = useMemoCache(3);
33 + const { propA, propB } = t0;
34 + let t1;
35 + if ($[0] !== propA || $[1] !== propB.x.y) {
36 + t1 = () => {
37 + if (propA) {
38 + return { value: propB.x.y };
39 + }
40 + };
41 + $[0] = propA;
42 + $[1] = propB.x.y;
43 + $[2] = t1;
44 + } else {
45 + t1 = $[2];
46 + }
47 + return t1;
48 +}
49 +
50 +export const FIXTURE_ENTRYPOINT = {
51 + fn: Component,
52 + params: [{ propA: 1, propB: { x: { y: [] } } }],
53 +};
54 +
55 +```
56 +
57 +### Eval output
58 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.ts new
+17
@@ -0,0 +1,17 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +
4 +function Component({ propA, propB }) {
5 + return useCallback(() => {
6 + if (propA) {
7 + return {
8 + value: propB.x.y,
9 + };
10 + }
11 + }, [propA, propB.x.y]);
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{ propA: 1, propB: { x: { y: [] } } }],
17 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.expect.md new
+91
@@ -0,0 +1,91 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback, useState } from "react";
7 +import { arrayPush } from "shared-runtime";
8 +
9 +// useCallback-produced values can exist in nested reactive blocks, as long
10 +// as their reactive dependencies are a subset of depslist from source
11 +function useFoo(minWidth, otherProp) {
12 + const [width, setWidth] = useState(1);
13 + const x = [];
14 + const style = useCallback(() => {
15 + return {
16 + width: Math.max(minWidth, width),
17 + };
18 + }, [width, minWidth]);
19 + arrayPush(x, otherProp);
20 + return [style, x];
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: useFoo,
25 + params: [2, "other"],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +// @validatePreserveExistingMemoizationGuarantees
34 +import {
35 + useCallback,
36 + useState,
37 + unstable_useMemoCache as useMemoCache,
38 +} from "react";
39 +import { arrayPush } from "shared-runtime";
40 +
41 +// useCallback-produced values can exist in nested reactive blocks, as long
42 +// as their reactive dependencies are a subset of depslist from source
43 +function useFoo(minWidth, otherProp) {
44 + const $ = useMemoCache(11);
45 + const [width] = useState(1);
46 + let style;
47 + let x;
48 + if ($[0] !== width || $[1] !== minWidth || $[2] !== otherProp) {
49 + x = [];
50 + let t0;
51 + if ($[5] !== minWidth || $[6] !== width) {
52 + t0 = () => ({ width: Math.max(minWidth, width) });
53 + $[5] = minWidth;
54 + $[6] = width;
55 + $[7] = t0;
56 + } else {
57 + t0 = $[7];
58 + }
59 + style = t0;
60 +
61 + arrayPush(x, otherProp);
62 + $[0] = width;
63 + $[1] = minWidth;
64 + $[2] = otherProp;
65 + $[3] = style;
66 + $[4] = x;
67 + } else {
68 + style = $[3];
69 + x = $[4];
70 + }
71 + let t0;
72 + if ($[8] !== style || $[9] !== x) {
73 + t0 = [style, x];
74 + $[8] = style;
75 + $[9] = x;
76 + $[10] = t0;
77 + } else {
78 + t0 = $[10];
79 + }
80 + return t0;
81 +}
82 +
83 +export const FIXTURE_ENTRYPOINT = {
84 + fn: useFoo,
85 + params: [2, "other"],
86 +};
87 +
88 +```
89 +
90 +### Eval output
91 +(kind: ok) ["[[ function params=0 ]]",["other"]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.ts new
+22
@@ -0,0 +1,22 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback, useState } from "react";
3 +import { arrayPush } from "shared-runtime";
4 +
5 +// useCallback-produced values can exist in nested reactive blocks, as long
6 +// as their reactive dependencies are a subset of depslist from source
7 +function useFoo(minWidth, otherProp) {
8 + const [width, setWidth] = useState(1);
9 + const x = [];
10 + const style = useCallback(() => {
11 + return {
12 + width: Math.max(minWidth, width),
13 + };
14 + }, [width, minWidth]);
15 + arrayPush(x, otherProp);
16 + return [style, x];
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: useFoo,
21 + params: [2, "other"],
22 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md new
+63
@@ -0,0 +1,63 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +import { identity, mutate } from "shared-runtime";
8 +
9 +function useHook(propA, propB) {
10 + return useCallback(() => {
11 + const x = {};
12 + if (identity(null) ?? propA.a) {
13 + mutate(x);
14 + return {
15 + value: propB.x.y,
16 + };
17 + }
18 + }, [propA.a, propB.x.y]);
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: useHook,
23 + params: [{ a: 1 }, { x: { y: 3 } }],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +// @validatePreserveExistingMemoizationGuarantees
32 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
33 +import { identity, mutate } from "shared-runtime";
34 +
35 +function useHook(propA, propB) {
36 + const $ = useMemoCache(3);
37 + let t0;
38 + if ($[0] !== propA.a || $[1] !== propB.x.y) {
39 + t0 = () => {
40 + const x = {};
41 + if (identity(null) ?? propA.a) {
42 + mutate(x);
43 + return { value: propB.x.y };
44 + }
45 + };
46 + $[0] = propA.a;
47 + $[1] = propB.x.y;
48 + $[2] = t0;
49 + } else {
50 + t0 = $[2];
51 + }
52 + return t0;
53 +}
54 +
55 +export const FIXTURE_ENTRYPOINT = {
56 + fn: useHook,
57 + params: [{ a: 1 }, { x: { y: 3 } }],
58 +};
59 +
60 +```
61 +
62 +### Eval output
63 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.ts new
+20
@@ -0,0 +1,20 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +import { identity, mutate } from "shared-runtime";
4 +
5 +function useHook(propA, propB) {
6 + return useCallback(() => {
7 + const x = {};
8 + if (identity(null) ?? propA.a) {
9 + mutate(x);
10 + return {
11 + value: propB.x.y,
12 + };
13 + }
14 + }, [propA.a, propB.x.y]);
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useHook,
19 + params: [{ a: 1 }, { x: { y: 3 } }],
20 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useCallback } from "react";
8 +
9 +// It's correct to produce memo blocks with fewer deps than source
10 +function useFoo(a, b) {
11 + return useCallback(() => [a], [a, b]);
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [1, 2],
17 +};
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +// @validatePreserveExistingMemoizationGuarantees
25 +
26 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
27 +
28 +// It's correct to produce memo blocks with fewer deps than source
29 +function useFoo(a, b) {
30 + const $ = useMemoCache(2);
31 + let t0;
32 + if ($[0] !== a) {
33 + t0 = () => [a];
34 + $[0] = a;
35 + $[1] = t0;
36 + } else {
37 + t0 = $[1];
38 + }
39 + return t0;
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: useFoo,
44 + params: [1, 2],
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.ts new
+13
@@ -0,0 +1,13 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useCallback } from "react";
4 +
5 +// It's correct to produce memo blocks with fewer deps than source
6 +function useFoo(a, b) {
7 + return useCallback(() => [a], [a, b]);
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: useFoo,
12 + params: [1, 2],
13 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.expect.md new
+59
@@ -0,0 +1,59 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +import { sum } from "shared-runtime";
8 +
9 +function useFoo() {
10 + const val = [1, 2, 3];
11 +
12 + return useCallback(() => {
13 + return sum(...val);
14 + }, [val]);
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useFoo,
19 + params: [],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +// @validatePreserveExistingMemoizationGuarantees
28 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
29 +import { sum } from "shared-runtime";
30 +
31 +function useFoo() {
32 + const $ = useMemoCache(2);
33 + let t0;
34 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35 + t0 = [1, 2, 3];
36 + $[0] = t0;
37 + } else {
38 + t0 = $[0];
39 + }
40 + const val = t0;
41 + let t1;
42 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
43 + t1 = () => sum(...val);
44 + $[1] = t1;
45 + } else {
46 + t1 = $[1];
47 + }
48 + return t1;
49 +}
50 +
51 +export const FIXTURE_ENTRYPOINT = {
52 + fn: useFoo,
53 + params: [],
54 +};
55 +
56 +```
57 +
58 +### Eval output
59 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.ts new
+16
@@ -0,0 +1,16 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +import { sum } from "shared-runtime";
4 +
5 +function useFoo() {
6 + const val = [1, 2, 3];
7 +
8 + return useCallback(() => {
9 + return sum(...val);
10 + }, [val]);
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: useFoo,
15 + params: [],
16 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.expect.md new
+51
@@ -0,0 +1,51 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useCallback } from "react";
8 +import { CONST_STRING0 } from "shared-runtime";
9 +
10 +// It's correct to infer a useCallback block has no reactive dependencies
11 +function useFoo() {
12 + return useCallback(() => [CONST_STRING0], [CONST_STRING0]);
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: useFoo,
17 + params: [],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +// @validatePreserveExistingMemoizationGuarantees
26 +
27 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
28 +import { CONST_STRING0 } from "shared-runtime";
29 +
30 +// It's correct to infer a useCallback block has no reactive dependencies
31 +function useFoo() {
32 + const $ = useMemoCache(1);
33 + let t0;
34 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35 + t0 = () => [CONST_STRING0];
36 + $[0] = t0;
37 + } else {
38 + t0 = $[0];
39 + }
40 + return t0;
41 +}
42 +
43 +export const FIXTURE_ENTRYPOINT = {
44 + fn: useFoo,
45 + params: [],
46 +};
47 +
48 +```
49 +
50 +### Eval output
51 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.ts new
+14
@@ -0,0 +1,14 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useCallback } from "react";
4 +import { CONST_STRING0 } from "shared-runtime";
5 +
6 +// It's correct to infer a useCallback block has no reactive dependencies
7 +function useFoo() {
8 + return useCallback(() => [CONST_STRING0], [CONST_STRING0]);
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: useFoo,
13 + params: [],
14 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md renamed
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.js renamed
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.expect.md renamed
+1 -1
@@ -9,7 +9,7 @@ function Component({ entity, children }) {
9 // showMessage doesn't escape so we don't memoize it.
10 // However, validatePreserveExistingMemoizationGuarantees only sees that the scope
11 // doesn't exist, and thinks the memoization was missed instead of being intentionally dropped.
12 - const showMessage = useCallback(() => entity != null);
12 + const showMessage = useCallback(() => entity != null, [entity]);
13
14 if (!showMessage()) {
15 return children;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.js renamed
+1 -1
@@ -5,7 +5,7 @@ function Component({ entity, children }) {
5 // showMessage doesn't escape so we don't memoize it.
6 // However, validatePreserveExistingMemoizationGuarantees only sees that the scope
7 // doesn't exist, and thinks the memoization was missed instead of being intentionally dropped.
8 - const showMessage = useCallback(() => entity != null);
8 + const showMessage = useCallback(() => entity != null, [entity]);
9
10 if (!showMessage()) {
11 return children;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md new
+104
@@ -0,0 +1,104 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useCallback } from "react";
6 +import { Stringify } from "shared-runtime";
7 +
8 +function Foo({ arr1, arr2, foo }) {
9 + const x = [arr1];
10 +
11 + let y = [];
12 +
13 + const getVal1 = useCallback(() => {
14 + return { x: 2 };
15 + }, []);
16 +
17 + const getVal2 = useCallback(() => {
18 + return [y];
19 + }, [foo ? (y = x.concat(arr2)) : y]);
20 +
21 + return <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Foo,
26 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
27 + sequentialRenders: [
28 + { arr1: [1, 2], arr2: [3, 4], foo: true },
29 + { arr1: [1, 2], arr2: [3, 4], foo: false },
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
39 +import { Stringify } from "shared-runtime";
40 +
41 +function Foo(t0) {
42 + const $ = useMemoCache(11);
43 + const { arr1, arr2, foo } = t0;
44 + let t1;
45 + if ($[0] !== arr1) {
46 + t1 = [arr1];
47 + $[0] = arr1;
48 + $[1] = t1;
49 + } else {
50 + t1 = $[1];
51 + }
52 + const x = t1;
53 + let t2;
54 + let getVal1;
55 + if ($[2] !== foo || $[3] !== x || $[4] !== arr2) {
56 + let y;
57 + y = [];
58 + let t3;
59 + if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
60 + t3 = () => ({ x: 2 });
61 + $[7] = t3;
62 + } else {
63 + t3 = $[7];
64 + }
65 + getVal1 = t3;
66 +
67 + t2 = () => [y];
68 + foo ? (y = x.concat(arr2)) : y;
69 + $[2] = foo;
70 + $[3] = x;
71 + $[4] = arr2;
72 + $[5] = t2;
73 + $[6] = getVal1;
74 + } else {
75 + t2 = $[5];
76 + getVal1 = $[6];
77 + }
78 + const getVal2 = t2;
79 + let t3;
80 + if ($[8] !== getVal1 || $[9] !== getVal2) {
81 + t3 = <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
82 + $[8] = getVal1;
83 + $[9] = getVal2;
84 + $[10] = t3;
85 + } else {
86 + t3 = $[10];
87 + }
88 + return t3;
89 +}
90 +
91 +export const FIXTURE_ENTRYPOINT = {
92 + fn: Foo,
93 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
94 + sequentialRenders: [
95 + { arr1: [1, 2], arr2: [3, 4], foo: true },
96 + { arr1: [1, 2], arr2: [3, 4], foo: false },
97 + ],
98 +};
99 +
100 +```
101 +
102 +### Eval output
103 +(kind: ok) <div>{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[[1,2],3,4]]},"shouldInvokeFns":true}</div>
104 +<div>{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[]]},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.tsx new
+27
@@ -0,0 +1,27 @@
1 +import { useCallback } from "react";
2 +import { Stringify } from "shared-runtime";
3 +
4 +function Foo({ arr1, arr2, foo }) {
5 + const x = [arr1];
6 +
7 + let y = [];
8 +
9 + const getVal1 = useCallback(() => {
10 + return { x: 2 };
11 + }, []);
12 +
13 + const getVal2 = useCallback(() => {
14 + return [y];
15 + }, [foo ? (y = x.concat(arr2)) : y]);
16 +
17 + return <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
23 + sequentialRenders: [
24 + { arr1: [1, 2], arr2: [3, 4], foo: true },
25 + { arr1: [1, 2], arr2: [3, 4], foo: false },
26 + ],
27 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useCallback } from "react";
6 +import { Stringify } from "shared-runtime";
7 +
8 +// We currently produce invalid output (incorrect scoping for `y` declaration)
9 +function useFoo(arr1, arr2) {
10 + const x = [arr1];
11 +
12 + let y;
13 + const getVal = useCallback(() => {
14 + return { y };
15 + }, [((y = x.concat(arr2)), y)]);
16 +
17 + return <Stringify getVal={getVal} shouldInvokeFns={true} />;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [
23 + [1, 2],
24 + [3, 4],
25 + ],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
34 +import { Stringify } from "shared-runtime";
35 +
36 +// We currently produce invalid output (incorrect scoping for `y` declaration)
37 +function useFoo(arr1, arr2) {
38 + const $ = useMemoCache(7);
39 + let t0;
40 + if ($[0] !== arr1) {
41 + t0 = [arr1];
42 + $[0] = arr1;
43 + $[1] = t0;
44 + } else {
45 + t0 = $[1];
46 + }
47 + const x = t0;
48 + let t1;
49 + if ($[2] !== x || $[3] !== arr2) {
50 + let y;
51 + t1 = () => ({ y });
52 +
53 + (y = x.concat(arr2)), y;
54 + $[2] = x;
55 + $[3] = arr2;
56 + $[4] = t1;
57 + } else {
58 + t1 = $[4];
59 + }
60 + const getVal = t1;
61 + let t2;
62 + if ($[5] !== getVal) {
63 + t2 = <Stringify getVal={getVal} shouldInvokeFns={true} />;
64 + $[5] = getVal;
65 + $[6] = t2;
66 + } else {
67 + t2 = $[6];
68 + }
69 + return t2;
70 +}
71 +
72 +export const FIXTURE_ENTRYPOINT = {
73 + fn: useFoo,
74 + params: [
75 + [1, 2],
76 + [3, 4],
77 + ],
78 +};
79 +
80 +```
81 +
82 +### Eval output
83 +(kind: ok) <div>{"getVal":{"kind":"Function","result":{"y":[[1,2],3,4]}},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.tsx new
+22
@@ -0,0 +1,22 @@
1 +import { useCallback } from "react";
2 +import { Stringify } from "shared-runtime";
3 +
4 +// We currently produce invalid output (incorrect scoping for `y` declaration)
5 +function useFoo(arr1, arr2) {
6 + const x = [arr1];
7 +
8 + let y;
9 + const getVal = useCallback(() => {
10 + return { y };
11 + }, [((y = x.concat(arr2)), y)]);
12 +
13 + return <Stringify getVal={getVal} shouldInvokeFns={true} />;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [
19 + [1, 2],
20 + [3, 4],
21 + ],
22 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.expect.md new
+54
@@ -0,0 +1,54 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useCallback } from "react";
7 +
8 +// Compiler can produce any memoization it finds valid if the
9 +// source listed no memo deps
10 +function Component({ propA }) {
11 + // @ts-ignore
12 + return useCallback(() => {
13 + return [propA];
14 + });
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{ propA: 2 }],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +// @validatePreserveExistingMemoizationGuarantees
28 +import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
29 +
30 +// Compiler can produce any memoization it finds valid if the
31 +// source listed no memo deps
32 +function Component(t0) {
33 + const $ = useMemoCache(2);
34 + const { propA } = t0;
35 + let t1;
36 + if ($[0] !== propA) {
37 + t1 = () => [propA];
38 + $[0] = propA;
39 + $[1] = t1;
40 + } else {
41 + t1 = $[1];
42 + }
43 + return t1;
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: Component,
48 + params: [{ propA: 2 }],
49 +};
50 +
51 +```
52 +
53 +### Eval output
54 +(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.ts new
+16
@@ -0,0 +1,16 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useCallback } from "react";
3 +
4 +// Compiler can produce any memoization it finds valid if the
5 +// source listed no memo deps
6 +function Component({ propA }) {
7 + // @ts-ignore
8 + return useCallback(() => {
9 + return [propA];
10 + });
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{ propA: 2 }],
16 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md new
+56
@@ -0,0 +1,56 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { sum } from "shared-runtime";
8 +
9 +function Component({ propA, propB }) {
10 + const x = propB.x.y;
11 + return useMemo(() => {
12 + return sum(propA.x, x);
13 + }, [propA.x, x]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +// @validatePreserveExistingMemoizationGuarantees
27 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
28 +import { sum } from "shared-runtime";
29 +
30 +function Component(t0) {
31 + const $ = useMemoCache(3);
32 + const { propA, propB } = t0;
33 + const x = propB.x.y;
34 + let t1;
35 + let t2;
36 + if ($[0] !== propA.x || $[1] !== x) {
37 + t2 = sum(propA.x, x);
38 + $[0] = propA.x;
39 + $[1] = x;
40 + $[2] = t2;
41 + } else {
42 + t2 = $[2];
43 + }
44 + t1 = t2;
45 + return t1;
46 +}
47 +
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: Component,
50 + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
51 +};
52 +
53 +```
54 +
55 +### Eval output
56 +(kind: ok) 5
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.ts new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { sum } from "shared-runtime";
4 +
5 +function Component({ propA, propB }) {
6 + const x = propB.x.y;
7 + return useMemo(() => {
8 + return sum(propA.x, x);
9 + }, [propA.x, x]);
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }],
15 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md new
+67
@@ -0,0 +1,67 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { identity } from "shared-runtime";
8 +
9 +function Component({ propA, propB }) {
10 + return useMemo(() => {
11 + return {
12 + value: identity(propB?.x.y),
13 + other: propA,
14 + };
15 + }, [propA, propB.x.y]);
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ propA: 2, propB: { x: { y: [] } } }],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +// @validatePreserveExistingMemoizationGuarantees
29 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
30 +import { identity } from "shared-runtime";
31 +
32 +function Component(t0) {
33 + const $ = useMemoCache(5);
34 + const { propA, propB } = t0;
35 + let t1;
36 +
37 + const t2 = propB?.x.y;
38 + let t3;
39 + if ($[0] !== t2) {
40 + t3 = identity(t2);
41 + $[0] = t2;
42 + $[1] = t3;
43 + } else {
44 + t3 = $[1];
45 + }
46 + let t4;
47 + if ($[2] !== t3 || $[3] !== propA) {
48 + t4 = { value: t3, other: propA };
49 + $[2] = t3;
50 + $[3] = propA;
51 + $[4] = t4;
52 + } else {
53 + t4 = $[4];
54 + }
55 + t1 = t4;
56 + return t1;
57 +}
58 +
59 +export const FIXTURE_ENTRYPOINT = {
60 + fn: Component,
61 + params: [{ propA: 2, propB: { x: { y: [] } } }],
62 +};
63 +
64 +```
65 +
66 +### Eval output
67 +(kind: ok) {"value":[],"other":2}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.ts new
+17
@@ -0,0 +1,17 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { identity } from "shared-runtime";
4 +
5 +function Component({ propA, propB }) {
6 + return useMemo(() => {
7 + return {
8 + value: identity(propB?.x.y),
9 + other: propA,
10 + };
11 + }, [propA, propB.x.y]);
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{ propA: 2, propB: { x: { y: [] } } }],
17 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md new
+57
@@ -0,0 +1,57 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +
8 +function Component({ propA, propB }) {
9 + return useMemo(() => {
10 + return {
11 + value: propB?.x.y,
12 + other: propA,
13 + };
14 + }, [propA, propB.x.y]);
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{ propA: 2, propB: { x: { y: [] } } }],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +// @validatePreserveExistingMemoizationGuarantees
28 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
29 +
30 +function Component(t0) {
31 + const $ = useMemoCache(3);
32 + const { propA, propB } = t0;
33 + let t1;
34 +
35 + const t2 = propB?.x.y;
36 + let t3;
37 + if ($[0] !== t2 || $[1] !== propA) {
38 + t3 = { value: t2, other: propA };
39 + $[0] = t2;
40 + $[1] = propA;
41 + $[2] = t3;
42 + } else {
43 + t3 = $[2];
44 + }
45 + t1 = t3;
46 + return t1;
47 +}
48 +
49 +export const FIXTURE_ENTRYPOINT = {
50 + fn: Component,
51 + params: [{ propA: 2, propB: { x: { y: [] } } }],
52 +};
53 +
54 +```
55 +
56 +### Eval output
57 +(kind: ok) {"value":[],"other":2}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.ts new
+16
@@ -0,0 +1,16 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +
4 +function Component({ propA, propB }) {
5 + return useMemo(() => {
6 + return {
7 + value: propB?.x.y,
8 + other: propA,
9 + };
10 + }, [propA, propB.x.y]);
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{ propA: 2, propB: { x: { y: [] } } }],
16 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.expect.md new
+61
@@ -0,0 +1,61 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +
8 +function Component({ propA, propB }) {
9 + return useMemo(() => {
10 + if (propA) {
11 + return {
12 + value: propB.x.y,
13 + };
14 + }
15 + }, [propA, propB.x.y]);
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ propA: 1, propB: { x: { y: [] } } }],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +// @validatePreserveExistingMemoizationGuarantees
29 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
30 +
31 +function Component(t0) {
32 + const $ = useMemoCache(2);
33 + const { propA, propB } = t0;
34 + let t1;
35 + bb6: {
36 + if (propA) {
37 + let t2;
38 + if ($[0] !== propB.x.y) {
39 + t2 = { value: propB.x.y };
40 + $[0] = propB.x.y;
41 + $[1] = t2;
42 + } else {
43 + t2 = $[1];
44 + }
45 + t1 = t2;
46 + break bb6;
47 + }
48 + t1 = undefined;
49 + }
50 + return t1;
51 +}
52 +
53 +export const FIXTURE_ENTRYPOINT = {
54 + fn: Component,
55 + params: [{ propA: 1, propB: { x: { y: [] } } }],
56 +};
57 +
58 +```
59 +
60 +### Eval output
61 +(kind: ok) {"value":[]}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.ts new
+17
@@ -0,0 +1,17 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +
4 +function Component({ propA, propB }) {
5 + return useMemo(() => {
6 + if (propA) {
7 + return {
8 + value: propB.x.y,
9 + };
10 + }
11 + }, [propA, propB.x.y]);
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{ propA: 1, propB: { x: { y: [] } } }],
17 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { identity } from "shared-runtime";
8 +
9 +function useFoo(cond) {
10 + const sourceDep = 0;
11 + const derived1 = useMemo(() => {
12 + return identity(sourceDep);
13 + }, [sourceDep]);
14 + const derived2 = cond ?? Math.min(sourceDep, 1) ? 1 : 2;
15 + const derived3 = useMemo(() => {
16 + return identity(sourceDep);
17 + }, [sourceDep]);
18 + const derived4 = Math.min(sourceDep, -1) ?? cond ? 1 : 2;
19 + return [derived1, derived2, derived3, derived4];
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [true],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +// @validatePreserveExistingMemoizationGuarantees
33 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
34 +import { identity } from "shared-runtime";
35 +
36 +function useFoo(cond) {
37 + const $ = useMemoCache(5);
38 + const sourceDep = 0;
39 + let t0;
40 + let t1;
41 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
42 + t1 = identity(0);
43 + $[0] = t1;
44 + } else {
45 + t1 = $[0];
46 + }
47 + t0 = t1;
48 + const derived1 = t0;
49 +
50 + const derived2 = cond ?? Math.min(0, 1) ? 1 : 2;
51 + let t2;
52 + let t3;
53 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
54 + t3 = identity(0);
55 + $[1] = t3;
56 + } else {
57 + t3 = $[1];
58 + }
59 + t2 = t3;
60 + const derived3 = t2;
61 +
62 + const derived4 = Math.min(0, -1) ?? cond ? 1 : 2;
63 + let t4;
64 + if ($[2] !== derived2 || $[3] !== derived4) {
65 + t4 = [derived1, derived2, derived3, derived4];
66 + $[2] = derived2;
67 + $[3] = derived4;
68 + $[4] = t4;
69 + } else {
70 + t4 = $[4];
71 + }
72 + return t4;
73 +}
74 +
75 +export const FIXTURE_ENTRYPOINT = {
76 + fn: useFoo,
77 + params: [true],
78 +};
79 +
80 +```
81 +
82 +### Eval output
83 +(kind: ok) [0,1,0,1]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.ts new
+21
@@ -0,0 +1,21 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { identity } from "shared-runtime";
4 +
5 +function useFoo(cond) {
6 + const sourceDep = 0;
7 + const derived1 = useMemo(() => {
8 + return identity(sourceDep);
9 + }, [sourceDep]);
10 + const derived2 = cond ?? Math.min(sourceDep, 1) ? 1 : 2;
11 + const derived3 = useMemo(() => {
12 + return identity(sourceDep);
13 + }, [sourceDep]);
14 + const derived4 = Math.min(sourceDep, -1) ?? cond ? 1 : 2;
15 + return [derived1, derived2, derived3, derived4];
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [true],
21 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.expect.md new
+58
@@ -0,0 +1,58 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +import { useHook } from "shared-runtime";
9 +
10 +// useMemo values may not be memoized in Forget output if we
11 +// infer that their deps always invalidate.
12 +// This is still correct as the useMemo in source was effectively
13 +// a no-op already.
14 +function useFoo(props) {
15 + const x = [];
16 + useHook();
17 + x.push(props);
18 +
19 + return useMemo(() => [x], [x]);
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: useFoo,
24 + params: [{}],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +// @validatePreserveExistingMemoizationGuarantees
33 +
34 +import { useMemo } from "react";
35 +import { useHook } from "shared-runtime";
36 +
37 +// useMemo values may not be memoized in Forget output if we
38 +// infer that their deps always invalidate.
39 +// This is still correct as the useMemo in source was effectively
40 +// a no-op already.
41 +function useFoo(props) {
42 + const x = [];
43 + useHook();
44 + x.push(props);
45 + let t0;
46 + t0 = [x];
47 + return t0;
48 +}
49 +
50 +export const FIXTURE_ENTRYPOINT = {
51 + fn: useFoo,
52 + params: [{}],
53 +};
54 +
55 +```
56 +
57 +### Eval output
58 +(kind: ok) [[{}]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.ts new
+21
@@ -0,0 +1,21 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +import { useHook } from "shared-runtime";
5 +
6 +// useMemo values may not be memoized in Forget output if we
7 +// infer that their deps always invalidate.
8 +// This is still correct as the useMemo in source was effectively
9 +// a no-op already.
10 +function useFoo(props) {
11 + const x = [];
12 + useHook();
13 + x.push(props);
14 +
15 + return useMemo(() => [x], [x]);
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useFoo,
20 + params: [{}],
21 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md new
+94
@@ -0,0 +1,94 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo, useState } from "react";
7 +import { arrayPush } from "shared-runtime";
8 +
9 +// useMemo-produced values can exist in nested reactive blocks, as long
10 +// as their reactive dependencies are a subset of depslist from source
11 +function useFoo(minWidth, otherProp) {
12 + const [width, setWidth] = useState(1);
13 + const x = [];
14 + const style = useMemo(() => {
15 + return {
16 + width: Math.max(minWidth, width),
17 + };
18 + }, [width, minWidth]);
19 + arrayPush(x, otherProp);
20 + return [style, x];
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: useFoo,
25 + params: [2, "other"],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +// @validatePreserveExistingMemoizationGuarantees
34 +import {
35 + useMemo,
36 + useState,
37 + unstable_useMemoCache as useMemoCache,
38 +} from "react";
39 +import { arrayPush } from "shared-runtime";
40 +
41 +// useMemo-produced values can exist in nested reactive blocks, as long
42 +// as their reactive dependencies are a subset of depslist from source
43 +function useFoo(minWidth, otherProp) {
44 + const $ = useMemoCache(10);
45 + const [width] = useState(1);
46 + let style;
47 + let x;
48 + if ($[0] !== width || $[1] !== minWidth || $[2] !== otherProp) {
49 + x = [];
50 + let t0;
51 +
52 + const t1 = Math.max(minWidth, width);
53 + let t2;
54 + if ($[5] !== t1) {
55 + t2 = { width: t1 };
56 + $[5] = t1;
57 + $[6] = t2;
58 + } else {
59 + t2 = $[6];
60 + }
61 + t0 = t2;
62 + style = t0;
63 +
64 + arrayPush(x, otherProp);
65 + $[0] = width;
66 + $[1] = minWidth;
67 + $[2] = otherProp;
68 + $[3] = style;
69 + $[4] = x;
70 + } else {
71 + style = $[3];
72 + x = $[4];
73 + }
74 + let t0;
75 + if ($[7] !== style || $[8] !== x) {
76 + t0 = [style, x];
77 + $[7] = style;
78 + $[8] = x;
79 + $[9] = t0;
80 + } else {
81 + t0 = $[9];
82 + }
83 + return t0;
84 +}
85 +
86 +export const FIXTURE_ENTRYPOINT = {
87 + fn: useFoo,
88 + params: [2, "other"],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) [{"width":2},["other"]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.ts new
+22
@@ -0,0 +1,22 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo, useState } from "react";
3 +import { arrayPush } from "shared-runtime";
4 +
5 +// useMemo-produced values can exist in nested reactive blocks, as long
6 +// as their reactive dependencies are a subset of depslist from source
7 +function useFoo(minWidth, otherProp) {
8 + const [width, setWidth] = useState(1);
9 + const x = [];
10 + const style = useMemo(() => {
11 + return {
12 + width: Math.max(minWidth, width),
13 + };
14 + }, [width, minWidth]);
15 + arrayPush(x, otherProp);
16 + return [style, x];
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: useFoo,
21 + params: [2, "other"],
22 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md new
+52
@@ -0,0 +1,52 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +// It's correct to produce memo blocks with fewer deps than source
10 +function useFoo(a, b) {
11 + return useMemo(() => [a], [a, b]);
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useFoo,
16 + params: [1, 2],
17 +};
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +// @validatePreserveExistingMemoizationGuarantees
25 +
26 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
27 +
28 +// It's correct to produce memo blocks with fewer deps than source
29 +function useFoo(a, b) {
30 + const $ = useMemoCache(2);
31 + let t0;
32 + let t1;
33 + if ($[0] !== a) {
34 + t1 = [a];
35 + $[0] = a;
36 + $[1] = t1;
37 + } else {
38 + t1 = $[1];
39 + }
40 + t0 = t1;
41 + return t0;
42 +}
43 +
44 +export const FIXTURE_ENTRYPOINT = {
45 + fn: useFoo,
46 + params: [1, 2],
47 +};
48 +
49 +```
50 +
51 +### Eval output
52 +(kind: ok) [1]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.ts new
+13
@@ -0,0 +1,13 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +// It's correct to produce memo blocks with fewer deps than source
6 +function useFoo(a, b) {
7 + return useMemo(() => [a], [a, b]);
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: useFoo,
12 + params: [1, 2],
13 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md new
+55
@@ -0,0 +1,55 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +// It's correct to infer a useMemo value is non-allocating
10 +// and not provide it with a reactive scope
11 +function useFoo(num1, num2) {
12 + return useMemo(() => Math.min(num1, num2), [num1, num2]);
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: useFoo,
17 + params: [2, 3],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +// @validatePreserveExistingMemoizationGuarantees
26 +
27 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
28 +
29 +// It's correct to infer a useMemo value is non-allocating
30 +// and not provide it with a reactive scope
31 +function useFoo(num1, num2) {
32 + const $ = useMemoCache(3);
33 + let t0;
34 + let t1;
35 + if ($[0] !== num1 || $[1] !== num2) {
36 + t1 = Math.min(num1, num2);
37 + $[0] = num1;
38 + $[1] = num2;
39 + $[2] = t1;
40 + } else {
41 + t1 = $[2];
42 + }
43 + t0 = t1;
44 + return t0;
45 +}
46 +
47 +export const FIXTURE_ENTRYPOINT = {
48 + fn: useFoo,
49 + params: [2, 3],
50 +};
51 +
52 +```
53 +
54 +### Eval output
55 +(kind: ok) 2
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.ts new
+14
@@ -0,0 +1,14 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +// It's correct to infer a useMemo value is non-allocating
6 +// and not provide it with a reactive scope
7 +function useFoo(num1, num2) {
8 + return useMemo(() => Math.min(num1, num2), [num1, num2]);
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: useFoo,
13 + params: [2, 3],
14 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md new
+53
@@ -0,0 +1,53 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +import { CONST_STRING0 } from "shared-runtime";
9 +
10 +// It's correct to infer a useMemo block has no reactive dependencies
11 +function useFoo() {
12 + return useMemo(() => [CONST_STRING0], [CONST_STRING0]);
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: useFoo,
17 + params: [],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +// @validatePreserveExistingMemoizationGuarantees
26 +
27 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
28 +import { CONST_STRING0 } from "shared-runtime";
29 +
30 +// It's correct to infer a useMemo block has no reactive dependencies
31 +function useFoo() {
32 + const $ = useMemoCache(1);
33 + let t0;
34 + let t1;
35 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36 + t1 = [CONST_STRING0];
37 + $[0] = t1;
38 + } else {
39 + t1 = $[0];
40 + }
41 + t0 = t1;
42 + return t0;
43 +}
44 +
45 +export const FIXTURE_ENTRYPOINT = {
46 + fn: useFoo,
47 + params: [],
48 +};
49 +
50 +```
51 +
52 +### Eval output
53 +(kind: ok) ["global string 0"]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.ts new
+14
@@ -0,0 +1,14 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +import { CONST_STRING0 } from "shared-runtime";
5 +
6 +// It's correct to infer a useMemo block has no reactive dependencies
7 +function useFoo() {
8 + return useMemo(() => [CONST_STRING0], [CONST_STRING0]);
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: useFoo,
13 + params: [],
14 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md new
+62
@@ -0,0 +1,62 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +import { identity } from "shared-runtime";
8 +
9 +function useFoo(data) {
10 + return useMemo(() => {
11 + const temp = identity(data.a);
12 + return { temp };
13 + }, [data.a]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ a: 2 }],
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +// @validatePreserveExistingMemoizationGuarantees
27 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
28 +import { identity } from "shared-runtime";
29 +
30 +function useFoo(data) {
31 + const $ = useMemoCache(4);
32 + let t0;
33 + let t1;
34 + if ($[0] !== data.a) {
35 + t1 = identity(data.a);
36 + $[0] = data.a;
37 + $[1] = t1;
38 + } else {
39 + t1 = $[1];
40 + }
41 + const temp = t1;
42 + let t2;
43 + if ($[2] !== temp) {
44 + t2 = { temp };
45 + $[2] = temp;
46 + $[3] = t2;
47 + } else {
48 + t2 = $[3];
49 + }
50 + t0 = t2;
51 + return t0;
52 +}
53 +
54 +export const FIXTURE_ENTRYPOINT = {
55 + fn: useFoo,
56 + params: [{ a: 2 }],
57 +};
58 +
59 +```
60 +
61 +### Eval output
62 +(kind: ok) {"temp":2}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.ts new
+15
@@ -0,0 +1,15 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +import { identity } from "shared-runtime";
4 +
5 +function useFoo(data) {
6 + return useMemo(() => {
7 + const temp = identity(data.a);
8 + return { temp };
9 + }, [data.a]);
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [{ a: 2 }],
15 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md new
+65
@@ -0,0 +1,65 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +
7 +import { useMemo } from "react";
8 +
9 +function useFoo({ callback }) {
10 + return useMemo(() => new Array(callback()), [callback]);
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: useFoo,
15 + params: [
16 + {
17 + callback: () => {
18 + "use no forget";
19 + return [1, 2, 3];
20 + },
21 + },
22 + ],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +// @validatePreserveExistingMemoizationGuarantees
31 +
32 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
33 +
34 +function useFoo(t0) {
35 + const $ = useMemoCache(2);
36 + const { callback } = t0;
37 + let t1;
38 + let t2;
39 + if ($[0] !== callback) {
40 + t2 = new Array(callback());
41 + $[0] = callback;
42 + $[1] = t2;
43 + } else {
44 + t2 = $[1];
45 + }
46 + t1 = t2;
47 + return t1;
48 +}
49 +
50 +export const FIXTURE_ENTRYPOINT = {
51 + fn: useFoo,
52 + params: [
53 + {
54 + callback: () => {
55 + "use no forget";
56 + return [1, 2, 3];
57 + },
58 + },
59 + ],
60 +};
61 +
62 +```
63 +
64 +### Eval output
65 +(kind: ok) [[1,2,3]]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.ts new
+19
@@ -0,0 +1,19 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +
3 +import { useMemo } from "react";
4 +
5 +function useFoo({ callback }) {
6 + return useMemo(() => new Array(callback()), [callback]);
7 +}
8 +
9 +export const FIXTURE_ENTRYPOINT = {
10 + fn: useFoo,
11 + params: [
12 + {
13 + callback: () => {
14 + "use no forget";
15 + return [1, 2, 3];
16 + },
17 + },
18 + ],
19 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md new
+77
@@ -0,0 +1,77 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useMemo } from "react";
6 +
7 +function useFoo(arr1, arr2) {
8 + const x = [arr1];
9 +
10 + let y;
11 + return useMemo(() => {
12 + return { y };
13 + }, [((y = x.concat(arr2)), y)]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [
19 + [1, 2],
20 + [3, 4],
21 + ],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
30 +
31 +function useFoo(arr1, arr2) {
32 + const $ = useMemoCache(7);
33 + let t0;
34 + if ($[0] !== arr1) {
35 + t0 = [arr1];
36 + $[0] = arr1;
37 + $[1] = t0;
38 + } else {
39 + t0 = $[1];
40 + }
41 + const x = t0;
42 + let y;
43 + if ($[2] !== x || $[3] !== arr2) {
44 + y;
45 + (y = x.concat(arr2)), y;
46 + $[2] = x;
47 + $[3] = arr2;
48 + $[4] = y;
49 + } else {
50 + y = $[4];
51 + }
52 + let t1;
53 + const t2 = y;
54 + let t3;
55 + if ($[5] !== t2) {
56 + t3 = { y: t2 };
57 + $[5] = t2;
58 + $[6] = t3;
59 + } else {
60 + t3 = $[6];
61 + }
62 + t1 = t3;
63 + return t1;
64 +}
65 +
66 +export const FIXTURE_ENTRYPOINT = {
67 + fn: useFoo,
68 + params: [
69 + [1, 2],
70 + [3, 4],
71 + ],
72 +};
73 +
74 +```
75 +
76 +### Eval output
77 +(kind: ok) {"y":[[1,2],3,4]}
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.ts new
+18
@@ -0,0 +1,18 @@
1 +import { useMemo } from "react";
2 +
3 +function useFoo(arr1, arr2) {
4 + const x = [arr1];
5 +
6 + let y;
7 + return useMemo(() => {
8 + return { y };
9 + }, [((y = x.concat(arr2)), y)]);
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [
15 + [1, 2],
16 + [3, 4],
17 + ],
18 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md new
+106
@@ -0,0 +1,106 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useMemo } from "react";
6 +import { Stringify } from "shared-runtime";
7 +
8 +function Foo({ arr1, arr2, foo }) {
9 + const x = [arr1];
10 +
11 + let y = [];
12 +
13 + const val1 = useMemo(() => {
14 + return { x: 2 };
15 + }, []);
16 +
17 + const val2 = useMemo(() => {
18 + return [y];
19 + }, [foo ? (y = x.concat(arr2)) : y]);
20 +
21 + return <Stringify val1={val1} val2={val2} />;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Foo,
26 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
27 + sequentialRenders: [
28 + { arr1: [1, 2], arr2: [3, 4], foo: true },
29 + { arr1: [1, 2], arr2: [3, 4], foo: false },
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
39 +import { Stringify } from "shared-runtime";
40 +
41 +function Foo(t0) {
42 + const $ = useMemoCache(11);
43 + const { arr1, arr2, foo } = t0;
44 + let t1;
45 + if ($[0] !== arr1) {
46 + t1 = [arr1];
47 + $[0] = arr1;
48 + $[1] = t1;
49 + } else {
50 + t1 = $[1];
51 + }
52 + const x = t1;
53 + let t2;
54 + let val1;
55 + if ($[2] !== foo || $[3] !== x || $[4] !== arr2) {
56 + let y;
57 + y = [];
58 + let t3;
59 + let t4;
60 + if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
61 + t4 = { x: 2 };
62 + $[7] = t4;
63 + } else {
64 + t4 = $[7];
65 + }
66 + t3 = t4;
67 + val1 = t3;
68 +
69 + foo ? (y = x.concat(arr2)) : y;
70 + t2 = (() => [y])();
71 + $[2] = foo;
72 + $[3] = x;
73 + $[4] = arr2;
74 + $[5] = t2;
75 + $[6] = val1;
76 + } else {
77 + t2 = $[5];
78 + val1 = $[6];
79 + }
80 + const val2 = t2;
81 + let t3;
82 + if ($[8] !== val1 || $[9] !== val2) {
83 + t3 = <Stringify val1={val1} val2={val2} />;
84 + $[8] = val1;
85 + $[9] = val2;
86 + $[10] = t3;
87 + } else {
88 + t3 = $[10];
89 + }
90 + return t3;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Foo,
95 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
96 + sequentialRenders: [
97 + { arr1: [1, 2], arr2: [3, 4], foo: true },
98 + { arr1: [1, 2], arr2: [3, 4], foo: false },
99 + ],
100 +};
101 +
102 +```
103 +
104 +### Eval output
105 +(kind: ok) <div>{"val1":{"x":2},"val2":[[[1,2],3,4]]}</div>
106 +<div>{"val1":{"x":2},"val2":[[]]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.tsx new
+27
@@ -0,0 +1,27 @@
1 +import { useMemo } from "react";
2 +import { Stringify } from "shared-runtime";
3 +
4 +function Foo({ arr1, arr2, foo }) {
5 + const x = [arr1];
6 +
7 + let y = [];
8 +
9 + const val1 = useMemo(() => {
10 + return { x: 2 };
11 + }, []);
12 +
13 + const val2 = useMemo(() => {
14 + return [y];
15 + }, [foo ? (y = x.concat(arr2)) : y]);
16 +
17 + return <Stringify val1={val1} val2={val2} />;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
23 + sequentialRenders: [
24 + { arr1: [1, 2], arr2: [3, 4], foo: true },
25 + { arr1: [1, 2], arr2: [3, 4], foo: false },
26 + ],
27 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md new
+56
@@ -0,0 +1,56 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import { useMemo } from "react";
7 +
8 +// Compiler can produce any memoization it finds valid if the
9 +// source listed no memo deps
10 +function Component({ propA }) {
11 + // @ts-ignore
12 + return useMemo(() => {
13 + return [propA];
14 + });
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{ propA: 2 }],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +// @validatePreserveExistingMemoizationGuarantees
28 +import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
29 +
30 +// Compiler can produce any memoization it finds valid if the
31 +// source listed no memo deps
32 +function Component(t0) {
33 + const $ = useMemoCache(2);
34 + const { propA } = t0;
35 + let t1;
36 + let t2;
37 + if ($[0] !== propA) {
38 + t2 = [propA];
39 + $[0] = propA;
40 + $[1] = t2;
41 + } else {
42 + t2 = $[1];
43 + }
44 + t1 = t2;
45 + return t1;
46 +}
47 +
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: Component,
50 + params: [{ propA: 2 }],
51 +};
52 +
53 +```
54 +
55 +### Eval output
56 +(kind: ok) [2]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.ts new
+16
@@ -0,0 +1,16 @@
1 +// @validatePreserveExistingMemoizationGuarantees
2 +import { useMemo } from "react";
3 +
4 +// Compiler can produce any memoization it finds valid if the
5 +// source listed no memo deps
6 +function Component({ propA }) {
7 + // @ts-ignore
8 + return useMemo(() => {
9 + return [propA];
10 + });
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{ propA: 2 }],
16 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md
+1 -1
@@ -27,7 +27,7 @@ function Component(props) {
27 x.value = props.value;
28 mutate(x, free, part);
29 return x;
30 - }, [props.value]);
30 + }, [props.value, free, part]);
31
32 // These calls should be inferred as non-mutating due to the above freeze inference
33 identity(free);
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js
+1 -1
@@ -23,7 +23,7 @@ function Component(props) {
23 x.value = props.value;
24 mutate(x, free, part);
25 return x;
26 - }, [props.value]);
26 + }, [props.value, free, part]);
27
28 // These calls should be inferred as non-mutating due to the above freeze inference
29 identity(free);
compiler/packages/snap/src/sprout/shared-runtime.ts
+4
@@ -96,6 +96,10 @@ export function setProperty(arg: any, property: any): void {
96 }
97 }
98
99 +export function arrayPush<T>(arr: Array<T>, ...values: Array<T>): void {
100 + arr.push(...values);
101 +}
102 +
103 export function graphql(value: string): string {
104 return value;
105 }