Compare commits

...

5 Commits

Author SHA1 Message Date
Mike Vitousek
0544af4dcc [compiler][ez] Rename disableMemoizationForDebugging to just disableMemoization
Summary: We don't really need to make positive claims about what a particular mode is for in the name

ghstack-source-id: f6fbb730ab
Pull Request resolved: https://github.com/facebook/react/pull/30191
2024-07-26 15:52:21 -07:00
Mike Vitousek
c014aada1c [compiler] Add wrapper functions to wrap change-detection storage and loading from the memo cache
Summary:
We may wish to perform some additional computation on values when they enter or exit the memo cache in change detection mode (e.g. make a deep copy, restore the original value). This builds support for doing so.

In addition, it drops the "ForDebugging" part of the flag name and makes it compatible with "disableMemoization".

ghstack-source-id: 06a998b534
Pull Request resolved: https://github.com/facebook/react/pull/30181
2024-07-26 15:52:19 -07:00
Mike Vitousek
bc0882dc3c [compiler] In change detection, assign reactive scopes to values that may have changed between renders
Summary:
Change detection is desgined to determine whether rules of react have been violated, and to do so better we need to loosen Forgets assumptions about what kinds of values don't need to be memoized. For example, the compiler typically doesn't think of o.x as something that needs to be memoized, because it does not allocate. However, we want to compare o.x in the current render with o.x in a previous one, so we now insert a "memoization" (comparison, really) block around this value.

The structure of this work is that we now add a reactive scope for identifiers if they originate from any instruction that could read from state that could have mutated between renders. This means that `LoadProperty` is always going to get a reactive scope; `LoadGlobal` will get a scope if we're not reading from something mutable, and `PrefixUpdate` won't (because the variable being incremented would have already been loaded and checked).

Supersedes #30180.

ghstack-source-id: a1b5392e20
Pull Request resolved: https://github.com/facebook/react/pull/30379
2024-07-26 15:52:17 -07:00
Mike Vitousek
b966dfa91e [compiler] Make compiler aware of whether a local binding is constlike or not
Summary:
Minor change which supports the changeDetection work later on. We can know whether an individual local binding is const or constlike.

ghstack-source-id: 815972ec9c
Pull Request resolved: https://github.com/facebook/react/pull/30378
2024-07-26 15:52:15 -07:00
Mike Vitousek
29019bf6ec [compiler] When preserving user-level useMemos, don't memoize arguments
Summary:
This work enhances the don't-drop-memoization mode to not memoize the arguments to useMemo and useCallback (or technically, to memoize them in the same block as the hook call -- but since hooks can't be memoized, this is equivalent to never memoizing them). The advantage here is that we preserve the source-level code structure around memoization callbacks more, and avoid the compiler bug of having function expression dependencies be unconditional when their actual semantics are conditional.

This conceptually replaces some of the work of #30177: some of that was motivated by avoiding the deps bug, and the rest of the motivation was to better exercise the compiler's semantics, which was really happening as much as I'd hoped, as mofeiZ explained to me in that PR and offline!

ghstack-source-id: 0f4fc6f13f
Pull Request resolved: https://github.com/facebook/react/pull/30377
2024-07-26 15:52:13 -07:00
33 changed files with 640 additions and 191 deletions

View File

