[refactor] Refactor Memoize to two instructions: Start and Finish

--- 

Previously, we always emitted `Memoize dep` instructions after the function 
expression literal and depslist instructions 

```js 

// source 

useManualMemo(() => {...}, [arg]) 

// lowered 

$0 = FunctionExpression(...) 

$1 = LoadLocal (arg) 

$2 = ArrayExpression [$1] 

$3 = Memoize (arg) 

$4 = Call / LoadLocal 

$5 = Memoize $4 

``` 

Now, we insert `Memoize dep` before the corresponding function expression 
literal: 

```js 

// lowered 

$0 = StartMemoize (arg)      <---- this moved up! 

$1 = FunctionExpression(...) 

$2 = LoadLocal (arg) 

$3 = ArrayExpression [$2] 

$4 = Call / LoadLocal 

$5 = FinishMemoize $4 

``` 

Design considerations: 

- #2663 needs to understand which lowered instructions belong to a manual 
memoization block, so we need to emit `StartMemoize` instructions before the 
`useMemo/useCallback` function argument, which contains relevant memoized 
instructions 

- we choose to insert StartMemoize instructions to (1) avoid unsafe instruction 
reordering of source and (2) to ensure that Forget output does not change when 
enabling validation 

This PR only renames `Memoize` -> `Start/FinishMemoize` and hoists 
`StartMemoize` as described. The latter may help with stricter validation for 
`useCallback`s, although testing is left to the next PR. 

