Compare commits

...

15 Commits

Author SHA1 Message Date
Mike Vitousek
3372820b7e Update base for Update on "[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-poisoned]
2024-07-26 15:52:20 -07:00
Mike Vitousek
8a524630bf Update base for Update on "[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-poisoned]
2024-07-26 15:16:29 -07:00
Mike Vitousek
9fa537c60f Update base for Update on "[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-poisoned]
2024-07-26 14:39:25 -07:00
Mike Vitousek
eec08b21bc Update base for Update on "[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-poisoned]
2024-07-18 16:14:21 -07:00
Mike Vitousek
c91449175d Update base for Update on "[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-poisoned]
2024-07-18 13:30:44 -07:00
Mike Vitousek
0782e4a7e4 Update base for Update on "[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-poisoned]
2024-07-18 09:42:01 -07:00
Mike Vitousek
1bc1c6533d Update base for Update on "[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-poisoned]
2024-07-18 09:18:10 -07:00
Mike Vitousek
ddacda023b Update base for Update on "[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-poisoned]
2024-07-02 09:59:45 -07:00
Mike Vitousek
d20c363231 Update on "[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": if memoization is disabled, we implement that by not restoring the old version of the value unless we're in a source-level memo block.

[ghstack-poisoned]
2024-07-02 00:41:29 -07:00
Mike Vitousek
37866ac627 Update base for Update on "[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": if memoization is disabled, we implement that by not restoring the old version of the value unless we're in a source-level memo block.

[ghstack-poisoned]
2024-07-02 00:41:28 -07:00
Mike Vitousek
8ec32bb270 [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": if memoization is disabled, we implement that by not restoring the old version of the value unless we're in a source-level memo block.

[ghstack-poisoned]
2024-07-01 18:02:29 -07:00
Mike Vitousek
7b22e7ca93 [compiler] Fewer assumptions about nonmutability when change detection enabled
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.

[ghstack-poisoned]
2024-07-01 18:02:24 -07:00
Mike Vitousek
b7325ff1f0 [compiler] Use dependencies from source for useMemo scopes
Summary: This modified the behavior of the compiler when preserving source-level useMemos as reactive scopes to use the depencies from the source as well. This accounts for the possibility that the useMemo in the source is more general than is required by local code, but is necessary due to rules of react violations.

With this change, ideally the disableMemoization mode will behave exactly like the uncompiled code.

[ghstack-poisoned]
2024-07-01 18:02:19 -07:00
Mike Vitousek
25f1a2f98f [compiler] Drop useMemos in memoization disabled mode, don't bail from source-level memo blocks
Summary: When testing Forget with the always-bail-out mode, we now preserve useMemos as reactive scope rather than hook calls, and we don't short-circuit those scopes.

[ghstack-poisoned]
2024-07-01 18:02:13 -07:00
Mike Vitousek
6e14dac327 [compiler] useMemo calls directly induce memoization blocks
Summary: To support the always-bailing-out and change-detection modes for the compiler, and to potentially support end-user codebases in some situations, we previously built a mode where user-level useMemos weren't dropped. This, however, results in codegen that is quite vastly different from the compiler's default mode, and which is likely to exercise different bugs.

This diff introduces a new mode that attempts to preserve user-level memoization in a way that is more compatible with the normal output of the compiler, dropping the literal useMemo calls and producing reactive scopes. The result of this is different from the existing @ensurePreserveMemoizationGuarantees in that the reactive scope is marked as originating from a useMemo, and cannot be merged with other memoization blocks, and that some operations are memoized that are not necessarily memoized today: specifically, `obj.x` and `f()`. This is to account for the fact that current useMemo calls may call non-idempotent functions inside useMemo--this is a violation of React's rules and is unsupported, but this mode attempts to support this behavior to make the compiler's behavior as close to user-level behavior as possible.

We build the user-level reactive scopes by simply adding all memoizable instructions between `StartMemo` and `FinishMemo` to their own reactive scope, possibly overwriting an existing scope. We do so before the scopes have been populated with dependencies or outputs so those passes can operate over these new scopes as normal.

[ghstack-poisoned]
2024-07-01 18:02:07 -07:00
32 changed files with 635 additions and 186 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

@@ -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.disableMemoizationForDebugging ||
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

@@ -670,14 +670,6 @@ 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,
},
);
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.disableMemoizationForDebugging) {
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
// @disableMemoizationForDebugging @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"; // @disableMemoizationForDebugging @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 @@
// @disableMemoizationForDebugging @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

@@ -25,33 +25,18 @@ import { c as _c } from "react/compiler-runtime"; // @disableMemoizationForDebug
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

@@ -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,