[compiler][bugfix] expand StoreContext to const / let / function variants (#32747)

```js
function Component() {
  useEffect(() => {
    let hasCleanedUp = false;
    document.addEventListener(..., () => hasCleanedUp ? foo() : bar());
    // effect return values shouldn't be typed as frozen
    return () => {
      hasCleanedUp = true;
    }
  };
}
```
### Problem
`PruneHoistedContexts` currently strips hoisted declarations and
rewrites the first `StoreContext` reassignment to a declaration. For
example, in the following example, instruction 0 is removed while a
synthetic `DeclareContext let` is inserted before instruction 1.

```js
// source
const cb = () => x; // reference that causes x to be hoisted

let x = 4;
x = 5;

// React Compiler IR
[0] DeclareContext HoistedLet 'x'
...
[1] StoreContext reassign 'x' = 4
[2] StoreContext reassign 'x' = 5
```

Currently, we don't account for `DeclareContext let`. As a result, we're
rewriting to insert duplicate declarations.
```js
// source
const cb = () => x; // reference that causes x to be hoisted

let x;
x = 5;

// React Compiler IR
[0] DeclareContext HoistedLet 'x'
...
[1] DeclareContext Let 'x'
[2] StoreContext reassign 'x' = 5
```

### Solution

Instead of always lowering context variables to a DeclareContext
followed by a StoreContext reassign, we can keep `kind: 'Const' | 'Let'
| 'Reassign' | etc` on StoreContext.
Pros:
- retain more information in HIR, so we can codegen easily `const` and
`let` context variable declarations back
- pruning hoisted `DeclareContext` instructions is simple.

Cons:
- passes are more verbose as we need to check for both `DeclareContext`
and `StoreContext` declarations

~(note: also see alternative implementation in
https://github.com/facebook/react/pull/32745)~

### Testing
Context variables are tricky. I synced and diffed changes in a large
meta codebase and feel pretty confident about landing this. About 0.01%
of compiled files changed. Among these changes, ~25% were [direct
bugfixes](https://www.internalfb.com/phabricator/paste/view/P1800029094).
The [other
changes](https://www.internalfb.com/phabricator/paste/view/P1800028575)
were primarily due to changed (corrected) mutable ranges from
https://github.com/facebook/react/pull/33047. I tried to represent most
interesting changes in new test fixtures

`
This commit is contained in:
mofeiZ 2025-04-30 17:18:58 -04:00 committed by GitHub
parent 12f4cb85c5
commit 9d795d3808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 916 additions and 232 deletions

View File

@ -3609,31 +3609,40 @@ function lowerAssignment(
let temporary;
if (builder.isContextIdentifier(lvalue)) {
if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
if (kind === InstructionKind.Const) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
}
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
lvalue: {
kind: InstructionKind.Let,
place: {...place},
},
loc: place.loc,
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
}
temporary = lowerValueToTemporary(builder, {
kind: 'StoreContext',
lvalue: {place: {...place}, kind: InstructionKind.Reassign},
value,
loc,
});
if (
kind !== InstructionKind.Const &&
kind !== InstructionKind.Reassign &&
kind !== InstructionKind.Let &&
kind !== InstructionKind.Function
) {
builder.errors.push({
reason: `Unexpected context variable kind`,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
temporary = lowerValueToTemporary(builder, {
kind: 'UnsupportedNode',
node: lvalueNode,
loc: lvalueNode.loc ?? GeneratedSource,
});
} else {
temporary = lowerValueToTemporary(builder, {
kind: 'StoreContext',
lvalue: {place: {...place}, kind},
value,
loc,
});
}
} else {
const typeAnnotation = lvalue.get('typeAnnotation');
let type: t.FlowType | t.TSType | null;

View File

@ -746,6 +746,27 @@ export enum InstructionKind {
Function = 'Function',
}
export function convertHoistedLValueKind(
kind: InstructionKind,
): InstructionKind | null {
switch (kind) {
case InstructionKind.HoistedLet:
return InstructionKind.Let;
case InstructionKind.HoistedConst:
return InstructionKind.Const;
case InstructionKind.HoistedFunction:
return InstructionKind.Function;
case InstructionKind.Let:
case InstructionKind.Const:
case InstructionKind.Function:
case InstructionKind.Reassign:
case InstructionKind.Catch:
return null;
default:
assertExhaustive(kind, 'Unexpected lvalue kind');
}
}
function _staticInvariantInstructionValueHasLocation(
value: InstructionValue,
): SourceLocation {
@ -880,8 +901,20 @@ export type InstructionValue =
| StoreLocal
| {
kind: 'StoreContext';
/**
* StoreContext kinds:
* Reassign: context variable reassignment in source
* Const: const declaration + assignment in source
* ('const' context vars are ones whose declarations are hoisted)
* Let: let declaration + assignment in source
* Function: function declaration in source (similar to `const`)
*/
lvalue: {
kind: InstructionKind.Reassign;
kind:
| InstructionKind.Reassign
| InstructionKind.Const
| InstructionKind.Let
| InstructionKind.Function;
place: Place;
};
value: Place;

View File

@ -30,6 +30,7 @@ import {
FunctionExpression,
ObjectMethod,
PropertyLiteral,
convertHoistedLValueKind,
} from './HIR';
import {
collectHoistablePropertyLoads,
@ -246,12 +247,18 @@ function isLoadContextMutable(
id: InstructionId,
): instrValue is LoadContext {
if (instrValue.kind === 'LoadContext') {
CompilerError.invariant(instrValue.place.identifier.scope != null, {
reason:
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
loc: instrValue.loc,
});
return id >= instrValue.place.identifier.scope.range.end;
/**
* Not all context variables currently have scopes due to limitations of
* mutability analysis for function expressions.
*
* Currently, many function expressions references are inferred to be
* 'Read' | 'Freeze' effects which don't replay mutable effects of captured
* context.
*/
return (
instrValue.place.identifier.scope != null &&
id >= instrValue.place.identifier.scope.range.end
);
}
return false;
}
@ -471,6 +478,9 @@ export class DependencyCollectionContext {
}
this.#reassignments.set(identifier, decl);
}
hasDeclared(identifier: Identifier): boolean {
return this.#declarations.has(identifier.declarationId);
}
// Checks if identifier is a valid dependency in the current scope
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
@ -672,21 +682,21 @@ export function handleInstruction(
});
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
/*
* Some variables may be declared and never initialized. We need
* to retain (and hoist) these declarations if they are included
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
* as scope declarations.
* Some variables may be declared and never initialized. We need to retain
* (and hoist) these declarations if they are included in a reactive scope.
* One approach is to simply add all `DeclareLocal`s as scope declarations.
*
* Context variables with hoisted declarations only become live after their
* first assignment. We only declare real DeclareLocal / DeclareContext
* instructions (not hoisted ones) to avoid generating dependencies on
* hoisted declarations.
*/
/*
* We add context variable declarations here, not at `StoreContext`, since
* context Store / Loads are modeled as reads and mutates to the underlying
* variable reference (instead of through intermediate / inlined temporaries)
*/
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
if (convertHoistedLValueKind(value.lvalue.kind) === null) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
}
} else if (value.kind === 'Destructure') {
context.visitOperand(value.value);
for (const place of eachPatternOperand(value.lvalue.pattern)) {
@ -698,6 +708,26 @@ export function handleInstruction(
scope: context.currentScope,
});
}
} else if (value.kind === 'StoreContext') {
/**
* Some StoreContext variables have hoisted declarations. If we're storing
* to a context variable that hasn't yet been declared, the StoreContext is
* the declaration.
* (see corresponding logic in PruneHoistedContext)
*/
if (
!context.hasDeclared(value.lvalue.place.identifier) ||
value.lvalue.kind !== InstructionKind.Reassign
) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
}
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);

View File

@ -176,9 +176,15 @@ export function inferMutableLifetimes(
if (
instr.value.kind === 'DeclareContext' ||
(instr.value.kind === 'StoreContext' &&
instr.value.lvalue.kind !== InstructionKind.Reassign)
instr.value.lvalue.kind !== InstructionKind.Reassign &&
!contextVariableDeclarationInstructions.has(
instr.value.lvalue.place.identifier,
))
) {
// Save declarations of context variables
/**
* Save declarations of context variables if they hasn't already been
* declared (due to hoisted declarations).
*/
contextVariableDeclarationInstructions.set(
instr.value.lvalue.place.identifier,
instr.id,

View File

@ -407,9 +407,14 @@ class InferenceState {
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
for (const value of values) {
if (value.kind === 'DeclareContext') {
if (
value.kind === 'DeclareContext' ||
(value.kind === 'StoreContext' &&
(value.lvalue.kind === InstructionKind.Let ||
value.lvalue.kind === InstructionKind.Const))
) {
/**
* Avoid freezing hoisted context declarations
* Avoid freezing context variable declarations, hoisted or otherwise
* function Component() {
* const cb = useBar(() => foo(2)); // produces a hoisted context declaration
* const foo = useFoo(); // reassigns to the context variable
@ -1606,6 +1611,14 @@ function inferBlock(
);
const lvalue = instr.lvalue;
if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
state.initialize(instrValue, {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
});
state.define(instrValue.lvalue.place, instrValue);
}
state.alias(lvalue, instrValue.value);
lvalue.effect = Effect.Store;
continuation = {kind: 'funeffects'};

View File

@ -1000,6 +1000,14 @@ function codegenTerminal(
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
@ -1092,6 +1100,14 @@ function codegenTerminal(
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,

View File

@ -5,14 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
DeclarationId,
convertHoistedLValueKind,
IdentifierId,
InstructionKind,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ReactiveStatement,
} from '../HIR';
import {empty, Stack} from '../Utils/Stack';
import {
ReactiveFunctionTransform,
Transformed,
@ -24,133 +26,54 @@ import {
* original instruction kind.
*/
export function pruneHoistedContexts(fn: ReactiveFunction): void {
const hoistedIdentifiers: HoistedIdentifiers = new Map();
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
visitReactiveFunction(fn, new Visitor(), {
activeScopes: empty(),
});
}
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
'REWRITTEN_HOISTED_CONST',
);
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
type VisitorState = {
activeScopes: Stack<Set<IdentifierId>>;
};
type HoistedIdentifiers = Map<
DeclarationId,
| InstructionKind
| typeof REWRITTEN_HOISTED_CONST
| typeof REWRITTEN_HOISTED_LET
>;
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
class Visitor extends ReactiveFunctionTransform<VisitorState> {
override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
state.activeScopes = state.activeScopes.push(
new Set(scope.scope.declarations.keys()),
);
this.traverseScope(scope, state);
state.activeScopes.pop();
}
override transformInstruction(
instruction: ReactiveInstruction,
state: HoistedIdentifiers,
state: VisitorState,
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
/**
* Remove hoisted declarations to preserve TDZ
*/
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedConst'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Const,
if (instruction.value.kind === 'DeclareContext') {
const maybeNonHoisted = convertHoistedLValueKind(
instruction.value.lvalue.kind,
);
return {kind: 'remove'};
if (maybeNonHoisted != null) {
return {kind: 'remove'};
}
}
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedLet'
instruction.value.kind === 'StoreContext' &&
instruction.value.lvalue.kind !== InstructionKind.Reassign
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Let,
/**
* Rewrite StoreContexts let/const/functions that will be pre-declared in
* codegen to reassignments.
*/
const lvalueId = instruction.value.lvalue.place.identifier.id;
const isDeclaredByScope = state.activeScopes.find(scope =>
scope.has(lvalueId),
);
return {kind: 'remove'};
}
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedFunction'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Function,
);
return {kind: 'remove'};
}
if (instruction.value.kind === 'StoreContext') {
const kind = state.get(
instruction.value.lvalue.place.identifier.declarationId,
);
if (kind != null) {
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
reason: 'Expected exactly one store to a hoisted const variable',
loc: instruction.loc,
});
if (
kind === InstructionKind.Const ||
kind === InstructionKind.Function
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
REWRITTEN_HOISTED_CONST,
);
return {
kind: 'replace',
value: {
kind: 'instruction',
instruction: {
...instruction,
value: {
...instruction.value,
lvalue: {
...instruction.value.lvalue,
kind,
},
type: null,
kind: 'StoreLocal',
},
},
},
};
} else if (kind !== REWRITTEN_HOISTED_LET) {
/**
* Context variables declared with let may have reassignments. Only
* insert a `DeclareContext` for the first encountered `StoreContext`
* instruction.
*/
state.set(
instruction.value.lvalue.place.identifier.declarationId,
REWRITTEN_HOISTED_LET,
);
return {
kind: 'replace-many',
value: [
{
kind: 'instruction',
instruction: {
id: instruction.id,
lvalue: null,
value: {
kind: 'DeclareContext',
lvalue: {
kind: InstructionKind.Let,
place: {...instruction.value.lvalue.place},
},
loc: instruction.value.loc,
},
loc: instruction.loc,
},
},
{kind: 'instruction', instruction},
],
};
}
if (isDeclaredByScope) {
instruction.value.lvalue.kind = InstructionKind.Reassign;
}
}

View File

@ -34,8 +34,7 @@ function bar(a, b) {
if ($[0] !== a || $[1] !== b) {
const x = [a, b];
y = {};
let t;
t = {};
let t = {};
y = x[0][1];
t = x[1][0];

View File

@ -35,8 +35,7 @@ function bar(a, b) {
if ($[0] !== a || $[1] !== b) {
const x = [a, b];
y = {};
let t;
t = {};
let t = {};
const f0 = function () {
y = x[0][1];
t = x[1][0];

View File

@ -33,8 +33,7 @@ function useTest() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let w;
w = {};
let w = {};
const t1 = (w = 42);
const t2 = w;

View File

@ -30,8 +30,7 @@ function Component(props) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let x;
x = null;
let x = null;
const callback = () => {
console.log(x);
};

View File

@ -2,13 +2,22 @@
## Input
```javascript
import {Stringify, useIdentity} from 'shared-runtime';
function Component() {
const data = useData();
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
const items = [];
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<div key={i} onClick={() => data.set(i)} />);
items.push(
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
);
}
return <>{items}</>;
}
@ -17,10 +26,6 @@ const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
return new Map();
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
@ -32,41 +37,47 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, useIdentity } from "shared-runtime";
function Component() {
const $ = _c(2);
const data = useData();
const $ = _c(3);
let t0;
if ($[0] !== data) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Map([
[0, "value0"],
[1, "value1"],
]);
$[0] = t0;
} else {
t0 = $[0];
}
const data = useIdentity(t0);
let t1;
if ($[1] !== data) {
const items = [];
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
items.push(<div key={i} onClick={() => data.set(i)} />);
items.push(
<Stringify
key={i}
onClick={() => data.get(i)}
shouldInvokeFns={true}
/>,
);
}
t0 = <>{items}</>;
$[0] = data;
$[1] = t0;
t1 = <>{items}</>;
$[1] = data;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
return t0;
return t1;
}
const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = new Map();
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,
@ -75,4 +86,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) <div></div><div></div><div></div><div></div>
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>

