diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts index a161c9a12b..5a22aeb5f5 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts @@ -632,14 +632,41 @@ export type Phi = { type: Type; }; +/** + * Valid ManualMemoDependencies are always of the form + * `sourceDeclaredVariable.a.b?.c`, since this is documented + * and enforced by the `react-hooks/exhaustive-deps` rule. + * + * `root` must either reference a ValidatedIdentifier or a global + * variable. + */ +export type ManualMemoDependency = { + root: + | { + kind: "NamedLocal"; + value: Place; + } + | { kind: "Global"; identifierName: string }; + path: Array; +}; + export type StartMemoize = { kind: "StartMemoize"; - deps: Array; + // Start/FinishMemoize markers should have matching ids + manualMemoId: number; + /** + * deps-list from source code, or null if one was not provided + * (e.g. useMemo without a second arg) + */ + deps: Array | null; loc: SourceLocation; }; export type FinishMemoize = { kind: "FinishMemoize"; + // Start/FinishMemoize markers should have matching ids + manualMemoId: number; decl: Place; + pruned?: true; loc: SourceLocation; }; diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts index ad8c96ac06..d24dde284e 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts @@ -19,6 +19,7 @@ import type { Instruction, InstructionValue, LValue, + ManualMemoDependency, MutableRange, ObjectMethod, ObjectPropertyKey, @@ -601,9 +602,10 @@ export function printInstructionValue(instrValue: ReactiveValue): string { break; } case "StartMemoize": { - value = `StartMemoize deps=${instrValue.deps.map((dep) => - printPlace(dep) - )}`; + value = `StartMemoize deps=${ + instrValue.deps?.map((dep) => printManualMemoDependency(dep, false)) ?? + "(none)" + }`; break; } case "FinishMemoize": { @@ -744,6 +746,25 @@ function printScope(scope: ReactiveScope | null): string { return `${scope !== null ? `_@${scope.id}` : ""}`; } +export function printManualMemoDependency( + val: ManualMemoDependency, + nameOnly: boolean +): string { + let rootStr; + if (val.root.kind === "Global") { + rootStr = val.root.identifierName; + } else { + CompilerError.invariant(val.root.value.identifier.name?.kind === "named", { + reason: "DepsValidation: expected named local variable in depslist", + suggestions: null, + loc: val.root.value.loc, + }); + rootStr = nameOnly + ? val.root.value.identifier.name.value + : printIdentifier(val.root.value.identifier); + } + return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`; +} export function printType(type: Type): string { if (type.kind === "Type") return ""; // TODO(mofeiZ): add debugName for generated ids diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts index 5af5248416..628446d9ac 100644 --- a/compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-forget/src/HIR/visitors.ts @@ -217,8 +217,12 @@ export function* eachInstructionValueOperand( break; } case "StartMemoize": { - for (const dep of instrValue.deps) { - yield dep; + if (instrValue.deps != null) { + for (const dep of instrValue.deps) { + if (dep.root.kind === "NamedLocal") { + yield dep.root.value; + } + } } break; } @@ -528,8 +532,12 @@ export function mapInstructionValueOperands( break; } case "StartMemoize": { - for (let i = 0; i < instrValue.deps.length; i++) { - instrValue.deps[i] = fn(instrValue.deps[i]); + if (instrValue.deps != null) { + for (const dep of instrValue.deps) { + if (dep.root.kind === "NamedLocal") { + dep.root.value = fn(dep.root.value); + } + } } break; } diff --git a/compiler/packages/babel-plugin-react-forget/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-forget/src/Inference/DropManualMemoization.ts index 0244a06687..570db17e15 100644 --- a/compiler/packages/babel-plugin-react-forget/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-forget/src/Inference/DropManualMemoization.ts @@ -16,8 +16,10 @@ import { IdentifierId, Instruction, InstructionId, + InstructionValue, LoadGlobal, LoadLocal, + ManualMemoDependency, MethodCall, Place, PropertyLoad, @@ -28,7 +30,6 @@ import { makeInstructionId, } from "../HIR"; import { createTemporaryPlace, markInstructionIds } from "../HIR/HIRBuilder"; -import { eachInstructionValueOperand } from "../HIR/visitors"; type ManualMemoCallee = { kind: "useMemo" | "useCallback"; @@ -39,14 +40,84 @@ type IdentifierSidemap = { functions: Map>; manualMemos: Map; react: Set; + maybeDepsLists: Map>; + maybeDeps: Map; }; +/** + * Collect loads from named variables and property reads from @value + * into `maybeDeps` + * Returns the variable + property reads represented by @instr + */ +export function collectMaybeMemoDependencies( + value: InstructionValue, + maybeDeps: Map +): ManualMemoDependency | null { + switch (value.kind) { + case "LoadGlobal": { + return { + root: { + kind: "Global", + identifierName: value.name, + }, + path: [], + }; + } + case "PropertyLoad": { + const object = maybeDeps.get(value.object.identifier.id); + if (object != null) { + return { + root: object.root, + path: [...object.path, value.property], + }; + } + break; + } + + case "LoadLocal": + case "LoadContext": { + const source = maybeDeps.get(value.place.identifier.id); + if (source != null) { + return source; + } else if ( + value.place.identifier.name != null && + value.place.identifier.name.kind === "named" + ) { + return { + root: { + kind: "NamedLocal", + value: { ...value.place }, + }, + path: [], + }; + } + break; + } + case "StoreLocal": { + /* + * Value blocks rely on StoreLocal to populate their return value. + * We need to track these as optional property chains are valid in + * source depslists + */ + const lvalue = value.lvalue.place.identifier; + const rvalue = value.value.identifier.id; + const aliased = maybeDeps.get(rvalue); + if (aliased != null && lvalue.name?.kind !== "named") { + maybeDeps.set(lvalue.id, aliased); + return aliased; + } + break; + } + } + return null; +} + function collectTemporaries( instr: Instruction, env: Environment, sidemap: IdentifierSidemap ): void { - const { value } = instr; + const { value, lvalue } = instr; switch (value.kind) { case "FunctionExpression": { sidemap.functions.set( @@ -80,14 +151,29 @@ function collectTemporaries( } break; } + case "ArrayExpression": { + if (value.elements.every((e) => e.kind === "Identifier")) { + sidemap.maybeDepsLists.set( + instr.lvalue.identifier.id, + value.elements as Array + ); + } + break; + } + } + const maybeDep = collectMaybeMemoDependencies(value, sidemap.maybeDeps); + // We don't expect named lvalues during this pass (unlike ValidatePreservingManualMemo) + if (maybeDep != null) { + sidemap.maybeDeps.set(lvalue.identifier.id, maybeDep); } } function makeManualMemoizationMarkers( fnExpr: Place, env: Environment, - depsList: Array, - memoDecl: Place + depsList: Array | null, + memoDecl: Place, + manualMemoId: number ): [TInstruction, TInstruction] { return [ { @@ -95,6 +181,7 @@ function makeManualMemoizationMarkers( lvalue: createTemporaryPlace(env), value: { kind: "StartMemoize", + manualMemoId, /* * Use deps list from source instead of inferred deps * as dependencies @@ -109,6 +196,7 @@ function makeManualMemoizationMarkers( lvalue: createTemporaryPlace(env), value: { kind: "FinishMemoize", + manualMemoId, decl: { ...memoDecl }, loc: fnExpr.loc, }, @@ -183,11 +271,13 @@ function getManualMemoizationReplacement( function extractManualMemoizationArgs( instr: TInstruction | TInstruction, - kind: "useCallback" | "useMemo" + kind: "useCallback" | "useMemo", + sidemap: IdentifierSidemap ): { fnPlace: Place; + depsList: Array | null; } { - const [fnPlace] = instr.value.args as Array< + const [fnPlace, depsListPlace] = instr.value.args as Array< Place | SpreadPattern | undefined >; if (fnPlace == null) { @@ -197,15 +287,40 @@ function extractManualMemoizationArgs( suggestions: null, }); } - if (fnPlace?.kind !== "Identifier") { + if (fnPlace?.kind !== "Identifier" || depsListPlace?.kind === "Spread") { CompilerError.throwInvalidReact({ reason: `Unexpected arguments to ${kind} call`, loc: instr.value.loc, suggestions: null, }); } + let depsList: Array | null = null; + if (depsListPlace != null) { + const maybeDepsList = sidemap.maybeDepsLists.get( + depsListPlace.identifier.id + ); + if (maybeDepsList == null) { + CompilerError.throwInvalidReact({ + reason: `Expected the dependency list for ${kind} to be an array literal without rest spreads`, + suggestions: null, + loc: depsListPlace.loc, + }); + } + depsList = maybeDepsList.map((dep) => { + const maybeDep = sidemap.maybeDeps.get(dep.identifier.id); + if (maybeDep == null) { + CompilerError.throwInvalidReact({ + reason: `Expected the dependency list for ${kind} to be an array of simple expressions`, + suggestions: null, + loc: dep.loc, + }); + } + return maybeDep; + }); + } return { fnPlace, + depsList, }; } @@ -225,7 +340,10 @@ export function dropManualMemoization(func: HIRFunction): void { functions: new Map(), manualMemos: new Map(), react: new Set(), + maybeDeps: new Map(), + maybeDepsLists: new Map(), }; + let nextManualMemoId = 0; /** * Phase 1: @@ -238,10 +356,7 @@ export function dropManualMemoization(func: HIRFunction): void { */ const queuedInserts: Map< InstructionId, - { - kind: "before" | "after"; - value: TInstruction | TInstruction; - } + TInstruction | TInstruction > = new Map(); for (const [_, block] of func.body.blocks) { for (let i = 0; i < block.instructions.length; i++) { @@ -257,9 +372,10 @@ export function dropManualMemoization(func: HIRFunction): void { const manualMemo = sidemap.manualMemos.get(id); if (manualMemo != null) { - const { fnPlace } = extractManualMemoizationArgs( + const { fnPlace, depsList } = extractManualMemoizationArgs( instr as TInstruction | TInstruction, - manualMemo.kind + manualMemo.kind, + sidemap ); instr.value = getManualMemoizationReplacement( fnPlace, @@ -267,11 +383,22 @@ export function dropManualMemoization(func: HIRFunction): void { manualMemo.kind ); if (isValidationEnabled) { - const inlineMemoFn = sidemap.functions.get(fnPlace.identifier.id); - if (inlineMemoFn == null) { + /** + * Explicitly bail out when we encounter manual memoization + * without inline instructions, as our current validation + * assumes that source depslists closely match inferred deps + * due to the `exhaustive-deps` lint rule (which only provides + * diagnostics for inline memo functions) + * ```js + * useMemo(opaqueFn, [dep1, dep2]); + * ``` + * While we could handle this by diffing reactive scope deps + * of the opaque arg against the source depslist, this pattern + * is rare and likely sketchy. + */ + if (!sidemap.functions.has(fnPlace.identifier.id)) { CompilerError.throwInvalidReact({ - reason: - "DepsValidation: Expected function literal as manual memoization callback", + reason: `Expected the first argument of ${manualMemo.kind} to be an inline function expression`, suggestions: [], loc: fnPlace.loc, }); @@ -290,24 +417,26 @@ export function dropManualMemoization(func: HIRFunction): void { const [startMarker, finishMarker] = makeManualMemoizationMarkers( fnPlace, func.env, - // Next PR will replace this with depslist from source - [...eachInstructionValueOperand(inlineMemoFn.value)], - memoDecl + depsList, + memoDecl, + nextManualMemoId++ ); - /* - * This PR reorders startMarker to right before the inlineMemoFn - * since startMarker references inlineMemoFn.deps. - * Next PR will move startMarker earlier, to after the `useMemo`/ - * `useCallback` load itself (as it also changes startMarker to - * not reference lowered deps anymore). + /** + * Insert StartMarker right after the `useMemo`/`useCallback` load to + * ensure all temporaries created when lowering the inline fn expression + * are included. + * e.g. + * ``` + * 0: LoadGlobal useMemo + * 1: StartMarker deps=[var] + * 2: t0 = LoadContext [var] + * 3: function deps=t0 + * ... + * ``` */ - queuedInserts.set(inlineMemoFn.id, { - kind: "before", - value: startMarker, - }); - queuedInserts.set(instr.id, { kind: "after", value: finishMarker }); - continue; + queuedInserts.set(manualMemo.loadInstr.id, startMarker); + queuedInserts.set(instr.id, finishMarker); } } } else { @@ -328,13 +457,8 @@ export function dropManualMemoization(func: HIRFunction): void { const insertInstr = queuedInserts.get(instr.id); if (insertInstr != null) { nextInstructions = nextInstructions ?? block.instructions.slice(0, i); - if (insertInstr.kind === "before") { - nextInstructions.push(insertInstr.value); - nextInstructions.push(instr); - } else { - nextInstructions.push(instr); - nextInstructions.push(insertInstr.value); - } + nextInstructions.push(instr); + nextInstructions.push(insertInstr); } else if (nextInstructions != null) { nextInstructions.push(instr); } diff --git a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts index 5bb9df1a5c..c2459b77e1 100644 --- a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts @@ -83,7 +83,7 @@ export function writeReactiveBlock( writer.writeLine("}"); } -function printDependency(dependency: ReactiveScopeDependency): string { +export function printDependency(dependency: ReactiveScopeDependency): string { const identifier = printIdentifier(dependency.identifier) + printType(dependency.identifier.type); diff --git a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts index 3e3c54422c..ed50e99ebd 100644 --- a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -933,7 +933,7 @@ class PruneScopesTransform extends ReactiveFunctionTransform< identifier.scope !== null && this.prunedScopes.has(identifier.scope.id) ) { - return { kind: "remove" }; + instruction.value.pruned = true; } } diff --git a/compiler/packages/babel-plugin-react-forget/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-forget/src/Validation/ValidatePreservedManualMemoization.ts index 702e2ff223..d401c51591 100644 --- a/compiler/packages/babel-plugin-react-forget/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-forget/src/Validation/ValidatePreservedManualMemoization.ts @@ -5,16 +5,25 @@ * LICENSE file in the root directory of this source tree. */ -import { CompilerError, ErrorSeverity } from ".."; +import { CompilerError, Effect, ErrorSeverity } from ".."; import { + GeneratedSource, Identifier, + IdentifierId, Instruction, + InstructionValue, + ManualMemoDependency, + Place, ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, + ReactiveScopeDependency, + ReactiveValue, ScopeId, } from "../HIR"; +import { printManualMemoDependency } from "../HIR/PrintHIR"; import { eachInstructionValueOperand } from "../HIR/visitors"; +import { collectMaybeMemoDependencies } from "../Inference/DropManualMemoization"; import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables"; import { ReactiveFunctionVisitor, @@ -29,22 +38,250 @@ import { * was pruned. */ export function validatePreservedManualMemoization(fn: ReactiveFunction): void { - const errors = new CompilerError(); - visitReactiveFunction(fn, new Visitor(), errors); - if (errors.hasErrors()) { - throw errors; + const state = { + errors: new CompilerError(), + manualMemoState: null, + }; + visitReactiveFunction(fn, new Visitor(), state); + if (state.errors.hasErrors()) { + throw state.errors; } } -class Visitor extends ReactiveFunctionVisitor { +type ManualMemoBlockState = { + /** + * Values produced within manual memoization blocks. + * We track these to ensure our inferred dependencies are + * produced before the manual memo block starts + * + * As an example: + * ```js + * // source + * const result = useMemo(() => { + * return [makeObject(input1), input2], + * }, [input1, input2]); + * ``` + * Here, we record inferred dependencies as [input1, input2] + * but not t0 + * ```js + * // StartMemoize + * let t0; + * if ($[0] != input1) { + * t0 = makeObject(input1); + * // ... + * } else { ... } + * + * let result; + * if ($[1] != t0 || $[2] != input2) { + * result = [t0, input2]; + * } else { ... } + * ``` + */ + decls: Set; + + /* + * normalized depslist from useMemo/useCallback + * callsite in source + */ + depsFromSource: Array | null; + manualMemoId: number; +}; + +type VisitorState = { + errors: CompilerError; + manualMemoState: ManualMemoBlockState | null; +}; + +function prettyPrintScopeDependency(val: ReactiveScopeDependency): string { + let rootStr; + if (val.identifier.name?.kind === "named") { + rootStr = val.identifier.name.value; + } else { + rootStr = "[unnamed]"; + } + return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`; +} +function depsEqual( + dep1: ManualMemoDependency, + dep2: ManualMemoDependency +): boolean { + const rootsEqual = + (dep1.root.kind === "Global" && + dep2.root.kind === "Global" && + dep1.root.identifierName === dep2.root.identifierName) || + (dep1.root.kind === "NamedLocal" && + dep2.root.kind === "NamedLocal" && + dep1.root.value.identifier.id === dep2.root.value.identifier.id); + return ( + rootsEqual && + dep1.path.length === dep2.path.length && + dep1.path.every((val, idx) => val === dep2.path[idx]) + ); +} + +function validateInferredDep( + dep: ReactiveScopeDependency, + temporaries: Map, + declsWithinMemoBlock: Set, + validDepsInMemoBlock: Array, + errorState: CompilerError +): void { + let normalizedDep: ManualMemoDependency; + const maybeNormalizedRoot = temporaries.get(dep.identifier.id); + if (maybeNormalizedRoot != null) { + normalizedDep = { + root: maybeNormalizedRoot.root, + path: [...maybeNormalizedRoot.path, ...dep.path], + }; + } else { + CompilerError.invariant(dep.identifier.name?.kind === "named", { + reason: + "ValidatePreservedManualMemoization: expected scope dependency to be named", + loc: GeneratedSource, + suggestions: null, + }); + normalizedDep = { + root: { + kind: "NamedLocal", + value: { + kind: "Identifier", + identifier: dep.identifier, + loc: GeneratedSource, + effect: Effect.Read, + reactive: false, + }, + }, + path: [...dep.path], + }; + } + for (const originalDep of validDepsInMemoBlock) { + if (depsEqual(originalDep, normalizedDep)) { + return; + } + } + for (const decl of declsWithinMemoBlock) { + const normalizedDecl = temporaries.get(decl); + if (normalizedDecl != null && depsEqual(normalizedDecl, normalizedDep)) { + return; + } else if ( + normalizedDep.root.kind === "NamedLocal" && + decl === normalizedDep.root.value.identifier.id + ) { + return; + } + } + errorState.push({ + severity: ErrorSeverity.Todo, + reason: + "Could not preserve manual memoization because an inferred dependency does not match the dependency list in source", + description: `The inferred dependency was \`${prettyPrintScopeDependency( + dep + )}\`, but the source dependencies were [${validDepsInMemoBlock + .map((dep) => printManualMemoDependency(dep, true)) + .join(", ")}]`, + loc: GeneratedSource, + suggestions: null, + }); +} + +class Visitor extends ReactiveFunctionVisitor { scopes: Set = new Set(); + scopeMapping = new Map(); + temporaries: Map = new Map(); + + collectMaybeMemoDependencies( + value: ReactiveValue, + state: VisitorState + ): ManualMemoDependency | null { + switch (value.kind) { + case "SequenceExpression": { + for (const instr of value.instructions) { + this.visitInstruction(instr, state); + } + const result = this.collectMaybeMemoDependencies(value.value, state); + + return result; + } + case "OptionalExpression": { + return this.collectMaybeMemoDependencies(value.value, state); + } + case "ReactiveFunctionValue": + case "ConditionalExpression": + case "LogicalExpression": { + return null; + } + default: { + const dep = collectMaybeMemoDependencies(value, this.temporaries); + if (value.kind === "StoreLocal" || value.kind === "StoreContext") { + const storeTarget = value.lvalue.place; + state.manualMemoState?.decls.add(storeTarget.identifier.id); + if (storeTarget.identifier.name?.kind === "named" && dep == null) { + const dep: ManualMemoDependency = { + root: { + kind: "NamedLocal", + value: storeTarget, + }, + path: [], + }; + this.temporaries.set(storeTarget.identifier.id, dep); + return dep; + } + } + return dep; + } + } + } + + recordTemporaries(instr: ReactiveInstruction, state: VisitorState): void { + const temporaries = this.temporaries; + const { value } = instr; + const lvalId = instr.lvalue?.identifier.id; + if (lvalId != null && temporaries.has(lvalId)) { + return; + } + const isNamedLocal = + lvalId != null && instr.lvalue?.identifier.name?.kind === "named"; + if (isNamedLocal && state.manualMemoState != null) { + state.manualMemoState.decls.add(lvalId); + } + + const maybeDep = this.collectMaybeMemoDependencies(value, state); + if (lvalId != null) { + if (maybeDep != null) { + temporaries.set(lvalId, maybeDep); + } else if (isNamedLocal) { + temporaries.set(lvalId, { + root: { + kind: "NamedLocal", + value: { ...(instr.lvalue as Place) }, + }, + path: [], + }); + } + } + } override visitScope( scopeBlock: ReactiveScopeBlock, - state: CompilerError + state: VisitorState ): void { this.traverseScope(scopeBlock, state); + if ( + state.manualMemoState != null && + state.manualMemoState.depsFromSource != null + ) { + for (const dep of scopeBlock.scope.dependencies) { + validateInferredDep( + dep, + this.temporaries, + state.manualMemoState.decls, + state.manualMemoState.depsFromSource, + state.errors + ); + } + } + /* * Record scopes that exist in the AST so we can later check to see if * effect dependencies which should be memoized (have a scope assigned) @@ -69,19 +306,54 @@ class Visitor extends ReactiveFunctionVisitor { override visitInstruction( instruction: ReactiveInstruction, - state: CompilerError + state: VisitorState ): void { this.traverseInstruction(instruction, state); - if ( - instruction.value.kind === "StartMemoize" || - instruction.value.kind === "FinishMemoize" - ) { - for (const value of eachInstructionValueOperand(instruction.value)) { + this.recordTemporaries(instruction, state); + if (instruction.value.kind === "StartMemoize") { + let depsFromSource: Array | null = null; + if (instruction.value.deps != null) { + depsFromSource = instruction.value.deps; + } + CompilerError.invariant(state.manualMemoState == null, { + reason: "Unexpected nested StartMemoize instructions", + description: `Bad manual memoization ids: ${state.manualMemoState?.manualMemoId}, ${instruction.value.manualMemoId}`, + loc: instruction.value.loc, + suggestions: null, + }); + + state.manualMemoState = { + decls: new Set(), + depsFromSource, + manualMemoId: instruction.value.manualMemoId, + }; + } + if (instruction.value.kind === "FinishMemoize") { + CompilerError.invariant( + state.manualMemoState != null && + state.manualMemoState.manualMemoId === instruction.value.manualMemoId, + { + reason: "Unexpected mismatch between StartMemoize and FinishMemoize", + description: `Encountered StartMemoize id=${state.manualMemoState?.manualMemoId} followed by FinishMemoize id=${instruction.value.manualMemoId}`, + loc: instruction.value.loc, + suggestions: null, + } + ); + state.manualMemoState = null; + } + + const isDep = instruction.value.kind === "StartMemoize"; + const isDecl = + instruction.value.kind === "FinishMemoize" && !instruction.value.pruned; + if (isDep || isDecl) { + for (const value of eachInstructionValueOperand( + instruction.value as InstructionValue + )) { if ( isMutable(instruction as Instruction, value) || - isUnmemoized(value.identifier, this.scopes) + (isDecl && isUnmemoized(value.identifier, this.scopes)) ) { - state.push({ + state.errors.push({ reason: "This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized", description: null, diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.expect.md deleted file mode 100644 index efaf98237a..0000000000 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.expect.md +++ /dev/null @@ -1,53 +0,0 @@ - -## Input - -```javascript -function App({ text, hasDeps }) { - const resolvedText = useMemo( - () => { - return text.toUpperCase(); - }, - hasDeps ? null : [text] // should be DCE'd - ); - return resolvedText; -} - -export const FIXTURE_ENTRYPOINT = { - fn: App, - params: ["TodoAdd"], - isComponent: "TodoAdd", -}; - -``` - -## Code - -```javascript -import { unstable_useMemoCache as useMemoCache } from "react"; -function App(t0) { - const $ = useMemoCache(2); - const { text, hasDeps } = t0; - - hasDeps ? null : [text]; - let t1; - let t2; - if ($[0] !== text) { - t2 = text.toUpperCase(); - $[0] = text; - $[1] = t2; - } else { - t2 = $[1]; - } - t1 = t2; - const resolvedText = t1; - return resolvedText; -} - -export const FIXTURE_ENTRYPOINT = { - fn: App, - params: ["TodoAdd"], - isComponent: "TodoAdd", -}; - -``` - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md index c0fd9b5c4b..795aa184e2 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md @@ -46,8 +46,6 @@ export const FIXTURE_ENTRYPOINT = { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 11 | }); | ^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11) - -[ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11) 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange 14 | const reset = () => { diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md index 6c57503e82..a1db951193 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md @@ -43,8 +43,6 @@ export const FIXTURE_ENTRYPOINT = { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 11 | }); | ^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11) - -[ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (7:11) 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange 14 | ref.current.inner = null; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.expect.md new file mode 100644 index 0000000000..b6c9a97c66 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +import { useMemo } from "react"; + +// react-hooks-deps would error on this code (complex expression in depslist), +// so Forget could bailout here +function App({ text, hasDeps }) { + const resolvedText = useMemo( + () => { + return text.toUpperCase(); + }, + hasDeps ? null : [text] // should be DCE'd + ); + return resolvedText; +} + +export const FIXTURE_ENTRYPOINT = { + fn: App, + params: ["TodoAdd"], + isComponent: "TodoAdd", +}; + +``` + + +## Error + +``` + 8 | return text.toUpperCase(); + 9 | }, +> 10 | hasDeps ? null : [text] // should be DCE'd + | ^^^^^^^^^^^^^^^^^^^^^^^ [ReactForget] InvalidReact: Expected the dependency list for useMemo to be an array literal without rest spreads (10:10) + 11 | ); + 12 | return resolvedText; + 13 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.ts similarity index 67% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.js rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.ts index 3dcbda0651..0a2b9a0f6f 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/bug.useMemo-deps-array-not-cleared.js +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.useMemo-non-literal-depslist.ts @@ -1,3 +1,7 @@ +import { useMemo } from "react"; + +// react-hooks-deps would error on this code (complex expression in depslist), +// so Forget could bailout here function App({ text, hasDeps }) { const resolvedText = useMemo( () => { diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.expect.md new file mode 100644 index 0000000000..0168421465 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.expect.md @@ -0,0 +1,28 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; + +// False positive as more specific memoization always results +// in fewer memo block executions. +// Precisely: +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev +// x.y.z_new != x.y.z_prev does imply x_new != x_prev +// One fix would be to depend on optional chains +function useHook(x) { + return useCallback(() => [x.y.z], [x]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.ts new file mode 100644 index 0000000000..ddb25337a3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.ts @@ -0,0 +1,13 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; + +// False positive as more specific memoization always results +// in fewer memo block executions. +// Precisely: +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev +// x.y.z_new != x.y.z_prev does imply x_new != x_prev +// One fix would be to depend on optional chains +function useHook(x) { + return useCallback(() => [x.y.z], [x]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.expect.md new file mode 100644 index 0000000000..37ab5f5543 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.expect.md @@ -0,0 +1,27 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// False positive as more specific memoization always results +// in fewer memo block executions. +// Precisely: +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev +// x.y.z_new != x.y.z_prev does imply x_new != x_prev +function useHook(x) { + return useMemo(() => [x.y.z], [x]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.ts new file mode 100644 index 0000000000..3047953ce3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.ts @@ -0,0 +1,12 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// False positive as more specific memoization always results +// in fewer memo block executions. +// Precisely: +// x_new != x_prev does not imply x.y.z_new != x.y.z_prev +// x.y.z_new != x.y.z_prev does imply x_new != x_prev +function useHook(x) { + return useMemo(() => [x.y.z], [x]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.expect.md new file mode 100644 index 0000000000..ace22180a0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +// This is a false positive as Forget's inferred memoization +// invalidates strictly less than source. We currently do not +// track transitive deps / invalidations of manual memo deps +// because of implementation complexity +function useFoo() { + const val = [1, 2, 3]; + + return useMemo(() => { + return identity(val); + }, [val]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + + +## Error + +``` + 10 | const val = [1, 2, 3]; + 11 | +> 12 | return useMemo(() => { + | ^^^^^^^ +> 13 | return identity(val); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +> 14 | }, [val]); + | ^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (12:14) + 15 | } + 16 | + 17 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.ts new file mode 100644 index 0000000000..d12f18d533 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-mutate-deps.ts @@ -0,0 +1,20 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +// This is a false positive as Forget's inferred memoization +// invalidates strictly less than source. We currently do not +// track transitive deps / invalidations of manual memo deps +// because of implementation complexity +function useFoo() { + const val = [1, 2, 3]; + + return useMemo(() => { + return identity(val); + }, [val]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.expect.md new file mode 100644 index 0000000000..093bc30543 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; +import { makeArray } from "shared-runtime"; + +// This case is already unsound in source, so we can safely bailout +function Foo(props) { + let x = []; + x.push(props); + + // makeArray() is captured, but depsList contains [props] + const cb = useCallback(() => [x], [x]); + + x = makeArray(); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + + +## Error + +``` + 10 | + 11 | // makeArray() is captured, but depsList contains [props] +> 12 | const cb = useCallback(() => [x], [x]); + | ^^^^^^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (12:12) + +[ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (12:12) + 13 | + 14 | x = makeArray(); + 15 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.ts new file mode 100644 index 0000000000..662b97fd6d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.invalid-useCallback-captures-reassigned-context.ts @@ -0,0 +1,21 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; +import { makeArray } from "shared-runtime"; + +// This case is already unsound in source, so we can safely bailout +function Foo(props) { + let x = []; + x.push(props); + + // makeArray() is captured, but depsList contains [props] + const cb = useCallback(() => [x], [x]); + + x = makeArray(); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md new file mode 100644 index 0000000000..8510b06d58 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function useHook(maybeRef) { + return useCallback(() => { + return [maybeRef.current]; + }, [maybeRef]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.ts new file mode 100644 index 0000000000..1784845047 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.ts @@ -0,0 +1,8 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function useHook(maybeRef) { + return useCallback(() => { + return [maybeRef.current]; + }, [maybeRef]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md new file mode 100644 index 0000000000..a40c0e4cb7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function useHook(maybeRef, shouldRead) { + return useMemo(() => { + return () => [maybeRef.current]; + }, [shouldRead, maybeRef]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.ts new file mode 100644 index 0000000000..f05052eb72 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.ts @@ -0,0 +1,8 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function useHook(maybeRef, shouldRead) { + return useMemo(() => { + return () => [maybeRef.current]; + }, [shouldRead, maybeRef]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.expect.md new file mode 100644 index 0000000000..6e76febc2d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; + +// False positive: +// We currently bail out on this because we don't understand +// that `() => [x]` gets pruned because `x` always invalidates. +function useFoo(props) { + const x = []; + useHook(); + x.push(props); + + return useCallback(() => [x], [x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; + +``` + + +## Error + +``` + 11 | x.push(props); + 12 | +> 13 | return useCallback(() => [x], [x]); + | ^^^^^^^^^ [ReactForget] InvalidReact: This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized (13:13) + 14 | } + 15 | + 16 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.ts new file mode 100644 index 0000000000..9ed54d96ab --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-invalidating-value.ts @@ -0,0 +1,19 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; + +// False positive: +// We currently bail out on this because we don't understand +// that `() => [x]` gets pruned because `x` always invalidates. +function useFoo(props) { + const x = []; + useHook(); + x.push(props); + + return useCallback(() => [x], [x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.expect.md new file mode 100644 index 0000000000..ae6c9b2146 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.expect.md @@ -0,0 +1,27 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +// This is technically a false positive, but source is already breaking +// `exhaustive-deps` lint rule (and can be considered invalid). +function useHook(x) { + const aliasedX = x; + const aliasedProp = x.y.z; + + return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]); +} + +``` + + +## Error + +``` +[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] + +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.ts new file mode 100644 index 0000000000..b6f64482a2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.ts @@ -0,0 +1,10 @@ +// @validatePreserveExistingMemoizationGuarantees + +// This is technically a false positive, but source is already breaking +// `exhaustive-deps` lint rule (and can be considered invalid). +function useHook(x) { + const aliasedX = x; + const aliasedProp = x.y.z; + + return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md new file mode 100644 index 0000000000..cc4883032f --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function Component({ propA, propB }) { + return useCallback(() => { + return { + value: propB?.x.y, + other: propA, + }; + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.ts new file mode 100644 index 0000000000..fc10cb9cdb --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.ts @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function Component({ propA, propB }) { + return useCallback(() => { + return { + value: propB?.x.y, + other: propA, + }; + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.expect.md new file mode 100644 index 0000000000..5b5f8eb4f6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { mutate } from "shared-runtime"; + +function Component({ propA, propB }) { + return useCallback(() => { + const x = {}; + if (propA?.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA?.a, propB.x.y]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.ts new file mode 100644 index 0000000000..c229263a5f --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-infer-less-specific-conditional-access.ts @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { mutate } from "shared-runtime"; + +function Component({ propA, propB }) { + return useCallback(() => { + const x = {}; + if (propA?.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA?.a, propB.x.y]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.expect.md new file mode 100644 index 0000000000..039ca8df1c --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function Component({ propA }) { + return useCallback(() => { + return propA.x(); + }, [propA.x]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.ts new file mode 100644 index 0000000000..9de911e2ca --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-property-call-dep.ts @@ -0,0 +1,8 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function Component({ propA }) { + return useCallback(() => { + return propA.x(); + }, [propA.x]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.expect.md new file mode 100644 index 0000000000..2e8a9465eb --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.expect.md @@ -0,0 +1,25 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +// This is technically a false positive, but source is already breaking +// `exhaustive-deps` lint rule (and can be considered invalid). +function useHook(x) { + const aliasedX = x; + const aliasedProp = x.y.z; + + return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.ts new file mode 100644 index 0000000000..e61c721034 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-aliased-var.ts @@ -0,0 +1,10 @@ +// @validatePreserveExistingMemoizationGuarantees + +// This is technically a false positive, but source is already breaking +// `exhaustive-deps` lint rule (and can be considered invalid). +function useHook(x) { + const aliasedX = x; + const aliasedProp = x.y.z; + + return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md new file mode 100644 index 0000000000..4e88cd2bb9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { makeArray } from "shared-runtime"; + +// We currently only recognize "hoistable" values (e.g. variable reads +// and property loads from named variables) in the source depslist. +// This makes validation logic simpler and follows the same constraints +// from the eslint react-hooks-deps plugin. +function Foo(props) { + const x = makeArray(props); + // react-hooks-deps lint would already fail here + return useMemo(() => [x[0]], [x[0]]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ val: 1 }], +}; + +``` + + +## Error + +``` + 11 | const x = makeArray(props); + 12 | // react-hooks-deps lint would already fail here +> 13 | return useMemo(() => [x[0]], [x[0]]); + | ^^^^ [ReactForget] InvalidReact: Expected the dependency list for useMemo to be an array of simple expressions (13:13) + 14 | } + 15 | + 16 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.ts new file mode 100644 index 0000000000..fe1e55791d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.ts @@ -0,0 +1,19 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { makeArray } from "shared-runtime"; + +// We currently only recognize "hoistable" values (e.g. variable reads +// and property loads from named variables) in the source depslist. +// This makes validation logic simpler and follows the same constraints +// from the eslint react-hooks-deps plugin. +function Foo(props) { + const x = makeArray(props); + // react-hooks-deps lint would already fail here + return useMemo(() => [x[0]], [x[0]]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ val: 1 }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md new file mode 100644 index 0000000000..f29bd83c83 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { mutate } from "shared-runtime"; + +function Component({ propA, propB }) { + return useMemo(() => { + const x = {}; + if (propA?.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA?.a, propB.x.y]); +} + +``` + + +## Error + +``` +[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] + +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.ts new file mode 100644 index 0000000000..e19b995c48 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.ts @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { mutate } from "shared-runtime"; + +function Component({ propA, propB }) { + return useMemo(() => { + const x = {}; + if (propA?.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA?.a, propB.x.y]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.expect.md new file mode 100644 index 0000000000..7bef888ac2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity, mutate } from "shared-runtime"; + +function Component({ propA, propB }) { + return useMemo(() => { + const x = {}; + if (identity(null) ?? propA.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA.a, propB.x.y]); +} + +``` + + +## Error + +``` +[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] + +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.ts new file mode 100644 index 0000000000..2a5e911838 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-value-block.ts @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity, mutate } from "shared-runtime"; + +function Component({ propA, propB }) { + return useMemo(() => { + const x = {}; + if (identity(null) ?? propA.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA.a, propB.x.y]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.expect.md new file mode 100644 index 0000000000..6002ab2bb1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.expect.md @@ -0,0 +1,25 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA }) { + return useMemo(() => { + return { + value: propA.x().y, + }; + }, [propA.x]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.ts new file mode 100644 index 0000000000..6723c57ba7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-chained-object.ts @@ -0,0 +1,10 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA }) { + return useMemo(() => { + return { + value: propA.x().y, + }; + }, [propA.x]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.expect.md new file mode 100644 index 0000000000..b8f1b0dafd --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.expect.md @@ -0,0 +1,23 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA }) { + return useMemo(() => { + return propA.x(); + }, [propA.x]); +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.ts new file mode 100644 index 0000000000..e2316b5caa --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-property-call-dep.ts @@ -0,0 +1,8 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA }) { + return useMemo(() => { + return propA.x(); + }, [propA.x]); +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md new file mode 100644 index 0000000000..e009af6236 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// Here, Forget infers that the memo block dependency is input1 +// 1. StartMemoize is emitted before the function expression +// (and thus before the depslist arg and its rvalues) +// 2. x and y's overlapping reactive scopes forces y's reactive +// scope to be extended to after the `mutate(x)` call, after +// the StartMemoize instruction. +// While this is technically a false positive, this example would +// already fail the exhaustive-deps eslint rule. +function useFoo(input1) { + const x = {}; + const y = [input1]; + const memoized = useMemo(() => { + return [y]; + }, [(mutate(x), y)]); + + return [x, memoized]; +} + +``` + + +## Error + +``` +[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] +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.ts new file mode 100644 index 0000000000..83c6af7e81 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.ts @@ -0,0 +1,21 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// Here, Forget infers that the memo block dependency is input1 +// 1. StartMemoize is emitted before the function expression +// (and thus before the depslist arg and its rvalues) +// 2. x and y's overlapping reactive scopes forces y's reactive +// scope to be extended to after the `mutate(x)` call, after +// the StartMemoize instruction. +// While this is technically a false positive, this example would +// already fail the exhaustive-deps eslint rule. +function useFoo(input1) { + const x = {}; + const y = [input1]; + const memoized = useMemo(() => { + return [y]; + }, [(mutate(x), y)]); + + return [x, memoized]; +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.expect.md new file mode 100644 index 0000000000..a115dab697 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +// We technically do not need to bailout here if we can check +// `someHelper`'s reactive deps are a subset of depslist from +// source. This check is somewhat incompatible with our current +// representation of manual memoization in HIR, so we bail out +// for now. +function Component(props) { + const x = useMemo(someHelper, []); + return x; +} + +``` + + +## Error + +``` + 7 | // for now. + 8 | function Component(props) { +> 9 | const x = useMemo(someHelper, []); + | ^^^^^^^^^^ [ReactForget] InvalidReact: Expected the first argument of useMemo to be an inline function expression (9:9) + 10 | return x; + 11 | } + 12 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.js new file mode 100644 index 0000000000..9281fbf670 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.validate-useMemo-named-function.js @@ -0,0 +1,11 @@ +// @validatePreserveExistingMemoizationGuarantees + +// We technically do not need to bailout here if we can check +// `someHelper`'s reactive deps are a subset of depslist from +// source. This check is somewhat incompatible with our current +// representation of manual memoization in HIR, so we bail out +// for now. +function Component(props) { + const x = useMemo(someHelper, []); + return x; +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md new file mode 100644 index 0000000000..fd007ab7c9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// This is currently considered valid because we don't ensure that every +// instruction within manual memoization gets assigned to a reactive scope +// (i.e. inferred non-mutable or non-escaping values don't get memoized) +function useFoo({ minWidth, styles, setStyles }) { + useMemo(() => { + if (styles.width > minWidth) { + setStyles(styles); + } + }, [styles, minWidth, setStyles]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ minWidth: 2, styles: { width: 1 }, setStyles: () => {} }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// This is currently considered valid because we don't ensure that every +// instruction within manual memoization gets assigned to a reactive scope +// (i.e. inferred non-mutable or non-escaping values don't get memoized) +function useFoo(t0) { + const { minWidth, styles, setStyles } = t0; + let t1; + if (styles.width > minWidth) { + setStyles(styles); + } + t1 = undefined; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ minWidth: 2, styles: { width: 1 }, setStyles: () => {} }], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.ts new file mode 100644 index 0000000000..426b59538a --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.ts @@ -0,0 +1,19 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// This is currently considered valid because we don't ensure that every +// instruction within manual memoization gets assigned to a reactive scope +// (i.e. inferred non-mutable or non-escaping values don't get memoized) +function useFoo({ minWidth, styles, setStyles }) { + useMemo(() => { + if (styles.width > minWidth) { + setStyles(styles); + } + }, [styles, minWidth, setStyles]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ minWidth: 2, styles: { width: 1 }, setStyles: () => {} }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md new file mode 100644 index 0000000000..c8cda966c4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md @@ -0,0 +1,62 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// Todo: we currently only generate a `constVal` declaration when +// validatePreserveExistingMemoizationGuarantees is enabled, as the +// StartMemoize instruction uses `constVal`. +// Fix is to rewrite StartMemoize instructions to remove constant +// propagated values +function useFoo() { + const constVal = 0; + + return useMemo(() => [constVal], [constVal]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +// Todo: we currently only generate a `constVal` declaration when +// validatePreserveExistingMemoizationGuarantees is enabled, as the +// StartMemoize instruction uses `constVal`. +// Fix is to rewrite StartMemoize instructions to remove constant +// propagated values +function useFoo() { + const $ = useMemoCache(1); + const constVal = 0; + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [0]; + $[0] = t1; + } else { + t1 = $[0]; + } + t0 = t1; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) [0] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.ts new file mode 100644 index 0000000000..cd6b54e3af --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.ts @@ -0,0 +1,19 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// Todo: we currently only generate a `constVal` declaration when +// validatePreserveExistingMemoizationGuarantees is enabled, as the +// StartMemoize instruction uses `constVal`. +// Fix is to rewrite StartMemoize instructions to remove constant +// propagated values +function useFoo() { + const constVal = 0; + + return useMemo(() => [constVal], [constVal]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.expect.md new file mode 100644 index 0000000000..2c7bd2413f --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { sum } from "shared-runtime"; + +function Component({ propA, propB }) { + const x = propB.x.y; + return useCallback(() => { + return sum(propA.x, x); + }, [propA.x, x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { sum } from "shared-runtime"; + +function Component(t0) { + const $ = useMemoCache(3); + const { propA, propB } = t0; + const x = propB.x.y; + let t1; + if ($[0] !== propA.x || $[1] !== x) { + t1 = () => sum(propA.x, x); + $[0] = propA.x; + $[1] = x; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.ts new file mode 100644 index 0000000000..2769fccd3d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-alias-property-load-dep.ts @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { sum } from "shared-runtime"; + +function Component({ propA, propB }) { + const x = propB.x.y; + return useCallback(() => { + return sum(propA.x, x); + }, [propA.x, x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md new file mode 100644 index 0000000000..84fdf1d808 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(props) { + let contextVar; + if (props.cond) { + contextVar = { val: 2 }; + } else { + contextVar = {}; + } + + const cb = useCallback(() => [contextVar.val], [contextVar.val]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ cond: true }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(props) { + const $ = useMemoCache(6); + let contextVar; + if ($[0] !== props.cond) { + if (props.cond) { + contextVar = { val: 2 }; + } else { + contextVar = {}; + } + $[0] = props.cond; + $[1] = contextVar; + } else { + contextVar = $[1]; + } + + const t0 = contextVar; + let t1; + if ($[2] !== t0.val) { + t1 = () => [contextVar.val]; + $[2] = t0.val; + $[3] = t1; + } else { + t1 = $[3]; + } + contextVar; + const cb = t1; + let t2; + if ($[4] !== cb) { + t2 = ; + $[4] = cb; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ cond: true }], +}; + +``` + +### Eval output +(kind: ok)
{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx new file mode 100644 index 0000000000..9da433e163 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx @@ -0,0 +1,21 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(props) { + let contextVar; + if (props.cond) { + contextVar = { val: 2 }; + } else { + contextVar = {}; + } + + const cb = useCallback(() => [contextVar.val], [contextVar.val]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ cond: true }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md new file mode 100644 index 0000000000..ecb5bbc805 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; +import { makeArray } from "shared-runtime"; + +// This case is fine, as all reassignments happen before the useCallback +function Foo(props) { + let x = []; + x.push(props); + x = makeArray(); + + const cb = useCallback(() => [x], [x]); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { makeArray } from "shared-runtime"; + +// This case is fine, as all reassignments happen before the useCallback +function Foo(props) { + const $ = useMemoCache(4); + let x; + if ($[0] !== props) { + x = []; + x.push(props); + x = makeArray(); + $[0] = props; + $[1] = x; + } else { + x = $[1]; + } + + const t0 = x; + let t1; + if ($[2] !== t0) { + t1 = () => [x]; + $[2] = t0; + $[3] = t1; + } else { + t1 = $[3]; + } + x; + const cb = t1; + return cb; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.ts new file mode 100644 index 0000000000..d42d93fde8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.ts @@ -0,0 +1,19 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; +import { makeArray } from "shared-runtime"; + +// This case is fine, as all reassignments happen before the useCallback +function Foo(props) { + let x = []; + x.push(props); + x = makeArray(); + + const cb = useCallback(() => [x], [x]); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md new file mode 100644 index 0000000000..b4189be555 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function Component({ propA, propB }) { + return useCallback(() => { + if (propA) { + return { + value: propB.x.y, + }; + } + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 1, propB: { x: { y: [] } } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; + +function Component(t0) { + const $ = useMemoCache(3); + const { propA, propB } = t0; + let t1; + if ($[0] !== propA || $[1] !== propB.x.y) { + t1 = () => { + if (propA) { + return { value: propB.x.y }; + } + }; + $[0] = propA; + $[1] = propB.x.y; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 1, propB: { x: { y: [] } } }], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.ts new file mode 100644 index 0000000000..05cacc27d0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.ts @@ -0,0 +1,17 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +function Component({ propA, propB }) { + return useCallback(() => { + if (propA) { + return { + value: propB.x.y, + }; + } + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 1, propB: { x: { y: [] } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.expect.md new file mode 100644 index 0000000000..ccbed13e46 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.expect.md @@ -0,0 +1,91 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, useState } from "react"; +import { arrayPush } from "shared-runtime"; + +// useCallback-produced values can exist in nested reactive blocks, as long +// as their reactive dependencies are a subset of depslist from source +function useFoo(minWidth, otherProp) { + const [width, setWidth] = useState(1); + const x = []; + const style = useCallback(() => { + return { + width: Math.max(minWidth, width), + }; + }, [width, minWidth]); + arrayPush(x, otherProp); + return [style, x]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, "other"], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { + useCallback, + useState, + unstable_useMemoCache as useMemoCache, +} from "react"; +import { arrayPush } from "shared-runtime"; + +// useCallback-produced values can exist in nested reactive blocks, as long +// as their reactive dependencies are a subset of depslist from source +function useFoo(minWidth, otherProp) { + const $ = useMemoCache(11); + const [width] = useState(1); + let style; + let x; + if ($[0] !== width || $[1] !== minWidth || $[2] !== otherProp) { + x = []; + let t0; + if ($[5] !== minWidth || $[6] !== width) { + t0 = () => ({ width: Math.max(minWidth, width) }); + $[5] = minWidth; + $[6] = width; + $[7] = t0; + } else { + t0 = $[7]; + } + style = t0; + + arrayPush(x, otherProp); + $[0] = width; + $[1] = minWidth; + $[2] = otherProp; + $[3] = style; + $[4] = x; + } else { + style = $[3]; + x = $[4]; + } + let t0; + if ($[8] !== style || $[9] !== x) { + t0 = [style, x]; + $[8] = style; + $[9] = x; + $[10] = t0; + } else { + t0 = $[10]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, "other"], +}; + +``` + +### Eval output +(kind: ok) ["[[ function params=0 ]]",["other"]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.ts new file mode 100644 index 0000000000..5fb14fa7d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-in-other-reactive-block.ts @@ -0,0 +1,22 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, useState } from "react"; +import { arrayPush } from "shared-runtime"; + +// useCallback-produced values can exist in nested reactive blocks, as long +// as their reactive dependencies are a subset of depslist from source +function useFoo(minWidth, otherProp) { + const [width, setWidth] = useState(1); + const x = []; + const style = useCallback(() => { + return { + width: Math.max(minWidth, width), + }; + }, [width, minWidth]); + arrayPush(x, otherProp); + return [style, x]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, "other"], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md new file mode 100644 index 0000000000..8943352821 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { identity, mutate } from "shared-runtime"; + +function useHook(propA, propB) { + return useCallback(() => { + const x = {}; + if (identity(null) ?? propA.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA.a, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ a: 1 }, { x: { y: 3 } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { identity, mutate } from "shared-runtime"; + +function useHook(propA, propB) { + const $ = useMemoCache(3); + let t0; + if ($[0] !== propA.a || $[1] !== propB.x.y) { + t0 = () => { + const x = {}; + if (identity(null) ?? propA.a) { + mutate(x); + return { value: propB.x.y }; + } + }; + $[0] = propA.a; + $[1] = propB.x.y; + $[2] = t0; + } else { + t0 = $[2]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ a: 1 }, { x: { y: 3 } }], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.ts new file mode 100644 index 0000000000..43cf0aedc8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.ts @@ -0,0 +1,20 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { identity, mutate } from "shared-runtime"; + +function useHook(propA, propB) { + return useCallback(() => { + const x = {}; + if (identity(null) ?? propA.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA.a, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ a: 1 }, { x: { y: 3 } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.expect.md new file mode 100644 index 0000000000..3eaf4da6e9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; + +// It's correct to produce memo blocks with fewer deps than source +function useFoo(a, b) { + return useCallback(() => [a], [a, b]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; + +// It's correct to produce memo blocks with fewer deps than source +function useFoo(a, b) { + const $ = useMemoCache(2); + let t0; + if ($[0] !== a) { + t0 = () => [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.ts new file mode 100644 index 0000000000..2781634596 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-fewer-deps.ts @@ -0,0 +1,13 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; + +// It's correct to produce memo blocks with fewer deps than source +function useFoo(a, b) { + return useCallback(() => [a], [a, b]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.expect.md new file mode 100644 index 0000000000..33e621a8b4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.expect.md @@ -0,0 +1,59 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { sum } from "shared-runtime"; + +function useFoo() { + const val = [1, 2, 3]; + + return useCallback(() => { + return sum(...val); + }, [val]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { sum } from "shared-runtime"; + +function useFoo() { + const $ = useMemoCache(2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = [1, 2, 3]; + $[0] = t0; + } else { + t0 = $[0]; + } + const val = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => sum(...val); + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.ts new file mode 100644 index 0000000000..aa0a9f0783 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-read-dep.ts @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; +import { sum } from "shared-runtime"; + +function useFoo() { + const val = [1, 2, 3]; + + return useCallback(() => { + return sum(...val); + }, [val]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.expect.md new file mode 100644 index 0000000000..c98d3595a8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; +import { CONST_STRING0 } from "shared-runtime"; + +// It's correct to infer a useCallback block has no reactive dependencies +function useFoo() { + return useCallback(() => [CONST_STRING0], [CONST_STRING0]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { CONST_STRING0 } from "shared-runtime"; + +// It's correct to infer a useCallback block has no reactive dependencies +function useFoo() { + const $ = useMemoCache(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => [CONST_STRING0]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.ts new file mode 100644 index 0000000000..9ab1e1742d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-scope-global.ts @@ -0,0 +1,14 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useCallback } from "react"; +import { CONST_STRING0 } from "shared-runtime"; + +// It's correct to infer a useCallback block has no reactive dependencies +function useFoo() { + return useCallback(() => [CONST_STRING0], [CONST_STRING0]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-invoked-callback-escaping-return.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md similarity index 100% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-invoked-callback-escaping-return.expect.md rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-invoked-callback-escaping-return.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.js similarity index 100% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-invoked-callback-escaping-return.js rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.js diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-value.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.expect.md similarity index 95% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-value.expect.md rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.expect.md index f5e23b485d..a611698261 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-value.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.expect.md @@ -9,7 +9,7 @@ function Component({ entity, children }) { // showMessage doesn't escape so we don't memoize it. // However, validatePreserveExistingMemoizationGuarantees only sees that the scope // doesn't exist, and thinks the memoization was missed instead of being intentionally dropped. - const showMessage = useCallback(() => entity != null); + const showMessage = useCallback(() => entity != null, [entity]); if (!showMessage()) { return children; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-value.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.js similarity index 91% rename from compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-value.js rename to compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.js index 0cf4280e03..9d9c9290ac 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-false-positive-preserve-memoization-nonescaping-value.js +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping.js @@ -5,7 +5,7 @@ function Component({ entity, children }) { // showMessage doesn't escape so we don't memoize it. // However, validatePreserveExistingMemoizationGuarantees only sees that the scope // doesn't exist, and thinks the memoization was missed instead of being intentionally dropped. - const showMessage = useCallback(() => entity != null); + const showMessage = useCallback(() => entity != null, [entity]); if (!showMessage()) { return children; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md new file mode 100644 index 0000000000..71a13cdba1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md @@ -0,0 +1,104 @@ + +## Input + +```javascript +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo({ arr1, arr2, foo }) { + const x = [arr1]; + + let y = []; + + const getVal1 = useCallback(() => { + return { x: 2 }; + }, []); + + const getVal2 = useCallback(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; + +``` + +## Code + +```javascript +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(t0) { + const $ = useMemoCache(11); + const { arr1, arr2, foo } = t0; + let t1; + if ($[0] !== arr1) { + t1 = [arr1]; + $[0] = arr1; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let t2; + let getVal1; + if ($[2] !== foo || $[3] !== x || $[4] !== arr2) { + let y; + y = []; + let t3; + if ($[7] === Symbol.for("react.memo_cache_sentinel")) { + t3 = () => ({ x: 2 }); + $[7] = t3; + } else { + t3 = $[7]; + } + getVal1 = t3; + + t2 = () => [y]; + foo ? (y = x.concat(arr2)) : y; + $[2] = foo; + $[3] = x; + $[4] = arr2; + $[5] = t2; + $[6] = getVal1; + } else { + t2 = $[5]; + getVal1 = $[6]; + } + const getVal2 = t2; + let t3; + if ($[8] !== getVal1 || $[9] !== getVal2) { + t3 = ; + $[8] = getVal1; + $[9] = getVal2; + $[10] = t3; + } else { + t3 = $[10]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[[1,2],3,4]]},"shouldInvokeFns":true}
+
{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[]]},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.tsx b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.tsx new file mode 100644 index 0000000000..105faa67b7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.tsx @@ -0,0 +1,27 @@ +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo({ arr1, arr2, foo }) { + const x = [arr1]; + + let y = []; + + const getVal1 = useCallback(() => { + return { x: 2 }; + }, []); + + const getVal2 = useCallback(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md new file mode 100644 index 0000000000..3158f3a503 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + const getVal = useCallback(() => { + return { y }; + }, [((y = x.concat(arr2)), y)]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +## Code + +```javascript +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; +import { Stringify } from "shared-runtime"; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const $ = useMemoCache(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let t1; + if ($[2] !== x || $[3] !== arr2) { + let y; + t1 = () => ({ y }); + + (y = x.concat(arr2)), y; + $[2] = x; + $[3] = arr2; + $[4] = t1; + } else { + t1 = $[4]; + } + const getVal = t1; + let t2; + if ($[5] !== getVal) { + t2 = ; + $[5] = getVal; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +### Eval output +(kind: ok)
{"getVal":{"kind":"Function","result":{"y":[[1,2],3,4]}},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.tsx b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.tsx new file mode 100644 index 0000000000..192fe4f4bb --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.tsx @@ -0,0 +1,22 @@ +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + const getVal = useCallback(() => { + return { y }; + }, [((y = x.concat(arr2)), y)]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.expect.md new file mode 100644 index 0000000000..606ac0452b --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +// Compiler can produce any memoization it finds valid if the +// source listed no memo deps +function Component({ propA }) { + // @ts-ignore + return useCallback(() => { + return [propA]; + }); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2 }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useCallback, unstable_useMemoCache as useMemoCache } from "react"; + +// Compiler can produce any memoization it finds valid if the +// source listed no memo deps +function Component(t0) { + const $ = useMemoCache(2); + const { propA } = t0; + let t1; + if ($[0] !== propA) { + t1 = () => [propA]; + $[0] = propA; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2 }], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.ts new file mode 100644 index 0000000000..620a057a84 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-with-no-depslist.ts @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useCallback } from "react"; + +// Compiler can produce any memoization it finds valid if the +// source listed no memo deps +function Component({ propA }) { + // @ts-ignore + return useCallback(() => { + return [propA]; + }); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2 }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md new file mode 100644 index 0000000000..e8f4a32ee9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { sum } from "shared-runtime"; + +function Component({ propA, propB }) { + const x = propB.x.y; + return useMemo(() => { + return sum(propA.x, x); + }, [propA.x, x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; +import { sum } from "shared-runtime"; + +function Component(t0) { + const $ = useMemoCache(3); + const { propA, propB } = t0; + const x = propB.x.y; + let t1; + let t2; + if ($[0] !== propA.x || $[1] !== x) { + t2 = sum(propA.x, x); + $[0] = propA.x; + $[1] = x; + $[2] = t2; + } else { + t2 = $[2]; + } + t1 = t2; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }], +}; + +``` + +### Eval output +(kind: ok) 5 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.ts new file mode 100644 index 0000000000..bcbec5f3d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.ts @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { sum } from "shared-runtime"; + +function Component({ propA, propB }) { + const x = propB.x.y; + return useMemo(() => { + return sum(propA.x, x); + }, [propA.x, x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: { x: 2 }, propB: { x: { y: 3 } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md new file mode 100644 index 0000000000..17490fc1a1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function Component({ propA, propB }) { + return useMemo(() => { + return { + value: identity(propB?.x.y), + other: propA, + }; + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function Component(t0) { + const $ = useMemoCache(5); + const { propA, propB } = t0; + let t1; + + const t2 = propB?.x.y; + let t3; + if ($[0] !== t2) { + t3 = identity(t2); + $[0] = t2; + $[1] = t3; + } else { + t3 = $[1]; + } + let t4; + if ($[2] !== t3 || $[3] !== propA) { + t4 = { value: t3, other: propA }; + $[2] = t3; + $[3] = propA; + $[4] = t4; + } else { + t4 = $[4]; + } + t1 = t4; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; + +``` + +### Eval output +(kind: ok) {"value":[],"other":2} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.ts new file mode 100644 index 0000000000..f1e5ec2e9a --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.ts @@ -0,0 +1,17 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function Component({ propA, propB }) { + return useMemo(() => { + return { + value: identity(propB?.x.y), + other: propA, + }; + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md new file mode 100644 index 0000000000..285b708d91 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA, propB }) { + return useMemo(() => { + return { + value: propB?.x.y, + other: propA, + }; + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +function Component(t0) { + const $ = useMemoCache(3); + const { propA, propB } = t0; + let t1; + + const t2 = propB?.x.y; + let t3; + if ($[0] !== t2 || $[1] !== propA) { + t3 = { value: t2, other: propA }; + $[0] = t2; + $[1] = propA; + $[2] = t3; + } else { + t3 = $[2]; + } + t1 = t3; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; + +``` + +### Eval output +(kind: ok) {"value":[],"other":2} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.ts new file mode 100644 index 0000000000..37c39171c4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.ts @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA, propB }) { + return useMemo(() => { + return { + value: propB?.x.y, + other: propA, + }; + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2, propB: { x: { y: [] } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.expect.md new file mode 100644 index 0000000000..e774691faa --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA, propB }) { + return useMemo(() => { + if (propA) { + return { + value: propB.x.y, + }; + } + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 1, propB: { x: { y: [] } } }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +function Component(t0) { + const $ = useMemoCache(2); + const { propA, propB } = t0; + let t1; + bb6: { + if (propA) { + let t2; + if ($[0] !== propB.x.y) { + t2 = { value: propB.x.y }; + $[0] = propB.x.y; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + break bb6; + } + t1 = undefined; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 1, propB: { x: { y: [] } } }], +}; + +``` + +### Eval output +(kind: ok) {"value":[]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.ts new file mode 100644 index 0000000000..390f9fde42 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-own-scope.ts @@ -0,0 +1,17 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +function Component({ propA, propB }) { + return useMemo(() => { + if (propA) { + return { + value: propB.x.y, + }; + } + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 1, propB: { x: { y: [] } } }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md new file mode 100644 index 0000000000..328e5222c9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(cond) { + const sourceDep = 0; + const derived1 = useMemo(() => { + return identity(sourceDep); + }, [sourceDep]); + const derived2 = cond ?? Math.min(sourceDep, 1) ? 1 : 2; + const derived3 = useMemo(() => { + return identity(sourceDep); + }, [sourceDep]); + const derived4 = Math.min(sourceDep, -1) ?? cond ? 1 : 2; + return [derived1, derived2, derived3, derived4]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(cond) { + const $ = useMemoCache(5); + const sourceDep = 0; + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = identity(0); + $[0] = t1; + } else { + t1 = $[0]; + } + t0 = t1; + const derived1 = t0; + + const derived2 = cond ?? Math.min(0, 1) ? 1 : 2; + let t2; + let t3; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t3 = identity(0); + $[1] = t3; + } else { + t3 = $[1]; + } + t2 = t3; + const derived3 = t2; + + const derived4 = Math.min(0, -1) ?? cond ? 1 : 2; + let t4; + if ($[2] !== derived2 || $[3] !== derived4) { + t4 = [derived1, derived2, derived3, derived4]; + $[2] = derived2; + $[3] = derived4; + $[4] = t4; + } else { + t4 = $[4]; + } + return t4; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; + +``` + +### Eval output +(kind: ok) [0,1,0,1] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.ts new file mode 100644 index 0000000000..880e90e4b6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.ts @@ -0,0 +1,21 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(cond) { + const sourceDep = 0; + const derived1 = useMemo(() => { + return identity(sourceDep); + }, [sourceDep]); + const derived2 = cond ?? Math.min(sourceDep, 1) ? 1 : 2; + const derived3 = useMemo(() => { + return identity(sourceDep); + }, [sourceDep]); + const derived4 = Math.min(sourceDep, -1) ?? cond ? 1 : 2; + return [derived1, derived2, derived3, derived4]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.expect.md new file mode 100644 index 0000000000..9d958cd2a0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { useHook } from "shared-runtime"; + +// useMemo values may not be memoized in Forget output if we +// infer that their deps always invalidate. +// This is still correct as the useMemo in source was effectively +// a no-op already. +function useFoo(props) { + const x = []; + useHook(); + x.push(props); + + return useMemo(() => [x], [x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { useHook } from "shared-runtime"; + +// useMemo values may not be memoized in Forget output if we +// infer that their deps always invalidate. +// This is still correct as the useMemo in source was effectively +// a no-op already. +function useFoo(props) { + const x = []; + useHook(); + x.push(props); + let t0; + t0 = [x]; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) [[{}]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.ts new file mode 100644 index 0000000000..7ee6f5958c --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.ts @@ -0,0 +1,21 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { useHook } from "shared-runtime"; + +// useMemo values may not be memoized in Forget output if we +// infer that their deps always invalidate. +// This is still correct as the useMemo in source was effectively +// a no-op already. +function useFoo(props) { + const x = []; + useHook(); + x.push(props); + + return useMemo(() => [x], [x]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md new file mode 100644 index 0000000000..f16b4a7ae3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md @@ -0,0 +1,94 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, useState } from "react"; +import { arrayPush } from "shared-runtime"; + +// useMemo-produced values can exist in nested reactive blocks, as long +// as their reactive dependencies are a subset of depslist from source +function useFoo(minWidth, otherProp) { + const [width, setWidth] = useState(1); + const x = []; + const style = useMemo(() => { + return { + width: Math.max(minWidth, width), + }; + }, [width, minWidth]); + arrayPush(x, otherProp); + return [style, x]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, "other"], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { + useMemo, + useState, + unstable_useMemoCache as useMemoCache, +} from "react"; +import { arrayPush } from "shared-runtime"; + +// useMemo-produced values can exist in nested reactive blocks, as long +// as their reactive dependencies are a subset of depslist from source +function useFoo(minWidth, otherProp) { + const $ = useMemoCache(10); + const [width] = useState(1); + let style; + let x; + if ($[0] !== width || $[1] !== minWidth || $[2] !== otherProp) { + x = []; + let t0; + + const t1 = Math.max(minWidth, width); + let t2; + if ($[5] !== t1) { + t2 = { width: t1 }; + $[5] = t1; + $[6] = t2; + } else { + t2 = $[6]; + } + t0 = t2; + style = t0; + + arrayPush(x, otherProp); + $[0] = width; + $[1] = minWidth; + $[2] = otherProp; + $[3] = style; + $[4] = x; + } else { + style = $[3]; + x = $[4]; + } + let t0; + if ($[7] !== style || $[8] !== x) { + t0 = [style, x]; + $[7] = style; + $[8] = x; + $[9] = t0; + } else { + t0 = $[9]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, "other"], +}; + +``` + +### Eval output +(kind: ok) [{"width":2},["other"]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.ts new file mode 100644 index 0000000000..e522426cb3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.ts @@ -0,0 +1,22 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, useState } from "react"; +import { arrayPush } from "shared-runtime"; + +// useMemo-produced values can exist in nested reactive blocks, as long +// as their reactive dependencies are a subset of depslist from source +function useFoo(minWidth, otherProp) { + const [width, setWidth] = useState(1); + const x = []; + const style = useMemo(() => { + return { + width: Math.max(minWidth, width), + }; + }, [width, minWidth]); + arrayPush(x, otherProp); + return [style, x]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, "other"], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md new file mode 100644 index 0000000000..a26d760158 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// It's correct to produce memo blocks with fewer deps than source +function useFoo(a, b) { + return useMemo(() => [a], [a, b]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +// It's correct to produce memo blocks with fewer deps than source +function useFoo(a, b) { + const $ = useMemoCache(2); + let t0; + let t1; + if ($[0] !== a) { + t1 = [a]; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + t0 = t1; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2], +}; + +``` + +### Eval output +(kind: ok) [1] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.ts new file mode 100644 index 0000000000..b080af5e6d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.ts @@ -0,0 +1,13 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// It's correct to produce memo blocks with fewer deps than source +function useFoo(a, b) { + return useMemo(() => [a], [a, b]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [1, 2], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md new file mode 100644 index 0000000000..acafbd072b --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// It's correct to infer a useMemo value is non-allocating +// and not provide it with a reactive scope +function useFoo(num1, num2) { + return useMemo(() => Math.min(num1, num2), [num1, num2]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, 3], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +// It's correct to infer a useMemo value is non-allocating +// and not provide it with a reactive scope +function useFoo(num1, num2) { + const $ = useMemoCache(3); + let t0; + let t1; + if ($[0] !== num1 || $[1] !== num2) { + t1 = Math.min(num1, num2); + $[0] = num1; + $[1] = num2; + $[2] = t1; + } else { + t1 = $[2]; + } + t0 = t1; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, 3], +}; + +``` + +### Eval output +(kind: ok) 2 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.ts new file mode 100644 index 0000000000..81f707049f --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.ts @@ -0,0 +1,14 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +// It's correct to infer a useMemo value is non-allocating +// and not provide it with a reactive scope +function useFoo(num1, num2) { + return useMemo(() => Math.min(num1, num2), [num1, num2]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [2, 3], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md new file mode 100644 index 0000000000..9fb909c33e --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { CONST_STRING0 } from "shared-runtime"; + +// It's correct to infer a useMemo block has no reactive dependencies +function useFoo() { + return useMemo(() => [CONST_STRING0], [CONST_STRING0]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; +import { CONST_STRING0 } from "shared-runtime"; + +// It's correct to infer a useMemo block has no reactive dependencies +function useFoo() { + const $ = useMemoCache(1); + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [CONST_STRING0]; + $[0] = t1; + } else { + t1 = $[0]; + } + t0 = t1; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: ok) ["global string 0"] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.ts new file mode 100644 index 0000000000..7470e42eab --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.ts @@ -0,0 +1,14 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { CONST_STRING0 } from "shared-runtime"; + +// It's correct to infer a useMemo block has no reactive dependencies +function useFoo() { + return useMemo(() => [CONST_STRING0], [CONST_STRING0]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md new file mode 100644 index 0000000000..caceee2aaf --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md @@ -0,0 +1,62 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(data) { + return useMemo(() => { + const temp = identity(data.a); + return { temp }; + }, [data.a]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ a: 2 }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(data) { + const $ = useMemoCache(4); + let t0; + let t1; + if ($[0] !== data.a) { + t1 = identity(data.a); + $[0] = data.a; + $[1] = t1; + } else { + t1 = $[1]; + } + const temp = t1; + let t2; + if ($[2] !== temp) { + t2 = { temp }; + $[2] = temp; + $[3] = t2; + } else { + t2 = $[3]; + } + t0 = t2; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ a: 2 }], +}; + +``` + +### Eval output +(kind: ok) {"temp":2} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.ts new file mode 100644 index 0000000000..64485e7599 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.ts @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function useFoo(data) { + return useMemo(() => { + const temp = identity(data.a); + return { temp }; + }, [data.a]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ a: 2 }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md new file mode 100644 index 0000000000..cb6ecde497 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +function useFoo({ callback }) { + return useMemo(() => new Array(callback()), [callback]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + { + callback: () => { + "use no forget"; + return [1, 2, 3]; + }, + }, + ], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +function useFoo(t0) { + const $ = useMemoCache(2); + const { callback } = t0; + let t1; + let t2; + if ($[0] !== callback) { + t2 = new Array(callback()); + $[0] = callback; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + { + callback: () => { + "use no forget"; + return [1, 2, 3]; + }, + }, + ], +}; + +``` + +### Eval output +(kind: ok) [[1,2,3]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.ts new file mode 100644 index 0000000000..ad3ad2d9b9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.ts @@ -0,0 +1,19 @@ +// @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; + +function useFoo({ callback }) { + return useMemo(() => new Array(callback()), [callback]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + { + callback: () => { + "use no forget"; + return [1, 2, 3]; + }, + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md new file mode 100644 index 0000000000..658aaf7bc2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +import { useMemo } from "react"; + +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + return useMemo(() => { + return { y }; + }, [((y = x.concat(arr2)), y)]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +## Code + +```javascript +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +function useFoo(arr1, arr2) { + const $ = useMemoCache(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x || $[3] !== arr2) { + y; + (y = x.concat(arr2)), y; + $[2] = x; + $[3] = arr2; + $[4] = y; + } else { + y = $[4]; + } + let t1; + const t2 = y; + let t3; + if ($[5] !== t2) { + t3 = { y: t2 }; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + t1 = t3; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +### Eval output +(kind: ok) {"y":[[1,2],3,4]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.ts new file mode 100644 index 0000000000..defb7ece1d --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.ts @@ -0,0 +1,18 @@ +import { useMemo } from "react"; + +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + return useMemo(() => { + return { y }; + }, [((y = x.concat(arr2)), y)]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md new file mode 100644 index 0000000000..1c5003702c --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md @@ -0,0 +1,106 @@ + +## Input + +```javascript +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo({ arr1, arr2, foo }) { + const x = [arr1]; + + let y = []; + + const val1 = useMemo(() => { + return { x: 2 }; + }, []); + + const val2 = useMemo(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; + +``` + +## Code + +```javascript +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(t0) { + const $ = useMemoCache(11); + const { arr1, arr2, foo } = t0; + let t1; + if ($[0] !== arr1) { + t1 = [arr1]; + $[0] = arr1; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let t2; + let val1; + if ($[2] !== foo || $[3] !== x || $[4] !== arr2) { + let y; + y = []; + let t3; + let t4; + if ($[7] === Symbol.for("react.memo_cache_sentinel")) { + t4 = { x: 2 }; + $[7] = t4; + } else { + t4 = $[7]; + } + t3 = t4; + val1 = t3; + + foo ? (y = x.concat(arr2)) : y; + t2 = (() => [y])(); + $[2] = foo; + $[3] = x; + $[4] = arr2; + $[5] = t2; + $[6] = val1; + } else { + t2 = $[5]; + val1 = $[6]; + } + const val2 = t2; + let t3; + if ($[8] !== val1 || $[9] !== val2) { + t3 = ; + $[8] = val1; + $[9] = val2; + $[10] = t3; + } else { + t3 = $[10]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"val1":{"x":2},"val2":[[[1,2],3,4]]}
+
{"val1":{"x":2},"val2":[[]]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.tsx b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.tsx new file mode 100644 index 0000000000..906b3a2447 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.tsx @@ -0,0 +1,27 @@ +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo({ arr1, arr2, foo }) { + const x = [arr1]; + + let y = []; + + const val1 = useMemo(() => { + return { x: 2 }; + }, []); + + const val2 = useMemo(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md new file mode 100644 index 0000000000..8b5f15af0b --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +// Compiler can produce any memoization it finds valid if the +// source listed no memo deps +function Component({ propA }) { + // @ts-ignore + return useMemo(() => { + return [propA]; + }); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2 }], +}; + +``` + +## Code + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import { useMemo, unstable_useMemoCache as useMemoCache } from "react"; + +// Compiler can produce any memoization it finds valid if the +// source listed no memo deps +function Component(t0) { + const $ = useMemoCache(2); + const { propA } = t0; + let t1; + let t2; + if ($[0] !== propA) { + t2 = [propA]; + $[0] = propA; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2 }], +}; + +``` + +### Eval output +(kind: ok) [2] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.ts b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.ts new file mode 100644 index 0000000000..8bd314e789 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.ts @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; + +// Compiler can produce any memoization it finds valid if the +// source listed no memo deps +function Component({ propA }) { + // @ts-ignore + return useMemo(() => { + return [propA]; + }); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ propA: 2 }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md index b49247e9c7..ce4c9ae130 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md @@ -27,7 +27,7 @@ function Component(props) { x.value = props.value; mutate(x, free, part); return x; - }, [props.value]); + }, [props.value, free, part]); // These calls should be inferred as non-mutating due to the above freeze inference identity(free); diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js index 276fe6aa0c..e445abe11f 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.js @@ -23,7 +23,7 @@ function Component(props) { x.value = props.value; mutate(x, free, part); return x; - }, [props.value]); + }, [props.value, free, part]); // These calls should be inferred as non-mutating due to the above freeze inference identity(free); diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index c13c60fdba..6b6b87c2ca 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -96,6 +96,10 @@ export function setProperty(arg: any, property: any): void { } } +export function arrayPush(arr: Array, ...values: Array): void { + arr.push(...values); +} + export function graphql(value: string): string { return value; }