Compare commits

..

11 Commits

Author SHA1 Message Date
Mofei Zhang
489fee593d [compiler][be] repro edge cases for noEmit and module opt-outs
see test fixtures
2025-05-07 17:09:04 -04:00
mofeiZ
c129c2424b [compiler][repro] Nested fbt test fixture (#32779)
Ideally we should detect and bail out on this case to avoid babel build
failures.
2025-05-05 11:52:45 -04:00
mofeiZ
0c1575cee8 [compiler][bugfix] Bail out when a memo block declares hoisted fns (#32765)
Note that bailing out adds false positives for hoisted functions whose
only references are within other functions. For example, this rewrite
would be safe.
```js
// source program
  function foo() {
    return bar();
  }
  function bar() {
    return 42;
  }

// compiler output
let bar;
if (/* deps changed */) {
  function foo() {
    return bar();
  }
  bar = function bar() {
    return 42;
  }
}
```
These false positives are difficult to detect because any maybe-call of
foo before the definition of bar would be invalid.

Instead of bailing out, we should rewrite hoisted function declarations
to the following form.
```js
let bar$0;
if (/* deps changed */) {
  // All references within the declaring memo block
  // or before the function declaration should use
  // the original identifier `bar`
  function foo() {
    return bar();
  }
  function bar() {
    return 42;
  }
  bar$0 = bar;
}
// All references after the declaring memo block
// or after the function declaration should use
// the rewritten declaration `bar$0`
```
2025-05-05 11:45:58 -04:00
Sebastian Markbåge
52ea641449 [Flight] Don't increase serializedSize for every recursive pass (#33123)
I noticed that we increase this in the recursive part of the algorithm.
This would mean that we'd count a key more than once if it has Server
Components inside it recursively resolving. This moves it out to where
we enter from toJSON. Which is called once per JSON entry (and therefore
once per key).
2025-05-05 11:37:39 -04:00
Stephen Zhou
3ec88e797f [eslint-plugin-react-hooks] update doc url for rules of hooks (#33118) 2025-05-05 17:37:06 +02:00
Sebastian "Sebbie" Silbermann
0ca8420f9d [Flight] Use valid CSS selectors in useId format (#33099) 2025-05-04 13:47:32 +02:00
Joe Savona
0db8db178c [compiler] Validate against mutable functions being frozen
This revisits a validation I built a while ago, trying to make it more strict this time to ensure that it's high-signal.

We detect function expressions which are *known* mutable — they definitely can modify a variable defined outside of the function expression itself (modulo control flow). This uses types to look for known Store and Mutate effects only, and disregards mutations of effects. Any such function passed to a location with a Freeze effect is reported as a validation error.

This is behind a flag and disabled by default. If folks agree this makes sense to revisit, i'll test out internally and we can consider enabling by default.

ghstack-source-id: 075a731444ce95e52dbd5ea3be85c16d428927f5
Pull Request resolved: https://github.com/facebook/react/pull/33079
2025-05-03 09:15:32 +09:00
Joe Savona
8570116bd1 [compiler] Fix for uncalled functions that are known-mutable
If a function captures a mutable value but never gets called, we don't infer a mutable range for that function. This means that we also don't alias the function with its mutable captures.

This case is tricky, because we don't generally know for sure what is a mutation and what may just be a normal function call. For example:

```js
hook useFoo() {
  const x = makeObject();
  return () => {
    return readObject(x); // could be a mutation!
  }
}
```

If we pessimistically assume that all such cases are mutations, we'd have to group lots of memo scopes together unnecessarily. However, if there is definitely a mutation:

```js
hook useFoo(createEntryForKey) {
  const cache = new WeakMap();
  return (key) => {
    let entry = cache.get(key);
    if (entry == null) {
      entry = createEntryForKey(key);
      cache.set(key, entry); // known mutation!
    }
    return entry;
  }
}
```

Then we have to ensure that the function and its mutable captures alias together and end up in the same scope. However, aliasing together isn't enough if the function and operands all have empty mutable ranges (end = start + 1).

This pass finds function expressions and object methods that have an empty mutable range and known-mutable operands which also don't have a mutable range, and ensures that the function and those operands are aliased together *and* that their ranges are updated to end after the function expression. This is sufficient to ensure that a reactive scope is created for the alias set.

NOTE: The alternative is to reject these cases. If we do that we'd also want to similarly disallow cases like passing a mutable function to a hook.

ghstack-source-id: 5d8158246a320e80d8da3f0e395ac1953d8920a2
Pull Request resolved: https://github.com/facebook/react/pull/33078
2025-05-03 09:15:32 +09:00
Joe Savona
4f1d2ddf95 [compiler] Add types for WeakMap, WeakSet, and reanimated shared values
Building on mofeiz's recent work to type constructors. Also, types for reanimated values which are useful in the next PR.

ghstack-source-id: 1c81e213a11337ac7e9c85a429ecf3f1d1adef66
Pull Request resolved: https://github.com/facebook/react/pull/33077
2025-05-03 09:15:32 +09:00
Joe Savona
73d7e816b7 [compiler] ValidatePreservedManualMemoization reports detailed errors
This pass didn't previously report the precise difference btw inferred/manual dependencies unless a debug flag was set. But the error message is really good (nice job mofeiz): the only catch is that in theory the inferred dep could be a temporary that can't trivially be reported to the user.

But the messages are really useful for quickly verifying why the compiler couldn't preserve memoization. So here we switch to outputting a detailed message about the discrepancy btw inferred/manual deps so long as the inferred dep root is a named variable. I also slightly adjusted the message to handle the case where there is no diagnostic, which can occur if there were no manual deps but the compiler inferred a dependency.

ghstack-source-id: 534f6f1fec0855e05e85077eba050eb2ba254ef8
Pull Request resolved: https://github.com/facebook/react/pull/33095
2025-05-03 09:09:34 +09:00
Joe Savona
ac2cae5245 [compiler] Fix for string attribute values with emoji
If a JSX attribute value is a string that contains unicode or other characters that need special escaping, we wrap the attribute value in an expression container. However, our unicode to detect this only handled the basic unicode character plane, not the "astral" plane which includes emojis.

This PR updates the regex to detect such extended characters and also use an expression container.

ghstack-source-id: 6d9c8e4dd22285077108e2fa53d66154d1b781fb
Pull Request resolved: https://github.com/facebook/react/pull/33096
2025-05-03 09:07:50 +09:00
64 changed files with 1612 additions and 264 deletions

View File

@@ -103,6 +103,7 @@ import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -274,6 +275,10 @@ function runWithEnvironment(
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
if (env.config.validateNoFreezingKnownMutableFunctions) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}
inferReactivePlaces(hir);

View File

@@ -367,6 +367,11 @@ const EnvironmentConfigSchema = z.object({
*/
validateNoImpureFunctionsInRender: z.boolean().default(false),
/**
* Validate against passing mutable functions to hooks
*/
validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
/*
* When enabled, the compiler assumes that hooks follow the Rules of React:
* - Hooks may memoize computation based on any of their parameters, thus

View File

@@ -25,6 +25,9 @@ import {
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
addHook,
@@ -491,6 +494,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
true,
),
],
[
'WeakMap',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
},
null,
true,
),
],
[
'WeakSet',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
},
null,
true,
),
],
// TODO: rest of Global objects
];
@@ -908,7 +943,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: {kind: 'Object', shapeId: ReanimatedSharedValueId},
returnValueKind: ValueKind.Mutable,
noAlias: true,
calleeEffect: Effect.Read,

View File

@@ -1725,6 +1725,18 @@ export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}
/*
* Returns true if the type is a Ref or a custom user type that acts like a ref when it
* shouldn't. For now the only other case of this is Reanimated's shared values.
*/
export function isRefOrRefLikeMutableType(type: Type): boolean {
return (
type.kind === 'Object' &&
(type.shapeId === 'BuiltInUseRefId' ||
type.shapeId == 'ReanimatedSharedValueId')
);
}
export function isSetStateType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
}

View File

@@ -203,6 +203,8 @@ export const BuiltInPropsId = 'BuiltInProps';
export const BuiltInArrayId = 'BuiltInArray';
export const BuiltInSetId = 'BuiltInSet';
export const BuiltInMapId = 'BuiltInMap';
export const BuiltInWeakSetId = 'BuiltInWeakSet';
export const BuiltInWeakMapId = 'BuiltInWeakMap';
export const BuiltInFunctionId = 'BuiltInFunction';
export const BuiltInJsxId = 'BuiltInJsx';
export const BuiltInObjectId = 'BuiltInObject';
@@ -225,6 +227,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
// ShapeRegistry with default definitions for built-ins.
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
@@ -764,6 +769,101 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [
[
/**
* add(value)
* Parameters
* value: the value of the element to add to the Set object.
* Returns the Set object with added value.
*/
'add',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
}),
],
[
/**
* setInstance.delete(value)
* Returns true if value was already in Set; otherwise false.
*/
'delete',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
}),
],
[
'has',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
]);
addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [
[
'delete',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
}),
],
[
'get',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Poly'},
calleeEffect: Effect.Capture,
returnValueKind: ValueKind.Mutable,
}),
],
[
'has',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
/**
* Params
* key: the key of the element to add to the Map object. The key may be
* any JavaScript type (any primitive value or any type of JavaScript
* object).
* value: the value of the element to add to the Map object.
* Returns the Map object.
*/
'set',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Capture, Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Mutable,
}),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
['0', {kind: 'Poly'}],
[

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
Effect,
HIRFunction,
Identifier,
isMutableEffect,
isRefOrRefLikeMutableType,
makeInstructionId,
} from '../HIR/HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
/**
* If a function captures a mutable value but never gets called, we don't infer a
* mutable range for that function. This means that we also don't alias the function
* with its mutable captures.
*
* This case is tricky, because we don't generally know for sure what is a mutation
* and what may just be a normal function call. For example:
*
* ```
* hook useFoo() {
* const x = makeObject();
* return () => {
* return readObject(x); // could be a mutation!
* }
* }
* ```
*
* If we pessimistically assume that all such cases are mutations, we'd have to group
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
*
* ```
* hook useFoo(createEntryForKey) {
* const cache = new WeakMap();
* return (key) => {
* let entry = cache.get(key);
* if (entry == null) {
* entry = createEntryForKey(key);
* cache.set(key, entry); // known mutation!
* }
* return entry;
* }
* }
* ```
*
* Then we have to ensure that the function and its mutable captures alias together and
* end up in the same scope. However, aliasing together isn't enough if the function
* and operands all have empty mutable ranges (end = start + 1).
*
* This pass finds function expressions and object methods that have an empty mutable range
* and known-mutable operands which also don't have a mutable range, and ensures that the
* function and those operands are aliased together *and* that their ranges are updated to
* end after the function expression. This is sufficient to ensure that a reactive scope is
* created for the alias set.
*/
export function inferAliasForUncalledFunctions(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const block of fn.body.blocks.values()) {
instrs: for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (
value.kind !== 'ObjectMethod' &&
value.kind !== 'FunctionExpression'
) {
continue;
}
/*
* If the function is known to be mutated, we will have
* already aliased any mutable operands with it
*/
const range = lvalue.identifier.mutableRange;
if (range.end > range.start + 1) {
continue;
}
/*
* If the function already has operands with an active mutable range,
* then we don't need to do anything — the function will have already
* been visited and included in some mutable alias set. This case can
* also occur due to visiting the same function in an earlier iteration
* of the outer fixpoint loop.
*/
for (const operand of eachInstructionValueOperand(value)) {
if (isMutable(instr, operand)) {
continue instrs;
}
}
const operands: Set<Identifier> = new Set();
for (const effect of value.loweredFunc.func.effects ?? []) {
if (effect.kind !== 'ContextMutation') {
continue;
}
/*
* We're looking for known-mutations only, so we look at the effects
* rather than function context
*/
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
for (const operand of effect.places) {
/*
* It's possible that function effect analysis thinks there was a context mutation,
* but then InferReferenceEffects figures out some operands are globals and therefore
* creates a non-mutable effect for those operands.
* We should change InferReferenceEffects to swap the ContextMutation for a global
* mutation in that case, but for now we just filter them out here
*/
if (
isMutableEffect(operand.effect, operand.loc) &&
!isRefOrRefLikeMutableType(operand.identifier.type)
) {
operands.add(operand.identifier);
}
}
}
}
if (operands.size !== 0) {
operands.add(lvalue.identifier);
aliases.union([...operands]);
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
for (const operand of operands) {
operand.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
}
}
}