View File

@ -1,10 +1,19 @@
import {Stringify, useIdentity} from 'shared-runtime';
function Component() {
const data = useData();
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
const items = [];
// NOTE: `i` is a context variable because it's reassigned and also referenced
// within a closure, the `onClick` handler of each item
for (let i = MIN; i <= MAX; i += INCREMENT) {
items.push(<div key={i} onClick={() => data.set(i)} />);
items.push(
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
);
}
return <>{items}</>;
}
@ -13,10 +22,6 @@ const MIN = 0;
const MAX = 3;
const INCREMENT = 1;
function useData() {
return new Map();
}
export const FIXTURE_ENTRYPOINT = {
params: [],
fn: Component,

View File

@ -0,0 +1,82 @@
## Input
```javascript
import {CONST_TRUE, useIdentity} from 'shared-runtime';
const hidden = CONST_TRUE;
function useFoo() {
const makeCb = useIdentity(() => {
const logIntervalId = () => {
log(intervalId);
};
let intervalId;
if (!hidden) {
intervalId = 2;
}
return () => {
logIntervalId();
};
});
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_TRUE, useIdentity } from "shared-runtime";
const hidden = CONST_TRUE;
function useFoo() {
const $ = _c(4);
const makeCb = useIdentity(_temp);
let t0;
if ($[0] !== makeCb) {
t0 = makeCb();
$[0] = makeCb;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== t0) {
t1 = <Stringify fn={t0} shouldInvokeFns={true} />;
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
function _temp() {
const logIntervalId = () => {
log(intervalId);
};
let intervalId;
if (!hidden) {
intervalId = 2;
}
return () => {
logIntervalId();
};
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
### Eval output
(kind: exception) Stringify is not defined

View File

@ -0,0 +1,25 @@
import {CONST_TRUE, useIdentity} from 'shared-runtime';
const hidden = CONST_TRUE;
function useFoo() {
const makeCb = useIdentity(() => {
const logIntervalId = () => {
log(intervalId);
};
let intervalId;
if (!hidden) {
intervalId = 2;
}
return () => {
logIntervalId();
};
});
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@ -30,8 +30,7 @@ function Foo() {
getX = () => x;
console.log(getX());
let x;
x = 4;
let x = 4;
x = x + 5;
$[0] = getX;
} else {

View File

@ -0,0 +1,64 @@
## Input
```javascript
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x;
if (cond) {
x = CONST_NUMBER1;
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_NUMBER1, Stringify } from "shared-runtime";
function useHook(t0) {
"use memo";
const $ = _c(2);
const { cond } = t0;
let t1;
if ($[0] !== cond) {
const getX = () => x;
let x;
if (cond) {
x = CONST_NUMBER1;
}
t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
$[0] = cond;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
};
```
### Eval output
(kind: ok)

View File

@ -0,0 +1,18 @@
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x;
if (cond) {
x = CONST_NUMBER1;
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: () => {},
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};

View File

@ -36,8 +36,7 @@ function hoisting(cond) {
items.push(bar());
};
let bar;
bar = _temp;
let bar = _temp;
foo();
}
$[0] = cond;

View File

@ -41,11 +41,9 @@ function hoisting() {
return result;
};
let foo;
foo = () => bar + baz;
let foo = () => bar + baz;
let bar;
bar = 3;
let bar = 3;
const baz = 2;
t0 = qux();
$[0] = t0;

View File

@ -37,8 +37,7 @@ function useHook(t0) {
if ($[0] !== cond) {
const getX = () => x;
let x;
x = CONST_NUMBER0;
let x = CONST_NUMBER0;
if (cond) {
x = x + CONST_NUMBER1;
x;

View File

@ -38,8 +38,7 @@ function useHook(t0) {
if ($[0] !== cond) {
const getX = () => x;
let x;
x = CONST_NUMBER0;
let x = CONST_NUMBER0;
if (cond) {
x = x + CONST_NUMBER1;
x;

View File

@ -29,10 +29,8 @@ function hoisting() {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
foo = () => bar + baz;
let bar;
bar = 3;
let baz;
baz = 2;
let bar = 3;
let baz = 2;
$[0] = foo;
} else {
foo = $[0];

View File

@ -0,0 +1,129 @@
## Input
```javascript
import {Stringify, useIdentity} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>
);
i = i + 1;
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop2}
shouldInvokeFns={true}
/>
);
return <>{items}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 'prop1', prop2: 'prop2'}],
sequentialRenders: [
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'changed', prop2: 'prop2'},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, useIdentity } from "shared-runtime";
function Component(t0) {
"use memo";
const $ = _c(12);
const { prop1, prop2 } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = new Map([
[0, "value0"],
[1, "value1"],
]);
$[0] = t1;
} else {
t1 = $[0];
}
const data = useIdentity(t1);
let t2;
if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>,
);
i = i + 1;
const t3 = i;
let t4;
if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
t4 = () => data.get(i) + prop2;
$[5] = data;
$[6] = i;
$[7] = prop2;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
if ($[9] !== t3 || $[10] !== t4) {
t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
$[9] = t3;
$[10] = t4;
$[11] = t5;
} else {
t5 = $[11];
}
items.push(t5);
t2 = <>{items}</>;
$[1] = data;
$[2] = prop1;
$[3] = prop2;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prop1: "prop1", prop2: "prop2" }],
sequentialRenders: [
{ prop1: "prop1", prop2: "prop2" },
{ prop1: "prop1", prop2: "prop2" },
{ prop1: "changed", prop2: "prop2" },
],
};
```
### Eval output
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>

View File

@ -0,0 +1,40 @@
import {Stringify, useIdentity} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
const data = useIdentity(
new Map([
[0, 'value0'],
[1, 'value1'],
])
);
let i = 0;
const items = [];
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop1}
shouldInvokeFns={true}
/>
);
i = i + 1;
items.push(
<Stringify
key={i}
onClick={() => data.get(i) + prop2}
shouldInvokeFns={true}
/>
);
return <>{items}</>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 'prop1', prop2: 'prop2'}],
sequentialRenders: [
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'prop1', prop2: 'prop2'},
{prop1: 'changed', prop2: 'prop2'},
],
};

View File

@ -37,8 +37,7 @@ function Component() {
}
const x = t0;
let x_0;
x_0 = 56;
let x_0 = 56;
const fn = function () {
x_0 = 42;
};

View File

@ -33,8 +33,7 @@ function component(a) {
m(x);
};
let x;
x = { a };
let x = { a };
m(x);
$[0] = a;
$[1] = y;

View File

@ -65,8 +65,7 @@ function useBar(t0, cond) {
} else {
t1 = $[0];
}
let x;
x = useIdentity(t1);
let x = useIdentity(t1);
if (cond) {
x = b;
}

View File

@ -47,8 +47,7 @@ function Foo(t0) {
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
const x = [arr1];
let y;
y = [];
let y = [];
getVal1 = _temp;

View File

@ -47,8 +47,7 @@ function Foo(t0) {
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
const x = [arr1];
let y;
y = [];
let y = [];
let t2;
let t3;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {

View File

@ -0,0 +1,108 @@
## Input
```javascript
import {useState, useEffect} from 'react';
import {invoke, Stringify} from 'shared-runtime';
function Content() {
const [announcement, setAnnouncement] = useState('');
const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
// This was originally passed down as an onClick, but React Compiler's test
// evaluator doesn't yet support events outside of React
useEffect(() => {
if (users.length === 2) {
let removedUserName = '';
setUsers(prevUsers => {
const newUsers = [...prevUsers];
removedUserName = newUsers.at(-1).name;
newUsers.pop();
return newUsers;
});
setAnnouncement(`Removed user (${removedUserName})`);
}
}, [users]);
return <Stringify users={users} announcement={announcement} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Content,
params: [{}],
sequentialRenders: [{}, {}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useState, useEffect } from "react";
import { invoke, Stringify } from "shared-runtime";
function Content() {
const $ = _c(8);
const [announcement, setAnnouncement] = useState("");
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [{ name: "John Doe" }, { name: "Jane Doe" }];
$[0] = t0;
} else {
t0 = $[0];
}
const [users, setUsers] = useState(t0);
let t1;
if ($[1] !== users.length) {
t1 = () => {
if (users.length === 2) {
let removedUserName = "";
setUsers((prevUsers) => {
const newUsers = [...prevUsers];
removedUserName = newUsers.at(-1).name;
newUsers.pop();
return newUsers;
});
setAnnouncement(`Removed user (${removedUserName})`);
}
};
$[1] = users.length;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== users) {
t2 = [users];
$[3] = users;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== announcement || $[6] !== users) {
t3 = <Stringify users={users} announcement={announcement} />;
$[5] = announcement;
$[6] = users;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Content,
params: [{}],
sequentialRenders: [{}, {}],
};
```
### Eval output
(kind: ok) <div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>
<div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>

View File

@ -0,0 +1,31 @@
import {useState, useEffect} from 'react';
import {invoke, Stringify} from 'shared-runtime';
function Content() {
const [announcement, setAnnouncement] = useState('');
const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
// This was originally passed down as an onClick, but React Compiler's test
// evaluator doesn't yet support events outside of React
useEffect(() => {
if (users.length === 2) {
let removedUserName = '';
setUsers(prevUsers => {
const newUsers = [...prevUsers];
removedUserName = newUsers.at(-1).name;
newUsers.pop();
return newUsers;
});
setAnnouncement(`Removed user (${removedUserName})`);
}
}, [users]);
return <Stringify users={users} announcement={announcement} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Content,
params: [{}],
sequentialRenders: [{}, {}],
};

View File

@ -62,8 +62,7 @@ function Foo(t0) {
myVar = _temp;
};
let myVar;
myVar = _temp2;
let myVar = _temp2;
useIdentity();
const fn = fnFactory();

View File

@ -0,0 +1,122 @@
## Input
```javascript
import {useEffect, useState} from 'react';
/**
* Example of a function expression whose return value shouldn't have
* a "freeze" effect on all operands.
*
* This is because the function expression is passed to `useEffect` and
* thus is not a render function. `cleanedUp` is also created within
* the effect and is not a render variable.
*/
function Component({prop}) {
const [cleanupCount, setCleanupCount] = useState(0);
useEffect(() => {
let cleanedUp = false;
setTimeout(() => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
}, 0);
// This return value should not have freeze effects
// on its operands
return () => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
};
}, [prop]);
return <div>{cleanupCount}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop: 5}],
sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useEffect, useState } from "react";
/**
* Example of a function expression whose return value shouldn't have
* a "freeze" effect on all operands.
*
* This is because the function expression is passed to `useEffect` and
* thus is not a render function. `cleanedUp` is also created within
* the effect and is not a render variable.
*/
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [cleanupCount, setCleanupCount] = useState(0);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
let cleanedUp = false;
setTimeout(() => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(_temp);
}
}, 0);
return () => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(_temp2);
}
};
};
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== prop) {
t2 = [prop];
$[1] = prop;
$[2] = t2;
} else {
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== cleanupCount) {
t3 = <div>{cleanupCount}</div>;
$[3] = cleanupCount;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
function _temp2(c_0) {
return c_0 + 1;
}
function _temp(c) {
return c + 1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prop: 5 }],
sequentialRenders: [{ prop: 5 }, { prop: 5 }, { prop: 6 }],
};
```
### Eval output
(kind: ok) <div>0</div>
<div>0</div>
<div>1</div>

View File

@ -0,0 +1,38 @@
import {useEffect, useState} from 'react';
/**
* Example of a function expression whose return value shouldn't have
* a "freeze" effect on all operands.
*
* This is because the function expression is passed to `useEffect` and
* thus is not a render function. `cleanedUp` is also created within
* the effect and is not a render variable.
*/
function Component({prop}) {
const [cleanupCount, setCleanupCount] = useState(0);
useEffect(() => {
let cleanedUp = false;
setTimeout(() => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
}, 0);
// This return value should not have freeze effects
// on its operands
return () => {
if (!cleanedUp) {
cleanedUp = true;
setCleanupCount(c => c + 1);
}
};
}, [prop]);
return <div>{cleanupCount}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop: 5}],
sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
};

View File

@ -79,8 +79,7 @@ function Component(props) {
function Inner(props) {
const $ = _c(7);
let input;
input = null;
let input = null;
if (props.cond) {
input = use(FooContext);
}