validatePreserveExistingMemoizationGuarantees ensures compiler preserves subset

of dependencies from source 

--- 

`validatePreserveExistingMemoizationGuarantees` previously checked 

- manual memoization dependencies and declarations (the returned value) do not 
"lose" memoization due to inferred mutations 

``` 

function useFoo() { 

const y = {}; 

// bail out because we infer that y cannot be a dependency of x as its 
mutableRange 

// extends beyond 

const x = useMemo(() => maybeMutate(y), [y]); 

// similarly, bail out if we find that x or y are mutated here 

return x; 

} 

``` 

- manual memoization deps and decls do not get deopted due to hook calls 

``` 

function useBar() { 

const x = getArray(); 

useHook(); 

mutate(x); 

return useCallback(() => [x], [x]); 

} 

``` 

This PR updates `validatePreserveExistingMemoizationGuarantees` with the 
following correctness conditions: 

*major change* All inferred dependencies of reactive scopes between 
`StartMemoize` and `StopMemoize` instructions (e.g. scopes containing manual 
memoization code) must either: 

1. be produced from earlier within the same manual memoization block 

2. exactly match an element of depslist from source 

This assumes that the source codebase mostly follows the `exhaustive-deps` lint 
rule, which ensures that deps lists are (1) simple expressions composing of 
reads from named identifiers + property loads and (2) exactly match deps usages 
in the useMemo/useCallback itself. 

--- 