View File

@@ -6,6 +6,7 @@
*/
import {HIRFunction, Identifier} from '../HIR/HIR';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
import {inferAliases} from './InferAlias';
import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
@@ -76,6 +77,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
while (true) {
inferMutableRangesForAlias(ir, aliases);
inferAliasForPhis(ir, aliases);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;

View File

@@ -2327,9 +2327,12 @@ function codegenInstructionValue(
* u0080 to u009F: C1 control codes
* u00A0 to uFFFF: All non-basic Latin characters
* https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
*
* u010000 to u10FFFF: Astral plane characters
* https://mathiasbynens.be/notes/javascript-unicode
*/
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u;
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u;
function codegenJsxAttribute(
cx: Context,
attribute: JsxAttribute,

View File

@@ -5,10 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
convertHoistedLValueKind,
IdentifierId,
InstructionId,
InstructionKind,
Place,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
@@ -24,15 +27,38 @@ import {
/*
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
* original instruction kind.
*
* Also detects and bails out on context variables which are:
* - function declarations, which are hoisted by JS engines to the nearest block scope
* - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
* - declared
*
* This is because React Compiler converts a `function foo()` function declaration to
* 1. a `let foo;` declaration before reactive memo blocks
* 2. a `foo = function foo() {}` assignment within the block
*
* This means references before the assignment are invalid (see fixture
* error.todo-functiondecl-hoisting)
*/
export function pruneHoistedContexts(fn: ReactiveFunction): void {
visitReactiveFunction(fn, new Visitor(), {
activeScopes: empty(),
uninitialized: new Map(),
});
}
type VisitorState = {
activeScopes: Stack<Set<IdentifierId>>;
uninitialized: Map<
IdentifierId,
| {
kind: 'unknown-kind';
}
| {
kind: 'func';
definition: Place | null;
}
>;
};
class Visitor extends ReactiveFunctionTransform<VisitorState> {
@@ -40,15 +66,39 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
state.activeScopes = state.activeScopes.push(
new Set(scope.scope.declarations.keys()),
);
/**
* Add declared but not initialized / assigned variables. This may include
* function declarations that escape the memo block.
*/
for (const decl of scope.scope.declarations.values()) {
state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});
}
this.traverseScope(scope, state);
state.activeScopes.pop();
for (const decl of scope.scope.declarations.values()) {
state.uninitialized.delete(decl.identifier.id);
}
}
override visitPlace(
_id: InstructionId,
place: Place,
state: VisitorState,
): void {
const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
if (
maybeHoistedFn?.kind === 'func' &&
maybeHoistedFn.definition !== place
) {
CompilerError.throwTodo({
reason: '[PruneHoistedContexts] Rewrite hoisted function references',
loc: place.loc,
});
}
}
override transformInstruction(
instruction: ReactiveInstruction,
state: VisitorState,
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
/**
* Remove hoisted declarations to preserve TDZ
*/
@@ -57,6 +107,18 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
instruction.value.lvalue.kind,
);
if (maybeNonHoisted != null) {
if (
maybeNonHoisted === InstructionKind.Function &&
state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
) {
state.uninitialized.set(
instruction.value.lvalue.place.identifier.id,
{
kind: 'func',
definition: null,
},
);
}
return {kind: 'remove'};
}
}
@@ -65,7 +127,7 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
instruction.value.lvalue.kind !== InstructionKind.Reassign
) {
/**
* Rewrite StoreContexts let/const/functions that will be pre-declared in
* Rewrite StoreContexts let/const that will be pre-declared in
* codegen to reassignments.
*/
const lvalueId = instruction.value.lvalue.place.identifier.id;
@@ -73,10 +135,36 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
scope.has(lvalueId),
);
if (isDeclaredByScope) {
instruction.value.lvalue.kind = InstructionKind.Reassign;
if (
instruction.value.lvalue.kind === InstructionKind.Let ||
instruction.value.lvalue.kind === InstructionKind.Const
) {
instruction.value.lvalue.kind = InstructionKind.Reassign;
} else if (instruction.value.lvalue.kind === InstructionKind.Function) {
const maybeHoistedFn = state.uninitialized.get(lvalueId);
if (maybeHoistedFn != null) {
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
reason: '[PruneHoistedContexts] Unexpected hoisted function',
loc: instruction.loc,
});
maybeHoistedFn.definition = instruction.value.lvalue.place;
/**
* References to hoisted functions are now "safe" as variable assignments
* have finished.
*/
state.uninitialized.delete(lvalueId);
}
} else {
CompilerError.throwTodo({
reason: '[PruneHoistedContexts] Unexpected kind',
description: `(${instruction.value.lvalue.kind})`,
loc: instruction.loc,
});
}
}
}
this.visitInstruction(instruction, state);
return {kind: 'keep'};
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, Effect, ErrorSeverity} from '..';
import {
FunctionEffect,
HIRFunction,
IdentifierId,
isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {Iterable_some} from '../Utils/utils';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
* where a frozen value is expected. Example:
*
* ```
* function Component() {
* const cache = new Map();
* const onClick = () => {
* cache.set(...);
* }
* useHook(onClick); // ERROR: cannot pass a mutable value
* return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
* }
* ```
*
* Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
* variables. But unlike other mutables values like an array, the receiver of the function has
* no way to avoid mutation — for example, a function can receive an array and choose not to mutate
* it, but there's no way to know that a function is mutable and avoid calling it.
*
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
* that are passed where a frozen value is expected and rejects them.
*/
export function validateNoFreezingKnownMutableFunctions(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
Extract<FunctionEffect, {kind: 'ContextMutation'}>
> = new Map();
function visitOperand(operand: Place): void {
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
errors.push({
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
loc: operand.loc,
severity: ErrorSeverity.InvalidReact,
});
errors.push({
reason: `The function modifies a local variable here`,
loc: effect.loc,
severity: ErrorSeverity.InvalidReact,
});
}
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadLocal': {
const effect = contextMutationEffects.get(value.place.identifier.id);
if (effect != null) {
contextMutationEffects.set(lvalue.identifier.id, effect);
}
break;
}
case 'StoreLocal': {
const effect = contextMutationEffects.get(value.value.identifier.id);
if (effect != null) {
contextMutationEffects.set(lvalue.identifier.id, effect);
contextMutationEffects.set(
value.lvalue.place.identifier.id,
effect,
);
}
break;
}
case 'FunctionExpression': {
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
effect => {
return (
effect.kind === 'ContextMutation' &&
(effect.effect === Effect.Store ||
effect.effect === Effect.Mutate) &&
Iterable_some(effect.places, place => {
return (
isMutableEffect(place.effect, place.loc) &&
!isRefOrRefLikeMutableType(place.identifier.type)
);
})
);
},
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(value)) {
visitOperand(operand);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visitOperand(operand);
}
}
return errors.asResult();
}