@@ -155,11 +155,7 @@ function* runWithEnvironment(
validateContextVariableLValues(hir);
validateUseMemo(hir);
if (
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
if (!env.preserveManualMemo()) {
dropManualMemoization(hir);
yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -415,7 +411,7 @@ function* runWithEnvironment(
value: reactiveFunction,
});
if (env.config.enableChangeDetectionForDebugging != null) {
if (env.config.enableChangeDetection != null) {
pruneInitializationDependencies(reactiveFunction);
yield log({
kind: 'reactive',

View File

@@ -538,11 +538,27 @@ export function compileProgram(
externalFunctions.push(enableEmitHookGuards);
}
if (pass.opts.environment?.enableChangeDetectionForDebugging != null) {
const enableChangeDetectionForDebugging = tryParseExternalFunction(
pass.opts.environment.enableChangeDetectionForDebugging,
);
externalFunctions.push(enableChangeDetectionForDebugging);
if (pass.opts.environment?.enableChangeDetection != null) {
const enableChangeDetection = tryParseExternalFunction({
importSpecifierName:
pass.opts.environment.enableChangeDetection.structuralCheck,
source: pass.opts.environment.enableChangeDetection.source,
});
externalFunctions.push(enableChangeDetection);
if (pass.opts.environment.enableChangeDetection.wrappers != null) {
const store = tryParseExternalFunction({
importSpecifierName:
pass.opts.environment.enableChangeDetection.wrappers.store,
source: pass.opts.environment.enableChangeDetection.source,
});
const restore = tryParseExternalFunction({
importSpecifierName:
pass.opts.environment.enableChangeDetection.wrappers.restore,
source: pass.opts.environment.enableChangeDetection.source,
});
externalFunctions.push(store);
externalFunctions.push(restore);
}
}
} catch (err) {
handleError(err, pass, null);

View File

@@ -385,7 +385,7 @@ const EnvironmentConfigSchema = z.object({
* @enablePreserveExistingManualUseMemo, because otherwise memoization in the
* original source will be disabled as well.
*/
disableMemoizationForDebugging: z.boolean().default(false),
disableMemoization: z.boolean().default(false),
/**
* When true, rather using memoized values, the compiler will always re-compute
@@ -393,7 +393,13 @@ const EnvironmentConfigSchema = z.object({
* computed one. This detects cases where rules of react violations may cause the
* compiled code to behave differently than the original.
*/
enableChangeDetectionForDebugging: ExternalFunctionSchema.nullish(),
enableChangeDetection: z
.object({
source: z.string(),
structuralCheck: z.string(),
wrappers: z.object({ store: z.string(), restore: z.string() }).nullish(),
})
.nullish(),
/**
* The react native re-animated library uses custom Babel transforms that
@@ -456,12 +462,27 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
}
if (
key === 'enableChangeDetectionForDebugging' &&
(val === undefined || val === 'true')
key === "enableChangeDetection" &&
(val === undefined || val === "true")
) {
maybeConfig[key] = {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
structuralCheck: "$structuralCheck",
};
continue;
}
if (
key === "enableChangeDetectionWrappers" &&
(val === undefined || val === "true")
) {
maybeConfig["enableChangeDetection"] = {
source: "react-compiler-runtime",
structuralCheck: '$structuralCheck',
wrappers: {
store: "$store",
restore: "$restore",
},
};
continue;
}
@@ -549,18 +570,6 @@ export class Environment {
this.#shapes = new Map(DEFAULT_SHAPES);
this.#globals = new Map(DEFAULT_GLOBALS);
if (
config.disableMemoizationForDebugging &&
config.enableChangeDetectionForDebugging != null
) {
CompilerError.throwInvalidConfig({
reason: `Invalid environment config: the 'disableMemoizationForDebugging' and 'enableChangeDetectionForDebugging' options cannot be used together`,
description: null,
loc: null,
suggestions: null,
});
}
for (const [hookName, hook] of this.config.customHooks) {
CompilerError.invariant(!this.#globals.has(hookName), {
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
@@ -765,6 +774,14 @@ export class Environment {
return DefaultMutatingHook;
}
}
preserveManualMemo(): boolean {
return (
this.config.enablePreserveExistingManualUseMemo ||
this.config.disableMemoization ||
this.config.enableChangeDetection != null
);
}
}
// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2

View File

@@ -1168,7 +1168,7 @@ export type NonLocalBinding =
imported: string;
}
// let, const, function, etc declared in the module but outside the current component/hook
| {kind: 'ModuleLocal'; name: string}
| {kind: 'ModuleLocal'; name: string, immutable: boolean}
// an unresolved binding
| {kind: 'Global'; name: string};

View File

@@ -271,9 +271,14 @@ export default class HIRBuilder {
module: importDeclaration.node.source.value,
};
} else {
const immutable =
(path.isVariableDeclaration() && path.node.kind === "const") ||
path.isClassDeclaration() ||
path.isClassExpression();
return {
kind: 'ModuleLocal',
name: originalName,
immutable,
};
}
}

View File

@@ -6,7 +6,7 @@ import {
makeInstructionId,
} from '.';
import {getPlaceScope} from '../ReactiveScopes/BuildReactiveBlocks';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {isMutableAtInstruction} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
import {getOrInsertDefault} from '../Utils/utils';
import {
@@ -254,7 +254,7 @@ function visitPlace(
* of the stack to the mutated outer scope.
*/
const placeScope = getPlaceScope(id, place);
if (placeScope != null && isMutable({id} as any, place)) {
if (placeScope != null && isMutableAtInstruction({id} as any, place)) {
const placeScopeIdx = activeScopes.indexOf(placeScope);
if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) {
joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]);

View File

@@ -26,7 +26,7 @@ import {
} from '../HIR/visitors';
import {
findDisjointMutableValues,
isMutable,
isMutableAtInstruction,
} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
@@ -232,7 +232,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.Mutate: {
if (isMutable(instruction, operand)) {
if (isMutableAtInstruction(instruction, operand)) {
const resolvedId = identifierMapping.get(operand.identifier);
if (resolvedId !== undefined) {
reactiveIdentifiers.markReactiveIdentifier(resolvedId);

View File

@@ -669,15 +669,7 @@ function codegenReactiveScope(
);
}
if (cx.env.config.disableMemoizationForDebugging) {
CompilerError.invariant(
cx.env.config.enableChangeDetectionForDebugging == null,
{
reason: `Expected to not have both change detection enabled and memoization disabled`,
description: `Incompatible config options`,
loc: null,
},
);
if (cx.env.config.disableMemoization) {
testCondition = t.logicalExpression(
'||',
testCondition,
@@ -687,41 +679,76 @@ function codegenReactiveScope(
let computationBlock = codegenBlock(cx, block);
let memoStatement;
if (
cx.env.config.enableChangeDetectionForDebugging != null &&
changeExpressions.length > 0
) {
if (cx.env.config.enableChangeDetection != null) {
const loc =
typeof scope.loc === 'symbol'
? 'unknown location'
: `(${scope.loc.start.line}:${scope.loc.end.line})`;
const detectionFunction =
cx.env.config.enableChangeDetectionForDebugging.importSpecifierName;
cx.env.config.enableChangeDetection.structuralCheck;
const cacheLoadOldValueStatements: Array<t.Statement> = [];
const restoreOldValueStatements: Array<t.Statement> = [];
const changeDetectionStatements: Array<t.Statement> = [];
const idempotenceDetectionStatements: Array<t.Statement> = [];
for (const {name, index, value} of cacheLoads) {
const loadName = cx.synthesizeName(`old$${name.name}`);
const slot = t.memberExpression(
t.identifier(cx.synthesizeName('$')),
for (const {
name: { name: nameStr },
index,
value,
} of cacheLoads) {
const baseSlot = t.memberExpression(
t.identifier(cx.synthesizeName("$")),
t.numericLiteral(index),
true,
);
const genSlot = (): t.MemberExpression => t.cloneNode(baseSlot, true);
const loadNameStr = cx.synthesizeName(`old$${nameStr}`);
let storedValue, restoredValue, restoredRecomputed;
if (cx.env.config.enableChangeDetection.wrappers != null) {
storedValue = t.callExpression(
t.identifier(cx.env.config.enableChangeDetection.wrappers.store),
[value]
);
restoredValue = t.callExpression(
t.identifier(cx.env.config.enableChangeDetection.wrappers.restore),
[t.identifier(loadNameStr)]
);
restoredRecomputed = t.callExpression(
t.identifier(cx.env.config.enableChangeDetection.wrappers.restore),
[genSlot()]
);
} else {
storedValue = value;
restoredValue = t.identifier(loadNameStr);
restoredRecomputed = genSlot();
}
cacheStoreStatements.push(
t.expressionStatement(t.assignmentExpression('=', slot, value)),
t.expressionStatement(
t.assignmentExpression("=", genSlot(), storedValue)
)
);
cacheLoadOldValueStatements.push(
t.variableDeclaration('let', [
t.variableDeclarator(t.identifier(loadName), slot),
t.variableDeclarator(t.identifier(loadNameStr), genSlot()),
]),
);
if (!cx.env.config.disableMemoization) {
restoreOldValueStatements.push(
t.expressionStatement(
t.assignmentExpression("=", t.identifier(nameStr), restoredValue)
)
);
}
changeDetectionStatements.push(
t.expressionStatement(
t.callExpression(t.identifier(detectionFunction), [
t.identifier(loadName),
t.cloneNode(name, true),
t.stringLiteral(name.name),
t.identifier(loadNameStr),
t.identifier(nameStr),
t.stringLiteral(nameStr),
t.stringLiteral(cx.fnName),
t.stringLiteral('cached'),
t.stringLiteral(loc),
@@ -731,9 +758,9 @@ function codegenReactiveScope(
idempotenceDetectionStatements.push(
t.expressionStatement(
t.callExpression(t.identifier(detectionFunction), [
t.cloneNode(slot, true),
t.cloneNode(name, true),
t.stringLiteral(name.name),
genSlot(),
t.identifier(nameStr),
t.stringLiteral(nameStr),
t.stringLiteral(cx.fnName),
t.stringLiteral('recomputed'),
t.stringLiteral(loc),
@@ -741,7 +768,9 @@ function codegenReactiveScope(
),
);
idempotenceDetectionStatements.push(
t.expressionStatement(t.assignmentExpression('=', name, slot)),
t.expressionStatement(
t.assignmentExpression("=", t.identifier(nameStr), restoredRecomputed)
)
);
}
const condition = cx.synthesizeName('condition');

View File

@@ -185,7 +185,10 @@ function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation {
}
// Is the operand mutable at this given instruction
export function isMutable({id}: Instruction, place: Place): boolean {
export function isMutableAtInstruction(
{id}: Instruction,
place: Place
): boolean {
const range = place.identifier.mutableRange;
return id >= range.start && id < range.end;
}
@@ -253,6 +256,89 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
}
}
/*
* These instructions may pick up external changes due to rules of react violations.
* Instructions should be included here if they may change without their inputs changing.
* For example, PostfixUpdate is not included because it only has a changed lval if
* it has a changed argument, but LoadProperty is included because the argument can be
* mutated elsewhere.
*/
function mayHaveChanged(env: Environment, instruction: Instruction): boolean {
if (env.config.enableChangeDetection == null) {
return false;
}
switch (instruction.value.kind) {
case "Await":
case "ComputedLoad":
case "Destructure":
case "GetIterator":
case "IteratorNext":
case "NextPropertyOf":
case "CallExpression":
case "MethodCall":
case "NewExpression": {
return true;
}
case "PropertyLoad": {
return instruction.value.property !== "current"
}
case "LoadGlobal": {
return (
instruction.value.binding.kind === "ModuleLocal" &&
!instruction.value.binding.immutable
);
}
case "PostfixUpdate":
case "PrefixUpdate":
case "DeclareLocal":
case "DeclareContext":
case "StoreLocal":
case "MetaProperty":
case "TypeCastExpression":
case "LoadLocal":
case "LoadContext":
case "StoreContext":
case "PropertyDelete":
case "ComputedDelete":
case "JSXText":
case "TemplateLiteral":
case "Primitive":
case "Debugger":
case "StartMemoize":
case "FinishMemoize":
case "UnaryExpression":
case "BinaryExpression":
case "StoreGlobal":
case "RegExpLiteral":
case "PropertyStore":
case "ComputedStore":
case "ArrayExpression":
case "JsxExpression":
case "JsxFragment":
case "ObjectExpression":
case "UnsupportedNode":
case "ObjectMethod":
case "FunctionExpression":
case "TaggedTemplateExpression": {
return false;
}
default: {
assertExhaustive(
instruction.value,
`Unexpected value kind \`${(instruction.value as any).kind}\``
);
}
}
}
function isIdentifierMutable(id: Identifier): boolean {
return id.mutableRange.end > id.mutableRange.start + 1;
}
function identifierHasMutableRange(id: Identifier): boolean {
return id.mutableRange.start > 0;
}
export function findDisjointMutableValues(
fn: HIRFunction,
): DisjointSet<Identifier> {
@@ -265,7 +351,7 @@ export function findDisjointMutableValues(
for (const phi of block.phis) {
if (
// The phi was reset because it was not mutated after creation
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
isIdentifierMutable(phi.id) &&
phi.id.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id)
) {
@@ -281,50 +367,52 @@ export function findDisjointMutableValues(
for (const instr of block.instructions) {
const operands: Array<Identifier> = [];
const range = instr.lvalue.identifier.mutableRange;
if (range.end > range.start + 1 || mayAllocate(fn.env, instr)) {
if (
isIdentifierMutable(instr.lvalue.identifier) ||
mayAllocate(fn.env, instr) ||
mayHaveChanged(fn.env, instr)
) {
operands.push(instr.lvalue!.identifier);
}
if (
instr.value.kind === 'StoreLocal' ||
instr.value.kind === 'StoreContext'
) {
if (
instr.value.lvalue.place.identifier.mutableRange.end >
instr.value.lvalue.place.identifier.mutableRange.start + 1
) {
if (isIdentifierMutable(instr.value.lvalue.place.identifier)) {
operands.push(instr.value.lvalue.place.identifier);
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
isMutableAtInstruction(instr, instr.value.value) &&
identifierHasMutableRange(instr.value.value.identifier)
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === 'Destructure') {
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
if (
place.identifier.mutableRange.end >
place.identifier.mutableRange.start + 1
isIdentifierMutable(place.identifier) ||
mayHaveChanged(fn.env, instr)
) {
operands.push(place.identifier);
}
}
if (
isMutable(instr, instr.value.value) &&
instr.value.value.identifier.mutableRange.start > 0
(isMutableAtInstruction(instr, instr.value.value) &&
identifierHasMutableRange(instr.value.value.identifier)) ||
mayHaveChanged(fn.env, instr)
) {
operands.push(instr.value.value.identifier);
}
} else if (instr.value.kind === 'MethodCall') {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
operand.identifier.mutableRange.start > 0
(isMutableAtInstruction(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
identifierHasMutableRange(operand.identifier)) ||
mayHaveChanged(fn.env, instr)
) {
operands.push(operand.identifier);
}
@@ -337,12 +425,13 @@ export function findDisjointMutableValues(
} else {
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
operand.identifier.mutableRange.start > 0
(isMutableAtInstruction(instr, operand) &&
/*
* exclude global variables from being added to scopes, we can't recreate them!
* TODO: improve handling of module-scoped variables and globals
*/
identifierHasMutableRange(operand.identifier)) ||
mayHaveChanged(fn.env, instr)
) {
operands.push(operand.identifier);
}

View File

@@ -45,6 +45,7 @@ export function memoizeFbtAndMacroOperandsInSameScope(
const fbtMacroTags = new Set([
...FBT_TAGS,
...(fn.env.config.customMacros ?? []),
...(fn.env.preserveManualMemo() ? ["useMemo", "useCallback"] : []),
]);
const fbtValues: Set<IdentifierId> = new Set();
while (true) {

View File

@@ -811,7 +811,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
this.env = env;
this.options = {
memoizeJsxElements: !this.env.config.enableForest,
forceMemoizePrimitives: this.env.config.enableForest,
forceMemoizePrimitives:
this.env.config.enableForest ||
this.env.config.enableChangeDetection != null,
};
}

View File

@@ -17,7 +17,7 @@ import {
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
} from '../HIR';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {isMutableAtInstruction} from '../ReactiveScopes/InferReactiveScopeVariables';
import {
ReactiveFunctionVisitor,
visitReactiveFunction,
@@ -103,7 +103,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
* TODO: isMutable is not safe to call here as it relies on identifier mutableRange which is no longer valid at this point
* in the pipeline
*/
(isMutable(instruction as Instruction, deps) ||
(isMutableAtInstruction(instruction as Instruction, deps) ||
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableChangeDetectionForDebugging
// @enableChangeDetection
function Component(props) {
let x = null;
if (props.cond) {
@@ -18,7 +18,7 @@ function Component(props) {
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionForDebugging
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetection
function Component(props) {
const $ = _c(2);
let x = null;

View File

@@ -1,4 +1,4 @@
// @enableChangeDetectionForDebugging
// @enableChangeDetection
function Component(props) {
let x = null;
if (props.cond) {

View File

@@ -0,0 +1,48 @@
## Input
```javascript
// @enableChangeDetectionWrappers
function Component(props) {
let x = null;
if (props.cond) {
x = [];
x.push(props.value);
}
return x;
}
```
## Code
```javascript
import { $structuralCheck, $store, $restore } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionWrappers
function Component(props) {
const $ = _c(2);
let x = null;
if (props.cond) {
{
x = [];
x.push(props.value);
let condition = $[0] !== props.value;
if (!condition) {
let old$x = $[1];
$structuralCheck(old$x, x, "x", "Component", "cached", "(3:6)");
}
$[0] = props.value;
$[1] = $store(x);
if (condition) {
x = [];
x.push(props.value);
$structuralCheck($[1], x, "x", "Component", "recomputed", "(3:6)");
x = $[1];
}
}
}
return x;
}
```

View File

@@ -0,0 +1,9 @@
// @enableChangeDetectionWrappers
function Component(props) {
let x = null;
if (props.cond) {
x = [];
x.push(props.value);
}
return x;
}

View File

@@ -0,0 +1,139 @@
## Input
```javascript
// @enableChangeDetection
let glob = 1;
function Component(props) {
const a = props.x;
const { b, ...c } = props.y;
const d = glob;
return (
<div>
{a}
{b}
{c.c}
{d}
</div>
);
}
```
## Code
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetection
let glob = 1;
function Component(props) {
const $ = _c(12);
let t0;
{
t0 = props.x;
let condition = $[0] !== props.x;
if (!condition) {
let old$t0 = $[1];
$structuralCheck(old$t0, t0, "t0", "Component", "cached", "(5:5)");
}
$[0] = props.x;
$[1] = t0;
if (condition) {
t0 = props.x;
$structuralCheck($[1], t0, "t0", "Component", "recomputed", "(5:5)");
t0 = $[1];
}
}
const a = t0;
let c;
let b;
{
({ b, ...c } = props.y);
let condition = $[2] !== props.y;
if (!condition) {
let old$c = $[3];
let old$b = $[4];
$structuralCheck(old$c, c, "c", "Component", "cached", "(6:6)");
$structuralCheck(old$b, b, "b", "Component", "cached", "(6:6)");
}
$[2] = props.y;
$[3] = c;
$[4] = b;
if (condition) {
({ b, ...c } = props.y);
$structuralCheck($[3], c, "c", "Component", "recomputed", "(6:6)");
c = $[3];
$structuralCheck($[4], b, "b", "Component", "recomputed", "(6:6)");
b = $[4];
}
}
let t1;
{
t1 = c.c;
let condition = $[5] !== c.c;
if (!condition) {
let old$t1 = $[6];
$structuralCheck(old$t1, t1, "t1", "Component", "cached", "(12:12)");
}
$[5] = c.c;
$[6] = t1;
if (condition) {
t1 = c.c;
$structuralCheck($[6], t1, "t1", "Component", "recomputed", "(12:12)");
t1 = $[6];
}
}
let t2;
{
t2 = glob;
let condition = $[7] === Symbol.for("react.memo_cache_sentinel");
if (!condition) {
let old$t2 = $[7];
$structuralCheck(old$t2, t2, "t2", "Component", "cached", "(13:13)");
}
$[7] = t2;
if (condition) {
t2 = glob;
$structuralCheck($[7], t2, "t2", "Component", "recomputed", "(13:13)");
t2 = $[7];
}
}
let t3;
{
t3 = (
<div>
{a}
{b}
{t1}
{t2}
</div>
);
let condition = $[8] !== a || $[9] !== b || $[10] !== t1;
if (!condition) {
let old$t3 = $[11];
$structuralCheck(old$t3, t3, "t3", "Component", "cached", "(9:14)");
}
$[8] = a;
$[9] = b;
$[10] = t1;
$[11] = t3;
if (condition) {
t3 = (
<div>
{a}
{b}
{t1}
{t2}
</div>
);
$structuralCheck($[11], t3, "t3", "Component", "recomputed", "(9:14)");
t3 = $[11];
}
}
return t3;
}
```

View File

@@ -0,0 +1,16 @@
// @enableChangeDetection
let glob = 1;
function Component(props) {
const a = props.x;
const { b, ...c } = props.y;
const d = glob;
return (
<div>
{a}
{b}
{c.c}
{d}
</div>
);
}

View File

@@ -1,17 +0,0 @@
## Input
```javascript
// @disableMemoizationForDebugging @enableChangeDetectionForDebugging
function Component(props) {}
```
## Error
```
InvalidConfig: Invalid environment config: the 'disableMemoizationForDebugging' and 'enableChangeDetectionForDebugging' options cannot be used together
```

View File

@@ -1,2 +0,0 @@
// @disableMemoizationForDebugging @enableChangeDetectionForDebugging
function Component(props) {}

View File

@@ -0,0 +1,107 @@
## Input
```javascript
// @disableMemoization @enableChangeDetection
import { useMemo } from "react";
function Component(props) {
const a = useMemo(() => <div>{props.a}</div>, [props]);
const b = <div>{props.b}</div>;
return (
<div>
{a}
{b}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 1, b: 2 }],
isComponent: true,
};
```
## Code
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @disableMemoization @enableChangeDetection
import { useMemo } from "react";
function Component(props) {
const $ = _c(7);
const a = useMemo(() => <div>{props.a}</div>, [props]);
let t0;
{
t0 = props.b;
let condition = $[0] !== props.b || true;
if (!condition) {
let old$t0 = $[1];
$structuralCheck(old$t0, t0, "t0", "Component", "cached", "(6:6)");
}
$[0] = props.b;
$[1] = t0;
if (condition) {
t0 = props.b;
$structuralCheck($[1], t0, "t0", "Component", "recomputed", "(6:6)");
t0 = $[1];
}
}
let t1;
{
t1 = <div>{t0}</div>;
let condition = $[2] !== t0 || true;
if (!condition) {
let old$t1 = $[3];
$structuralCheck(old$t1, t1, "t1", "Component", "cached", "(6:6)");
}
$[2] = t0;
$[3] = t1;
if (condition) {
t1 = <div>{t0}</div>;
$structuralCheck($[3], t1, "t1", "Component", "recomputed", "(6:6)");
t1 = $[3];
}
}
const b = t1;
let t2;
{
t2 = (
<div>
{a}
{b}
</div>
);
let condition = $[4] !== a || $[5] !== b || true;
if (!condition) {
let old$t2 = $[6];
$structuralCheck(old$t2, t2, "t2", "Component", "cached", "(8:11)");
}
$[4] = a;
$[5] = b;
$[6] = t2;
if (condition) {
t2 = (
<div>
{a}
{b}
</div>
);
$structuralCheck($[6], t2, "t2", "Component", "recomputed", "(8:11)");
t2 = $[6];
}
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 1, b: 2 }],
isComponent: true,
};
```

View File

@@ -0,0 +1,19 @@
// @disableMemoization @enableChangeDetection
import { useMemo } from "react";
function Component(props) {
const a = useMemo(() => <div>{props.a}</div>, [props]);
const b = <div>{props.b}</div>;
return (
<div>
{a}
{b}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 1, b: 2 }],
isComponent: true,
};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @disableMemoizationForDebugging
// @disableMemoization
import {useMemo} from 'react';
function Component({a}) {
@@ -21,37 +21,22 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @disableMemoizationForDebugging
import { c as _c } from "react/compiler-runtime"; // @disableMemoization
import { useMemo } from "react";
function Component(t0) {
const $ = _c(5);
const $ = _c(2);
const { a } = t0;
const x = useMemo(() => [a], []);
let t1;
if ($[0] !== a || true) {
t1 = () => [a];
$[0] = a;
if ($[0] !== x || true) {
t1 = <div>{x}</div>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel") || true) {
t2 = [];
$[2] = t2;
} else {
t2 = $[2];
}
const x = useMemo(t1, t2);
let t3;
if ($[3] !== x || true) {
t3 = <div>{x}</div>;
$[3] = x;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -1,4 +1,4 @@
// @disableMemoizationForDebugging
// @disableMemoization
import {useMemo} from 'react';
function Component({a}) {

View File

@@ -25,33 +25,18 @@ import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingManu
import { useMemo } from "react";
function Component(t0) {
const $ = _c(5);
const $ = _c(2);
const { a } = t0;
const x = useMemo(() => [a], []);
let t1;
if ($[0] !== a) {
t1 = () => [a];
$[0] = a;
if ($[0] !== x) {
t1 = <div>{x}</div>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[2] = t2;
} else {
t2 = $[2];
}
const x = useMemo(t1, t2);
let t3;
if ($[3] !== x) {
t3 = <div>{x}</div>;
$[3] = x;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
return t1;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
import {useState} from 'react'; // @enableChangeDetectionForDebugging
import {useState} from 'react'; // @enableChangeDetection
function useOther(x) {
return x;
@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime";
import { useState } from "react"; // @enableChangeDetectionForDebugging
import { useState } from "react"; // @enableChangeDetection
function useOther(x) {
return x;

View File

@@ -1,4 +1,4 @@
import {useState} from 'react'; // @enableChangeDetectionForDebugging
import {useState} from 'react'; // @enableChangeDetection
function useOther(x) {
return x;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableChangeDetectionForDebugging
// @enableChangeDetection
import {useState} from 'react';
function Component(props) {
@@ -16,36 +16,29 @@ function Component(props) {
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetectionForDebugging
import { c as _c } from "react/compiler-runtime"; // @enableChangeDetection
import { useState } from "react";
function Component(props) {
const $ = _c(3);
const $ = _c(2);
const [x] = useState(f(props.x));
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = f(props.x);
$[0] = t0;
} else {
t0 = $[0];
}
const [x] = useState(t0);
let t1;
{
t1 = <div>{x}</div>;
let condition = $[1] !== x;
t0 = <div>{x}</div>;
let condition = $[0] !== x;
if (!condition) {
let old$t1 = $[2];
$structuralCheck(old$t1, t1, "t1", "Component", "cached", "(6:6)");
let old$t0 = $[1];
$structuralCheck(old$t0, t0, "t0", "Component", "cached", "(6:6)");
}
$[1] = x;
$[2] = t1;
$[0] = x;
$[1] = t0;
if (condition) {
t1 = <div>{x}</div>;
$structuralCheck($[2], t1, "t1", "Component", "recomputed", "(6:6)");
t1 = $[2];
t0 = <div>{x}</div>;
$structuralCheck($[1], t0, "t0", "Component", "recomputed", "(6:6)");
t0 = $[1];
}
}
return t1;
return t0;
}
```

View File

@@ -1,4 +1,4 @@
// @enableChangeDetectionForDebugging
// @enableChangeDetection
import {useState} from 'react';
function Component(props) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
import {useState} from 'react'; // @enableChangeDetectionForDebugging
import {useState} from 'react'; // @enableChangeDetection
function Component(props) {
const w = f(props.x);
@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { $structuralCheck } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime";
import { useState } from "react"; // @enableChangeDetectionForDebugging
import { useState } from "react"; // @enableChangeDetection
function Component(props) {
const $ = _c(5);

View File

@@ -1,4 +1,4 @@
import {useState} from 'react'; // @enableChangeDetectionForDebugging
import {useState} from 'react'; // @enableChangeDetection
function Component(props) {
const w = f(props.x);

View File

@@ -493,12 +493,14 @@ const skipFilter = new Set([
// 'react-compiler-runtime' not yet supported
'flag-enable-emit-hook-guards',
'fast-refresh-refresh-on-const-changes-dev',
'useState-pruned-dependency-change-detect',
'useState-unpruned-dependency',
'useState-and-other-hook-unpruned-dependency',
'change-detect-reassign',
"change-detect",
"nomemo-and-change-detect",
"change-detect-wrapper",
// needs to be executed as a module
'meta-property',

View File

@@ -45,7 +45,7 @@ function makePluginOptions(
let hookPattern: string | null = null;
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
let validatePreserveExistingMemoizationGuarantees = false;
let enableChangeDetectionForDebugging = null;
let enableChangeDetection = null;
let customMacros = null;
if (firstLine.indexOf('@compilationMode(annotation)') !== -1) {
@@ -124,10 +124,20 @@ function makePluginOptions(
validatePreserveExistingMemoizationGuarantees = true;
}
if (firstLine.includes('@enableChangeDetectionForDebugging')) {
enableChangeDetectionForDebugging = {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
if (firstLine.includes("@enableChangeDetection")) {
enableChangeDetection = {
source: "react-compiler-runtime",
structuralCheck: "$structuralCheck",
};
}
if (firstLine.includes("@enableChangeDetectionWrappers")) {
enableChangeDetection = {
source: "react-compiler-runtime",
structuralCheck: "$structuralCheck",
wrappers: {
store: "$store",
restore: "$restore",
},
};
}
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
@@ -206,7 +216,7 @@ function makePluginOptions(
enableSharedRuntime__testonly: true,
hookPattern,
validatePreserveExistingMemoizationGuarantees,
enableChangeDetectionForDebugging,
enableChangeDetection,
},
compilationMode,
logger,