Validated that this does not change source by running internally on ~50k files 
(no validation on `main`, no validation on this PR, and validation on this PR).
This commit is contained in:
Mofei Zhang
2024-03-18 14:50:15 -04:00
parent 81695f62c2
commit cea84a41bc
115 changed files with 3875 additions and 125 deletions
@@ -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<string>;
};
export type StartMemoize = {
kind: "StartMemoize";
deps: Array<Place>;
// 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<ManualMemoDependency> | null;
loc: SourceLocation;
};
export type FinishMemoize = {
kind: "FinishMemoize";
// Start/FinishMemoize markers should have matching ids
manualMemoId: number;
decl: Place;
pruned?: true;
loc: SourceLocation;
};
@@ -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
@@ -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;
}
@@ -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<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
};
/**
* 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<IdentifierId, ManualMemoDependency>
): 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<Place>
);
}
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<Place>,
memoDecl: Place
depsList: Array<ManualMemoDependency> | null,
memoDecl: Place,
manualMemoId: number
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
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<CallExpression> | TInstruction<MethodCall>,
kind: "useCallback" | "useMemo"
kind: "useCallback" | "useMemo",
sidemap: IdentifierSidemap
): {
fnPlace: Place;
depsList: Array<ManualMemoDependency> | 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<ManualMemoDependency> | 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<StartMemoize> | TInstruction<FinishMemoize>;
}
TInstruction<StartMemoize> | TInstruction<FinishMemoize>
> = 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<CallExpression> | TInstruction<MethodCall>,
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);
}
@@ -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);
@@ -933,7 +933,7 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
identifier.scope !== null &&
this.prunedScopes.has(identifier.scope.id)
) {
return { kind: "remove" };
instruction.value.pruned = true;
}
}
@@ -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<CompilerError> {
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<IdentifierId>;
/*
* normalized depslist from useMemo/useCallback
* callsite in source
*/
depsFromSource: Array<ManualMemoDependency> | 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<IdentifierId, ManualMemoDependency>,
declsWithinMemoBlock: Set<IdentifierId>,
validDepsInMemoBlock: Array<ManualMemoDependency>,
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<VisitorState> {
scopes: Set<ScopeId> = new Set();
scopeMapping = new Map();
temporaries: Map<IdentifierId, ManualMemoDependency> = 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<CompilerError> {
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<ManualMemoDependency> | 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,
@@ -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",
};
```
@@ -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 = () => {
@@ -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;
@@ -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 | }
```
@@ -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(
() => {
@@ -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]
```
@@ -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]);
}
@@ -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]
```
@@ -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]);
}
@@ -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 = {
```
@@ -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: [],
};
@@ -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 |
```
@@ -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: [{}],
};
@@ -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]
```
@@ -0,0 +1,8 @@
// @validatePreserveExistingMemoizationGuarantees
import { useCallback } from "react";
function useHook(maybeRef) {
return useCallback(() => {
return [maybeRef.current];
}, [maybeRef]);
}
@@ -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]
```
@@ -0,0 +1,8 @@
// @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
function useHook(maybeRef, shouldRead) {
return useMemo(() => {
return () => [maybeRef.current];
}, [shouldRead, maybeRef]);
}
@@ -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 = {
```
@@ -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: [{}],
};
@@ -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]
```
@@ -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]);
}
@@ -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]
```
@@ -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: [] } } }],
};
@@ -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]
```
@@ -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]);
}
@@ -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]
```
@@ -0,0 +1,8 @@
// @validatePreserveExistingMemoizationGuarantees
import { useCallback } from "react";
function Component({ propA }) {
return useCallback(() => {
return propA.x();
}, [propA.x]);
}
@@ -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]
```
@@ -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]);
}
@@ -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 = {
```
@@ -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 }],
};
@@ -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]
```
@@ -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]);
}
@@ -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]
```
@@ -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]);
}
@@ -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]
```
@@ -0,0 +1,10 @@
// @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
function Component({ propA }) {
return useMemo(() => {
return {
value: propA.x().y,
};
}, [propA.x]);
}
@@ -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]
```
@@ -0,0 +1,8 @@
// @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
function Component({ propA }) {
return useMemo(() => {
return propA.x();
}, [propA.x]);
}
@@ -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]
```
@@ -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];
}
@@ -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 |
```
@@ -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;
}
@@ -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)
@@ -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: () => {} }],
};
@@ -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]
@@ -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: [{}],
};
@@ -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 ]]"
@@ -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 } } }],
};
@@ -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 <Stringify cb={cb} shouldInvokeFns={true} />;
}
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 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[4] = cb;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
};
```
### Eval output
(kind: ok) <div>{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}</div>
@@ -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 <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
};
@@ -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 ]]"
@@ -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: [{}],
};
@@ -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 ]]"
@@ -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: [] } } }],
};
@@ -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"]]
@@ -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"],
};
@@ -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 ]]"
@@ -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 } }],
};
@@ -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 ]]"
@@ -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],
};
@@ -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 ]]"
@@ -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: [],
};
@@ -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 ]]"
@@ -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: [],
};
@@ -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;
@@ -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;
@@ -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 <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
}
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 = <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
$[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) <div>{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[[1,2],3,4]]},"shouldInvokeFns":true}</div>
<div>{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[]]},"shouldInvokeFns":true}</div>
@@ -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 <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
}
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 },
],
};
@@ -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 <Stringify getVal={getVal} shouldInvokeFns={true} />;
}
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 = <Stringify getVal={getVal} shouldInvokeFns={true} />;
$[5] = getVal;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [
[1, 2],
[3, 4],
],
};
```
### Eval output
(kind: ok) <div>{"getVal":{"kind":"Function","result":{"y":[[1,2],3,4]}},"shouldInvokeFns":true}</div>
@@ -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 <Stringify getVal={getVal} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [
[1, 2],
[3, 4],
],
};
@@ -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 ]]"
@@ -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 }],
};
@@ -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
@@ -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 } } }],
};
@@ -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}
@@ -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: [] } } }],
};
@@ -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}
@@ -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: [] } } }],
};
@@ -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":[]}
@@ -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: [] } } }],
};
@@ -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]
@@ -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],
};
@@ -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) [[{}]]
@@ -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: [{}],
};
@@ -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"]]
@@ -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"],
};
@@ -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]
@@ -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],
};
@@ -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
@@ -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],
};

Some files were not shown because too many files have changed in this diff Show More