View File

@@ -141,14 +141,14 @@ function getCompareDependencyResultDescription(
): string {
switch (result) {
case CompareDependencyResult.Ok:
return 'dependencies equal';
return 'Dependencies equal';
case CompareDependencyResult.RootDifference:
case CompareDependencyResult.PathDifference:
return 'inferred different dependency than source';
return 'Inferred different dependency than source';
case CompareDependencyResult.RefAccessDifference:
return 'differences in ref.current access';
return 'Differences in ref.current access';
case CompareDependencyResult.Subpath:
return 'inferred less specific property than source';
return 'Inferred less specific property than source';
}
}
@@ -279,17 +279,20 @@ function validateInferredDep(
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
description: DEBUG
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. Detail: ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'none'
}`
: null,
description:
DEBUG ||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
? `The inferred dependency was \`${prettyPrintScopeDependency(
dep,
)}\`, but the source dependencies were [${validDepsInMemoBlock
.map(dep => printManualMemoDependency(dep, true))
.join(', ')}]. ${
errorDiagnostic
? getCompareDependencyResultDescription(errorDiagnostic)
: 'Inferred dependency not present in source'
}`
: null,
loc: memoLocation,
suggestions: null,
});

View File

@@ -1,79 +0,0 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Fixture currently fails with
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
* Forget:
* (kind: exception) bar is not a function
*/
function Foo({value}) {
const result = bar();
function bar() {
return {value};
}
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{value: 2}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
/**
* Fixture currently fails with
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
* Forget:
* (kind: exception) bar is not a function
*/
function Foo(t0) {
const $ = _c(6);
const { value } = t0;
let bar;
let result;
if ($[0] !== value) {
result = bar();
bar = function bar() {
return { value };
};
$[0] = value;
$[1] = bar;
$[2] = result;
} else {
bar = $[1];
result = $[2];
}
let t1;
if ($[3] !== bar || $[4] !== result) {
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
$[3] = bar;
$[4] = result;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ value: 2 }],
};
```

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -0,0 +1,34 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
function useFoo() {
const cache = new Map();
useHook(() => {
cache.set('key', 'value');
});
}
```
## Error
```
3 | function useFoo() {
4 | const cache = new Map();
> 5 | useHook(() => {
| ^^^^^^^
> 6 | cache.set('key', 'value');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | });
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
InvalidReact: The function modifies a local variable here (6:6)
8 | }
9 |
```