#2663 contains all validation changes
This commit is contained in:
Mofei Zhang
2024-03-18 12:09:37 -04:00
parent 37452089fb
commit 81695f62c2
16 changed files with 453 additions and 324 deletions
@@ -528,6 +528,13 @@ export type Instruction = {
loc: SourceLocation;
};
export type TInstruction<T extends InstructionValue> = {
id: InstructionId;
lvalue: Place;
value: T;
loc: SourceLocation;
};
export type LValue = {
place: Place;
kind: InstructionKind;
@@ -625,6 +632,17 @@ export type Phi = {
type: Type;
};
export type StartMemoize = {
kind: "StartMemoize";
deps: Array<Place>;
loc: SourceLocation;
};
export type FinishMemoize = {
kind: "FinishMemoize";
decl: Place;
loc: SourceLocation;
};
/*
* Forget currently does not handle MethodCall correctly in
* all cases. Specifically, we do not bind the receiver and method property
@@ -657,6 +675,12 @@ export type CallExpression = {
typeArguments?: Array<t.FlowType>;
};
export type LoadLocal = {
kind: "LoadLocal";
place: Place;
loc: SourceLocation;
};
/*
* The value of a given instruction. Note that values are not recursive: complex
* values such as objects or arrays are always defined by instructions to define
@@ -667,11 +691,7 @@ export type CallExpression = {
*/
export type InstructionValue =
| {
kind: "LoadLocal";
place: Place;
loc: SourceLocation;
}
| LoadLocal
| {
kind: "LoadContext";
place: Place;
@@ -773,12 +793,7 @@ export type InstructionValue =
loc: SourceLocation;
}
// load `object.property`
| {
kind: "PropertyLoad";
object: Place;
property: string;
loc: SourceLocation;
}
| PropertyLoad
// `delete object.property`
| {
kind: "PropertyDelete";
@@ -873,7 +888,8 @@ export type InstructionValue =
* during codegen. It can't be pruned during DCE because we need to preserve the
* instruction so it can be visible in InferReferenceEffects.
*/
| { kind: "Memoize"; value: Place; loc: SourceLocation }
| StartMemoize
| FinishMemoize
/*
* Catch-all for statements such as type imports, nested class declarations, etc
* which are not directly represented, but included for completeness and to allow
@@ -929,6 +945,13 @@ export type Primitive = {
export type JSXText = { kind: "JSXText"; value: string; loc: SourceLocation };
export type PropertyLoad = {
kind: "PropertyLoad";
object: Place;
property: string;
loc: SourceLocation;
};
export type LoadGlobal = {
kind: "LoadGlobal";
name: string;
@@ -600,8 +600,14 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
} ${printPlace(instrValue.value)}`;
break;
}
case "Memoize": {
value = `Memoize ${printPlace(instrValue.value)}`;
case "StartMemoize": {
value = `StartMemoize deps=${instrValue.deps.map((dep) =>
printPlace(dep)
)}`;
break;
}
case "FinishMemoize": {
value = `FinishMemoize decl=${printPlace(instrValue.decl)}`;
break;
}
case "ReactiveFunctionValue": {
@@ -216,8 +216,14 @@ export function* eachInstructionValueOperand(
yield instrValue.value;
break;
}
case "Memoize": {
yield instrValue.value;
case "StartMemoize": {
for (const dep of instrValue.deps) {
yield dep;
}
break;
}
case "FinishMemoize": {
yield instrValue.decl;
break;
}
case "Debugger":
@@ -521,8 +527,14 @@ export function mapInstructionValueOperands(
instrValue.value = fn(instrValue.value);
break;
}
case "Memoize": {
instrValue.value = fn(instrValue.value);
case "StartMemoize": {
for (let i = 0; i < instrValue.deps.length; i++) {
instrValue.deps[i] = fn(instrValue.deps[i]);
}
break;
}
case "FinishMemoize": {
instrValue.decl = fn(instrValue.decl);
break;
}
case "Debugger":
@@ -5,22 +5,210 @@
* LICENSE file in the root directory of this source tree.
*/
import { CompilerError } from "..";
import { CompilerError, SourceLocation } from "..";
import {
CallExpression,
Effect,
Environment,
FinishMemoize,
FunctionExpression,
HIRFunction,
IdentifierId,
Instruction,
InstructionId,
LoadGlobal,
LoadLocal,
MethodCall,
Place,
PropertyLoad,
SpreadPattern,
StartMemoize,
TInstruction,
getHookKindForType,
makeInstructionId,
} from "../HIR";
import { createTemporaryPlace, markInstructionIds } from "../HIR/HIRBuilder";
import { HookKind } from "../HIR/ObjectShape";
import { eachInstructionValueOperand } from "../HIR/visitors";
type ManualMemoCallee = {
kind: "useMemo" | "useCallback";
loadInstr: TInstruction<LoadGlobal> | TInstruction<PropertyLoad>;
};
type IdentifierSidemap = {
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
};
function collectTemporaries(
instr: Instruction,
env: Environment,
sidemap: IdentifierSidemap
): void {
const { value } = instr;
switch (value.kind) {
case "FunctionExpression": {
sidemap.functions.set(
instr.lvalue.identifier.id,
instr as TInstruction<FunctionExpression>
);
break;
}
case "LoadGlobal": {
const global = env.getGlobalDeclaration(value.name);
const hookKind = global !== null ? getHookKindForType(env, global) : null;
const lvalId = instr.lvalue.identifier.id;
if (hookKind === "useMemo" || hookKind === "useCallback") {
sidemap.manualMemos.set(lvalId, {
kind: hookKind,
loadInstr: instr as TInstruction<LoadGlobal>,
});
} else if (value.name === "React") {
sidemap.react.add(lvalId);
}
break;
}
case "PropertyLoad": {
if (sidemap.react.has(value.object.identifier.id)) {
if (value.property === "useMemo" || value.property === "useCallback") {
sidemap.manualMemos.set(instr.lvalue.identifier.id, {
kind: value.property,
loadInstr: instr as TInstruction<PropertyLoad>,
});
}
}
break;
}
}
}
function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<Place>,
memoDecl: Place
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
return [
{
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env),
value: {
kind: "StartMemoize",
/*
* Use deps list from source instead of inferred deps
* as dependencies
*/
deps: depsList,
loc: fnExpr.loc,
},
loc: fnExpr.loc,
},
{
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env),
value: {
kind: "FinishMemoize",
decl: { ...memoDecl },
loc: fnExpr.loc,
},
loc: fnExpr.loc,
},
];
}
function getManualMemoizationReplacement(
fn: Place,
loc: SourceLocation,
kind: "useMemo" | "useCallback"
): LoadLocal | CallExpression {
if (kind === "useMemo") {
/*
* Replace the hook callee with the fn arg.
*
* before:
* $1 = LoadGlobal useMemo // load the useMemo global
* $2 = FunctionExpression ... // memo function
* $3 = ArrayExpression [ ... ] // deps array
* $4 = Call $1 ($2, $3 ) // invoke useMemo w fn and deps
*
* after:
* $1 = LoadGlobal useMemo // load the useMemo global (dead code)
* $2 = FunctionExpression ... // memo function
* $3 = ArrayExpression [ ... ] // deps array (dead code)
* $4 = Call $2 () // invoke the memo function itself
*
* Note that a later pass (InlineImmediatelyInvokedFunctionExpressions) will
* inline the useMemo callback along with any other immediately invoked IIFEs.
*/
return {
kind: "CallExpression",
callee: fn,
/*
* Drop the args, including the deps array which DCE will remove
* later.
*/
args: [],
loc,
};
} else {
/*
* Instead of a Call, just alias the callback directly.
*
* before:
* $1 = LoadGlobal useCallback
* $2 = FunctionExpression ... // the callback being memoized
* $3 = ArrayExpression ... // deps array
* $4 = Call $1 ( $2, $3 ) // invoke useCallback
*
* after:
* $1 = LoadGlobal useCallback // dead code
* $2 = FunctionExpression ... // the callback being memoized
* $3 = ArrayExpression ... // deps array (dead code)
* $4 = LoadLocal $2 // reference the function
*/
return {
kind: "LoadLocal",
place: {
kind: "Identifier",
identifier: fn.identifier,
effect: Effect.Unknown,
reactive: false,
loc,
},
loc,
};
}
}
function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: "useCallback" | "useMemo"
): {
fnPlace: Place;
} {
const [fnPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null) {
CompilerError.throwInvalidReact({
reason: `Expected ${kind} call to pass a callback function`,
loc: instr.value.loc,
suggestions: null,
});
}
if (fnPlace?.kind !== "Identifier") {
CompilerError.throwInvalidReact({
reason: `Unexpected arguments to ${kind} call`,
loc: instr.value.loc,
suggestions: null,
});
}
return {
fnPlace,
};
}
/*
* Removes manual memoization using the `useMemo` and `useCallback` APIs. This pass is designed
* to compose with InlineImmediatelyInvokedFunctionExpressions, and needs to run prior to entering
@@ -30,274 +218,135 @@ import { eachInstructionValueOperand } from "../HIR/visitors";
* eg `React.useMemo()`.
*/
export function dropManualMemoization(func: HIRFunction): void {
const functions = new Map<IdentifierId, FunctionExpression>();
const hooks = new Map<IdentifierId, HookKind>();
const react = new Set<IdentifierId>();
let hasChanges = false;
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
func.env.config.enablePreserveExistingMemoizationGuarantees;
const sidemap: IdentifierSidemap = {
functions: new Map(),
manualMemos: new Map(),
react: new Set(),
};
/**
* Phase 1:
* - Overwrite manual memoization from
* CallExpression callee="useMemo/Callback", args=[fnArg, depslist])
* to either
* CallExpression callee=fnArg
* LoadLocal fnArg
* - (if validation is enabled) collect manual memoization markers
*/
const queuedInserts: Map<
InstructionId,
{
kind: "before" | "after";
value: TInstruction<StartMemoize> | TInstruction<FinishMemoize>;
}
> = new Map();
for (const [_, block] of func.body.blocks) {
let nextInstructions: Array<Instruction> | null = null;
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i]!;
switch (instr.value.kind) {
case "FunctionExpression": {
functions.set(instr.lvalue.identifier.id, instr.value);
break;
}
case "LoadGlobal": {
const global = func.env.getGlobalDeclaration(instr.value.name);
const hookKind =
global !== null ? getHookKindForType(func.env, global) : null;
if (hookKind === "useMemo" || hookKind === "useCallback") {
hooks.set(instr.lvalue.identifier.id, hookKind);
} else if (instr.value.name === "React") {
react.add(instr.lvalue.identifier.id);
}
break;
}
case "PropertyLoad": {
if (react.has(instr.value.object.identifier.id)) {
if (
instr.value.property === "useMemo" ||
instr.value.property === "useCallback"
) {
hooks.set(instr.lvalue.identifier.id, instr.value.property);
if (
instr.value.kind === "CallExpression" ||
instr.value.kind === "MethodCall"
) {
const id =
instr.value.kind === "CallExpression"
? instr.value.callee.identifier.id
: instr.value.property.identifier.id;
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const { fnPlace } = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind
);
instr.value = getManualMemoizationReplacement(
fnPlace,
instr.value.loc,
manualMemo.kind
);
if (isValidationEnabled) {
const inlineMemoFn = sidemap.functions.get(fnPlace.identifier.id);
if (inlineMemoFn == null) {
CompilerError.throwInvalidReact({
reason:
"DepsValidation: Expected function literal as manual memoization callback",
suggestions: [],
loc: fnPlace.loc,
});
}
}
break;
}
case "MethodCall":
case "CallExpression": {
const id =
instr.value.kind === "CallExpression"
? instr.value.callee.identifier.id
: instr.value.property.identifier.id;
const hookKind = hooks.get(id);
if (hookKind != null) {
if (hookKind === "useMemo") {
const [fn] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fn == null) {
CompilerError.throwInvalidReact({
reason: "Expected useMemo call to pass a callback function",
loc: instr.loc,
suggestions: null,
});
}
/*
* Replace the hook callee with the fn arg.
*
* before:
* $1 = LoadGlobal useMemo // load the useMemo global
* $2 = FunctionExpression ... // memo function
* $3 = ArrayExpression [ ... ] // deps array
* $4 = Call $1 ($2, $3 ) // invoke useMemo w fn and deps
*
* after:
* $1 = LoadGlobal useMemo // load the useMemo global (dead code)
* $2 = FunctionExpression ... // memo function
* $3 = ArrayExpression [ ... ] // deps array (dead code)
* $4 = Call $2 () // invoke the memo function itself
*
* Note that a later pass (InlineImmediatelyInvokedFunctionExpressions) will
* inline the useMemo callback along with any other immediately invoked IIFEs.
*/
if (fn.kind === "Identifier") {
instr.value = {
kind: "CallExpression",
callee: fn,
/*
* Drop the args, including the deps array which DCE will remove
* later.
*/
args: [],
loc: instr.value.loc,
};
if (
func.env.config.enablePreserveExistingMemoizationGuarantees ||
func.env.config.validatePreserveExistingMemoizationGuarantees
) {
/**
* When this flag is enabled we also compile in a 'Memoize' instruction
* to preserve the intended memoization boundary:
*
* Normal output:
* $1 = LoadGlobal useMemo // load the useMemo global (dead code)
* $2 = FunctionExpression ... // memo function
* $3 = ArrayExpression [ ... ] // deps array (dead code)
* $4 = Call $2 () // invoke the memo function itself
*
* Output w flag enabled:
* $1 = LoadGlobal useMemo // load the useMemo global (dead code)
* $2 = FunctionExpression ... // memo function
* $3 = ArrayExpression [ ... ] // deps array (dead code)
* .. = Memoize ... // memoize dependencies
* $4 = Call $2 () // invoke the memo function itself
* .. = Memoize $4 // preserve memo information
*
* Note that Memoize does not produce a result and is called for its side
* effects only.
*/
nextInstructions =
nextInstructions ?? block.instructions.slice(0, i);
const functionExpression = functions.get(fn.identifier.id);
if (functionExpression !== undefined) {
for (const operand of eachInstructionValueOperand(
functionExpression
)) {
const temp = createTemporaryPlace(func.env);
nextInstructions.push({
id: makeInstructionId(0),
lvalue: temp,
value: {
kind: "Memoize",
value: { ...operand },
loc: instr.loc,
},
loc: instr.loc,
});
}
}
nextInstructions.push(instr);
const temp = createTemporaryPlace(func.env);
nextInstructions.push({
id: makeInstructionId(0),
lvalue: temp,
value: {
kind: "Memoize",
value: { ...instr.lvalue },
loc: instr.loc,
},
loc: instr.loc,
});
continue;
}
}
} else if (hookKind === "useCallback") {
const [fn] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fn == null) {
CompilerError.throwInvalidReact({
reason: "Expected useMemo call to pass a callback function",
loc: instr.loc,
suggestions: null,
});
}
/*
* Instead of a Call, just alias the callback directly.
*
* before:
* $1 = LoadGlobal useCallback
* $2 = FunctionExpression ... // the callback being memoized
* $3 = ArrayExpression ... // deps array
* $4 = Call $1 ( $2, $3 ) // invoke useCallback
*
* after:
* $1 = LoadGlobal useCallback // dead code
* $2 = FunctionExpression ... // the callback being memoized
* $3 = ArrayExpression ... // deps array (dead code)
* $4 = LoadLocal $2 // reference the function
*/
if (fn.kind === "Identifier") {
instr.value = {
kind: "LoadLocal",
place: {
const memoDecl: Place =
manualMemo.kind === "useMemo"
? instr.lvalue
: {
kind: "Identifier",
identifier: fn.identifier,
identifier: fnPlace.identifier,
effect: Effect.Unknown,
reactive: false,
loc: instr.value.loc,
},
loc: instr.value.loc,
};
if (
func.env.config.enablePreserveExistingMemoizationGuarantees ||
func.env.config.validatePreserveExistingMemoizationGuarantees
) {
nextInstructions =
nextInstructions ?? block.instructions.slice(0, i);
/**
* With the flag enabled the output changes to use a Memoize instruction instead
* a loadlocal to load the function expression into the original temporary:
*
* Normal output:
* $1 = LoadGlobal useCallback // dead code
* $2 = FunctionExpression ... // the callback being memoized
* $3 = ArrayExpression ... // deps array (dead code)
* $4 = LoadLocal $2 // reference the function
*
* With flag enabled:
* $1 = LoadGlobal useCallback // dead code
* $2 = FunctionExpression ... // the callback being memoized
* $3 = ArrayExpression ... // deps array (dead code)
* .. = Memoize ... // memoize dependencies
* $n = Memoize $2 // reference the function
* $4 = LoadLocal $2 // reference the function
*
* Note that Memoize does not produce a result and is called for its side effects
* only.
*/
const functionExpression = functions.get(fn.identifier.id);
if (functionExpression !== undefined) {
for (const operand of eachInstructionValueOperand(
functionExpression
)) {
const temp = createTemporaryPlace(func.env);
nextInstructions.push({
id: makeInstructionId(0),
lvalue: temp,
value: {
kind: "Memoize",
value: { ...operand },
loc: instr.loc,
},
loc: instr.loc,
});
}
}
nextInstructions.push(instr);
loc: fnPlace.loc,
};
const temp = createTemporaryPlace(func.env);
nextInstructions.push({
id: makeInstructionId(0),
lvalue: { ...temp },
value: {
kind: "Memoize",
value: {
kind: "Identifier",
identifier: fn.identifier,
effect: Effect.Unknown,
reactive: false,
loc: instr.value.loc,
},
loc: instr.value.loc,
},
loc: instr.loc,
});
continue;
}
}
}
const [startMarker, finishMarker] = makeManualMemoizationMarkers(
fnPlace,
func.env,
// Next PR will replace this with depslist from source
[...eachInstructionValueOperand(inlineMemoFn.value)],
memoDecl
);
/*
* 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).
*/
queuedInserts.set(inlineMemoFn.id, {
kind: "before",
value: startMarker,
});
queuedInserts.set(instr.id, { kind: "after", value: finishMarker });
continue;
}
break;
}
} else {
collectTemporaries(instr, func.env, sidemap);
}
}
}
/**
* Phase 2: Insert manual memoization markers as needed
*/
if (queuedInserts.size > 0) {
let hasChanges = false;
for (const [_, block] of func.body.blocks) {
let nextInstructions: Array<Instruction> | null = null;
for (let i = 0; i < block.instructions.length; i++) {
const instr = block.instructions[i];
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);
}
} else if (nextInstructions != null) {
nextInstructions.push(instr);
}
}
if (nextInstructions !== null) {
nextInstructions.push(instr);
block.instructions = nextInstructions;
hasChanges = true;
}
}
if (nextInstructions !== null) {
block.instructions = nextInstructions;
hasChanges = true;
if (hasChanges) {
markInstructionIds(func.body);
}
}
if (hasChanges) {
markInstructionIds(func.body);
}
}
@@ -1391,21 +1391,24 @@ function inferBlock(
state.alias(lvalue, instrValue.value);
continue;
}
case "Memoize": {
if (env.config.enablePreserveExistingMemoizationGuarantees) {
state.reference(
instrValue.value,
functionEffects,
Effect.Freeze,
ValueReason.Other
);
} else {
state.reference(
instrValue.value,
functionEffects,
Effect.Read,
ValueReason.Other
);
case "StartMemoize":
case "FinishMemoize": {
for (const val of eachInstructionValueOperand(instrValue)) {
if (env.config.enablePreserveExistingMemoizationGuarantees) {
state.reference(
val,
functionEffects,
Effect.Freeze,
ValueReason.Other
);
} else {
state.reference(
val,
functionEffects,
Effect.Read,
ValueReason.Other
);
}
}
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
@@ -338,7 +338,8 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
case "StoreContext": {
return false;
}
case "Memoize": {
case "StartMemoize":
case "FinishMemoize": {
/**
* This instruction is used by the @enablePreserveExistingMemoizationGuarantees feature
* to preserve information about memoization semantics in the original code. We can't
@@ -939,7 +939,10 @@ function codegenInstructionNullable(
assertExhaustive(kind, `Unexpected instruction kind '${kind}'`);
}
}
} else if (instr.value.kind === "Memoize") {
} else if (
instr.value.kind === "StartMemoize" ||
instr.value.kind === "FinishMemoize"
) {
return null;
} else if (instr.value.kind === "Debugger") {
return t.debuggerStatement();
@@ -1787,7 +1790,8 @@ function codegenInstructionValue(
break;
}
case "ReactiveFunctionValue":
case "Memoize":
case "StartMemoize":
case "FinishMemoize":
case "Debugger":
case "DeclareLocal":
case "DeclareContext":
@@ -154,7 +154,8 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case "NextIterableOf":
case "NextPropertyOf":
case "Debugger":
case "Memoize":
case "StartMemoize":
case "FinishMemoize":
case "UnaryExpression":
case "BinaryExpression":
case "PropertyLoad": {
@@ -453,7 +453,8 @@ function computeMemoizationInputs(
};
}
case "NextPropertyOf":
case "Memoize":
case "StartMemoize":
case "FinishMemoize":
case "Debugger":
case "ComputedDelete":
case "PropertyDelete":
@@ -926,8 +927,8 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
* need to be memoized. Remove associated `Memoize` instructions so that
* we don't report false positives on "missing" memoization of these values.
*/
if (instruction.value.kind === "Memoize") {
const identifier = instruction.value.value.identifier;
if (instruction.value.kind === "FinishMemoize") {
const identifier = instruction.value.decl.identifier;
if (
identifier.scope !== null &&
this.prunedScopes.has(identifier.scope.id)
@@ -335,7 +335,8 @@ function* generateInstructionTypes(
case "NextIterableOf":
case "UnsupportedNode":
case "Debugger":
case "Memoize": {
case "FinishMemoize":
case "StartMemoize": {
break;
}
default:
@@ -14,6 +14,7 @@ import {
ReactiveScopeBlock,
ScopeId,
} from "../HIR";
import { eachInstructionValueOperand } from "../HIR/visitors";
import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables";
import {
ReactiveFunctionVisitor,
@@ -71,20 +72,24 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
state: CompilerError
): void {
this.traverseInstruction(instruction, state);
if (instruction.value.kind === "Memoize") {
const value = instruction.value.value;
if (
isMutable(instruction as Instruction, value) ||
isUnmemoized(value.identifier, this.scopes)
) {
state.push({
reason:
"This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized",
description: null,
severity: ErrorSeverity.InvalidReact,
loc: typeof instruction.loc !== "symbol" ? instruction.loc : null,
suggestions: null,
});
if (
instruction.value.kind === "StartMemoize" ||
instruction.value.kind === "FinishMemoize"
) {
for (const value of eachInstructionValueOperand(instruction.value)) {
if (
isMutable(instruction as Instruction, value) ||
isUnmemoized(value.identifier, this.scopes)
) {
state.push({
reason:
"This value was manually memoized, but cannot be memoized under Forget because it may be mutated after it is memoized",
description: null,
severity: ErrorSeverity.InvalidReact,
loc: typeof instruction.loc !== "symbol" ? instruction.loc : null,
suggestions: null,
});
}
}
}
}
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
5 | const ref = useRef({ inner: null });
6 |
> 7 | const onChange = useCallback((event) => {
| ^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^
> 8 | // The ref should still be mutable here even though function deps are frozen in
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | // @enablePreserveExistingMemoizationGuarantees mode
@@ -45,7 +45,7 @@ export const FIXTURE_ENTRYPOINT = {
> 10 | ref.current.inner = event.target.value;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 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)
[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 |
@@ -34,7 +34,7 @@ export const FIXTURE_ENTRYPOINT = {
5 | const ref = useRef({ inner: null });
6 |
> 7 | const onChange = useCallback((event) => {
| ^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^
> 8 | // The ref should still be mutable here even though function deps are frozen in
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | // @enablePreserveExistingMemoizationGuarantees mode
@@ -42,7 +42,7 @@ export const FIXTURE_ENTRYPOINT = {
> 10 | ref.current.inner = event.target.value;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 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)
[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 |
@@ -2,22 +2,32 @@
## Input
```javascript
function Component(props) {
const x = useMemo(someHelper, []);
import { useMemo } from "react";
import { makeArray } from "shared-runtime";
function Component() {
const x = useMemo(makeArray, []);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
import { makeArray } from "shared-runtime";
function Component() {
const $ = useMemoCache(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = someHelper();
t0 = makeArray();
$[0] = t0;
} else {
t0 = $[0];
@@ -26,5 +36,10 @@ function Component(props) {
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
@@ -1,4 +0,0 @@
function Component(props) {
const x = useMemo(someHelper, []);
return x;
}
@@ -0,0 +1,12 @@
import { useMemo } from "react";
import { makeArray } from "shared-runtime";
function Component() {
const x = useMemo(makeArray, []);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};