View File

@@ -0,0 +1,8 @@
// @validateNoFreezingKnownMutableFunctions
function useFoo() {
const cache = new Map();
useHook(() => {
cache.set('key', 'value');
});
}

View File

@@ -29,7 +29,7 @@ function Component(props) {
> 6 | // deps are optional
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [props.items?.edges?.nodes]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:7)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items.edges.nodes`, but the source dependencies were [props.items?.edges?.nodes]. Inferred different dependency than source (3:7)
8 | return <Foo data={data} />;
9 | }
10 |

View File

@@ -0,0 +1,30 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
function Component() {
const cache = new Map();
const fn = () => {
cache.set('key', 'value');
};
return <Foo fn={fn} />;
}
```
## Error
```
5 | cache.set('key', 'value');
6 | };
> 7 | return <Foo fn={fn} />;
| ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
InvalidReact: The function modifies a local variable here (5:5)
8 | }
9 |
```

View File

@@ -0,0 +1,8 @@
// @validateNoFreezingKnownMutableFunctions
function Component() {
const cache = new Map();
const fn = () => {
cache.set('key', 'value');
};
return <Foo fn={fn} />;
}

View File

@@ -0,0 +1,36 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
import {useHook} from 'shared-runtime';
function useFoo() {
useHook(); // for inference to kick in
const cache = new Map();
return () => {
cache.set('key', 'value');
};
}
```
## Error
```
5 | useHook(); // for inference to kick in
6 | const cache = new Map();
> 7 | return () => {
| ^^^^^^^
> 8 | cache.set('key', 'value');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | };
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
InvalidReact: The function modifies a local variable here (8:8)
10 | }
11 |
```

View File

@@ -0,0 +1,10 @@
// @validateNoFreezingKnownMutableFunctions
import {useHook} from 'shared-runtime';
function useFoo() {
useHook(); // for inference to kick in
const cache = new Map();
return () => {
cache.set('key', 'value');
};
}

View File

@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
> 12 | Ref.current?.click();
| ^^^^^^^^^^^^^^^^^^^^^^^^^
> 13 | }, []);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13)
14 |
15 | return <button onClick={onClick} />;
16 | }

View File

@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
> 12 | notaref.current?.click();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 13 | }, []);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13)
14 |
15 | return <button onClick={onClick} />;
16 | }

View File

@@ -0,0 +1,43 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Fixture currently fails with
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
* Forget:
* (kind: exception) bar is not a function
*/
function Foo({value}) {
const result = bar();
function bar() {
return {value};
}
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{value: 2}],
};
```
## Error
```
10 | */
11 | function Foo({value}) {
> 12 | const result = bar();
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (12:12)
13 | function bar() {
14 | return {value};
15 | }
```

View File

@@ -0,0 +1,46 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
/**
* Also see error.todo-functiondecl-hoisting.tsx which shows *invalid*
* compilation cases.
*
* This bailout specifically is a false positive for since this function's only
* reference-before-definition are within other functions which are not invoked.
*/
function Foo() {
'use memo';
function foo() {
return bar();
}
function bar() {
return 42;
}
return <Stringify fn1={foo} fn2={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
## Error
```
13 | return bar();
14 | }
> 15 | function bar() {
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (15:15)
16 | return 42;
17 | }
18 |
```

View File

@@ -0,0 +1,25 @@
import {Stringify} from 'shared-runtime';
/**
* Also see error.todo-functiondecl-hoisting.tsx which shows *invalid*
* compilation cases.
*
* This bailout specifically is a false positive for since this function's only
* reference-before-definition are within other functions which are not invoked.
*/
function Foo() {
'use memo';
function foo() {
return bar();
}
function bar() {
return 42;
}
return <Stringify fn1={foo} fn2={bar} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};

View File

@@ -0,0 +1,56 @@
## Input
```javascript
import fbt from 'fbt';
import {Stringify} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component({firstname, lastname}) {
'use memo';
return (
<Stringify>
{fbt(
[
'Name: ',
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
<Stringify key={0} name={lastname}>
{fbt('(inner fbt)', 'Inner fbt value')}
</Stringify>
),
],
'Name'
)}
</Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstname: 'first', lastname: 'last'}],
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
};
```
## Error
```
Line 19 Column 11: fbt: unsupported babel node: Identifier
---
t3
---
```

View File

@@ -0,0 +1,38 @@
import fbt from 'fbt';
import {Stringify} from 'shared-runtime';
/**
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
* ---
* t3
* ---
*/
function Component({firstname, lastname}) {
'use memo';
return (
<Stringify>
{fbt(
[
'Name: ',
fbt.param('firstname', <Stringify key={0} name={firstname} />),
', ',
fbt.param(
'lastname',
<Stringify key={0} name={lastname}>
{fbt('(inner fbt)', 'Inner fbt value')}
</Stringify>
),
],
'Name'
)}
</Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstname: 'first', lastname: 'last'}],
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
};

View File

@@ -11,6 +11,7 @@ function Component() {
<Text value={'Lauren'} />
<Text value={'சத்யா'} />
<Text value={'Sathya'} />
<Text value={'welcome 👋'} />
</div>
);
}
@@ -42,6 +43,7 @@ function Component() {
<Text value="Lauren" />
<Text value={"\u0B9A\u0BA4\u0BCD\u0BAF\u0BBE"} />
<Text value="Sathya" />
<Text value={"welcome \uD83D\uDC4B"} />
</div>
);
$[0] = t0;
@@ -74,4 +76,4 @@ export const FIXTURE_ENTRYPOINT = {
### Eval output
(kind: ok) <div><span>
</span><span>A E</span><span>나은</span><span>Lauren</span><span>சத்யா</span><span>Sathya</span></div>
</span><span>A E</span><span>나은</span><span>Lauren</span><span>சத்யா</span><span>Sathya</span><span>welcome 👋</span></div>

View File

@@ -7,6 +7,7 @@ function Component() {
<Text value={'Lauren'} />
<Text value={'சத்யா'} />
<Text value={'Sathya'} />
<Text value={'welcome 👋'} />
</div>
);
}

View File

@@ -41,7 +41,7 @@ export const FIXTURE_ENTRYPOINT = {
> 10 | }
| ^^^^^^^^^^^^^^^^
> 11 | }, [propA, propB.x.y]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:11)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA, propB.x.y]. Inferred less specific property than source (5:11)
12 | }
13 |
14 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -48,9 +48,9 @@ export const FIXTURE_ENTRYPOINT = {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
17 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -24,7 +24,7 @@ function useHook(maybeRef) {
> 6 | return [maybeRef.current];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [maybeRef]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:7)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `maybeRef.current`, but the source dependencies were [maybeRef]. Differences in ref.current access (5:7)
8 | }
9 |
```

View File

@@ -24,7 +24,7 @@ function useHook(maybeRef, shouldRead) {
> 6 | return () => [maybeRef.current];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [shouldRead, maybeRef]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:7)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `maybeRef.current`, but the source dependencies were [shouldRead, maybeRef]. Differences in ref.current access (5:7)
8 | }
9 |
```

View File

@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
> 12 | }
| ^^^^^^^^^^^^^^^^^^^^^^
> 13 | }, []);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (9:13)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source (9:13)
14 | }
15 |
16 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -22,7 +22,7 @@ function useHook(x) {
7 | const aliasedProp = x.y.z;
8 |
> 9 | return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]);
| ^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (9:9)
| ^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `aliasedX`, but the source dependencies were [x, aliasedProp]. Inferred different dependency than source (9:9)
10 | }
11 |
```

View File

@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
> 9 | };
| ^^^^^^^^^^^^
> 10 | }, [propA, propB.x.y]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:10)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB?.x.y`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source (5:10)
11 | }
12 |
13 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -43,7 +43,7 @@ function Component({propA, propB}) {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA?.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
```

View File

@@ -24,7 +24,7 @@ function Component({propA}) {
> 6 | return propA.x();
| ^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [propA.x]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:7)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:7)
8 | }
9 |
```

View File

@@ -22,7 +22,7 @@ function useHook(x) {
7 | const aliasedProp = x.y.z;
8 |
> 9 | return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]);
| ^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (9:9)
| ^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x`, but the source dependencies were [aliasedX, aliasedProp]. Inferred different dependency than source (9:9)
10 | }
11 |
```

View File

@@ -43,7 +43,7 @@ function Component({propA, propB}) {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA?.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
```

View File

@@ -43,9 +43,9 @@ function Component({propA, propB}) {
> 13 | }
| ^^^^^^^^^^^^^^^^^
> 14 | }, [propA.a, propB.x.y]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
15 | }
16 |
```

View File

@@ -30,7 +30,7 @@ function Component({propA}) {
> 8 | };
| ^^^^^^^^^^^^
> 9 | }, [propA.x]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:9)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:9)
10 | }
11 |
```

View File

@@ -24,7 +24,7 @@ function Component({propA}) {
> 6 | return propA.x();
| ^^^^^^^^^^^^^^^^^^^^^
> 7 | }, [propA.x]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (5:7)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:7)
8 | }
9 |
```

View File

@@ -37,7 +37,7 @@ function useFoo(input1) {
> 17 | return [y];
| ^^^^^^^^^^^^^^^
> 18 | }, [(mutate(x), y)]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (16:18)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source (16:18)
19 |
20 | return [x, memoized];
21 | }

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -41,7 +41,7 @@ function Component(props) {
> 10 | return x;
| ^^^^^^^^^^^^^^^^^
> 11 | }, [props?.items, props.cond]);
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11)
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
12 | return (
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
14 | );

View File

@@ -0,0 +1,30 @@
## Input
```javascript
// @panicThreshold(none)
'use no memo';
function Foo() {
return <button onClick={() => alert('hello!')}>Click me!</button>;
}
```
## Code
```javascript
// @panicThreshold(none)
"use no memo";
function Foo() {
return <button onClick={() => alert("hello!")}>Click me!</button>;
}
function _temp() {
return alert("hello!");
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,6 @@
// @panicThreshold(none)
'use no memo';
function Foo() {
return <button onClick={() => alert('hello!')}>Click me!</button>;
}

View File

@@ -0,0 +1,77 @@
## Input
```javascript
// @flow
/**
* This hook returns a function that when called with an input object,
* will return the result of mapping that input with the supplied map
* function. Results are cached, so if the same input is passed again,
* the same output object will be returned.
*
* Note that this technically violates the rules of React and is unsafe:
* hooks must return immutable objects and be pure, and a function which
* captures and mutates a value when called is inherently not pure.
*
* However, in this case it is technically safe _if_ the mapping function
* is pure *and* the resulting objects are never modified. This is because
* the function only caches: the result of `returnedFunction(someInput)`
* strictly depends on `returnedFunction` and `someInput`, and cannot
* otherwise change over time.
*/
hook useMemoMap<TInput: interface {}, TOutput>(
map: TInput => TOutput
): TInput => TOutput {
return useMemo(() => {
// The original issue is that `cache` was not memoized together with the returned
// function. This was because neither appears to ever be mutated — the function
// is known to mutate `cache` but the function isn't called.
//
// The fix is to detect cases like this — functions that are mutable but not called -
// and ensure that their mutable captures are aliased together into the same scope.
const cache = new WeakMap<TInput, TOutput>();
return input => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
}, [map]);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useMemoMap(map) {
const $ = _c(2);
let t0;
let t1;
if ($[0] !== map) {
const cache = new WeakMap();
t1 = (input) => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
$[0] = map;
$[1] = t1;
} else {
t1 = $[1];
}
t0 = t1;
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,38 @@
// @flow
/**
* This hook returns a function that when called with an input object,
* will return the result of mapping that input with the supplied map
* function. Results are cached, so if the same input is passed again,
* the same output object will be returned.
*
* Note that this technically violates the rules of React and is unsafe:
* hooks must return immutable objects and be pure, and a function which
* captures and mutates a value when called is inherently not pure.
*
* However, in this case it is technically safe _if_ the mapping function
* is pure *and* the resulting objects are never modified. This is because
* the function only caches: the result of `returnedFunction(someInput)`
* strictly depends on `returnedFunction` and `someInput`, and cannot
* otherwise change over time.
*/
hook useMemoMap<TInput: interface {}, TOutput>(
map: TInput => TOutput
): TInput => TOutput {
return useMemo(() => {
// The original issue is that `cache` was not memoized together with the returned
// function. This was because neither appears to ever be mutated — the function
// is known to mutate `cache` but the function isn't called.
//
// The fix is to detect cases like this — functions that are mutable but not called -
// and ensure that their mutable captures are aliased together into the same scope.
const cache = new WeakMap<TInput, TOutput>();
return input => {
let output = cache.get(input);
if (output == null) {
output = map(input);
cache.set(input, output);
}
return output;
};
}, [map]);
}

View File

@@ -0,0 +1,48 @@
## Input
```javascript
// @noEmit
function Foo() {
'use memo';
return <button onClick={() => alert('hello!')}>Click me!</button>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @noEmit
function Foo() {
"use memo";
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <button onClick={_temp}>Click me!</button>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
return alert("hello!");
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
### Eval output
(kind: ok) <button>Click me!</button>

View File

@@ -0,0 +1,11 @@
// @noEmit
function Foo() {
'use memo';
return <button onClick={() => alert('hello!')}>Click me!</button>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};

View File

@@ -0,0 +1,176 @@
## Input
```javascript
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const map = new WeakMap();
const mapAlias = map.set(a, 0);
mapAlias.set(c, 0);
const hasB = map.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={map} />
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { ValidateMemoization } from "shared-runtime";
function Component(t0) {
const $ = _c(27);
const { a, b, c } = t0;
let map;
let mapAlias;
if ($[0] !== a || $[1] !== c) {
map = new WeakMap();
mapAlias = map.set(a, 0);
mapAlias.set(c, 0);
$[0] = a;
$[1] = c;
$[2] = map;
$[3] = mapAlias;
} else {
map = $[2];
mapAlias = $[3];
}
const hasB = map.has(b);
let t1;
if ($[4] !== a || $[5] !== c) {
t1 = [a, c];
$[4] = a;
$[5] = c;
$[6] = t1;
} else {
t1 = $[6];
}
let t2;
if ($[7] !== map || $[8] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={map} />;
$[7] = map;
$[8] = t1;
$[9] = t2;
} else {
t2 = $[9];
}
let t3;
if ($[10] !== a || $[11] !== c) {
t3 = [a, c];
$[10] = a;
$[11] = c;
$[12] = t3;
} else {
t3 = $[12];
}
let t4;
if ($[13] !== mapAlias || $[14] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={mapAlias} />;
$[13] = mapAlias;
$[14] = t3;
$[15] = t4;
} else {
t4 = $[15];
}
let t5;
if ($[16] !== b) {
t5 = [b];
$[16] = b;
$[17] = t5;
} else {
t5 = $[17];
}
let t6;
if ($[18] !== hasB) {
t6 = [hasB];
$[18] = hasB;
$[19] = t6;
} else {
t6 = $[19];
}
let t7;
if ($[20] !== t5 || $[21] !== t6) {
t7 = <ValidateMemoization inputs={t5} output={t6} />;
$[20] = t5;
$[21] = t6;
$[22] = t7;
} else {
t7 = $[22];
}
let t8;
if ($[23] !== t2 || $[24] !== t4 || $[25] !== t7) {
t8 = (
<>
{t2}
{t4}
{t7}
</>
);
$[23] = t2;
$[24] = t4;
$[25] = t7;
$[26] = t8;
} else {
t8 = $[26];
}
return t8;
}
const v1 = { value: 1 };
const v2 = { value: 2 };
const v3 = { value: 3 };
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: v1, b: v1, c: v1 }],
sequentialRenders: [
{ a: v1, b: v1, c: v1 },
{ a: v2, b: v1, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v1, b: v2, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v1, b: v1, c: v1 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":2}],"output":[false]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>

View File

@@ -0,0 +1,35 @@
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const map = new WeakMap();
const mapAlias = map.set(a, 0);
mapAlias.set(c, 0);
const hasB = map.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={map} />
<ValidateMemoization inputs={[a, c]} output={mapAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};

View File

@@ -0,0 +1,176 @@
## Input
```javascript
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const set = new WeakSet();
const setAlias = set.add(a);
setAlias.add(c);
const hasB = set.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={set} />
<ValidateMemoization inputs={[a, c]} output={setAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { ValidateMemoization } from "shared-runtime";
function Component(t0) {
const $ = _c(27);
const { a, b, c } = t0;
let set;
let setAlias;
if ($[0] !== a || $[1] !== c) {
set = new WeakSet();
setAlias = set.add(a);
setAlias.add(c);
$[0] = a;
$[1] = c;
$[2] = set;
$[3] = setAlias;
} else {
set = $[2];
setAlias = $[3];
}
const hasB = set.has(b);
let t1;
if ($[4] !== a || $[5] !== c) {
t1 = [a, c];
$[4] = a;
$[5] = c;
$[6] = t1;
} else {
t1 = $[6];
}
let t2;
if ($[7] !== set || $[8] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={set} />;
$[7] = set;
$[8] = t1;
$[9] = t2;
} else {
t2 = $[9];
}
let t3;
if ($[10] !== a || $[11] !== c) {
t3 = [a, c];
$[10] = a;
$[11] = c;
$[12] = t3;
} else {
t3 = $[12];
}
let t4;
if ($[13] !== setAlias || $[14] !== t3) {
t4 = <ValidateMemoization inputs={t3} output={setAlias} />;
$[13] = setAlias;
$[14] = t3;
$[15] = t4;
} else {
t4 = $[15];
}
let t5;
if ($[16] !== b) {
t5 = [b];
$[16] = b;
$[17] = t5;
} else {
t5 = $[17];
}
let t6;
if ($[18] !== hasB) {
t6 = [hasB];
$[18] = hasB;
$[19] = t6;
} else {
t6 = $[19];
}
let t7;
if ($[20] !== t5 || $[21] !== t6) {
t7 = <ValidateMemoization inputs={t5} output={t6} />;
$[20] = t5;
$[21] = t6;
$[22] = t7;
} else {
t7 = $[22];
}
let t8;
if ($[23] !== t2 || $[24] !== t4 || $[25] !== t7) {
t8 = (
<>
{t2}
{t4}
{t7}
</>
);
$[23] = t2;
$[24] = t4;
$[25] = t7;
$[26] = t8;
} else {
t8 = $[26];
}
return t8;
}
const v1 = { value: 1 };
const v2 = { value: 2 };
const v3 = { value: 3 };
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: v1, b: v1, c: v1 }],
sequentialRenders: [
{ a: v1, b: v1, c: v1 },
{ a: v2, b: v1, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v1, b: v2, c: v1 },
{ a: v1, b: v1, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v3, b: v3, c: v1 },
{ a: v1, b: v1, c: v1 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":2},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":2}],"output":[false]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3},{"value":1}],"output":{}}</div><div>{"inputs":[{"value":3}],"output":[true]}</div>
<div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1},"[[ cyclic ref *2 ]]"],"output":{}}</div><div>{"inputs":[{"value":1}],"output":[true]}</div>

View File

@@ -0,0 +1,35 @@
import {ValidateMemoization} from 'shared-runtime';
function Component({a, b, c}) {
const set = new WeakSet();
const setAlias = set.add(a);
setAlias.add(c);
const hasB = set.has(b);
return (
<>
<ValidateMemoization inputs={[a, c]} output={set} />
<ValidateMemoization inputs={[a, c]} output={setAlias} />
<ValidateMemoization inputs={[b]} output={[hasB]} />
</>
);
}
const v1 = {value: 1};
const v2 = {value: 2};
const v3 = {value: 3};
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: v1, b: v1, c: v1}],
sequentialRenders: [
{a: v1, b: v1, c: v1},
{a: v2, b: v1, c: v1},
{a: v1, b: v1, c: v1},
{a: v1, b: v2, c: v1},
{a: v1, b: v1, c: v1},
{a: v3, b: v3, c: v1},
{a: v3, b: v3, c: v1},
{a: v1, b: v1, c: v1},
],
};

View File

@@ -22,12 +22,6 @@ import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
import {measurePerformance} from './tools/runtimePerf';
function calculateMean(values: number[]): string {
return values.length > 0
? values.reduce((acc, curr) => acc + curr, 0) / values.length + 'ms'
: 'could not collect';
}
const server = new McpServer({
name: 'React',
version: '0.0.0',
@@ -332,16 +326,17 @@ server.tool(
# React Component Performance Results
## Mean Render Time
${calculateMean(results.renderTime)}
${results.renderTime / iterations}ms
## Mean Web Vitals
- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)}
- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)}
- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)}
- Cumulative Layout Shift (CLS): ${results.webVitals.cls / iterations}ms
- Largest Contentful Paint (LCP): ${results.webVitals.lcp / iterations}ms
- Interaction to Next Paint (INP): ${results.webVitals.inp / iterations}ms
- First Input Delay (FID): ${results.webVitals.fid / iterations}ms
## Mean React Profiler
- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)}
- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)}
- Actual Duration: ${results.reactProfiler.actualDuration / iterations}ms
- Base Duration: ${results.reactProfiler.baseDuration / iterations}ms
`;
return {

View File

@@ -8,51 +8,25 @@ import * as babelPresetEnv from '@babel/preset-env';
import * as babelPresetReact from '@babel/preset-react';
type PerformanceResults = {
renderTime: number[];
renderTime: number;
webVitals: {
cls: number[];
lcp: number[];
inp: number[];
fid: number[];
ttfb: number[];
cls: number;
lcp: number;
inp: number;
fid: number;
ttfb: number;
};
reactProfiler: {
id: number[];
phase: number[];
actualDuration: number[];
baseDuration: number[];
startTime: number[];
commitTime: number[];
id: number;
phase: number;
actualDuration: number;
baseDuration: number;
startTime: number;
commitTime: number;
};
error: Error | null;
};
type EvaluationResults = {
renderTime: number | null;
webVitals: {
cls: number | null;
lcp: number | null;
inp: number | null;
fid: number | null;
ttfb: number | null;
};
reactProfiler: {
id: number | null;
phase: number | null;
actualDuration: number | null;
baseDuration: number | null;
startTime: number | null;
commitTime: number | null;
};
error: Error | null;
};
function delay(time: number) {
return new Promise(function (resolve) {
setTimeout(resolve, time);
});
}
export async function measurePerformance(
code: string,
iterations: number,
@@ -98,21 +72,21 @@ export async function measurePerformance(
const html = buildHtml(transpiled);
let performanceResults: PerformanceResults = {
renderTime: [],
renderTime: 0,
webVitals: {
cls: [],
lcp: [],
inp: [],
fid: [],
ttfb: [],
cls: 0,
lcp: 0,
inp: 0,
fid: 0,
ttfb: 0,
},
reactProfiler: {
id: [],
phase: [],
actualDuration: [],
baseDuration: [],
startTime: [],
commitTime: [],
id: 0,
phase: 0,
actualDuration: 0,
baseDuration: 0,
startTime: 0,
commitTime: 0,
},
error: null,
};
@@ -122,73 +96,38 @@ export async function measurePerformance(
await page.waitForFunction(
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
);
// ui chaos monkey
const selectors = await page.evaluate(() => {
window.__INTERACTABLE_SELECTORS__ = [];
const elements = Array.from(document.querySelectorAll('a')).concat(
Array.from(document.querySelectorAll('button')),
);
for (const el of elements) {
window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase());
await page.waitForFunction(`window.__RESULT__ !== undefined && (function() {
for (const el of [...document.querySelectorAll('a'), ...document.querySelectorAll('button')]) {
console.log(el);
el.click();
}
return window.__INTERACTABLE_SELECTORS__;
});
await Promise.all(
selectors.map(async (selector: string) => {
try {
await page.click(selector);
} catch (e) {
console.log(`warning: Could not click ${selector}: ${e.message}`);
}
}),
);
await delay(500);
// Visit a new page for 1s to background the current page so that WebVitals can finish being calculated
const tempPage = await browser.newPage();
await tempPage.evaluate(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 1000);
});
});
await tempPage.close();
const evaluationResult: EvaluationResults = await page.evaluate(() => {
return true;
})() `);
const evaluationResult: PerformanceResults = await page.evaluate(() => {
return (window as any).__RESULT__;
});
if (evaluationResult.renderTime !== null) {
performanceResults.renderTime.push(evaluationResult.renderTime);
}
// TODO: investigate why webvital metrics are not populating correctly
performanceResults.renderTime += evaluationResult.renderTime;
performanceResults.webVitals.cls += evaluationResult.webVitals.cls || 0;
performanceResults.webVitals.lcp += evaluationResult.webVitals.lcp || 0;
performanceResults.webVitals.inp += evaluationResult.webVitals.inp || 0;
performanceResults.webVitals.fid += evaluationResult.webVitals.fid || 0;
performanceResults.webVitals.ttfb += evaluationResult.webVitals.ttfb || 0;
const webVitalMetrics = ['cls', 'lcp', 'inp', 'fid', 'ttfb'] as const;
for (const metric of webVitalMetrics) {
if (evaluationResult.webVitals[metric] !== null) {
performanceResults.webVitals[metric].push(
evaluationResult.webVitals[metric],
);
}
}
const profilerMetrics = [
'id',
'phase',
'actualDuration',
'baseDuration',
'startTime',
'commitTime',
] as const;
for (const metric of profilerMetrics) {
if (evaluationResult.reactProfiler[metric] !== null) {
performanceResults.reactProfiler[metric].push(
evaluationResult.reactProfiler[metric],
);
}
}
performanceResults.reactProfiler.id +=
evaluationResult.reactProfiler.actualDuration || 0;
performanceResults.reactProfiler.phase +=
evaluationResult.reactProfiler.phase || 0;
performanceResults.reactProfiler.actualDuration +=
evaluationResult.reactProfiler.actualDuration || 0;
performanceResults.reactProfiler.baseDuration +=
evaluationResult.reactProfiler.baseDuration || 0;
performanceResults.reactProfiler.startTime +=
evaluationResult.reactProfiler.startTime || 0;
performanceResults.reactProfiler.commitTime +=
evaluationResult.reactProfiler.commitTime || 0;
performanceResults.error = evaluationResult.error;
}
@@ -220,14 +159,14 @@ function buildHtml(transpiled: string) {
renderTime: null,
webVitals: {},
reactProfiler: {},
error: null,
error: null
};
webVitals.onCLS(({value}) => { window.__RESULT__.webVitals.cls = value; });
webVitals.onLCP(({value}) => { window.__RESULT__.webVitals.lcp = value; });
webVitals.onINP(({value}) => { window.__RESULT__.webVitals.inp = value; });
webVitals.onFID(({value}) => { window.__RESULT__.webVitals.fid = value; });
webVitals.onTTFB(({value}) => { window.__RESULT__.webVitals.ttfb = value; });
webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
try {
${transpiled}

View File

@@ -128,7 +128,7 @@ const rule = {
docs: {
description: 'enforces the Rules of Hooks',
recommended: true,
url: 'https://reactjs.org/docs/hooks-rules.html',
url: 'https://react.dev/reference/rules/rules-of-hooks',
},
},
create(context: Rule.RuleContext) {

View File

@@ -1957,8 +1957,8 @@ describe('ReactFlight', () => {
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":S1:" />
<div prop=":S2:" />
<div prop="«S1»" />
<div prop="«S2»" />
</>,
);
});
@@ -1981,8 +1981,8 @@ describe('ReactFlight', () => {
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":fooS1:" />
<div prop=":fooS2:" />
<div prop="«fooS1»" />
<div prop="«fooS2»" />
</>,
);
});
@@ -2021,8 +2021,8 @@ describe('ReactFlight', () => {
assertLog(['ClientDoubler']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":S1:">:S1:</div>
<div prop=":S1:">:S1:</div>
<div prop="«S1»">«S1»</div>
<div prop="«S1»">«S1»</div>
</>,
);
});

View File

@@ -120,7 +120,13 @@ function useId(): string {
}
const id = currentRequest.identifierCount++;
// use 'S' for Flight components to distinguish from 'R' and 'r' in Fizz/Client
return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
return (
'\u00AB' +
currentRequest.identifierPrefix +
'S' +
id.toString(32) +
'\u00BB'
);
}
function use<T>(usable: Usable<T>): T {

View File

@@ -2302,6 +2302,9 @@ function renderModel(
key: string,
value: ReactClientValue,
): ReactJSONValue {
// First time we're serializing the key, we should add it to the size.
serializedSize += key.length;
const prevKeyPath = task.keyPath;
const prevImplicitSlot = task.implicitSlot;
try {
@@ -2416,8 +2419,6 @@ function renderModelDestructive(
// Set the currently rendering model
task.model = value;
serializedSize += parentPropertyName.length;
// Special Symbol, that's very common.
if (value === REACT_ELEMENT_TYPE) {
return '$';